feat: Tooltips and Better Surface Management
This commit is contained in:
parent
c7edd37b03
commit
337b80d4ca
90 changed files with 3651 additions and 977 deletions
15
Cargo.toml
15
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",
|
||||
|
|
|
|||
|
|
@ -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<Id>,
|
||||
example_row: bool,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
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<cosmic::app::Message<Self::Message>>) {
|
||||
fn init(core: Core, _flags: Self::Flags) -> (Self, Task<Message>) {
|
||||
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<cosmic::app::Message<Self::Message>> {
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
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::Message> {
|
||||
self.core
|
||||
.applet
|
||||
.icon_button("display-symbolic")
|
||||
.on_press(Message::TogglePopup)
|
||||
.into()
|
||||
fn view(&self) -> Element<Message> {
|
||||
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::<Window>(
|
||||
|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::<Message>(
|
||||
btn,
|
||||
"test",
|
||||
self.popup.is_some(),
|
||||
|a| Message::Surface(a),
|
||||
))
|
||||
}
|
||||
|
||||
fn view_window(&self, _id: Id) -> Element<Self::Message> {
|
||||
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<Message> {
|
||||
"oops".into()
|
||||
}
|
||||
|
||||
fn style(&self) -> Option<cosmic::iced_runtime::Appearance> {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<iced::id::Id> = 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<dyn std::error::Error>> {
|
||||
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<KeyBind, Action>,
|
||||
}
|
||||
|
||||
/// 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<Element<Self::Message>> {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -74,10 +74,7 @@ impl cosmic::Application for MultiWindow {
|
|||
})
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Self::Message,
|
||||
) -> iced::Task<cosmic::app::Message<Self::Message>> {
|
||||
fn update(&mut self, message: Self::Message) -> iced::Task<cosmic::Action<Self::Message>> {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -70,10 +70,10 @@ pub enum NavMenuAction {
|
|||
}
|
||||
|
||||
impl menu::Action for NavMenuAction {
|
||||
type Message = cosmic::app::Message<Message>;
|
||||
type Message = cosmic::Action<Message>;
|
||||
|
||||
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<Vec<menu::Tree<cosmic::app::Message<Self::Message>>>> {
|
||||
) -> Option<Vec<menu::Tree<cosmic::Action<Self::Message>>>> {
|
||||
Some(menu::items(
|
||||
&HashMap::new(),
|
||||
vec![
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ pub enum Message {
|
|||
OpenError(Arc<file_chooser::Error>),
|
||||
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()
|
||||
|
|
|
|||
2
iced
2
iced
|
|
@ -1 +1 @@
|
|||
Subproject commit a8c31a0982b134278e7ecb5a91e0d3ba0fa6d2dd
|
||||
Subproject commit 0788075435aa7a6532327b9435fcfc7a12e1b6a6
|
||||
40
src/action.rs
Normal file
40
src/action.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
#[cfg(feature = "winit")]
|
||||
use crate::app;
|
||||
#[cfg(feature = "single-instance")]
|
||||
use crate::dbus_activation;
|
||||
|
||||
pub const fn app<M>(message: M) -> Action<M> {
|
||||
Action::App(message)
|
||||
}
|
||||
#[cfg(feature = "winit")]
|
||||
pub const fn cosmic<M>(message: app::Action) -> Action<M> {
|
||||
Action::Cosmic(message)
|
||||
}
|
||||
|
||||
pub const fn none<M>() -> Action<M> {
|
||||
Action::None
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[must_use]
|
||||
pub enum Action<M> {
|
||||
/// 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<M> From<M> for Action<M> {
|
||||
fn from(value: M) -> Self {
|
||||
Self::App(value)
|
||||
}
|
||||
}
|
||||
75
src/app/action.rs
Normal file
75
src/app/action.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// 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<iced::Size>),
|
||||
/// 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),
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use iced::window;
|
||||
|
||||
/// Asynchronous actions for COSMIC applications.
|
||||
use super::Message;
|
||||
|
||||
/// Commands for COSMIC applications.
|
||||
pub type Task<M> = iced::Task<Message<M>>;
|
||||
|
||||
/// Creates a task which yields a [`crate::app::Message`].
|
||||
pub fn message<M: Send + 'static>(message: Message<M>) -> Task<M> {
|
||||
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<M: Send + 'static>(message: M) -> crate::app::Task<M> {
|
||||
super::message(super::Message::App(message))
|
||||
}
|
||||
|
||||
/// Creates a task which yields a cosmic message.
|
||||
pub fn cosmic<M: Send + 'static>(message: crate::app::cosmic::Message) -> crate::app::Task<M> {
|
||||
super::message(super::Message::Cosmic(message))
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::app::Core {
|
||||
pub fn drag<M: Send + 'static>(&self, id: Option<window::Id>) -> iced::Task<Message<M>> {
|
||||
let Some(id) = id.or(self.main_window) else {
|
||||
return iced::Task::none();
|
||||
};
|
||||
crate::task::drag(id).map(Message::Cosmic)
|
||||
}
|
||||
|
||||
pub fn maximize<M: Send + 'static>(
|
||||
&self,
|
||||
id: Option<window::Id>,
|
||||
maximized: bool,
|
||||
) -> iced::Task<Message<M>> {
|
||||
let Some(id) = id.or(self.main_window) else {
|
||||
return iced::Task::none();
|
||||
};
|
||||
crate::task::maximize(id, maximized).map(Message::Cosmic)
|
||||
}
|
||||
|
||||
pub fn minimize<M: Send + 'static>(&self, id: Option<window::Id>) -> iced::Task<Message<M>> {
|
||||
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<M: Send + 'static>(&self, factor: f32) -> iced::Task<Message<M>> {
|
||||
message::cosmic(super::cosmic::Message::ScaleFactor(factor))
|
||||
}
|
||||
|
||||
pub fn set_title<M: Send + 'static>(
|
||||
&self,
|
||||
id: Option<window::Id>,
|
||||
title: String,
|
||||
) -> iced::Task<Message<M>> {
|
||||
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<M: Send + 'static>(
|
||||
&self,
|
||||
id: Option<window::Id>,
|
||||
) -> iced::Task<Message<M>> {
|
||||
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<M: Send + 'static>(
|
||||
&self,
|
||||
id: Option<window::Id>,
|
||||
) -> iced::Task<Message<M>> {
|
||||
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<M: Send + 'static>(theme: crate::Theme) -> iced::Task<Message<M>> {
|
||||
message::cosmic(super::cosmic::Message::AppThemeChange(theme))
|
||||
}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// 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<Message: Clone + 'static>(
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<iced::Size>),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Cosmic<App> {
|
||||
pub struct Cosmic<App: Application> {
|
||||
pub app: App,
|
||||
#[cfg(feature = "wayland")]
|
||||
pub surface_views: HashMap<
|
||||
window::Id,
|
||||
Box<dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action<App::Message>>>,
|
||||
>,
|
||||
}
|
||||
|
||||
impl<T: Application> Cosmic<T>
|
||||
|
|
@ -91,7 +35,7 @@ where
|
|||
{
|
||||
pub fn init(
|
||||
(mut core, flags): (Core, T::Flags),
|
||||
) -> (Self, iced::Task<super::Message<T::Message>>) {
|
||||
) -> (Self, iced::Task<crate::Action<T::Message>>) {
|
||||
#[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<crate::Action<T::Message>> {
|
||||
#[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::<Box<dyn Fn(&mut T) -> 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::<Box<
|
||||
dyn for<'a> Fn(&'a T) -> Element<'a, crate::Action<T::Message>>
|
||||
+ 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::<Box<dyn Fn() -> 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::<Box<
|
||||
dyn Fn() -> Element<'static, crate::Action<T::Message>> + 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::<Box<dyn Fn(&mut T) -> 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::<Box<
|
||||
dyn for<'a> Fn(&'a T) -> Element<'a, crate::Action<T::Message>>
|
||||
+ 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::<Box<dyn Fn() -> 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::<Box<
|
||||
dyn Fn() -> Element<'static, crate::Action<T::Message>> + 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<T::Message>,
|
||||
) -> iced::Task<super::Message<T::Message>> {
|
||||
message: crate::Action<T::Message>,
|
||||
) -> iced::Task<crate::Action<T::Message>> {
|
||||
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<super::Message<T::Message>> {
|
||||
pub fn subscription(&self) -> Subscription<crate::Action<T::Message>> {
|
||||
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::CosmicTk>(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::<ThemeMode>(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::<T>());
|
||||
subscriptions.push(crate::dbus_activation::subscription::<T>());
|
||||
}
|
||||
|
||||
Subscription::batch(subscriptions)
|
||||
|
|
@ -286,20 +371,24 @@ where
|
|||
}
|
||||
|
||||
#[cfg(feature = "multi-window")]
|
||||
pub fn view(&self, id: window::Id) -> Element<super::Message<T::Message>> {
|
||||
pub fn view(&self, id: window::Id) -> Element<crate::Action<T::Message>> {
|
||||
#[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<super::Message<T::Message>> {
|
||||
pub fn view(&self) -> Element<crate::Action<T::Message>> {
|
||||
let view = self.app.view_main();
|
||||
|
||||
#[cfg(target_env = "gnu")]
|
||||
|
|
@ -321,7 +410,7 @@ where
|
|||
|
||||
impl<T: Application> Cosmic<T> {
|
||||
#[allow(clippy::unused_self)]
|
||||
pub fn close(&mut self) -> iced::Task<super::Message<T::Message>> {
|
||||
pub fn close(&mut self) -> iced::Task<crate::Action<T::Message>> {
|
||||
if let Some(id) = self.app.core().main_window_id() {
|
||||
iced::window::close(id)
|
||||
} else {
|
||||
|
|
@ -330,9 +419,9 @@ impl<T: Application> Cosmic<T> {
|
|||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn cosmic_update(&mut self, message: Message) -> iced::Task<super::Message<T::Message>> {
|
||||
fn cosmic_update(&mut self, message: Action) -> iced::Task<crate::Action<T::Message>> {
|
||||
match message {
|
||||
Message::WindowMaximized(id, maximized) => {
|
||||
Action::WindowMaximized(id, maximized) => {
|
||||
if self
|
||||
.app
|
||||
.core()
|
||||
|
|
@ -343,7 +432,7 @@ impl<T: Application> Cosmic<T> {
|
|||
}
|
||||
}
|
||||
|
||||
Message::WindowResize(id, width, height) => {
|
||||
Action::WindowResize(id, width, height) => {
|
||||
if self
|
||||
.app
|
||||
.core()
|
||||
|
|
@ -358,12 +447,12 @@ impl<T: Application> Cosmic<T> {
|
|||
|
||||
//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<T: Application> Cosmic<T> {
|
|||
}
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
Message::WmCapabilities(id, capabilities) => {
|
||||
Action::WmCapabilities(id, capabilities) => {
|
||||
if self
|
||||
.app
|
||||
.core()
|
||||
|
|
@ -399,49 +488,49 @@ impl<T: Application> Cosmic<T> {
|
|||
}
|
||||
}
|
||||
|
||||
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<T: Application> Cosmic<T> {
|
|||
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<T: Application> Cosmic<T> {
|
|||
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<T: Application> Cosmic<T> {
|
|||
}
|
||||
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<T: Application> Cosmic<T> {
|
|||
);
|
||||
}
|
||||
}
|
||||
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<T: Application> Cosmic<T> {
|
|||
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::<super::Message<T::Message>>()]);
|
||||
ret = Task::batch(vec![iced::exit::<crate::Action<T::Message>>()]);
|
||||
}
|
||||
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<T: Application> Cosmic<T> {
|
|||
}
|
||||
}
|
||||
#[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<T: Application> Cosmic<T> {
|
|||
}
|
||||
}
|
||||
#[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<T: Application> Cosmic<T> {
|
|||
*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<T: Application> Cosmic<T> {
|
|||
|
||||
impl<App: Application> Cosmic<App> {
|
||||
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<App::Message>> + Send + Sync,
|
||||
>,
|
||||
) -> Task<crate::Action<App::Message>> {
|
||||
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<App::Message>> + Send + Sync,
|
||||
>,
|
||||
) -> Task<crate::Action<App::Message>> {
|
||||
use iced_winit::commands::popup::get_popup;
|
||||
|
||||
self.surface_views.insert(settings.id, view);
|
||||
get_popup(settings)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
338
src/app/mod.rs
338
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<M> {
|
||||
/// 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<M> = iced::Task<crate::Action<M>>;
|
||||
|
||||
pub const fn app<M>(message: M) -> Message<M> {
|
||||
Message::App(message)
|
||||
}
|
||||
|
||||
pub const fn cosmic<M>(message: super::cosmic::Message) -> Message<M> {
|
||||
Message::Cosmic(message)
|
||||
}
|
||||
|
||||
pub const fn none<M>() -> Message<M> {
|
||||
Message::None
|
||||
}
|
||||
|
||||
impl<M> From<M> for Message<M> {
|
||||
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<App: Application>(
|
||||
settings: Settings,
|
||||
|
|
@ -143,6 +100,7 @@ pub fn run<App: Application>(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::<App>(settings, flags);
|
||||
#[cfg(not(feature = "multi-window"))]
|
||||
{
|
||||
|
|
@ -179,142 +137,6 @@ pub fn run<App: Application>(settings: Settings, flags: App::Flags) -> iced::Res
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "single-instance")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DbusActivationMessage<Action = String, Args = Vec<String>> {
|
||||
pub activation_token: Option<String>,
|
||||
pub desktop_startup_id: Option<String>,
|
||||
pub msg: DbusActivationDetails<Action, Args>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DbusActivationDetails<Action = String, Args = Vec<String>> {
|
||||
Activate,
|
||||
Open {
|
||||
url: Vec<Url>,
|
||||
},
|
||||
/// action can be deserialized as Flags
|
||||
ActivateAction {
|
||||
action: Action,
|
||||
args: Args,
|
||||
},
|
||||
}
|
||||
#[cfg(feature = "single-instance")]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DbusActivation(Option<Sender<DbusActivationMessage>>);
|
||||
#[cfg(feature = "single-instance")]
|
||||
impl DbusActivation {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self(None)
|
||||
}
|
||||
|
||||
pub fn rx(&mut self) -> Receiver<DbusActivationMessage> {
|
||||
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::<App>(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<Element<Message<Self::Message>>> {
|
||||
fn nav_bar(&self) -> Option<Element<crate::Action<Self::Message>>> {
|
||||
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<Vec<menu::Tree<Message<Self::Message>>>> {
|
||||
fn nav_context_menu(
|
||||
&self,
|
||||
id: nav_bar::Id,
|
||||
) -> Option<Vec<menu::Tree<crate::Action<Self::Message>>>> {
|
||||
None
|
||||
}
|
||||
|
||||
|
|
@ -605,7 +433,7 @@ where
|
|||
|
||||
/// Handles dbus activation messages
|
||||
#[cfg(feature = "single-instance")]
|
||||
fn dbus_activation(&mut self, msg: DbusActivationMessage) -> Task<Self::Message> {
|
||||
fn dbus_activation(&mut self, msg: crate::dbus_activation::Message) -> Task<Self::Message> {
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
|
|
@ -648,7 +476,21 @@ pub trait ApplicationExt: Application {
|
|||
fn set_window_title(&mut self, title: String, id: window::Id) -> Task<Self::Message>;
|
||||
|
||||
/// View template for the main window.
|
||||
fn view_main(&self) -> Element<Message<Self::Message>>;
|
||||
fn view_main(&self) -> Element<crate::Action<Self::Message>>;
|
||||
|
||||
fn watch_config<T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq>(
|
||||
&self,
|
||||
id: &'static str,
|
||||
) -> iced::Subscription<cosmic_config::Update<T>> {
|
||||
self.core().watch_config(id)
|
||||
}
|
||||
|
||||
fn watch_state<T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq>(
|
||||
&self,
|
||||
id: &'static str,
|
||||
) -> iced::Subscription<cosmic_config::Update<T>> {
|
||||
self.core().watch_state(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<App: Application> ApplicationExt for App {
|
||||
|
|
@ -695,7 +537,7 @@ impl<App: Application> ApplicationExt for App {
|
|||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
/// Creates the view for the main window.
|
||||
fn view_main(&self) -> Element<Message<Self::Message>> {
|
||||
fn view_main(&self) -> Element<crate::Action<Self::Message>> {
|
||||
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<App: Application> 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<App: Application> 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<App: Application> 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<App: Application> 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<App: Application> 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<App: Application> 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<App: Application> ApplicationExt for App {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "single-instance")]
|
||||
fn single_instance_subscription<App: ApplicationExt>() -> Subscription<Message<App::Message>> {
|
||||
use iced_futures::futures::StreamExt;
|
||||
iced_futures::Subscription::run_with_id(
|
||||
TypeId::of::<DbusActivation>(),
|
||||
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)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<iced::id::Id> =
|
|||
LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize"));
|
||||
static AUTOSIZE_MAIN_ID: LazyLock<iced::id::Id> =
|
||||
LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize-main"));
|
||||
static TOOLTIP_ID: LazyLock<crate::widget::Id> = LazyLock::new(|| iced::id::Id::new("subsurface"));
|
||||
static TOOLTIP_WINDOW_ID: LazyLock<window::Id> = 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<Element<'a, Message>>,
|
||||
tooltip: impl Into<Cow<'static, str>>,
|
||||
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::<Message, _, Renderer>::new(
|
||||
Container::<Message, _, Renderer>::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<Element<'a, Message>>,
|
||||
) -> 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)
|
||||
|
|
|
|||
45
src/command.rs
Normal file
45
src/command.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use iced::window;
|
||||
|
||||
/// Initiates a window drag.
|
||||
pub fn drag<M>(id: window::Id) -> iced::Task<crate::Action<M>> {
|
||||
iced_runtime::window::drag(id)
|
||||
}
|
||||
|
||||
/// Maximizes the window.
|
||||
pub fn maximize<M>(id: window::Id, maximized: bool) -> iced::Task<crate::Action<M>> {
|
||||
iced_runtime::window::maximize(id, maximized)
|
||||
}
|
||||
|
||||
/// Minimizes the window.
|
||||
pub fn minimize<M>(id: window::Id) -> iced::Task<crate::Action<M>> {
|
||||
iced_runtime::window::minimize(id, true)
|
||||
}
|
||||
|
||||
/// Sets the title of a window.
|
||||
#[allow(unused_variables, clippy::needless_pass_by_value)]
|
||||
pub fn set_title<M>(id: window::Id, title: String) -> iced::Task<crate::Action<M>> {
|
||||
iced::Task::none()
|
||||
}
|
||||
|
||||
#[cfg(feature = "winit")]
|
||||
pub fn set_scaling_factor<M: Send + 'static>(factor: f32) -> iced::Task<crate::Action<M>> {
|
||||
iced::Task::done(crate::app::Action::ScaleFactor(factor)).map(crate::Action::Cosmic)
|
||||
}
|
||||
|
||||
#[cfg(feature = "winit")]
|
||||
pub fn set_theme<M: Send + 'static>(theme: crate::Theme) -> iced::Task<crate::Action<M>> {
|
||||
iced::Task::done(crate::app::Action::AppThemeChange(theme)).map(crate::Action::Cosmic)
|
||||
}
|
||||
|
||||
/// Sets the window mode to windowed.
|
||||
pub fn set_windowed<M>(id: window::Id) -> iced::Task<crate::Action<M>> {
|
||||
iced_runtime::window::change_mode(id, window::Mode::Windowed)
|
||||
}
|
||||
|
||||
/// Toggles the windows' maximize state.
|
||||
pub fn toggle_maximize<M>(id: window::Id) -> iced::Task<crate::Action<M>> {
|
||||
iced_runtime::window::toggle_maximize(id)
|
||||
}
|
||||
|
|
@ -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<Mutex<BTreeSet<&'static str>>> = LazyLock::new(|| Mutex::default());
|
||||
pub static FAMILY_MAP: LazyLock<Mutex<BTreeSet<&'static str>>> = LazyLock::new(Mutex::default);
|
||||
|
||||
pub static COSMIC_TK: LazyLock<RwLock<CosmicTk>> = LazyLock::new(|| {
|
||||
RwLock::new(
|
||||
|
|
@ -153,7 +153,7 @@ impl From<FontConfig> 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);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// 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<window::Id>,
|
||||
|
||||
pub(crate) exit_on_main_window_closed: bool,
|
||||
|
||||
pub(crate) menu_bars: HashMap<crate::widget::Id, (Limits, Size)>,
|
||||
}
|
||||
|
||||
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<window::Id> {
|
||||
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<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
|
||||
let Some(id) = id.or(self.main_window) else {
|
||||
return iced::Task::none();
|
||||
};
|
||||
crate::command::drag(id)
|
||||
}
|
||||
|
||||
#[cfg(feature = "winit")]
|
||||
pub fn maximize<M: Send + 'static>(
|
||||
&self,
|
||||
id: Option<window::Id>,
|
||||
maximized: bool,
|
||||
) -> crate::app::Task<M> {
|
||||
let Some(id) = id.or(self.main_window) else {
|
||||
return iced::Task::none();
|
||||
};
|
||||
crate::command::maximize(id, maximized)
|
||||
}
|
||||
|
||||
#[cfg(feature = "winit")]
|
||||
pub fn minimize<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
|
||||
let Some(id) = id.or(self.main_window) else {
|
||||
return iced::Task::none();
|
||||
};
|
||||
crate::command::minimize(id)
|
||||
}
|
||||
|
||||
#[cfg(feature = "winit")]
|
||||
pub fn set_title<M: Send + 'static>(
|
||||
&self,
|
||||
id: Option<window::Id>,
|
||||
title: String,
|
||||
) -> crate::app::Task<M> {
|
||||
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<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
|
||||
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<M: Send + 'static>(
|
||||
&self,
|
||||
id: Option<window::Id>,
|
||||
) -> crate::app::Task<M> {
|
||||
let Some(id) = id.or(self.main_window) else {
|
||||
return iced::Task::none();
|
||||
};
|
||||
|
||||
crate::command::toggle_maximize(id)
|
||||
}
|
||||
}
|
||||
214
src/dbus_activation.rs
Normal file
214
src/dbus_activation.rs
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// 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<App: ApplicationExt>() -> Subscription<crate::Action<App::Message>> {
|
||||
use iced_futures::futures::StreamExt;
|
||||
iced_futures::Subscription::run_with_id(
|
||||
TypeId::of::<DbusActivation>(),
|
||||
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<Action = String, Args = Vec<String>> {
|
||||
pub activation_token: Option<String>,
|
||||
pub desktop_startup_id: Option<String>,
|
||||
pub msg: Details<Action, Args>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Details<Action = String, Args = Vec<String>> {
|
||||
Activate,
|
||||
Open {
|
||||
url: Vec<Url>,
|
||||
},
|
||||
/// action can be deserialized as Flags
|
||||
ActivateAction {
|
||||
action: Action,
|
||||
args: Args,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DbusActivation(Option<Sender<Message>>);
|
||||
|
||||
impl DbusActivation {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self(None)
|
||||
}
|
||||
|
||||
pub fn rx(&mut self) -> Receiver<Message> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ pub trait ElementExt {
|
|||
fn debug(self, debug: bool) -> Self;
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static> ElementExt for crate::Element<'a, Message> {
|
||||
impl<Message: 'static> ElementExt for crate::Element<'_, Message> {
|
||||
fn debug(self, debug: bool) -> Self {
|
||||
if debug {
|
||||
self.explain(Color::WHITE)
|
||||
|
|
|
|||
|
|
@ -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<Message> {
|
||||
pub fn subscription() -> Subscription<Action> {
|
||||
listen_raw(|event, status, _| {
|
||||
if event::Status::Ignored != status {
|
||||
return None;
|
||||
|
|
@ -30,18 +30,18 @@ pub fn subscription() -> Subscription<Message> {
|
|||
}) => 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<Message> {
|
|||
modifiers,
|
||||
..
|
||||
}) if c == "f" && modifiers.control() => {
|
||||
return Some(Message::Search);
|
||||
return Some(Action::Search);
|
||||
}
|
||||
|
||||
_ => (),
|
||||
|
|
|
|||
25
src/lib.rs
25
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)]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright 2025 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use std::os::raw::c_int;
|
||||
|
||||
const M_MMAP_THRESHOLD: c_int = -3;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// 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};
|
||||
|
|
|
|||
152
src/surface/action.rs
Normal file
152
src/surface/action.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
// Copyright 2025 System76 <info@system76.com>
|
||||
// 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<App: Application>(
|
||||
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<App::Message>>
|
||||
+ 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<dyn Any + Send + Sync + 'static> = Box::new(boxed);
|
||||
|
||||
Action::AppPopup(
|
||||
Arc::new(boxed),
|
||||
view.map(|view| {
|
||||
let boxed: Box<dyn Any + Send + Sync + 'static> = 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<Message: 'static, V>(
|
||||
settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
view: Option<
|
||||
Box<dyn Fn() -> crate::Element<'static, crate::Action<Message>> + 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<dyn Any + Send + Sync + 'static> = Box::new(boxed);
|
||||
|
||||
Action::Subsurface(
|
||||
Arc::new(boxed),
|
||||
view.map(|view| {
|
||||
let boxed: Box<dyn Any + Send + Sync + 'static> = 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<Message: 'static, V>(
|
||||
settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
view: Option<
|
||||
impl Fn() -> crate::Element<'static, crate::Action<Message>> + 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<dyn Any + Send + Sync + 'static> = Box::new(boxed);
|
||||
|
||||
Action::Popup(
|
||||
Arc::new(boxed),
|
||||
view.map(|view| {
|
||||
let boxed: Box<
|
||||
dyn Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static,
|
||||
> = Box::new(view);
|
||||
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
|
||||
Arc::new(boxed)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wayland", feature = "winit"))]
|
||||
#[must_use]
|
||||
pub fn subsurface<App: Application>(
|
||||
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<App::Message>>
|
||||
+ 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<dyn Any + Send + Sync + 'static> = Box::new(boxed);
|
||||
|
||||
Action::AppSubsurface(
|
||||
Arc::new(boxed),
|
||||
view.map(|view| {
|
||||
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(view);
|
||||
Arc::new(boxed)
|
||||
}),
|
||||
)
|
||||
}
|
||||
85
src/surface/mod.rs
Normal file
85
src/surface/mod.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright 2025 System76 <info@system76.com>
|
||||
// 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<Box<dyn std::any::Any + Send + Sync>>,
|
||||
Option<std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>>,
|
||||
),
|
||||
/// Create a subsurface with a view function
|
||||
Subsurface(
|
||||
std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>,
|
||||
Option<std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>>,
|
||||
),
|
||||
/// 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<Box<dyn std::any::Any + Send + Sync>>,
|
||||
Option<std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>>,
|
||||
),
|
||||
/// Create a popup
|
||||
Popup(
|
||||
std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>,
|
||||
Option<std::sync::Arc<Box<dyn std::any::Any + Send + Sync>>>,
|
||||
),
|
||||
/// 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<dyn Fn() -> Task<Action> + 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/task.rs
Normal file
25
src/task.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// 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<X: Send + 'static + Into<Y>, Y: Send + 'static>(
|
||||
tasks: impl IntoIterator<Item = iced::Task<X>>,
|
||||
) -> iced::Task<Y> {
|
||||
iced::Task::batch(tasks).map(Into::into)
|
||||
}
|
||||
|
||||
/// Yields a task which will run the future on the runtime executor.
|
||||
pub fn future<X: Into<Y>, Y: 'static>(
|
||||
future: impl Future<Output = X> + Send + 'static,
|
||||
) -> iced::Task<Y> {
|
||||
iced::Task::future(async move { future.await.into() })
|
||||
}
|
||||
|
||||
/// Yields a task which will return a message.
|
||||
pub fn message<X: Send + 'static + Into<Y>, Y: 'static>(message: X) -> iced::Task<Y> {
|
||||
future(async move { message.into() })
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// 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<X: Send + 'static + Into<Y>, Y: Send + 'static>(
|
||||
tasks: impl IntoIterator<Item = Task<X>>,
|
||||
) -> Task<Y> {
|
||||
Task::batch(tasks).map(Into::into)
|
||||
}
|
||||
|
||||
/// Yields a task which will run the future on the runtime executor.
|
||||
pub fn future<X: Into<Y>, Y: 'static>(future: impl Future<Output = X> + Send + 'static) -> Task<Y> {
|
||||
Task::future(async move { future.await.into() })
|
||||
}
|
||||
|
||||
/// Yields a task which will return a message.
|
||||
pub fn message<X: Send + 'static + Into<Y>, Y: 'static>(message: X) -> Task<Y> {
|
||||
future(async move { message.into() })
|
||||
}
|
||||
|
||||
/// Initiates a window drag.
|
||||
pub fn drag<M>(id: window::Id) -> Task<M> {
|
||||
iced_runtime::window::drag(id)
|
||||
}
|
||||
|
||||
/// Maximizes the window.
|
||||
pub fn maximize<M>(id: window::Id, maximized: bool) -> Task<M> {
|
||||
iced_runtime::window::maximize(id, maximized)
|
||||
}
|
||||
|
||||
/// Minimizes the window.
|
||||
pub fn minimize<M>(id: window::Id) -> Task<M> {
|
||||
iced_runtime::window::minimize(id, true)
|
||||
}
|
||||
|
||||
/// Sets the title of a window.
|
||||
#[allow(unused_variables, clippy::needless_pass_by_value)]
|
||||
pub fn set_title<M>(id: window::Id, title: String) -> Task<M> {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Sets the window mode to windowed.
|
||||
pub fn set_windowed<M>(id: window::Id) -> Task<M> {
|
||||
iced_runtime::window::change_mode(id, Mode::Windowed)
|
||||
}
|
||||
|
||||
/// Toggles the windows' maximize state.
|
||||
pub fn toggle_maximize<M>(id: window::Id) -> Task<M> {
|
||||
iced_runtime::window::toggle_maximize(id)
|
||||
}
|
||||
|
|
@ -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<Desktop> {
|
|||
.await;
|
||||
#[cfg(not(feature = "tokio"))]
|
||||
{
|
||||
pending::<()>().await;
|
||||
futures::future::pending::<()>().await;
|
||||
unreachable!();
|
||||
}
|
||||
attempts += 1;
|
||||
|
|
|
|||
|
|
@ -756,9 +756,7 @@ impl slider::Catalog for Theme {
|
|||
impl menu::Catalog for Theme {
|
||||
type Class<'a> = ();
|
||||
|
||||
fn default<'a>() -> <Self as menu::Catalog>::Class<'a> {
|
||||
()
|
||||
}
|
||||
fn default<'a>() -> <Self as menu::Catalog>::Class<'a> {}
|
||||
|
||||
fn style(&self, class: &<Self as menu::Catalog>::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>() -> <Self as pick_list::Catalog>::Class<'a> {
|
||||
()
|
||||
}
|
||||
fn default<'a>() -> <Self as pick_list::Catalog>::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>() -> <Self as pane_grid::Catalog>::Class<'a> {
|
||||
()
|
||||
}
|
||||
fn default<'a>() -> <Self as pane_grid::Catalog>::Class<'a> {}
|
||||
|
||||
fn style(&self, class: &<Self as pane_grid::Catalog>::Class<'_>) -> pane_grid::Style {
|
||||
let theme = self.cosmic();
|
||||
|
|
|
|||
|
|
@ -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<dyn StyleSheet<Style = Theme>>),
|
||||
Custom(Arc<dyn StyleSheet<Style = Theme>>),
|
||||
}
|
||||
|
||||
impl From<fn(&Theme) -> Appearance> for MenuBarStyle {
|
||||
fn from(f: fn(&Theme) -> Appearance) -> Self {
|
||||
Self::Custom(Box::new(f))
|
||||
Self::Custom(Arc::new(f))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
31
src/theme/style/tooltip.rs
Normal file
31
src/theme/style/tooltip.rs
Normal file
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<Message, Renderer> AspectRatio<'_, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
|
|
@ -146,8 +145,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, crate::Theme, Renderer>
|
||||
for AspectRatio<'a, Message, Renderer>
|
||||
impl<Message, Renderer> Widget<Message, crate::Theme, Renderer>
|
||||
for AspectRatio<'_, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Autosize<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Autosize<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ pub fn icon<'a, Message>(handle: impl Into<Handle>) -> Button<'a, Message> {
|
|||
})
|
||||
}
|
||||
|
||||
impl<'a, Message> Button<'a, Message> {
|
||||
impl<Message> Button<'_, Message> {
|
||||
pub fn new(icon: Icon) -> Self {
|
||||
let guard = crate::theme::THEME.lock().unwrap();
|
||||
let theme = guard.cosmic();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use super::{Builder, Style};
|
||||
use super::Builder;
|
||||
use crate::{
|
||||
widget::{self, image::Handle},
|
||||
Element,
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ pub struct Builder<'a, Message, Variant> {
|
|||
variant: Variant,
|
||||
}
|
||||
|
||||
impl<'a, Message, Variant> Builder<'a, Message, Variant> {
|
||||
impl<Message, Variant> Builder<'_, Message, Variant> {
|
||||
/// Set the value of [`on_press`] as either `Some` or `None`.
|
||||
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
|
||||
self.on_press = on_press;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// 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<icon::Handle>,
|
||||
}
|
||||
|
||||
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<Message> Button<'_, Message> {
|
||||
pub fn new(text: Text) -> Self {
|
||||
let guard = crate::theme::THEME.lock().unwrap();
|
||||
let theme = guard.cosmic();
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ pub struct Button<'a, Message> {
|
|||
selected: bool,
|
||||
style: crate::theme::Button,
|
||||
variant: Variant<Message>,
|
||||
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<Message, crate::Theme, crate::Renderer>
|
|||
|
||||
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::<State>();
|
||||
|
|
@ -583,12 +593,7 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
|
|||
}
|
||||
match self.description.as_ref() {
|
||||
Some(iced_accessibility::Description::Id(id)) => {
|
||||
node.set_described_by(
|
||||
id.iter()
|
||||
.cloned()
|
||||
.map(|id| NodeId::from(id))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
node.set_described_by(id.iter().cloned().map(NodeId::from).collect::<Vec<_>>());
|
||||
}
|
||||
Some(iced_accessibility::Description::Text(text)) => {
|
||||
node.set_description(text.clone());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<AtomicBool>,
|
||||
}
|
||||
|
||||
impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for ColorPicker<'a, Message>
|
||||
impl<Message> Widget<Message, crate::Theme, crate::Renderer> for ColorPicker<'_, Message>
|
||||
where
|
||||
Message: Clone + 'static,
|
||||
{
|
||||
|
|
@ -874,7 +874,7 @@ impl State {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> ColorPicker<'a, Message> where Message: Clone + 'static {}
|
||||
impl<Message> 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);
|
||||
|
|
|
|||
|
|
@ -17,8 +17,7 @@ pub(super) struct Overlay<'a, 'b, Message> {
|
|||
pub(super) width: f32,
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message> overlay::Overlay<Message, crate::Theme, crate::Renderer>
|
||||
for Overlay<'a, 'b, Message>
|
||||
impl<Message> overlay::Overlay<Message, crate::Theme, crate::Renderer> for Overlay<'_, '_, Message>
|
||||
where
|
||||
Message: Clone,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone> Widget<Message, crate::Theme, Renderer> for ContextDrawer<'a, Message> {
|
||||
impl<Message: Clone> Widget<Message, crate::Theme, Renderer> for ContextDrawer<'_, Message> {
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.content), Tree::new(&self.drawer)]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,9 +46,7 @@ pub struct ContextMenu<'a, Message> {
|
|||
context_menu: Option<Vec<menu::Tree<'a, Message>>>,
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone> Widget<Message, crate::Theme, crate::Renderer>
|
||||
for ContextMenu<'a, Message>
|
||||
{
|
||||
impl<Message: Clone> Widget<Message, crate::Theme, crate::Renderer> for ContextMenu<'_, Message> {
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<LocalState>()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ pub struct Dialog<'a, Message> {
|
|||
tertiary_action: Option<Element<'a, Message>>,
|
||||
}
|
||||
|
||||
impl<Message> Default for Dialog<'_, Message> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> Dialog<'a, Message> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
|
|
|||
|
|
@ -243,8 +243,8 @@ impl<'a, Message: 'static> DndDestination<'a, Message> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
|
||||
for DndDestination<'a, Message>
|
||||
impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
|
||||
for DndDestination<'_, Message>
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.container)]
|
||||
|
|
|
|||
|
|
@ -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<Message, crate::Theme, crate::Renderer> for DndSource<'a, Message, D>
|
||||
> Widget<Message, crate::Theme, crate::Renderer> for DndSource<'_, Message, D>
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.container)]
|
||||
|
|
|
|||
|
|
@ -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<str>,
|
||||
[S]: std::borrow::ToOwned,
|
||||
{
|
||||
state: &'a mut State,
|
||||
options: &'a [S],
|
||||
icons: &'a [icon::Handle],
|
||||
hovered_option: &'a mut Option<usize>,
|
||||
state: State,
|
||||
options: Cow<'a, [S]>,
|
||||
icons: Cow<'a, [icon::Handle]>,
|
||||
hovered_option: Arc<Mutex<Option<usize>>>,
|
||||
selected_option: Option<usize>,
|
||||
on_selected: Box<dyn FnMut(usize) -> Message + 'a>,
|
||||
close_on_selected: Option<Message>,
|
||||
on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
|
||||
width: f32,
|
||||
padding: Padding,
|
||||
|
|
@ -36,17 +42,21 @@ where
|
|||
style: (),
|
||||
}
|
||||
|
||||
impl<'a, S: AsRef<str>, Message: 'a> Menu<'a, S, Message> {
|
||||
impl<'a, S: AsRef<str>, 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<usize>,
|
||||
state: State,
|
||||
options: Cow<'a, [S]>,
|
||||
icons: Cow<'a, [icon::Handle]>,
|
||||
hovered_option: Arc<Mutex<Option<usize>>>,
|
||||
selected_option: Option<usize>,
|
||||
on_selected: impl FnMut(usize) -> Message + 'a,
|
||||
on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
|
||||
close_on_selected: Option<Message>,
|
||||
) -> Self {
|
||||
Menu {
|
||||
state,
|
||||
|
|
@ -61,6 +71,7 @@ impl<'a, S: AsRef<str>, 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<str>, 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<Tree>,
|
||||
}
|
||||
|
||||
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<Tree>,
|
||||
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<S: AsRef<str>>(
|
||||
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<Message, crate::Theme, crate::Renderer>
|
||||
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<Message, crate::Theme, crate::Renderer>
|
|||
)
|
||||
.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<Message, crate::Theme, crate::Renderer>
|
|||
) -> 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<Message, crate::Theme, crate::Renderer>
|
|||
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<str>, Message> {
|
||||
options: &'a [S],
|
||||
icons: &'a [icon::Handle],
|
||||
hovered_option: &'a mut Option<usize>,
|
||||
impl<'a, Message: Clone + 'a> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
|
||||
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<Message, crate::Theme, crate::Renderer>
|
||||
for Overlay<'a, Message>
|
||||
{
|
||||
fn size(&self) -> Size<Length> {
|
||||
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<Overlay<'a, Message>> for crate::Element<'a, Message> {
|
||||
fn from(widget: Overlay<'a, Message>) -> Self {
|
||||
Element::new(widget)
|
||||
}
|
||||
}
|
||||
|
||||
struct List<'a, S: AsRef<str>, Message>
|
||||
where
|
||||
[S]: std::borrow::ToOwned,
|
||||
{
|
||||
options: Cow<'a, [S]>,
|
||||
icons: Cow<'a, [icon::Handle]>,
|
||||
hovered_option: Arc<Mutex<Option<usize>>>,
|
||||
selected_option: Option<usize>,
|
||||
on_selected: Box<dyn FnMut(usize) -> Message + 'a>,
|
||||
close_on_selected: Option<Message>,
|
||||
on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
|
||||
padding: Padding,
|
||||
text_size: Option<f32>,
|
||||
text_line_height: text::LineHeight,
|
||||
}
|
||||
|
||||
impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
||||
for List<'a, S, Message>
|
||||
impl<S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer> for List<'_, S, Message>
|
||||
where
|
||||
[S]: std::borrow::ToOwned,
|
||||
Message: Clone,
|
||||
{
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size::new(Length::Fill, Length::Shrink)
|
||||
|
|
@ -330,9 +473,13 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
|||
) -> 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<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
|||
+ 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<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
|||
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<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
|||
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<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
|||
);
|
||||
|
||||
(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<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
|||
|
||||
impl<'a, S: AsRef<str>, Message: 'a> From<List<'a, S, Message>>
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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<str>, Message: 'a>(
|
||||
selections: &'a [S],
|
||||
pub fn dropdown<
|
||||
S: AsRef<str> + std::clone::Clone + Send + Sync + 'static,
|
||||
Message: 'static + Clone,
|
||||
>(
|
||||
selections: &[S],
|
||||
selected: Option<usize>,
|
||||
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<str> + std::clone::Clone + Send + Sync + 'static,
|
||||
Message: 'static + Clone,
|
||||
AppMessage: 'static + Clone,
|
||||
>(
|
||||
selections: &'a [S],
|
||||
selected: Option<usize>,
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,9 +180,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
|
||||
for Overlay<'a, Message>
|
||||
{
|
||||
impl<Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer> 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<Message, crate::Theme, crate::Renderer>
|
||||
for InnerList<'a, S, Item, Message>
|
||||
impl<S, Item, Message> Widget<Message, crate::Theme, crate::Renderer>
|
||||
for InnerList<'_, S, Item, Message>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
Item: Clone + PartialEq,
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ impl<'a, S: AsRef<str>, 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<str>, 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,
|
||||
|
|
|
|||
|
|
@ -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<Message> = Arc<dyn Fn() -> Element<'static, Message> + Send + Sync>;
|
||||
static AUTOSIZE_ID: LazyLock<crate::widget::Id> =
|
||||
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<str>, Message> {
|
||||
pub struct Dropdown<'a, S: AsRef<str> + Send + Sync + Clone + 'static, Message, AppMessage>
|
||||
where
|
||||
[S]: std::borrow::ToOwned,
|
||||
{
|
||||
#[setters(skip)]
|
||||
on_selected: Box<dyn Fn(usize) -> Message + 'a>,
|
||||
on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync>,
|
||||
#[setters(skip)]
|
||||
selections: &'a [S],
|
||||
#[setters]
|
||||
|
|
@ -38,9 +49,21 @@ pub struct Dropdown<'a, S: AsRef<str>, Message> {
|
|||
text_line_height: text::LineHeight,
|
||||
#[setters(strip_option)]
|
||||
font: Option<crate::font::Font>,
|
||||
#[setters(skip)]
|
||||
on_surface_action: Option<Arc<dyn Fn(surface::Action) -> Message + Send + Sync + 'static>>,
|
||||
#[setters(skip)]
|
||||
action_map: Option<Arc<dyn Fn(Message) -> AppMessage + 'static + Send + Sync>>,
|
||||
#[setters(strip_option)]
|
||||
window_id: Option<window::Id>,
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
|
||||
}
|
||||
|
||||
impl<'a, S: AsRef<str>, Message> Dropdown<'a, S, Message> {
|
||||
impl<'a, S: AsRef<str> + 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<str>, Message> Dropdown<'a, S, Message> {
|
|||
pub fn new(
|
||||
selections: &'a [S],
|
||||
selected: Option<usize>,
|
||||
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<str>, 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<NewAppMessage>(
|
||||
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<str>, Message: 'a> Widget<Message, crate::Theme, crate::Renderer>
|
||||
for Dropdown<'a, S, Message>
|
||||
impl<
|
||||
S: AsRef<str> + Send + Sync + Clone + 'static,
|
||||
Message: 'static + Clone,
|
||||
AppMessage: 'static + Clone,
|
||||
> Widget<Message, crate::Theme, crate::Renderer> for Dropdown<'_, S, Message, AppMessage>
|
||||
where
|
||||
[S]: std::borrow::ToOwned,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
|
|
@ -153,15 +237,26 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
|
|||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
update(
|
||||
update::<S, Message, AppMessage>(
|
||||
&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::<State>(),
|
||||
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<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
|
|||
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,
|
||||
theme,
|
||||
|
|
@ -211,6 +306,11 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
|
|||
renderer: &crate::Renderer,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
|
||||
#[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::<State>();
|
||||
|
||||
overlay(
|
||||
|
|
@ -225,8 +325,9 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
|
|||
self.selections,
|
||||
self.icons,
|
||||
self.selected,
|
||||
&self.on_selected,
|
||||
self.on_selected.as_ref(),
|
||||
translation,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -242,24 +343,31 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
|
|||
// }
|
||||
}
|
||||
|
||||
impl<'a, S: AsRef<str>, Message: 'a> From<Dropdown<'a, S, Message>>
|
||||
for crate::Element<'a, Message>
|
||||
impl<
|
||||
'a,
|
||||
S: AsRef<str> + Send + Sync + Clone + 'static,
|
||||
Message: 'static + std::clone::Clone,
|
||||
AppMessage: 'static + std::clone::Clone,
|
||||
> From<Dropdown<'a, S, Message, AppMessage>> 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<svg::Handle>,
|
||||
menu: menu::State,
|
||||
keyboard_modifiers: keyboard::Modifiers,
|
||||
is_open: bool,
|
||||
hovered_option: Option<usize>,
|
||||
is_open: Arc<AtomicBool>,
|
||||
hovered_option: Arc<Mutex<Option<usize>>>,
|
||||
hashes: Vec<u64>,
|
||||
selections: Vec<crate::Plain>,
|
||||
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<str>, Message>(
|
||||
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
|
||||
pub fn update<
|
||||
'a,
|
||||
S: AsRef<str> + 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<dyn Fn(usize) -> Message + Send + Sync + 'static>,
|
||||
selected: Option<usize>,
|
||||
selections: &[S],
|
||||
state: impl FnOnce() -> &'a mut State,
|
||||
_window_id: Option<window::Id>,
|
||||
on_surface_action: Option<Arc<dyn Fn(surface::Action) -> Message + Send + Sync + 'static>>,
|
||||
action_map: Option<Arc<dyn Fn(Message) -> AppMessage + Send + Sync + 'static>>,
|
||||
icons: &[icon::Handle],
|
||||
gap: f32,
|
||||
padding: Padding,
|
||||
text_size: Option<f32>,
|
||||
font: Option<crate::font::Font>,
|
||||
selected_option: Option<usize>,
|
||||
) -> 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<AppMessage>>
|
||||
+ 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<AppMessage>> =
|
||||
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<str>, 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<str> + 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<usize>,
|
||||
on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync + 'static>,
|
||||
close_on_selected: Option<Message>,
|
||||
) -> 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<str>, Message: 'a>(
|
||||
pub fn overlay<'a, S: AsRef<str> + 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<str>, Message: 'a>(
|
|||
selected_option: Option<usize>,
|
||||
on_selected: &'a dyn Fn(usize) -> Message,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'a, Message, crate::Theme, crate::Renderer>> {
|
||||
if state.is_open {
|
||||
close_on_selected: Option<Message>,
|
||||
) -> Option<overlay::Element<'a, Message, crate::Theme, crate::Renderer>>
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -85,9 +85,7 @@ impl<'a, Message> FlexRow<'a, Message> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static + Clone> Widget<Message, crate::Theme, Renderer>
|
||||
for FlexRow<'a, Message>
|
||||
{
|
||||
impl<Message: 'static + Clone> Widget<Message, crate::Theme, Renderer> for FlexRow<'_, Message> {
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.children.iter().map(Tree::new).collect()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,12 @@ pub struct Grid<'a, Message> {
|
|||
row: u16,
|
||||
}
|
||||
|
||||
impl<Message> 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<Message, crate::Theme, Renderer> for Grid<'a, Message> {
|
||||
impl<Message: 'static + Clone> Widget<Message, crate::Theme, Renderer> for Grid<'_, Message> {
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -120,8 +120,8 @@ pub struct HeaderBarWidget<'a, Message> {
|
|||
header_bar_inner: Element<'a, Message>,
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer>
|
||||
for HeaderBarWidget<'a, Message>
|
||||
impl<Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer>
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ impl From<Named> for Icon {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static> From<Named> for crate::Element<'a, Message> {
|
||||
impl<Message: 'static> From<Named> for crate::Element<'_, Message> {
|
||||
fn from(builder: Named) -> Self {
|
||||
builder.icon().into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for IdContainer<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for IdContainer<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -138,8 +138,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Theme, Renderer>
|
||||
for LayerContainer<'a, Message, Renderer>
|
||||
impl<Message, Renderer> Widget<Message, Theme, Renderer> for LayerContainer<'_, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ pub struct ListColumn<'a, Message> {
|
|||
children: Vec<Element<'a, Message>>,
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static> Default for ListColumn<'a, Message> {
|
||||
impl<Message: 'static> Default for ListColumn<'_, Message> {
|
||||
fn default() -> Self {
|
||||
let cosmic_theme::Spacing {
|
||||
space_xxs, space_m, ..
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
//!
|
||||
|
||||
pub mod action;
|
||||
|
||||
pub use action::MenuAction as Action;
|
||||
|
||||
mod flex;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ impl Default for MenuBarState {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn menu_roots_children<'a, Message, Renderer>(
|
||||
menu_roots: &Vec<MenuTree<'a, Message, Renderer>>,
|
||||
pub(crate) fn menu_roots_children<Message, Renderer>(
|
||||
menu_roots: &Vec<MenuTree<'_, Message, Renderer>>,
|
||||
) -> Vec<Tree>
|
||||
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<MenuTree<'a, Message, Renderer>>,
|
||||
pub(crate) fn menu_roots_diff<Message, Renderer>(
|
||||
menu_roots: &mut Vec<MenuTree<'_, Message, Renderer>>,
|
||||
tree: &mut Tree,
|
||||
) where
|
||||
Renderer: renderer::Renderer,
|
||||
|
|
@ -280,8 +280,7 @@ where
|
|||
self
|
||||
}
|
||||
}
|
||||
impl<'a, Message, Renderer> Widget<Message, crate::Theme, Renderer>
|
||||
for MenuBar<'a, Message, Renderer>
|
||||
impl<Message, Renderer> Widget<Message, crate::Theme, Renderer> 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<overlay::Element<'b, Message, crate::Theme, Renderer>> {
|
||||
// #[cfg(feature = "wayland")]
|
||||
// return None;
|
||||
|
||||
let state = tree.state.downcast_ref::<MenuBarState>();
|
||||
if !state.open {
|
||||
return None;
|
||||
|
|
|
|||
|
|
@ -447,7 +447,7 @@ where
|
|||
pub(crate) style: &'b <crate::Theme as StyleSheet>::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<Message, crate::Theme, Renderer>
|
||||
for Menu<'a, 'b, Message, Renderer>
|
||||
impl<Message, Renderer> overlay::Overlay<Message, crate::Theme, Renderer>
|
||||
for Menu<'_, '_, Message, Renderer>
|
||||
where
|
||||
Renderer: renderer::Renderer,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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: MenuAction, L: Into<Cow<'static, str>>> {
|
|||
/// - A button for the root menu item.
|
||||
pub fn menu_root<'a, Message, Renderer: renderer::Renderer>(
|
||||
label: impl Into<Cow<'a, str>> + 'a,
|
||||
) -> iced::Element<'a, Message, crate::Theme, Renderer>
|
||||
) -> Button<'a, Message>
|
||||
where
|
||||
Element<'a, Message, crate::Theme, Renderer>: From<widget::Button<'a, Message>>,
|
||||
{
|
||||
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`.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ pub fn nav_bar_toggle<Message>() -> NavBarToggle<Message> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static + Clone> From<NavBarToggle<Message>> for Element<'a, Message> {
|
||||
impl<Message: 'static + Clone> From<NavBarToggle<Message>> for Element<'_, Message> {
|
||||
fn from(nav_bar_toggle: NavBarToggle<Message>) -> Self {
|
||||
let icon = if nav_bar_toggle.active {
|
||||
widget::icon::from_svg_bytes(
|
||||
|
|
|
|||
|
|
@ -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<Message, crate::Theme, Renderer>
|
||||
for Popover<'a, Message, Renderer>
|
||||
impl<Message: Clone, Renderer> Widget<Message, crate::Theme, Renderer>
|
||||
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<Message, crate::Theme, Renderer>
|
||||
for Overlay<'a, 'b, Message, Renderer>
|
||||
impl<Message, Renderer> overlay::Overlay<Message, crate::Theme, Renderer>
|
||||
for Overlay<'_, '_, Message, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Renderer: iced_core::Renderer,
|
||||
|
|
@ -425,7 +425,7 @@ where
|
|||
) -> Option<overlay::Element<'c, Message, crate::Theme, Renderer>> {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.overlay(&mut self.tree, layout, renderer, Default::default())
|
||||
.overlay(self.tree, layout, renderer, Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Theme, Renderer> for Radio<'a, Message, Renderer>
|
||||
impl<Message, Renderer> Widget<Message, Theme, Renderer> for Radio<'_, Message, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Renderer: iced_core::Renderer,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
299
src/widget/responsive_container.rs
Normal file
299
src/widget/responsive_container.rs
Normal file
|
|
@ -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<Element<'a, Message, Theme, crate::Renderer>>,
|
||||
Theme: iced_widget::container::Catalog,
|
||||
<Theme as iced_widget::container::Catalog>::Class<'a>: From<crate::theme::Container<'a>>,
|
||||
{
|
||||
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<Size>,
|
||||
on_action: Box<dyn Fn(crate::surface::Action) -> Message>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> ResponsiveContainer<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
/// Creates an empty [`IdContainer`].
|
||||
pub(crate) fn new<T>(
|
||||
content: T,
|
||||
id: Id,
|
||||
on_action: impl Fn(crate::surface::Action) -> Message + 'static,
|
||||
) -> Self
|
||||
where
|
||||
T: Into<Element<'a, Message, Theme, Renderer>>,
|
||||
{
|
||||
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<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for ResponsiveContainer<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::new())
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
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<Length> {
|
||||
self.content.as_widget().size()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
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::<State>();
|
||||
|
||||
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<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
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<crate::widget::Id> {
|
||||
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<ResponsiveContainer<'a, Message, Theme, Renderer>>
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/widget/responsive_menu_bar.rs
Normal file
78
src/widget/responsive_menu_bar.rs
Normal file
|
|
@ -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<Message = Message>>(
|
||||
core: &Core,
|
||||
key_binds: &HashMap<menu::KeyBind, A>,
|
||||
id: crate::widget::Id,
|
||||
action_message: impl Fn(crate::surface::Action) -> Message + 'static,
|
||||
trees: Vec<(
|
||||
std::borrow::Cow<'static, str>,
|
||||
Vec<menu::Item<A, std::borrow::Cow<'static, str>>>,
|
||||
)>,
|
||||
) -> 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -30,8 +30,8 @@ where
|
|||
SegmentedButton::new(model)
|
||||
}
|
||||
|
||||
impl<'a, SelectionMode, Message> SegmentedVariant
|
||||
for SegmentedButton<'a, Horizontal, SelectionMode, Message>
|
||||
impl<SelectionMode, Message> SegmentedVariant
|
||||
for SegmentedButton<'_, Horizontal, SelectionMode, Message>
|
||||
where
|
||||
Model<SelectionMode>: Selectable,
|
||||
SelectionMode: Default,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ pub struct EntityMut<'a, SelectionMode: Default> {
|
|||
pub(super) model: &'a mut Model<SelectionMode>,
|
||||
}
|
||||
|
||||
impl<'a, SelectionMode: Default> EntityMut<'a, SelectionMode>
|
||||
impl<SelectionMode: Default> EntityMut<'_, SelectionMode>
|
||||
where
|
||||
Model<SelectionMode>: Selectable,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ where
|
|||
SegmentedButton::new(model)
|
||||
}
|
||||
|
||||
impl<'a, SelectionMode, Message> SegmentedVariant
|
||||
for SegmentedButton<'a, Vertical, SelectionMode, Message>
|
||||
impl<SelectionMode, Message> SegmentedVariant
|
||||
for SegmentedButton<'_, Vertical, SelectionMode, Message>
|
||||
where
|
||||
Model<SelectionMode>: Selectable,
|
||||
SelectionMode: Default,
|
||||
|
|
|
|||
|
|
@ -547,8 +547,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Variant, SelectionMode, Message> Widget<Message, crate::Theme, Renderer>
|
||||
for SegmentedButton<'a, Variant, SelectionMode, Message>
|
||||
impl<Variant, SelectionMode, Message> Widget<Message, crate::Theme, Renderer>
|
||||
for SegmentedButton<'_, Variant, SelectionMode, Message>
|
||||
where
|
||||
Self: SegmentedVariant,
|
||||
Model<SelectionMode>: 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)
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<Message: 'static>(children: ListColumn<'_, Message>) -> Section<'_, Message> {
|
||||
Section {
|
||||
title: Cow::Borrowed(""),
|
||||
children,
|
||||
|
|
|
|||
|
|
@ -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<T, Message>(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message>
|
||||
where
|
||||
Message: Clone + 'static,
|
||||
T: Copy + Sub<Output = T> + Add<Output = T> + 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<T, Message>(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message>
|
||||
where
|
||||
Message: Clone + 'static,
|
||||
T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
|
||||
|
|
|
|||
|
|
@ -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<Cow<'a, str>>,
|
||||
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
|
||||
on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
|
||||
on_submit: Option<Message>,
|
||||
on_submit: Option<Box<dyn Fn(String) -> Message + 'a>>,
|
||||
on_toggle_edit: Option<Box<dyn Fn(bool) -> Message + 'a>>,
|
||||
leading_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>,
|
||||
trailing_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>,
|
||||
|
|
@ -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<F>(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<Message>) -> Self {
|
||||
self.on_submit = message;
|
||||
pub fn on_submit_maybe<F>(mut self, callback: Option<F>) -> 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<Message, crate::Theme, crate::Renderer> for TextInput<'a, Message>
|
||||
impl<Message> Widget<Message, crate::Theme, crate::Renderer> 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::<State>();
|
||||
|
||||
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::<State>(),
|
||||
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<Id> {
|
||||
|
|
@ -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<Message>,
|
||||
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(),
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ impl<'a, Message, Theme, Renderer> Toaster<'a, Message, Theme, Renderer> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Toaster<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Toaster<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
|
|
@ -191,8 +191,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Theme, Renderer> Overlay<Message, Theme, Renderer>
|
||||
for ToasterOverlay<'a, 'b, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Overlay<Message, Theme, Renderer>
|
||||
for ToasterOverlay<'_, '_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: renderer::Renderer,
|
||||
{
|
||||
|
|
|
|||
1
src/widget/wayland/mod.rs
Normal file
1
src/widget/wayland/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod tooltip;
|
||||
76
src/widget/wayland/tooltip/mod.rs
Normal file
76
src/widget/wayland/tooltip/mod.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
//! Change the apperance of a tooltip.
|
||||
|
||||
pub mod widget;
|
||||
|
||||
// Copyright 2023 System76 <info@system76.com>
|
||||
// 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<Background>,
|
||||
|
||||
/// 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<Color>,
|
||||
|
||||
/// 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;
|
||||
}
|
||||
684
src/widget/wayland/tooltip/widget.rs
Normal file
684
src/widget/wayland/tooltip/widget.rs
Normal file
|
|
@ -0,0 +1,684 @@
|
|||
// Copyright 2019 H<>ctor Ram<61>n, Iced contributors
|
||||
// Copyright 2023 System76 <info@system76.com>
|
||||
// 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<Message> {
|
||||
Normal,
|
||||
Image {
|
||||
close_icon: svg::Handle,
|
||||
on_remove: Option<Message>,
|
||||
},
|
||||
}
|
||||
|
||||
/// 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<std::borrow::Cow<'a, str>>,
|
||||
#[cfg(feature = "a11y")]
|
||||
description: Option<iced_accessibility::Description<'a>>,
|
||||
#[cfg(feature = "a11y")]
|
||||
label: Option<Vec<iced_accessibility::accesskit::NodeId>>,
|
||||
content: crate::Element<'a, Message>,
|
||||
on_leave: Message,
|
||||
on_surface_action: Box<dyn Fn(crate::surface::Action) -> Message>,
|
||||
width: Length,
|
||||
height: Length,
|
||||
padding: Padding,
|
||||
selected: bool,
|
||||
style: crate::theme::Tooltip,
|
||||
delay: Option<Duration>,
|
||||
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<TopLevelMessage>> + 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<crate::Element<'a, Message>>,
|
||||
settings: Option<
|
||||
impl Fn(Rectangle) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
>,
|
||||
view: impl Fn() -> crate::Element<'static, crate::Action<TopLevelMessage>>
|
||||
+ 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<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Tooltip`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`Tooltip`].
|
||||
pub fn padding<P: Into<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<std::borrow::Cow<'a, str>>) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "a11y")]
|
||||
/// Sets the description of the [`Tooltip`].
|
||||
pub fn description_widget<T: iced_accessibility::Describes>(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<std::borrow::Cow<'a, str>>) -> 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<Message, crate::Theme, crate::Renderer> for Tooltip<'a, Message, TopLevelMessage>
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::default())
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
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<Length> {
|
||||
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::<State>(),
|
||||
);
|
||||
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::<State>();
|
||||
|
||||
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<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
|
||||
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<Id> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn set_id(&mut self, id: Id) {
|
||||
self.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>
|
||||
From<Tooltip<'a, Message, TopLevelMessage>> 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<Mutex<bool>>,
|
||||
}
|
||||
|
||||
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<TopLevelMessage>> + Send + Sync + 'static,
|
||||
>,
|
||||
delay: Option<Duration>,
|
||||
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<dyn Any + Send + Sync + 'static> =
|
||||
Box::new(boxed);
|
||||
crate::surface::Action::Popup(
|
||||
Arc::new(boxed),
|
||||
Some({
|
||||
let boxed: Box<
|
||||
dyn Fn() -> crate::Element<
|
||||
'static,
|
||||
crate::Action<TopLevelMessage>,
|
||||
> + Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
> = Box::new(move || view());
|
||||
let boxed: Box<dyn Any + Send + Sync + 'static> =
|
||||
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<dyn Any + Send + Sync + 'static> = Box::new(boxed);
|
||||
|
||||
let sm = crate::surface::Action::Popup(
|
||||
Arc::new(boxed),
|
||||
Some({
|
||||
let boxed: Box<
|
||||
dyn Fn() -> crate::Element<
|
||||
'static,
|
||||
crate::Action<TopLevelMessage>,
|
||||
> + Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
> = Box::new(move || view());
|
||||
let boxed: Box<dyn Any + Send + Sync + 'static> =
|
||||
Box::new(boxed);
|
||||
Arc::new(boxed)
|
||||
}),
|
||||
);
|
||||
shell.publish((on_surface_action)(sm));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn draw<Renderer: iced_core::Renderer, Theme>(
|
||||
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: &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])
|
||||
}
|
||||
220
src/widget/wrapper.rs
Normal file
220
src/widget/wrapper.rs
Normal file
|
|
@ -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<T> {
|
||||
pub(crate) data: Rc<RefCell<T>>,
|
||||
pub(crate) thread_id: ThreadId,
|
||||
}
|
||||
|
||||
impl<T> Clone for RcWrapper<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
data: self.data.clone(),
|
||||
thread_id: self.thread_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<M: 'static> Send for RcWrapper<M> {}
|
||||
unsafe impl<M: 'static> Sync for RcWrapper<M> {}
|
||||
|
||||
impl<T> RcWrapper<T> {
|
||||
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<O>(&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<O>(&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<M> {
|
||||
pub(crate) element: RcWrapper<Element<'static, M>>,
|
||||
}
|
||||
|
||||
impl<M> RcElementWrapper<M> {
|
||||
#[must_use]
|
||||
pub fn new(element: Element<'static, M>) -> Self {
|
||||
RcElementWrapper {
|
||||
element: RcWrapper::new(element),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Widget<M, crate::Theme, crate::Renderer> for RcElementWrapper<M> {
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.element.with_data(|e| e.as_widget().size())
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> Size<Length> {
|
||||
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<tree::Tree> {
|
||||
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<crate::iced_core::overlay::Element<'a, M, crate::Theme, crate::Renderer>> {
|
||||
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<Id> {
|
||||
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<Message: 'static> From<RcElementWrapper<Message>> for Element<'static, Message> {
|
||||
fn from(wrapper: RcElementWrapper<Message>) -> Self {
|
||||
Element::new(wrapper)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message: 'static> From<Element<'static, Message>> for RcElementWrapper<Message> {
|
||||
fn from(e: Element<'static, Message>) -> Self {
|
||||
RcElementWrapper::new(e)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue