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 window support
|
||||||
wayland = [
|
wayland = [
|
||||||
"ashpd?/wayland",
|
"ashpd?/wayland",
|
||||||
|
"autosize",
|
||||||
"iced_runtime/wayland",
|
"iced_runtime/wayland",
|
||||||
"iced/wayland",
|
"iced/wayland",
|
||||||
"iced_winit/wayland",
|
"iced_winit/wayland",
|
||||||
"cctk",
|
"cctk",
|
||||||
|
"surface-message",
|
||||||
]
|
]
|
||||||
|
surface-message = []
|
||||||
# multi-window support
|
# multi-window support
|
||||||
multi-window = ["iced/multi-window"]
|
multi-window = ["iced/multi-window"]
|
||||||
# Render with wgpu
|
# Render with wgpu
|
||||||
|
|
@ -84,11 +87,19 @@ winit_wgpu = ["winit", "wgpu"]
|
||||||
xdg-portal = ["ashpd"]
|
xdg-portal = ["ashpd"]
|
||||||
qr_code = ["iced/qr_code"]
|
qr_code = ["iced/qr_code"]
|
||||||
markdown = ["iced/markdown"]
|
markdown = ["iced/markdown"]
|
||||||
|
async-std = [
|
||||||
|
"dep:async-std",
|
||||||
|
"ashpd/async-std",
|
||||||
|
"rfd?/async-std",
|
||||||
|
"zbus?/async-io",
|
||||||
|
"iced/async-std",
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
apply = "0.3.0"
|
apply = "0.3.0"
|
||||||
ashpd = { version = "0.9.1", default-features = false, optional = true }
|
ashpd = { version = "0.9.1", default-features = false, optional = true }
|
||||||
async-fs = { version = "2.1", 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 }
|
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "178eb0b", optional = true }
|
||||||
chrono = "0.4.35"
|
chrono = "0.4.35"
|
||||||
cosmic-config = { path = "cosmic-config" }
|
cosmic-config = { path = "cosmic-config" }
|
||||||
|
|
@ -104,7 +115,9 @@ libc = { version = "0.2.155", optional = true }
|
||||||
license = { version = "3.5.1", optional = true }
|
license = { version = "3.5.1", optional = true }
|
||||||
mime = { version = "0.3.17", optional = true }
|
mime = { version = "0.3.17", optional = true }
|
||||||
palette = "0.7.3"
|
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 = [
|
rustix = { version = "0.38.34", features = [
|
||||||
"pipe",
|
"pipe",
|
||||||
"process",
|
"process",
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,38 @@
|
||||||
use cosmic::app::Core;
|
use cosmic::app::{Core, Task};
|
||||||
use cosmic::iced::application;
|
|
||||||
use cosmic::iced::platform_specific::shell::commands::popup::{destroy_popup, get_popup};
|
|
||||||
use cosmic::iced::window::Id;
|
use cosmic::iced::window::Id;
|
||||||
use cosmic::iced::{Length, Limits, Task};
|
use cosmic::iced::Length;
|
||||||
use cosmic::iced_runtime::core::window;
|
use cosmic::iced_runtime::core::window;
|
||||||
use cosmic::theme::iced;
|
use cosmic::surface::action::{app_popup, destroy_popup};
|
||||||
use cosmic::widget::{list_column, settings, toggler};
|
use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler};
|
||||||
use cosmic::{Element, Theme};
|
use cosmic::Element;
|
||||||
|
|
||||||
const ID: &str = "com.system76.CosmicAppletExample";
|
const ID: &str = "com.system76.CosmicAppletExample";
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Window {
|
pub struct Window {
|
||||||
core: Core,
|
core: Core,
|
||||||
popup: Option<Id>,
|
popup: Option<Id>,
|
||||||
example_row: bool,
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
TogglePopup,
|
|
||||||
PopupClosed(Id),
|
PopupClosed(Id),
|
||||||
ToggleExampleRow(bool),
|
ToggleExampleRow(bool),
|
||||||
|
Selected(usize),
|
||||||
|
Surface(cosmic::surface::Action),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl cosmic::Application for Window {
|
impl cosmic::Application for Window {
|
||||||
|
|
@ -38,7 +49,7 @@ impl cosmic::Application for Window {
|
||||||
&mut self.core
|
&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 {
|
let window = Window {
|
||||||
core,
|
core,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
@ -50,60 +61,85 @@ impl cosmic::Application for Window {
|
||||||
Some(Message::PopupClosed(id))
|
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 {
|
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) => {
|
Message::PopupClosed(id) => {
|
||||||
if self.popup.as_ref() == Some(&id) {
|
if self.popup.as_ref() == Some(&id) {
|
||||||
self.popup = None;
|
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()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self) -> Element<Self::Message> {
|
fn view(&self) -> Element<Message> {
|
||||||
self.core
|
let btn = self.core.applet.icon_button("display-symbolic").on_press(
|
||||||
.applet
|
if let Some(id) = self.popup {
|
||||||
.icon_button("display-symbolic")
|
Message::Surface(destroy_popup(id))
|
||||||
.on_press(Message::TogglePopup)
|
} else {
|
||||||
.into()
|
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> {
|
fn view_window(&self, _id: Id) -> Element<Message> {
|
||||||
let content_list = list_column().padding(5).spacing(0).add(settings::item(
|
"oops".into()
|
||||||
"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 style(&self) -> Option<cosmic::iced_runtime::Appearance> {
|
fn style(&self) -> Option<cosmic::iced_runtime::Appearance> {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ name = "application"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["wayland"]
|
||||||
|
wayland = ["libcosmic/wayland"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tracing-subscriber = "0.3.17"
|
tracing-subscriber = "0.3.17"
|
||||||
|
|
@ -18,7 +22,6 @@ features = [
|
||||||
"xdg-portal",
|
"xdg-portal",
|
||||||
"dbus-config",
|
"dbus-config",
|
||||||
"a11y",
|
"a11y",
|
||||||
"wayland",
|
|
||||||
"wgpu",
|
"wgpu",
|
||||||
"single-instance",
|
"single-instance",
|
||||||
"multi-window",
|
"multi-window",
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,27 @@
|
||||||
|
|
||||||
//! Application API example
|
//! Application API example
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use cosmic::app::{Core, Settings, Task};
|
use cosmic::app::{Core, Settings, Task};
|
||||||
|
use cosmic::iced::alignment::{Horizontal, Vertical};
|
||||||
use cosmic::iced::widget::column;
|
use cosmic::iced::widget::column;
|
||||||
|
use cosmic::iced::Length;
|
||||||
use cosmic::iced_core::Size;
|
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};
|
use cosmic::{executor, iced, ApplicationExt, Element};
|
||||||
|
|
||||||
|
static MENU_ID: LazyLock<iced::id::Id> = LazyLock::new(|| iced::id::Id::new("menu_id"));
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum Page {
|
pub enum Page {
|
||||||
Page1,
|
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
|
/// Runs application with these settings
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
tracing_subscriber::fmt::init();
|
// tracing_subscriber::fmt::init();
|
||||||
let _ = tracing_log::LogTracer::init();
|
// let _ = tracing_log::LogTracer::init();
|
||||||
|
|
||||||
let input = vec![
|
let input = vec![
|
||||||
(Page::Page1, "🖖 Hello from libcosmic.".into()),
|
(Page::Page1, "🖖 Hello from libcosmic.".into()),
|
||||||
|
|
@ -56,6 +84,8 @@ pub enum Message {
|
||||||
Input2(String),
|
Input2(String),
|
||||||
Ignore,
|
Ignore,
|
||||||
ToggleHide,
|
ToggleHide,
|
||||||
|
Surface(cosmic::surface::Action),
|
||||||
|
Hi,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The [`App`] stores application-specific state.
|
/// The [`App`] stores application-specific state.
|
||||||
|
|
@ -65,6 +95,7 @@ pub struct App {
|
||||||
input_1: String,
|
input_1: String,
|
||||||
input_2: String,
|
input_2: String,
|
||||||
hidden: bool,
|
hidden: bool,
|
||||||
|
keybinds: HashMap<KeyBind, Action>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implement [`cosmic::Application`] to integrate with COSMIC.
|
/// Implement [`cosmic::Application`] to integrate with COSMIC.
|
||||||
|
|
@ -105,6 +136,7 @@ impl cosmic::Application for App {
|
||||||
input_1: String::new(),
|
input_1: String::new(),
|
||||||
input_2: String::new(),
|
input_2: String::new(),
|
||||||
hidden: true,
|
hidden: true,
|
||||||
|
keybinds: HashMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let command = app.update_title();
|
let command = app.update_title();
|
||||||
|
|
@ -136,6 +168,12 @@ impl cosmic::Application for App {
|
||||||
Message::ToggleHide => {
|
Message::ToggleHide => {
|
||||||
self.hidden = !self.hidden;
|
self.hidden = !self.hidden;
|
||||||
}
|
}
|
||||||
|
Message::Surface(_) => {
|
||||||
|
// unimplemented!()
|
||||||
|
}
|
||||||
|
Message::Hi => {
|
||||||
|
dbg!("hi");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -178,6 +216,122 @@ impl cosmic::Application for App {
|
||||||
|
|
||||||
Element::from(centered)
|
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
|
impl App
|
||||||
|
|
|
||||||
|
|
@ -74,10 +74,7 @@ impl cosmic::Application for MultiWindow {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(
|
fn update(&mut self, message: Self::Message) -> iced::Task<cosmic::Action<Self::Message>> {
|
||||||
&mut self,
|
|
||||||
message: Self::Message,
|
|
||||||
) -> iced::Task<cosmic::app::Message<Self::Message>> {
|
|
||||||
match message {
|
match message {
|
||||||
Message::CloseWindow(id) => window::close(id),
|
Message::CloseWindow(id) => window::close(id),
|
||||||
Message::WindowClosed(id) => {
|
Message::WindowClosed(id) => {
|
||||||
|
|
@ -110,7 +107,7 @@ impl cosmic::Application for MultiWindow {
|
||||||
);
|
);
|
||||||
_ = self.set_window_title(format!("window_{}", count), id);
|
_ = 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) => {
|
Message::Input(id, value) => {
|
||||||
if let Some((_, w)) = self.windows.iter_mut().find(|e| e.1.input_id == id) {
|
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 {
|
impl menu::Action for NavMenuAction {
|
||||||
type Message = cosmic::app::Message<Message>;
|
type Message = cosmic::Action<Message>;
|
||||||
|
|
||||||
fn message(&self) -> Self::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(
|
fn nav_context_menu(
|
||||||
&self,
|
&self,
|
||||||
id: nav_bar::Id,
|
id: nav_bar::Id,
|
||||||
) -> Option<Vec<menu::Tree<cosmic::app::Message<Self::Message>>>> {
|
) -> Option<Vec<menu::Tree<cosmic::Action<Self::Message>>>> {
|
||||||
Some(menu::items(
|
Some(menu::items(
|
||||||
&HashMap::new(),
|
&HashMap::new(),
|
||||||
vec![
|
vec![
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ pub enum Message {
|
||||||
OpenError(Arc<file_chooser::Error>),
|
OpenError(Arc<file_chooser::Error>),
|
||||||
OpenFile,
|
OpenFile,
|
||||||
Selected(Url),
|
Selected(Url),
|
||||||
|
Surface(cosmic::surface::Action),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The [`App`] stores application-specific state.
|
/// The [`App`] stores application-specific state.
|
||||||
|
|
@ -91,13 +92,11 @@ impl cosmic::Application for App {
|
||||||
Message::Cancelled => {
|
Message::Cancelled => {
|
||||||
eprintln!("open file dialog cancelled");
|
eprintln!("open file dialog cancelled");
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::FileRead(url, contents) => {
|
Message::FileRead(url, contents) => {
|
||||||
eprintln!("read file");
|
eprintln!("read file");
|
||||||
self.selected_file = Some(url);
|
self.selected_file = Some(url);
|
||||||
self.file_contents = contents;
|
self.file_contents = contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::Selected(url) => {
|
Message::Selected(url) => {
|
||||||
eprintln!("selected file");
|
eprintln!("selected file");
|
||||||
|
|
||||||
|
|
@ -142,8 +141,6 @@ impl cosmic::Application for App {
|
||||||
Message::FileRead(url, contents)
|
Message::FileRead(url, contents)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new open dialog.
|
|
||||||
Message::OpenFile => {
|
Message::OpenFile => {
|
||||||
return cosmic::task::future(async move {
|
return cosmic::task::future(async move {
|
||||||
eprintln!("opening new dialog");
|
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) => {
|
Message::Error(why) => {
|
||||||
self.error_status = Some(why);
|
self.error_status = Some(why);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Displays an error in the application's warning bar.
|
|
||||||
Message::OpenError(why) => {
|
Message::OpenError(why) => {
|
||||||
if let Some(why) = Arc::into_inner(why) {
|
if let Some(why) = Arc::into_inner(why) {
|
||||||
let mut source: &dyn std::error::Error = &why;
|
let mut source: &dyn std::error::Error = &why;
|
||||||
|
|
@ -190,10 +183,10 @@ impl cosmic::Application for App {
|
||||||
self.error_status = Some(string);
|
self.error_status = Some(string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::CloseError => {
|
Message::CloseError => {
|
||||||
self.error_status = None;
|
self.error_status = None;
|
||||||
}
|
}
|
||||||
|
Message::Surface(surface) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Task::none()
|
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 std::borrow::Cow;
|
||||||
|
|
||||||
use crate::Element;
|
use crate::Element;
|
||||||
|
|
@ -12,11 +15,11 @@ pub struct ContextDrawer<'a, Message: Clone + 'static> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "about")]
|
#[cfg(feature = "about")]
|
||||||
pub fn about<'a, Message: Clone + 'static>(
|
pub fn about<Message: Clone + 'static>(
|
||||||
about: &'a crate::widget::about::About,
|
about: &crate::widget::about::About,
|
||||||
on_url_press: impl Fn(String) -> Message,
|
on_url_press: impl Fn(String) -> Message,
|
||||||
on_close: Message,
|
on_close: Message,
|
||||||
) -> ContextDrawer<'a, Message> {
|
) -> ContextDrawer<'_, Message> {
|
||||||
context_drawer(crate::widget::about(about, on_url_press), on_close)
|
context_drawer(crate::widget::about(about, on_url_press), on_close)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,12 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use std::borrow::Borrow;
|
use std::borrow::Borrow;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::{Application, ApplicationExt, Core, Subscription};
|
use super::{Action, Application, ApplicationExt, Subscription};
|
||||||
use crate::config::CosmicTk;
|
|
||||||
use crate::theme::{Theme, ThemeType, THEME};
|
use crate::theme::{Theme, ThemeType, THEME};
|
||||||
use crate::widget::nav_bar;
|
use crate::{keyboard_nav, Core, Element};
|
||||||
use crate::{keyboard_nav, Element};
|
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "wayland")]
|
||||||
use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState};
|
use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState};
|
||||||
use cosmic_theme::ThemeMode;
|
use cosmic_theme::ThemeMode;
|
||||||
|
|
@ -20,69 +19,14 @@ use iced::{window, Task};
|
||||||
use iced_futures::event::listen_with;
|
use iced_futures::event::listen_with;
|
||||||
use palette::color_difference::EuclideanDistance;
|
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)]
|
#[derive(Default)]
|
||||||
pub struct Cosmic<App> {
|
pub struct Cosmic<App: Application> {
|
||||||
pub app: App,
|
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>
|
impl<T: Application> Cosmic<T>
|
||||||
|
|
@ -91,7 +35,7 @@ where
|
||||||
{
|
{
|
||||||
pub fn init(
|
pub fn init(
|
||||||
(mut core, flags): (Core, T::Flags),
|
(mut core, flags): (Core, T::Flags),
|
||||||
) -> (Self, iced::Task<super::Message<T::Message>>) {
|
) -> (Self, iced::Task<crate::Action<T::Message>>) {
|
||||||
#[cfg(feature = "dbus-config")]
|
#[cfg(feature = "dbus-config")]
|
||||||
{
|
{
|
||||||
use iced_futures::futures::executor::block_on;
|
use iced_futures::futures::executor::block_on;
|
||||||
|
|
@ -113,16 +57,157 @@ where
|
||||||
self.app.title(id).to_string()
|
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(
|
pub fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
message: super::Message<T::Message>,
|
message: crate::Action<T::Message>,
|
||||||
) -> iced::Task<super::Message<T::Message>> {
|
) -> iced::Task<crate::Action<T::Message>> {
|
||||||
let message = match message {
|
let message = match message {
|
||||||
super::Message::App(message) => self.app.update(message),
|
crate::Action::App(message) => self.app.update(message),
|
||||||
super::Message::Cosmic(message) => self.cosmic_update(message),
|
crate::Action::Cosmic(message) => self.cosmic_update(message),
|
||||||
super::Message::None => iced::Task::none(),
|
crate::Action::None => iced::Task::none(),
|
||||||
#[cfg(feature = "single-instance")]
|
#[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")]
|
#[cfg(target_env = "gnu")]
|
||||||
|
|
@ -158,29 +243,29 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[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| {
|
let window_events = listen_with(|event, _, id| {
|
||||||
match event {
|
match event {
|
||||||
iced::Event::Window(window::Event::Resized(iced::Size { width, height })) => {
|
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) => {
|
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::Focused) => return Some(Action::Focus(id)),
|
||||||
iced::Event::Window(window::Event::Unfocused) => return Some(Message::Unfocus(id)),
|
iced::Event::Window(window::Event::Unfocused) => return Some(Action::Unfocus(id)),
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "wayland")]
|
||||||
iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => {
|
iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => {
|
||||||
match event {
|
match event {
|
||||||
wayland::Event::Popup(wayland::PopupEvent::Done, _, id)
|
wayland::Event::Popup(wayland::PopupEvent::Done, _, id)
|
||||||
| wayland::Event::Layer(wayland::LayerEvent::Done, _, id) => {
|
| wayland::Event::Layer(wayland::LayerEvent::Done, _, id) => {
|
||||||
return Some(Message::SurfaceClosed(id));
|
return Some(Action::SurfaceClosed(id));
|
||||||
}
|
}
|
||||||
#[cfg(feature = "applet")]
|
#[cfg(feature = "applet")]
|
||||||
wayland::Event::Window(
|
wayland::Event::Window(
|
||||||
iced::event::wayland::WindowEvent::SuggestedBounds(b),
|
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![
|
let mut subscriptions = vec![
|
||||||
self.app.subscription().map(super::Message::App),
|
self.app.subscription().map(crate::Action::App),
|
||||||
self.app
|
self.app
|
||||||
.core()
|
.core()
|
||||||
.watch_config::<crate::config::CosmicTk>(crate::config::ID)
|
.watch_config::<crate::config::CosmicTk>(crate::config::ID)
|
||||||
|
|
@ -205,7 +290,7 @@ where
|
||||||
tracing::error!(?why, "cosmic toolkit config update error");
|
tracing::error!(?why, "cosmic toolkit config update error");
|
||||||
}
|
}
|
||||||
|
|
||||||
super::Message::Cosmic(Message::ToolkitConfig(update.config))
|
crate::Action::Cosmic(Action::ToolkitConfig(update.config))
|
||||||
}),
|
}),
|
||||||
self.app
|
self.app
|
||||||
.core()
|
.core()
|
||||||
|
|
@ -232,12 +317,12 @@ where
|
||||||
{
|
{
|
||||||
tracing::error!(?why, "cosmic theme config update error");
|
tracing::error!(?why, "cosmic theme config update error");
|
||||||
}
|
}
|
||||||
Message::SystemThemeChange(
|
Action::SystemThemeChange(
|
||||||
update.keys,
|
update.keys,
|
||||||
crate::theme::Theme::system(Arc::new(update.config)),
|
crate::theme::Theme::system(Arc::new(update.config)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.map(super::Message::Cosmic),
|
.map(crate::Action::Cosmic),
|
||||||
self.app
|
self.app
|
||||||
.core()
|
.core()
|
||||||
.watch_config::<ThemeMode>(cosmic_theme::THEME_MODE_ID)
|
.watch_config::<ThemeMode>(cosmic_theme::THEME_MODE_ID)
|
||||||
|
|
@ -249,27 +334,27 @@ where
|
||||||
{
|
{
|
||||||
tracing::error!(?error, "error reading system theme mode update");
|
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),
|
.map(crate::Action::Cosmic),
|
||||||
window_events.map(super::Message::Cosmic),
|
window_events.map(crate::Action::Cosmic),
|
||||||
#[cfg(feature = "xdg-portal")]
|
#[cfg(feature = "xdg-portal")]
|
||||||
crate::theme::portal::desktop_settings()
|
crate::theme::portal::desktop_settings()
|
||||||
.map(Message::DesktopSettings)
|
.map(Action::DesktopSettings)
|
||||||
.map(super::Message::Cosmic),
|
.map(crate::Action::Cosmic),
|
||||||
];
|
];
|
||||||
|
|
||||||
if self.app.core().keyboard_nav {
|
if self.app.core().keyboard_nav {
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
keyboard_nav::subscription()
|
keyboard_nav::subscription()
|
||||||
.map(Message::KeyboardNav)
|
.map(Action::KeyboardNav)
|
||||||
.map(super::Message::Cosmic),
|
.map(crate::Action::Cosmic),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "single-instance")]
|
#[cfg(feature = "single-instance")]
|
||||||
if self.app.core().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)
|
Subscription::batch(subscriptions)
|
||||||
|
|
@ -286,20 +371,24 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "multi-window")]
|
#[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
|
if !self
|
||||||
.app
|
.app
|
||||||
.core()
|
.core()
|
||||||
.main_window_id()
|
.main_window_id()
|
||||||
.is_some_and(|main_id| main_id == 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 {
|
let view = if self.app.core().window.use_template {
|
||||||
self.app.view_main()
|
self.app.view_main()
|
||||||
} else {
|
} else {
|
||||||
self.app.view().map(super::Message::App)
|
self.app.view().map(crate::Action::App)
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(target_env = "gnu")]
|
#[cfg(target_env = "gnu")]
|
||||||
|
|
@ -309,7 +398,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "multi-window"))]
|
#[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();
|
let view = self.app.view_main();
|
||||||
|
|
||||||
#[cfg(target_env = "gnu")]
|
#[cfg(target_env = "gnu")]
|
||||||
|
|
@ -321,7 +410,7 @@ where
|
||||||
|
|
||||||
impl<T: Application> Cosmic<T> {
|
impl<T: Application> Cosmic<T> {
|
||||||
#[allow(clippy::unused_self)]
|
#[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() {
|
if let Some(id) = self.app.core().main_window_id() {
|
||||||
iced::window::close(id)
|
iced::window::close(id)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -330,9 +419,9 @@ impl<T: Application> Cosmic<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[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 {
|
match message {
|
||||||
Message::WindowMaximized(id, maximized) => {
|
Action::WindowMaximized(id, maximized) => {
|
||||||
if self
|
if self
|
||||||
.app
|
.app
|
||||||
.core()
|
.core()
|
||||||
|
|
@ -343,7 +432,7 @@ impl<T: Application> Cosmic<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::WindowResize(id, width, height) => {
|
Action::WindowResize(id, width, height) => {
|
||||||
if self
|
if self
|
||||||
.app
|
.app
|
||||||
.core()
|
.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)
|
//TODO: more efficient test of maximized (winit has no event for maximize if set by the OS)
|
||||||
return iced::window::get_maximized(id).map(move |maximized| {
|
return iced::window::get_maximized(id).map(move |maximized| {
|
||||||
super::Message::Cosmic(Message::WindowMaximized(id, maximized))
|
crate::Action::Cosmic(Action::WindowMaximized(id, maximized))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "wayland")]
|
||||||
Message::WindowState(id, state) => {
|
Action::WindowState(id, state) => {
|
||||||
if self
|
if self
|
||||||
.app
|
.app
|
||||||
.core()
|
.core()
|
||||||
|
|
@ -383,7 +472,7 @@ impl<T: Application> Cosmic<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "wayland")]
|
||||||
Message::WmCapabilities(id, capabilities) => {
|
Action::WmCapabilities(id, capabilities) => {
|
||||||
if self
|
if self
|
||||||
.app
|
.app
|
||||||
.core()
|
.core()
|
||||||
|
|
@ -399,49 +488,49 @@ impl<T: Application> Cosmic<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::KeyboardNav(message) => match message {
|
Action::KeyboardNav(message) => match message {
|
||||||
keyboard_nav::Message::FocusNext => {
|
keyboard_nav::Action::FocusNext => {
|
||||||
return iced::widget::focus_next().map(super::Message::Cosmic)
|
return iced::widget::focus_next().map(crate::Action::Cosmic)
|
||||||
}
|
}
|
||||||
keyboard_nav::Message::FocusPrevious => {
|
keyboard_nav::Action::FocusPrevious => {
|
||||||
return iced::widget::focus_previous().map(super::Message::Cosmic)
|
return iced::widget::focus_previous().map(crate::Action::Cosmic)
|
||||||
}
|
}
|
||||||
keyboard_nav::Message::Escape => return self.app.on_escape(),
|
keyboard_nav::Action::Escape => return self.app.on_escape(),
|
||||||
keyboard_nav::Message::Search => return self.app.on_search(),
|
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);
|
self.app.core_mut().set_show_context(show);
|
||||||
return self.app.on_context_drawer();
|
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);
|
self.app.core_mut().nav_bar_set_toggled_condensed(false);
|
||||||
return self.app.on_nav_select(key);
|
return self.app.on_nav_select(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::NavBarContext(key) => {
|
Action::NavBarContext(key) => {
|
||||||
self.app.core_mut().nav_bar_set_context(key);
|
self.app.core_mut().nav_bar_set_context(key);
|
||||||
return self.app.on_nav_context(key);
|
return self.app.on_nav_context(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::ToggleNavBar => {
|
Action::ToggleNavBar => {
|
||||||
self.app.core_mut().nav_bar_toggle();
|
self.app.core_mut().nav_bar_toggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::ToggleNavBarCondensed => {
|
Action::ToggleNavBarCondensed => {
|
||||||
self.app.core_mut().nav_bar_toggle_condensed();
|
self.app.core_mut().nav_bar_toggle_condensed();
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::AppThemeChange(mut theme) => {
|
Action::AppThemeChange(mut theme) => {
|
||||||
if let ThemeType::System { theme: _, .. } = theme.theme_type {
|
if let ThemeType::System { theme: _, .. } = theme.theme_type {
|
||||||
self.app.core_mut().theme_sub_counter += 1;
|
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);
|
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();
|
let cur_is_dark = THEME.lock().unwrap().theme_type.is_dark();
|
||||||
// Ignore updates if the current theme mode does not match.
|
// Ignore updates if the current theme mode does not match.
|
||||||
if cur_is_dark != theme.cosmic().is_dark {
|
if cur_is_dark != theme.cosmic().is_dark {
|
||||||
|
|
@ -495,17 +584,17 @@ impl<T: Application> Cosmic<T> {
|
||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::ScaleFactor(factor) => {
|
Action::ScaleFactor(factor) => {
|
||||||
self.app.core_mut().set_scale_factor(factor);
|
self.app.core_mut().set_scale_factor(factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::Close => {
|
Action::Close => {
|
||||||
return match self.app.on_app_exit() {
|
return match self.app.on_app_exit() {
|
||||||
Some(message) => self.app.update(message),
|
Some(message) => self.app.update(message),
|
||||||
None => self.close(),
|
None => self.close(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Message::SystemThemeModeChange(keys, mode) => {
|
Action::SystemThemeModeChange(keys, mode) => {
|
||||||
if !keys.contains(&"is_dark") {
|
if !keys.contains(&"is_dark") {
|
||||||
return iced::Task::none();
|
return iced::Task::none();
|
||||||
}
|
}
|
||||||
|
|
@ -557,7 +646,7 @@ impl<T: Application> Cosmic<T> {
|
||||||
}
|
}
|
||||||
return Task::batch(cmds);
|
return Task::batch(cmds);
|
||||||
}
|
}
|
||||||
Message::Activate(_token) =>
|
Action::Activate(_token) =>
|
||||||
{
|
{
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "wayland")]
|
||||||
if let Some(id) = self.app.core().main_window_id() {
|
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) {
|
let mut ret = if let Some(msg) = self.app.on_close_requested(id) {
|
||||||
self.app.update(msg)
|
self.app.update(msg)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -578,17 +670,19 @@ impl<T: Application> Cosmic<T> {
|
||||||
if core.exit_on_main_window_closed
|
if core.exit_on_main_window_closed
|
||||||
&& core.main_window_id().is_some_and(|m_id| id == m_id)
|
&& 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;
|
return ret;
|
||||||
}
|
}
|
||||||
Message::ShowWindowMenu => {
|
|
||||||
|
Action::ShowWindowMenu => {
|
||||||
if let Some(id) = self.app.core().main_window_id() {
|
if let Some(id) = self.app.core().main_window_id() {
|
||||||
return iced::window::show_system_menu(id);
|
return iced::window::show_system_menu(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "xdg-portal")]
|
#[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;
|
use ashpd::desktop::settings::ColorScheme;
|
||||||
if match THEME.lock().unwrap().theme_type {
|
if match THEME.lock().unwrap().theme_type {
|
||||||
ThemeType::System {
|
ThemeType::System {
|
||||||
|
|
@ -628,7 +722,7 @@ impl<T: Application> Cosmic<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "xdg-portal")]
|
#[cfg(feature = "xdg-portal")]
|
||||||
Message::DesktopSettings(crate::theme::portal::Desktop::Accent(c)) => {
|
Action::DesktopSettings(crate::theme::portal::Desktop::Accent(c)) => {
|
||||||
use palette::Srgba;
|
use palette::Srgba;
|
||||||
let c = Srgba::new(c.red() as f32, c.green() as f32, c.blue() as f32, 1.0);
|
let c = Srgba::new(c.red() as f32, c.green() as f32, c.blue() as f32, 1.0);
|
||||||
let core = self.app.core_mut();
|
let core = self.app.core_mut();
|
||||||
|
|
@ -657,11 +751,11 @@ impl<T: Application> Cosmic<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "xdg-portal")]
|
#[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
|
// 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.
|
// Change the icon theme if not defined by the application.
|
||||||
if !self.app.core().icon_theme_override
|
if !self.app.core().icon_theme_override
|
||||||
&& crate::icon_theme::default() != config.icon_theme
|
&& crate::icon_theme::default() != config.icon_theme
|
||||||
|
|
@ -672,18 +766,18 @@ impl<T: Application> Cosmic<T> {
|
||||||
*crate::config::COSMIC_TK.write().unwrap() = config;
|
*crate::config::COSMIC_TK.write().unwrap() = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::Focus(f) => {
|
Action::Focus(f) => {
|
||||||
self.app.core_mut().focused_window = Some(f);
|
self.app.core_mut().focused_window = Some(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::Unfocus(id) => {
|
Action::Unfocus(id) => {
|
||||||
let core = self.app.core_mut();
|
let core = self.app.core_mut();
|
||||||
if core.focused_window.as_ref().is_some_and(|cur| *cur == id) {
|
if core.focused_window.as_ref().is_some_and(|cur| *cur == id) {
|
||||||
core.focused_window = None;
|
core.focused_window = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "applet")]
|
#[cfg(feature = "applet")]
|
||||||
Message::SuggestedBounds(b) => {
|
Action::SuggestedBounds(b) => {
|
||||||
tracing::info!("Suggested bounds: {b:?}");
|
tracing::info!("Suggested bounds: {b:?}");
|
||||||
let core = self.app.core_mut();
|
let core = self.app.core_mut();
|
||||||
core.applet.suggested_bounds = b;
|
core.applet.suggested_bounds = b;
|
||||||
|
|
@ -697,6 +791,40 @@ impl<T: Application> Cosmic<T> {
|
||||||
|
|
||||||
impl<App: Application> Cosmic<App> {
|
impl<App: Application> Cosmic<App> {
|
||||||
pub fn new(app: App) -> Self {
|
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)
|
//! Check out our [application](https://github.com/pop-os/libcosmic/tree/master/examples/application)
|
||||||
//! example in our repository.
|
//! example in our repository.
|
||||||
|
|
||||||
pub mod command;
|
mod action;
|
||||||
|
pub use action::Action;
|
||||||
|
use cosmic_config::CosmicConfigEntry;
|
||||||
pub mod context_drawer;
|
pub mod context_drawer;
|
||||||
mod core;
|
|
||||||
pub mod cosmic;
|
pub mod cosmic;
|
||||||
#[cfg(all(feature = "winit", feature = "multi-window"))]
|
#[cfg(all(feature = "winit", feature = "multi-window"))]
|
||||||
pub(crate) mod multi_window;
|
pub(crate) mod multi_window;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
|
||||||
pub mod message {
|
pub type Task<M> = iced::Task<crate::Action<M>>;
|
||||||
#[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 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::prelude::*;
|
||||||
use crate::theme::THEME;
|
use crate::theme::THEME;
|
||||||
use crate::widget::{container, horizontal_space, id_container, menu, nav_bar, popover};
|
use crate::widget::{container, horizontal_space, id_container, menu, nav_bar, popover};
|
||||||
|
pub use crate::Core;
|
||||||
use apply::Apply;
|
use apply::Apply;
|
||||||
use context_drawer::ContextDrawer;
|
use context_drawer::ContextDrawer;
|
||||||
use iced::window;
|
use iced::window;
|
||||||
use iced::{Length, Subscription};
|
use iced::{Length, Subscription};
|
||||||
pub use message::Message;
|
pub use settings::Settings;
|
||||||
use url::Url;
|
use std::borrow::Cow;
|
||||||
#[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(crate) fn iced_settings<App: Application>(
|
pub(crate) fn iced_settings<App: Application>(
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
|
|
@ -143,6 +100,7 @@ pub fn run<App: Application>(settings: Settings, flags: App::Flags) -> iced::Res
|
||||||
crate::malloc::limit_mmap_threshold(threshold);
|
crate::malloc::limit_mmap_threshold(threshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let default_font = settings.default_font;
|
||||||
let (settings, mut flags, window_settings) = iced_settings::<App>(settings, flags);
|
let (settings, mut flags, window_settings) = iced_settings::<App>(settings, flags);
|
||||||
#[cfg(not(feature = "multi-window"))]
|
#[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")]
|
#[cfg(feature = "single-instance")]
|
||||||
/// Launch a COSMIC application with the given [`Settings`].
|
/// Launch a COSMIC application with the given [`Settings`].
|
||||||
/// If the application is already running, the arguments will be passed to the
|
/// If the application is already running, the arguments will be passed to the
|
||||||
|
|
@ -326,6 +148,8 @@ where
|
||||||
App::Flags: CosmicFlags,
|
App::Flags: CosmicFlags,
|
||||||
App::Message: Clone + std::fmt::Debug + Send + 'static,
|
App::Message: Clone + std::fmt::Debug + Send + 'static,
|
||||||
{
|
{
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok();
|
let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok();
|
||||||
|
|
||||||
let override_single = std::env::var("COSMIC_SINGLE_INSTANCE")
|
let override_single = std::env::var("COSMIC_SINGLE_INSTANCE")
|
||||||
|
|
@ -342,14 +166,14 @@ where
|
||||||
return run::<App>(settings, flags);
|
return run::<App>(settings, flags);
|
||||||
};
|
};
|
||||||
|
|
||||||
if DbusActivationInterfaceProxyBlocking::builder(&conn)
|
if crate::dbus_activation::DbusActivationInterfaceProxyBlocking::builder(&conn)
|
||||||
.destination(App::APP_ID)
|
.destination(App::APP_ID)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|b| b.path(path).ok())
|
.and_then(|b| b.path(path).ok())
|
||||||
.and_then(|b| b.destination(App::APP_ID).ok())
|
.and_then(|b| b.destination(App::APP_ID).ok())
|
||||||
.and_then(|b| b.build().ok())
|
.and_then(|b| b.build().ok())
|
||||||
.is_some_and(|mut p| {
|
.is_some_and(|mut p| {
|
||||||
match {
|
let res = {
|
||||||
let mut platform_data = HashMap::new();
|
let mut platform_data = HashMap::new();
|
||||||
if let Some(activation_token) = activation_token {
|
if let Some(activation_token) = activation_token {
|
||||||
platform_data.insert("activation-token", activation_token.into());
|
platform_data.insert("activation-token", activation_token.into());
|
||||||
|
|
@ -363,7 +187,8 @@ where
|
||||||
} else {
|
} else {
|
||||||
p.activate(platform_data)
|
p.activate(platform_data)
|
||||||
}
|
}
|
||||||
} {
|
};
|
||||||
|
match res {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
tracing::info!("Successfully activated another instance");
|
tracing::info!("Successfully activated another instance");
|
||||||
true
|
true
|
||||||
|
|
@ -491,7 +316,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allows overriding the default nav bar widget.
|
/// 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() {
|
if !self.core().nav_bar_active() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
@ -499,8 +324,8 @@ where
|
||||||
let nav_model = self.nav_model()?;
|
let nav_model = self.nav_model()?;
|
||||||
|
|
||||||
let mut nav =
|
let mut nav =
|
||||||
crate::widget::nav_bar(nav_model, |id| Message::Cosmic(cosmic::Message::NavBar(id)))
|
crate::widget::nav_bar(nav_model, |id| crate::Action::Cosmic(Action::NavBar(id)))
|
||||||
.on_context(|id| Message::Cosmic(cosmic::Message::NavBarContext(id)))
|
.on_context(|id| crate::Action::Cosmic(Action::NavBarContext(id)))
|
||||||
.context_menu(self.nav_context_menu(self.core().nav_bar_context()))
|
.context_menu(self.nav_context_menu(self.core().nav_bar_context()))
|
||||||
.into_container()
|
.into_container()
|
||||||
// XXX both must be shrink to avoid flex layout from ignoring it
|
// 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.
|
/// 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
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -605,7 +433,7 @@ where
|
||||||
|
|
||||||
/// Handles dbus activation messages
|
/// Handles dbus activation messages
|
||||||
#[cfg(feature = "single-instance")]
|
#[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()
|
Task::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -648,7 +476,21 @@ pub trait ApplicationExt: Application {
|
||||||
fn set_window_title(&mut self, title: String, id: window::Id) -> Task<Self::Message>;
|
fn set_window_title(&mut self, title: String, id: window::Id) -> Task<Self::Message>;
|
||||||
|
|
||||||
/// View template for the main window.
|
/// 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 {
|
impl<App: Application> ApplicationExt for App {
|
||||||
|
|
@ -695,7 +537,7 @@ impl<App: Application> ApplicationExt for App {
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
/// Creates the view for the main window.
|
/// 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 core = self.core();
|
||||||
let is_condensed = core.is_condensed();
|
let is_condensed = core.is_condensed();
|
||||||
// TODO: More granularity might be needed for different resize border
|
// TODO: More granularity might be needed for different resize border
|
||||||
|
|
@ -762,13 +604,13 @@ impl<App: Application> ApplicationExt for App {
|
||||||
[0, 0, 0, 0]
|
[0, 0, 0, 0]
|
||||||
})
|
})
|
||||||
.apply(Element::from)
|
.apply(Element::from)
|
||||||
.map(Message::App),
|
.map(crate::Action::App),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
//TODO: container and padding are temporary, until
|
//TODO: container and padding are temporary, until
|
||||||
//the `resize_border` is moved to not cover window content
|
//the `resize_border` is moved to not cover window content
|
||||||
widgets.push(
|
widgets.push(
|
||||||
container(main_content.map(Message::App))
|
container(main_content.map(crate::Action::App))
|
||||||
.padding(main_content_padding)
|
.padding(main_content_padding)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
|
@ -778,7 +620,7 @@ impl<App: Application> ApplicationExt for App {
|
||||||
//TODO: container and padding are temporary, until
|
//TODO: container and padding are temporary, until
|
||||||
//the `resize_border` is moved to not cover window content
|
//the `resize_border` is moved to not cover window content
|
||||||
widgets.push(
|
widgets.push(
|
||||||
container(main_content.map(Message::App))
|
container(main_content.map(crate::Action::App))
|
||||||
.padding(main_content_padding)
|
.padding(main_content_padding)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
|
@ -794,7 +636,7 @@ impl<App: Application> ApplicationExt for App {
|
||||||
context_width,
|
context_width,
|
||||||
)
|
)
|
||||||
.apply(Element::from)
|
.apply(Element::from)
|
||||||
.map(Message::App)
|
.map(crate::Action::App)
|
||||||
.apply(container)
|
.apply(container)
|
||||||
.width(context_width)
|
.width(context_width)
|
||||||
.apply(|drawer| {
|
.apply(|drawer| {
|
||||||
|
|
@ -824,7 +666,7 @@ impl<App: Application> ApplicationExt for App {
|
||||||
.push(content_row)
|
.push(content_row)
|
||||||
.push_maybe(
|
.push_maybe(
|
||||||
self.footer()
|
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 {
|
let content: Element<_> = if core.window.content_container {
|
||||||
content_col
|
content_col
|
||||||
|
|
@ -851,45 +693,45 @@ impl<App: Application> ApplicationExt for App {
|
||||||
let mut header = crate::widget::header_bar()
|
let mut header = crate::widget::header_bar()
|
||||||
.focused(focused)
|
.focused(focused)
|
||||||
.title(&core.window.header_title)
|
.title(&core.window.header_title)
|
||||||
.on_drag(Message::Cosmic(cosmic::Message::Drag))
|
.on_drag(crate::Action::Cosmic(Action::Drag))
|
||||||
.on_right_click(Message::Cosmic(cosmic::Message::ShowWindowMenu))
|
.on_right_click(crate::Action::Cosmic(Action::ShowWindowMenu))
|
||||||
.on_double_click(Message::Cosmic(cosmic::Message::Maximize));
|
.on_double_click(crate::Action::Cosmic(Action::Maximize));
|
||||||
|
|
||||||
if self.nav_model().is_some() {
|
if self.nav_model().is_some() {
|
||||||
let toggle = crate::widget::nav_bar_toggle()
|
let toggle = crate::widget::nav_bar_toggle()
|
||||||
.active(core.nav_bar_active())
|
.active(core.nav_bar_active())
|
||||||
.selected(focused)
|
.selected(focused)
|
||||||
.on_toggle(if is_condensed {
|
.on_toggle(if is_condensed {
|
||||||
Message::Cosmic(cosmic::Message::ToggleNavBarCondensed)
|
crate::Action::Cosmic(Action::ToggleNavBarCondensed)
|
||||||
} else {
|
} else {
|
||||||
Message::Cosmic(cosmic::Message::ToggleNavBar)
|
crate::Action::Cosmic(Action::ToggleNavBar)
|
||||||
});
|
});
|
||||||
|
|
||||||
header = header.start(toggle);
|
header = header.start(toggle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if core.window.show_close {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
for element in self.header_end() {
|
||||||
header = header.end(element.map(Message::App));
|
header = header.end(element.map(crate::Action::App));
|
||||||
}
|
}
|
||||||
|
|
||||||
if content_container {
|
if content_container {
|
||||||
|
|
@ -951,7 +793,7 @@ impl<App: Application> ApplicationExt for App {
|
||||||
.dialog()
|
.dialog()
|
||||||
.map(|w| Element::from(id_container(w, iced_core::id::Id::new("COSMIC_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();
|
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]] = &[
|
const EMBEDDED_FONTS: &[&[u8]] = &[
|
||||||
include_bytes!("../../res/open-sans/OpenSans-Light.ttf"),
|
include_bytes!("../../res/open-sans/OpenSans-Light.ttf"),
|
||||||
include_bytes!("../../res/open-sans/OpenSans-Regular.ttf"),
|
include_bytes!("../../res/open-sans/OpenSans-Regular.ttf"),
|
||||||
|
|
@ -1043,6 +817,6 @@ fn preload_fonts() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
EMBEDDED_FONTS
|
EMBEDDED_FONTS
|
||||||
.into_iter()
|
.iter()
|
||||||
.for_each(move |font| font_system.load_font(Cow::Borrowed(font)));
|
.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.
|
//! Create and run daemons that run in the background.
|
||||||
//! Copied from iced 0.13, but adds optional initial window
|
//! Copied from iced 0.13, but adds optional initial window
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
pub mod token;
|
pub mod token;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{self, iced_settings, Core},
|
app::iced_settings,
|
||||||
cctk::sctk,
|
cctk::sctk,
|
||||||
iced::{
|
iced::{
|
||||||
self,
|
self,
|
||||||
|
|
@ -14,20 +14,17 @@ use crate::{
|
||||||
theme::{self, system_dark, system_light, Button, THEME},
|
theme::{self, system_dark, system_light, Button, THEME},
|
||||||
widget::{
|
widget::{
|
||||||
self,
|
self,
|
||||||
autosize::{autosize, Autosize},
|
autosize::{self, autosize, Autosize},
|
||||||
layer_container,
|
layer_container,
|
||||||
},
|
},
|
||||||
Application, Element, Renderer,
|
Application, Element, Renderer,
|
||||||
};
|
};
|
||||||
use cctk::sctk::shell::xdg::window::WindowConfigure;
|
|
||||||
pub use cosmic_panel_config;
|
pub use cosmic_panel_config;
|
||||||
use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize};
|
use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize};
|
||||||
use cosmic_theme::Theme;
|
use iced_core::{Layout, Padding, Shadow};
|
||||||
use iced::Pixels;
|
|
||||||
use iced_core::{Padding, Shadow};
|
|
||||||
use iced_widget::runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner};
|
use iced_widget::runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner};
|
||||||
use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity};
|
use 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 tracing::info;
|
||||||
|
|
||||||
use crate::app::cosmic;
|
use crate::app::cosmic;
|
||||||
|
|
@ -35,6 +32,8 @@ static AUTOSIZE_ID: LazyLock<iced::id::Id> =
|
||||||
LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize"));
|
LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize"));
|
||||||
static AUTOSIZE_MAIN_ID: LazyLock<iced::id::Id> =
|
static AUTOSIZE_MAIN_ID: LazyLock<iced::id::Id> =
|
||||||
LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize-main"));
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
|
|
@ -161,11 +160,7 @@ impl Context {
|
||||||
let height = f32::from(height) + applet_padding as f32 * 2.;
|
let height = f32::from(height) + applet_padding as f32 * 2.;
|
||||||
let mut settings = crate::app::Settings::default()
|
let mut settings = crate::app::Settings::default()
|
||||||
.size(iced_core::Size::new(width, height))
|
.size(iced_core::Size::new(width, height))
|
||||||
.size_limits(
|
.size_limits(Limits::NONE.min_height(height).min_width(width))
|
||||||
Limits::NONE
|
|
||||||
.min_height(height as f32)
|
|
||||||
.min_width(width as f32),
|
|
||||||
)
|
|
||||||
.resizable(None)
|
.resizable(None)
|
||||||
.default_text_size(14.0)
|
.default_text_size(14.0)
|
||||||
.default_font(crate::font::default())
|
.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
|
// TODO popup container which tracks the size of itself and requests the popup to resize to match
|
||||||
pub fn popup_container<'a, Message: 'static>(
|
pub fn popup_container<'a, Message: 'static>(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -240,7 +299,7 @@ impl Context {
|
||||||
Container::<Message, _, Renderer>::new(
|
Container::<Message, _, Renderer>::new(
|
||||||
Container::<Message, _, Renderer>::new(content).style(|theme| {
|
Container::<Message, _, Renderer>::new(content).style(|theme| {
|
||||||
let cosmic = theme.cosmic();
|
let cosmic = theme.cosmic();
|
||||||
let corners = cosmic.corner_radii.clone();
|
let corners = cosmic.corner_radii;
|
||||||
iced_widget::container::Style {
|
iced_widget::container::Style {
|
||||||
text_color: Some(cosmic.background.on.into()),
|
text_color: Some(cosmic.background.on.into()),
|
||||||
background: Some(Color::from(cosmic.background.base).into()),
|
background: Some(Color::from(cosmic.background.base).into()),
|
||||||
|
|
@ -262,10 +321,10 @@ impl Context {
|
||||||
)
|
)
|
||||||
.limits(
|
.limits(
|
||||||
Limits::NONE
|
Limits::NONE
|
||||||
.min_width(1.)
|
|
||||||
.min_height(1.)
|
.min_height(1.)
|
||||||
.max_width(500.)
|
.min_width(360.0)
|
||||||
.max_height(1000.),
|
.max_width(360.0)
|
||||||
|
.max_height(1000.0),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -304,10 +363,16 @@ impl Context {
|
||||||
},
|
},
|
||||||
reactive: true,
|
reactive: true,
|
||||||
constraint_adjustment: 15, // slide_y, slide_x, flip_x, flip_y
|
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,
|
parent_size: None,
|
||||||
grab: true,
|
grab: true,
|
||||||
|
close_with_children: false,
|
||||||
|
input_zone: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -315,7 +380,7 @@ impl Context {
|
||||||
&self,
|
&self,
|
||||||
content: impl Into<Element<'a, Message>>,
|
content: impl Into<Element<'a, Message>>,
|
||||||
) -> Autosize<'a, Message, crate::Theme, crate::Renderer> {
|
) -> 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 w = autosize(content, AUTOSIZE_MAIN_ID.clone());
|
||||||
let mut limits = Limits::NONE;
|
let mut limits = Limits::NONE;
|
||||||
let suggested_window_size = self.suggested_window_size();
|
let suggested_window_size = self.suggested_window_size();
|
||||||
|
|
@ -326,7 +391,7 @@ impl Context {
|
||||||
.filter(|c| c.width as i32 > 0)
|
.filter(|c| c.width as i32 > 0)
|
||||||
.map(|c| c.width)
|
.map(|c| c.width)
|
||||||
{
|
{
|
||||||
limits = limits.width(width as f32);
|
limits = limits.width(width);
|
||||||
}
|
}
|
||||||
if let Some(height) = self
|
if let Some(height) = self
|
||||||
.suggested_bounds
|
.suggested_bounds
|
||||||
|
|
@ -334,7 +399,7 @@ impl Context {
|
||||||
.filter(|c| c.height as i32 > 0)
|
.filter(|c| c.height as i32 > 0)
|
||||||
.map(|c| c.height)
|
.map(|c| c.height)
|
||||||
{
|
{
|
||||||
limits = limits.height(height as f32);
|
limits = limits.height(height);
|
||||||
}
|
}
|
||||||
|
|
||||||
w.limits(limits)
|
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";
|
const SANS_FAMILY_DEFAULT: &str = "Open Sans";
|
||||||
|
|
||||||
/// Stores static strings of the family names for `iced::Font` compatibility.
|
/// 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(|| {
|
pub static COSMIC_TK: LazyLock<RwLock<CosmicTk>> = LazyLock::new(|| {
|
||||||
RwLock::new(
|
RwLock::new(
|
||||||
|
|
@ -153,7 +153,7 @@ impl From<FontConfig> for iced::Font {
|
||||||
|
|
||||||
let name: &'static str = family_map
|
let name: &'static str = family_map
|
||||||
.get(font.family.as_str())
|
.get(font.family.as_str())
|
||||||
.map(|&x| x)
|
.copied()
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
let value = font.family.clone().leak();
|
let value = font.family.clone().leak();
|
||||||
family_map.insert(value);
|
family_map.insert(value);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
// Copyright 2023 System76 <info@system76.com>
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use std::{cell::OnceCell, collections::HashMap};
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::widget::nav_bar;
|
use crate::widget::nav_bar;
|
||||||
use cosmic_config::CosmicConfigEntry;
|
use cosmic_config::CosmicConfigEntry;
|
||||||
use cosmic_theme::ThemeMode;
|
use cosmic_theme::ThemeMode;
|
||||||
use iced::window;
|
use iced::{window, Limits, Size};
|
||||||
use iced_core::window::Id;
|
use iced_core::window::Id;
|
||||||
use palette::Srgba;
|
use palette::Srgba;
|
||||||
use slotmap::Key;
|
use slotmap::Key;
|
||||||
|
|
@ -95,6 +95,8 @@ pub struct Core {
|
||||||
pub(crate) main_window: Option<window::Id>,
|
pub(crate) main_window: Option<window::Id>,
|
||||||
|
|
||||||
pub(crate) exit_on_main_window_closed: bool,
|
pub(crate) exit_on_main_window_closed: bool,
|
||||||
|
|
||||||
|
pub(crate) menu_bars: HashMap<crate::widget::Id, (Limits, Size)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Core {
|
impl Default for Core {
|
||||||
|
|
@ -151,6 +153,7 @@ impl Default for Core {
|
||||||
portal_is_high_contrast: None,
|
portal_is_high_contrast: None,
|
||||||
main_window: None,
|
main_window: None,
|
||||||
exit_on_main_window_closed: true,
|
exit_on_main_window_closed: true,
|
||||||
|
menu_bars: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -205,7 +208,7 @@ impl Core {
|
||||||
// Context drawer min width (344px) + padding (8px)
|
// Context drawer min width (344px) + padding (8px)
|
||||||
breakpoint += 344.0 + 8.0;
|
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();
|
self.nav_bar_update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -218,7 +221,7 @@ impl Core {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn context_width(&self, has_nav: bool) -> f32 {
|
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)
|
// Content width (360px) + padding (8px)
|
||||||
let mut reserved_width = 360.0 + 8.0;
|
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
|
/// Whether the nav panel is visible or not
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn nav_bar_active(&self) -> bool {
|
pub fn nav_bar_active(&self) -> bool {
|
||||||
|
|
@ -353,7 +360,7 @@ impl Core {
|
||||||
/// Get the current focused window if it exists
|
/// Get the current focused window if it exists
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn focused_window(&self) -> Option<window::Id> {
|
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
|
/// 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);
|
std::mem::swap(&mut self.main_window, &mut id);
|
||||||
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))]
|
#[cfg(not(windows))]
|
||||||
pub fn app_id_or_fallback_matches(app_id: &str, entry: &DesktopEntryData) -> bool {
|
pub fn app_id_or_fallback_matches(app_id: &str, entry: &DesktopEntryData) -> bool {
|
||||||
let lowercase_wm_class = match entry.wm_class.as_ref() {
|
let lowercase_wm_class = entry.wm_class.as_ref().map(|s| s.to_lowercase());
|
||||||
Some(s) => Some(s.to_lowercase()),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
app_id == entry.id
|
app_id == entry.id
|
||||||
|| Some(app_id.to_lowercase()) == lowercase_wm_class
|
|| Some(app_id.to_lowercase()) == lowercase_wm_class
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ pub trait ElementExt {
|
||||||
fn debug(self, debug: bool) -> Self;
|
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 {
|
fn debug(self, debug: bool) -> Self {
|
||||||
if debug {
|
if debug {
|
||||||
self.explain(Color::WHITE)
|
self.explain(Color::WHITE)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use iced_core::keyboard::key::Named;
|
||||||
use iced_futures::event::listen_raw;
|
use iced_futures::event::listen_raw;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
pub enum Message {
|
pub enum Action {
|
||||||
Escape,
|
Escape,
|
||||||
FocusNext,
|
FocusNext,
|
||||||
FocusPrevious,
|
FocusPrevious,
|
||||||
|
|
@ -16,7 +16,7 @@ pub enum Message {
|
||||||
Search,
|
Search,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscription() -> Subscription<Message> {
|
pub fn subscription() -> Subscription<Action> {
|
||||||
listen_raw(|event, status, _| {
|
listen_raw(|event, status, _| {
|
||||||
if event::Status::Ignored != status {
|
if event::Status::Ignored != status {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -30,18 +30,18 @@ pub fn subscription() -> Subscription<Message> {
|
||||||
}) => match key {
|
}) => match key {
|
||||||
Named::Tab if !modifiers.control() => {
|
Named::Tab if !modifiers.control() => {
|
||||||
return Some(if modifiers.shift() {
|
return Some(if modifiers.shift() {
|
||||||
Message::FocusPrevious
|
Action::FocusPrevious
|
||||||
} else {
|
} else {
|
||||||
Message::FocusNext
|
Action::FocusNext
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Named::Escape => {
|
Named::Escape => {
|
||||||
return Some(Message::Escape);
|
return Some(Action::Escape);
|
||||||
}
|
}
|
||||||
|
|
||||||
Named::F11 => {
|
Named::F11 => {
|
||||||
return Some(Message::Fullscreen);
|
return Some(Action::Fullscreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => (),
|
_ => (),
|
||||||
|
|
@ -51,7 +51,7 @@ pub fn subscription() -> Subscription<Message> {
|
||||||
modifiers,
|
modifiers,
|
||||||
..
|
..
|
||||||
}) if c == "f" && modifiers.control() => {
|
}) 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::*;
|
pub use crate::ext::*;
|
||||||
#[cfg(feature = "winit")]
|
#[cfg(feature = "winit")]
|
||||||
pub use crate::ApplicationExt;
|
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};
|
pub use apply::{Also, Apply};
|
||||||
|
|
||||||
|
/// Actions are managed internally by the cosmic runtime.
|
||||||
|
pub mod action;
|
||||||
|
pub use action::Action;
|
||||||
|
|
||||||
#[cfg(feature = "winit")]
|
#[cfg(feature = "winit")]
|
||||||
pub mod app;
|
pub mod app;
|
||||||
#[cfg(feature = "winit")]
|
#[cfg(feature = "winit")]
|
||||||
|
#[doc(inline)]
|
||||||
pub use app::{Application, ApplicationExt};
|
pub use app::{Application, ApplicationExt};
|
||||||
|
|
||||||
#[cfg(feature = "applet")]
|
#[cfg(feature = "applet")]
|
||||||
pub mod applet;
|
pub mod applet;
|
||||||
|
|
||||||
pub use iced::Task;
|
pub mod command;
|
||||||
pub mod task;
|
|
||||||
|
/// State which is managed by the cosmic runtime.
|
||||||
|
pub mod core;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use core::Core;
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
||||||
|
|
@ -33,6 +42,11 @@ pub use cosmic_config;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use cosmic_theme;
|
pub use cosmic_theme;
|
||||||
|
|
||||||
|
#[cfg(feature = "single-instance")]
|
||||||
|
pub mod dbus_activation;
|
||||||
|
#[cfg(feature = "single-instance")]
|
||||||
|
pub use dbus_activation::DbusActivation;
|
||||||
|
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
pub mod desktop;
|
pub mod desktop;
|
||||||
|
|
||||||
|
|
@ -85,6 +99,11 @@ pub mod process;
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "wayland")]
|
||||||
pub use cctk;
|
pub use cctk;
|
||||||
|
|
||||||
|
pub mod surface;
|
||||||
|
|
||||||
|
pub use iced::Task;
|
||||||
|
pub mod task;
|
||||||
|
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
|
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright 2025 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use std::os::raw::c_int;
|
use std::os::raw::c_int;
|
||||||
|
|
||||||
const M_MMAP_THRESHOLD: c_int = -3;
|
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")))]
|
#[cfg(all(feature = "smol", not(feature = "tokio")))]
|
||||||
use smol::io::AsyncReadExt;
|
use smol::io::AsyncReadExt;
|
||||||
use std::fs::File;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::os::fd::OwnedFd;
|
use std::os::fd::OwnedFd;
|
||||||
use std::process::{exit, Command, Stdio};
|
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::settings::{ColorScheme, Contrast};
|
||||||
use ashpd::desktop::Color;
|
use ashpd::desktop::Color;
|
||||||
use iced::futures::{self, select, FutureExt, SinkExt, StreamExt};
|
use iced::futures::{self, select, FutureExt, SinkExt, StreamExt};
|
||||||
use iced_futures::{stream, subscription};
|
use iced_futures::stream;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -27,7 +27,7 @@ pub fn desktop_settings() -> iced_futures::Subscription<Desktop> {
|
||||||
.await;
|
.await;
|
||||||
#[cfg(not(feature = "tokio"))]
|
#[cfg(not(feature = "tokio"))]
|
||||||
{
|
{
|
||||||
pending::<()>().await;
|
futures::future::pending::<()>().await;
|
||||||
unreachable!();
|
unreachable!();
|
||||||
}
|
}
|
||||||
attempts += 1;
|
attempts += 1;
|
||||||
|
|
|
||||||
|
|
@ -756,9 +756,7 @@ impl slider::Catalog for Theme {
|
||||||
impl menu::Catalog for Theme {
|
impl menu::Catalog for Theme {
|
||||||
type Class<'a> = ();
|
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 {
|
fn style(&self, class: &<Self as menu::Catalog>::Class<'_>) -> menu::Style {
|
||||||
let cosmic = self.cosmic();
|
let cosmic = self.cosmic();
|
||||||
|
|
@ -779,9 +777,7 @@ impl menu::Catalog for Theme {
|
||||||
impl pick_list::Catalog for Theme {
|
impl pick_list::Catalog for Theme {
|
||||||
type Class<'a> = ();
|
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(
|
fn style(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -824,9 +820,7 @@ impl pick_list::Catalog for Theme {
|
||||||
impl radio::Catalog for Theme {
|
impl radio::Catalog for Theme {
|
||||||
type Class<'a> = ();
|
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 {
|
fn style(&self, class: &Self::Class<'_>, status: radio::Status) -> radio::Style {
|
||||||
let theme = self.cosmic();
|
let theme = self.cosmic();
|
||||||
|
|
@ -878,9 +872,7 @@ impl radio::Catalog for Theme {
|
||||||
impl toggler::Catalog for Theme {
|
impl toggler::Catalog for Theme {
|
||||||
type Class<'a> = ();
|
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 {
|
fn style(&self, class: &Self::Class<'_>, status: toggler::Status) -> toggler::Style {
|
||||||
let cosmic = self.cosmic();
|
let cosmic = self.cosmic();
|
||||||
|
|
@ -935,9 +927,7 @@ impl toggler::Catalog for Theme {
|
||||||
impl pane_grid::Catalog for Theme {
|
impl pane_grid::Catalog for Theme {
|
||||||
type Class<'a> = ();
|
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 {
|
fn style(&self, class: &<Self as pane_grid::Catalog>::Class<'_>) -> pane_grid::Style {
|
||||||
let theme = self.cosmic();
|
let theme = self.cosmic();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// From iced_aw, license MIT
|
// From iced_aw, license MIT
|
||||||
|
|
||||||
//! Change the appearance of menu bars and their menus.
|
//! Change the appearance of menu bars and their menus.
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::Theme;
|
use crate::Theme;
|
||||||
use iced_widget::core::Color;
|
use iced_widget::core::Color;
|
||||||
|
|
||||||
|
|
@ -33,19 +35,19 @@ pub trait StyleSheet {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The style of a menu bar and its menus
|
/// The style of a menu bar and its menus
|
||||||
#[derive(Default)]
|
#[derive(Default, Clone)]
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub enum MenuBarStyle {
|
pub enum MenuBarStyle {
|
||||||
/// The default style.
|
/// The default style.
|
||||||
#[default]
|
#[default]
|
||||||
Default,
|
Default,
|
||||||
/// A [`Theme`] that uses a `Custom` palette.
|
/// 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 {
|
impl From<fn(&Theme) -> Appearance> for MenuBarStyle {
|
||||||
fn from(f: fn(&Theme) -> Appearance) -> Self {
|
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;
|
mod text_input;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use self::text_input::TextInput;
|
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),
|
.align_y(Alignment::Center),
|
||||||
)
|
)
|
||||||
.class(crate::theme::Button::Text)
|
.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),
|
.width(Length::Fill),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ use iced_core::{
|
||||||
Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget,
|
Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget,
|
||||||
};
|
};
|
||||||
|
|
||||||
use iced_widget::container;
|
|
||||||
pub use iced_widget::container::{Catalog, Style};
|
pub use iced_widget::container::{Catalog, Style};
|
||||||
|
|
||||||
pub fn aspect_ratio_container<'a, Message: 'static, T>(
|
pub fn aspect_ratio_container<'a, Message: 'static, T>(
|
||||||
|
|
@ -35,7 +34,7 @@ where
|
||||||
container: Container<'a, Message, crate::Theme, Renderer>,
|
container: Container<'a, Message, crate::Theme, Renderer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message, Renderer> AspectRatio<'a, Message, Renderer>
|
impl<Message, Renderer> AspectRatio<'_, Message, Renderer>
|
||||||
where
|
where
|
||||||
Renderer: iced_core::Renderer,
|
Renderer: iced_core::Renderer,
|
||||||
{
|
{
|
||||||
|
|
@ -146,8 +145,8 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message, Renderer> Widget<Message, crate::Theme, Renderer>
|
impl<Message, Renderer> Widget<Message, crate::Theme, Renderer>
|
||||||
for AspectRatio<'a, Message, Renderer>
|
for AspectRatio<'_, Message, Renderer>
|
||||||
where
|
where
|
||||||
Renderer: iced_core::Renderer,
|
Renderer: iced_core::Renderer,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -90,8 +90,8 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
for Autosize<'a, Message, Theme, Renderer>
|
for Autosize<'_, Message, Theme, Renderer>
|
||||||
where
|
where
|
||||||
Renderer: iced_core::Renderer,
|
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 {
|
pub fn new(icon: Icon) -> Self {
|
||||||
let guard = crate::theme::THEME.lock().unwrap();
|
let guard = crate::theme::THEME.lock().unwrap();
|
||||||
let theme = guard.cosmic();
|
let theme = guard.cosmic();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2023 System76 <info@system76.com>
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use super::{Builder, Style};
|
use super::Builder;
|
||||||
use crate::{
|
use crate::{
|
||||||
widget::{self, image::Handle},
|
widget::{self, image::Handle},
|
||||||
Element,
|
Element,
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ pub struct Builder<'a, Message, Variant> {
|
||||||
variant: 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`.
|
/// Set the value of [`on_press`] as either `Some` or `None`.
|
||||||
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
|
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
|
||||||
self.on_press = on_press;
|
self.on_press = on_press;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2023 System76 <info@system76.com>
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use super::{Builder, ButtonClass, Style};
|
use super::{Builder, ButtonClass};
|
||||||
use crate::widget::{icon, row, tooltip};
|
use crate::widget::{icon, row, tooltip};
|
||||||
use crate::{ext::CollectionWidget, Element};
|
use crate::{ext::CollectionWidget, Element};
|
||||||
use apply::Apply;
|
use apply::Apply;
|
||||||
|
|
@ -42,6 +42,12 @@ pub struct Text {
|
||||||
pub(super) trailing_icon: Option<icon::Handle>,
|
pub(super) trailing_icon: Option<icon::Handle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Text {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Text {
|
impl Text {
|
||||||
pub const fn new() -> Self {
|
pub const fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -51,7 +57,7 @@ impl Text {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message> Button<'a, Message> {
|
impl<Message> Button<'_, Message> {
|
||||||
pub fn new(text: Text) -> Self {
|
pub fn new(text: Text) -> Self {
|
||||||
let guard = crate::theme::THEME.lock().unwrap();
|
let guard = crate::theme::THEME.lock().unwrap();
|
||||||
let theme = guard.cosmic();
|
let theme = guard.cosmic();
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ pub struct Button<'a, Message> {
|
||||||
selected: bool,
|
selected: bool,
|
||||||
style: crate::theme::Button,
|
style: crate::theme::Button,
|
||||||
variant: Variant<Message>,
|
variant: Variant<Message>,
|
||||||
|
force_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message> Button<'a, Message> {
|
impl<'a, Message> Button<'a, Message> {
|
||||||
|
|
@ -77,6 +78,7 @@ impl<'a, Message> Button<'a, Message> {
|
||||||
selected: false,
|
selected: false,
|
||||||
style: crate::theme::Button::default(),
|
style: crate::theme::Button::default(),
|
||||||
variant: Variant::Normal,
|
variant: Variant::Normal,
|
||||||
|
force_enabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,6 +92,7 @@ impl<'a, Message> Button<'a, Message> {
|
||||||
name: None,
|
name: None,
|
||||||
#[cfg(feature = "a11y")]
|
#[cfg(feature = "a11y")]
|
||||||
description: None,
|
description: None,
|
||||||
|
force_enabled: false,
|
||||||
#[cfg(feature = "a11y")]
|
#[cfg(feature = "a11y")]
|
||||||
label: None,
|
label: None,
|
||||||
content: content.into(),
|
content: content.into(),
|
||||||
|
|
@ -163,6 +166,12 @@ impl<'a, Message> Button<'a, Message> {
|
||||||
self
|
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.
|
/// Sets the widget to a selected state.
|
||||||
///
|
///
|
||||||
/// Displays a selection indicator on image buttons.
|
/// 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 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 is_mouse_over = cursor.position().is_some_and(|p| bounds.contains(p));
|
||||||
|
|
||||||
let state = tree.state.downcast_ref::<State>();
|
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() {
|
match self.description.as_ref() {
|
||||||
Some(iced_accessibility::Description::Id(id)) => {
|
Some(iced_accessibility::Description::Id(id)) => {
|
||||||
node.set_described_by(
|
node.set_described_by(id.iter().cloned().map(NodeId::from).collect::<Vec<_>>());
|
||||||
id.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|id| NodeId::from(id))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Some(iced_accessibility::Description::Text(text)) => {
|
Some(iced_accessibility::Description::Text(text)) => {
|
||||||
node.set_description(text.clone());
|
node.set_description(text.clone());
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ impl CalendarModel {
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
let naive_now = NaiveDate::from(now.naive_local());
|
let naive_now = NaiveDate::from(now.naive_local());
|
||||||
CalendarModel {
|
CalendarModel {
|
||||||
selected: naive_now.clone(),
|
selected: naive_now,
|
||||||
visible: naive_now,
|
visible: naive_now,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,36 +65,34 @@ impl CalendarModel {
|
||||||
pub fn show_prev_month(&mut self) {
|
pub fn show_prev_month(&mut self) {
|
||||||
let prev_month_date = self
|
let prev_month_date = self
|
||||||
.visible
|
.visible
|
||||||
.clone()
|
|
||||||
.checked_sub_months(Months::new(1))
|
.checked_sub_months(Months::new(1))
|
||||||
.expect("valid naivedate");
|
.expect("valid naivedate");
|
||||||
|
|
||||||
self.visible = prev_month_date.clone();
|
self.visible = prev_month_date;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_next_month(&mut self) {
|
pub fn show_next_month(&mut self) {
|
||||||
let next_month_date = self
|
let next_month_date = self
|
||||||
.visible
|
.visible
|
||||||
.clone()
|
|
||||||
.checked_add_months(Months::new(1))
|
.checked_add_months(Months::new(1))
|
||||||
.expect("valid naivedate");
|
.expect("valid naivedate");
|
||||||
|
|
||||||
self.visible = next_month_date.clone();
|
self.visible = next_month_date;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_prev_month(&mut self) {
|
pub fn set_prev_month(&mut self) {
|
||||||
self.show_prev_month();
|
self.show_prev_month();
|
||||||
self.selected = self.visible.clone();
|
self.selected = self.visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_next_month(&mut self) {
|
pub fn set_next_month(&mut self) {
|
||||||
self.show_next_month();
|
self.show_next_month();
|
||||||
self.selected = self.visible.clone();
|
self.selected = self.visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_selected_visible(&mut self, selected: NaiveDate) {
|
pub fn set_selected_visible(&mut self, selected: NaiveDate) {
|
||||||
self.selected = selected;
|
self.selected = selected;
|
||||||
self.visible = self.selected.clone();
|
self.visible = self.selected;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -469,7 +469,7 @@ where
|
||||||
text_input("", self.input_color)
|
text_input("", self.input_color)
|
||||||
.on_input(move |s| on_update(ColorPickerUpdate::Input(s)))
|
.on_input(move |s| on_update(ColorPickerUpdate::Input(s)))
|
||||||
.on_paste(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(
|
.leading_icon(
|
||||||
color_button(
|
color_button(
|
||||||
None,
|
None,
|
||||||
|
|
@ -611,7 +611,7 @@ pub struct ColorPicker<'a, Message> {
|
||||||
must_clear_cache: Rc<AtomicBool>,
|
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
|
where
|
||||||
Message: Clone + 'static,
|
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
|
// TODO convert active color to hex or rgba
|
||||||
fn color_to_string(c: palette::Hsv, is_hex: bool) -> String {
|
fn color_to_string(c: palette::Hsv, is_hex: bool) -> String {
|
||||||
let srgb = palette::Srgb::from_color(c);
|
let srgb = palette::Srgb::from_color(c);
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,7 @@ pub(super) struct Overlay<'a, 'b, Message> {
|
||||||
pub(super) width: f32,
|
pub(super) width: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b, Message> overlay::Overlay<Message, crate::Theme, crate::Renderer>
|
impl<Message> overlay::Overlay<Message, crate::Theme, crate::Renderer> for Overlay<'_, '_, Message>
|
||||||
for Overlay<'a, 'b, Message>
|
|
||||||
where
|
where
|
||||||
Message: Clone,
|
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> {
|
fn children(&self) -> Vec<Tree> {
|
||||||
vec![Tree::new(&self.content), Tree::new(&self.drawer)]
|
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>>>,
|
context_menu: Option<Vec<menu::Tree<'a, Message>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message: Clone> Widget<Message, crate::Theme, crate::Renderer>
|
impl<Message: Clone> Widget<Message, crate::Theme, crate::Renderer> for ContextMenu<'_, Message> {
|
||||||
for ContextMenu<'a, Message>
|
|
||||||
{
|
|
||||||
fn tag(&self) -> tree::Tag {
|
fn tag(&self) -> tree::Tag {
|
||||||
tree::Tag::of::<LocalState>()
|
tree::Tag::of::<LocalState>()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,12 @@ pub struct Dialog<'a, Message> {
|
||||||
tertiary_action: Option<Element<'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> {
|
impl<'a, Message> Dialog<'a, Message> {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
|
||||||
|
|
@ -243,8 +243,8 @@ impl<'a, Message: 'static> DndDestination<'a, Message> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
|
impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
for DndDestination<'a, Message>
|
for DndDestination<'_, Message>
|
||||||
{
|
{
|
||||||
fn children(&self) -> Vec<Tree> {
|
fn children(&self) -> Vec<Tree> {
|
||||||
vec![Tree::new(&self.container)]
|
vec![Tree::new(&self.container)]
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ impl<
|
||||||
clipboard,
|
clipboard,
|
||||||
false,
|
false,
|
||||||
if let Some(window) = self.window.as_ref() {
|
if let Some(window) = self.window.as_ref() {
|
||||||
Some(iced_core::clipboard::DndSource::Surface(window.clone()))
|
Some(iced_core::clipboard::DndSource::Surface(*window))
|
||||||
} else {
|
} else {
|
||||||
Some(iced_core::clipboard::DndSource::Widget(self.id.clone()))
|
Some(iced_core::clipboard::DndSource::Widget(self.id.clone()))
|
||||||
},
|
},
|
||||||
|
|
@ -153,10 +153,9 @@ impl<
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<
|
impl<
|
||||||
'a,
|
|
||||||
Message: Clone + 'static,
|
Message: Clone + 'static,
|
||||||
D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + '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> {
|
fn children(&self) -> Vec<Tree> {
|
||||||
vec![Tree::new(&self.container)]
|
vec![Tree::new(&self.container)]
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,13 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0 AND MIT
|
// SPDX-License-Identifier: MPL-2.0 AND MIT
|
||||||
|
|
||||||
mod appearance;
|
mod appearance;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
pub use appearance::{Appearance, StyleSheet};
|
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::event::{self, Event};
|
||||||
use iced_core::layout::{self, Layout};
|
use iced_core::layout::{self, Layout};
|
||||||
use iced_core::text::{self, Text};
|
use iced_core::text::{self, Text};
|
||||||
|
|
@ -21,13 +25,15 @@ use iced_widget::scrollable::Scrollable;
|
||||||
pub struct Menu<'a, S, Message>
|
pub struct Menu<'a, S, Message>
|
||||||
where
|
where
|
||||||
S: AsRef<str>,
|
S: AsRef<str>,
|
||||||
|
[S]: std::borrow::ToOwned,
|
||||||
{
|
{
|
||||||
state: &'a mut State,
|
state: State,
|
||||||
options: &'a [S],
|
options: Cow<'a, [S]>,
|
||||||
icons: &'a [icon::Handle],
|
icons: Cow<'a, [icon::Handle]>,
|
||||||
hovered_option: &'a mut Option<usize>,
|
hovered_option: Arc<Mutex<Option<usize>>>,
|
||||||
selected_option: Option<usize>,
|
selected_option: Option<usize>,
|
||||||
on_selected: Box<dyn FnMut(usize) -> Message + 'a>,
|
on_selected: Box<dyn FnMut(usize) -> Message + 'a>,
|
||||||
|
close_on_selected: Option<Message>,
|
||||||
on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
|
on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
|
||||||
width: f32,
|
width: f32,
|
||||||
padding: Padding,
|
padding: Padding,
|
||||||
|
|
@ -36,17 +42,21 @@ where
|
||||||
style: (),
|
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
|
/// Creates a new [`Menu`] with the given [`State`], a list of options, and
|
||||||
/// the message to produced when an option is selected.
|
/// the message to produced when an option is selected.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
state: &'a mut State,
|
state: State,
|
||||||
options: &'a [S],
|
options: Cow<'a, [S]>,
|
||||||
icons: &'a [icon::Handle],
|
icons: Cow<'a, [icon::Handle]>,
|
||||||
hovered_option: &'a mut Option<usize>,
|
hovered_option: Arc<Mutex<Option<usize>>>,
|
||||||
selected_option: Option<usize>,
|
selected_option: Option<usize>,
|
||||||
on_selected: impl FnMut(usize) -> Message + 'a,
|
on_selected: impl FnMut(usize) -> Message + 'a,
|
||||||
on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
|
on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
|
||||||
|
close_on_selected: Option<Message>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Menu {
|
Menu {
|
||||||
state,
|
state,
|
||||||
|
|
@ -61,6 +71,7 @@ impl<'a, S: AsRef<str>, Message: 'a> Menu<'a, S, Message> {
|
||||||
text_size: None,
|
text_size: None,
|
||||||
text_line_height: text::LineHeight::default(),
|
text_line_height: text::LineHeight::default(),
|
||||||
style: Default::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<'a, Message, crate::Theme, crate::Renderer> {
|
||||||
overlay::Element::new(Box::new(Overlay::new(self, target_height, position)))
|
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`].
|
/// The local state of a [`Menu`].
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
tree: Tree,
|
pub(crate) tree: RcWrapper<Tree>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
/// Creates a new [`State`] for a [`Menu`].
|
/// Creates a new [`State`] for a [`Menu`].
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
tree: Tree::empty(),
|
tree: RcWrapper::new(Tree::empty()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +149,7 @@ impl Default for State {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Overlay<'a, Message> {
|
struct Overlay<'a, Message> {
|
||||||
state: &'a mut Tree,
|
state: RcWrapper<Tree>,
|
||||||
container: Container<'a, Message, crate::Theme, crate::Renderer>,
|
container: Container<'a, Message, crate::Theme, crate::Renderer>,
|
||||||
width: f32,
|
width: f32,
|
||||||
target_height: f32,
|
target_height: f32,
|
||||||
|
|
@ -135,12 +157,15 @@ struct Overlay<'a, Message> {
|
||||||
position: Point,
|
position: Point,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message: 'a> Overlay<'a, Message> {
|
impl<'a, Message: Clone + 'a> Overlay<'a, Message> {
|
||||||
pub fn new<S: AsRef<str>>(
|
pub fn new<S: AsRef<str>>(
|
||||||
menu: Menu<'a, S, Message>,
|
menu: Menu<'a, S, Message>,
|
||||||
target_height: f32,
|
target_height: f32,
|
||||||
position: Point,
|
position: Point,
|
||||||
) -> Self {
|
) -> Self
|
||||||
|
where
|
||||||
|
[S]: ToOwned,
|
||||||
|
{
|
||||||
let Menu {
|
let Menu {
|
||||||
state,
|
state,
|
||||||
options,
|
options,
|
||||||
|
|
@ -154,6 +179,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
|
||||||
text_size,
|
text_size,
|
||||||
text_line_height,
|
text_line_height,
|
||||||
style,
|
style,
|
||||||
|
close_on_selected,
|
||||||
} = menu;
|
} = menu;
|
||||||
|
|
||||||
let mut container = Container::new(Scrollable::new(
|
let mut container = Container::new(Scrollable::new(
|
||||||
|
|
@ -163,6 +189,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
|
||||||
hovered_option,
|
hovered_option,
|
||||||
selected_option,
|
selected_option,
|
||||||
on_selected,
|
on_selected,
|
||||||
|
close_on_selected,
|
||||||
on_option_hovered,
|
on_option_hovered,
|
||||||
text_size,
|
text_size,
|
||||||
text_line_height,
|
text_line_height,
|
||||||
|
|
@ -172,10 +199,12 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
|
||||||
))
|
))
|
||||||
.class(crate::style::Container::Dropdown);
|
.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 {
|
Self {
|
||||||
state: &mut state.tree,
|
state: state.tree.clone(),
|
||||||
container,
|
container,
|
||||||
width,
|
width,
|
||||||
target_height,
|
target_height,
|
||||||
|
|
@ -183,20 +212,15 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
|
||||||
position,
|
position,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
|
fn _layout(&self, renderer: &crate::Renderer, bounds: Size) -> layout::Node {
|
||||||
for Overlay<'a, Message>
|
let space_below = bounds.height - (self.position.y + self.target_height);
|
||||||
{
|
let space_above = self.position.y;
|
||||||
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;
|
|
||||||
|
|
||||||
let limits = layout::Limits::new(
|
let limits = layout::Limits::new(
|
||||||
Size::ZERO,
|
Size::ZERO,
|
||||||
Size::new(
|
Size::new(
|
||||||
bounds.width - position.x,
|
bounds.width - self.position.x,
|
||||||
if space_below > space_above {
|
if space_below > space_above {
|
||||||
space_below
|
space_below
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -206,16 +230,18 @@ impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
|
||||||
)
|
)
|
||||||
.width(self.width);
|
.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 {
|
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 {
|
} 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,
|
&mut self,
|
||||||
event: Event,
|
event: Event,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
|
|
@ -226,23 +252,27 @@ impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
|
||||||
) -> event::Status {
|
) -> event::Status {
|
||||||
let bounds = layout.bounds();
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
self.container.on_event(
|
self.state.with_data_mut(|tree| {
|
||||||
self.state, event, layout, cursor, renderer, clipboard, shell, &bounds,
|
self.container.on_event(
|
||||||
)
|
tree, event, layout, cursor, renderer, clipboard, shell, &bounds,
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mouse_interaction(
|
fn _mouse_interaction(
|
||||||
&self,
|
&self,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
renderer: &crate::Renderer,
|
renderer: &crate::Renderer,
|
||||||
) -> mouse::Interaction {
|
) -> mouse::Interaction {
|
||||||
self.container
|
self.state.with_data(|tree| {
|
||||||
.mouse_interaction(self.state, layout, cursor, viewport, renderer)
|
self.container
|
||||||
|
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(
|
fn _draw(
|
||||||
&self,
|
&self,
|
||||||
renderer: &mut crate::Renderer,
|
renderer: &mut crate::Renderer,
|
||||||
theme: &crate::Theme,
|
theme: &crate::Theme,
|
||||||
|
|
@ -266,25 +296,138 @@ impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
|
||||||
appearance.background,
|
appearance.background,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.container
|
self.state.with_data(|tree| {
|
||||||
.draw(self.state, renderer, theme, style, layout, cursor, &bounds);
|
self.container
|
||||||
|
.draw(tree, renderer, theme, style, layout, cursor, &bounds)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct List<'a, S: AsRef<str>, Message> {
|
impl<'a, Message: Clone + 'a> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
|
||||||
options: &'a [S],
|
for Overlay<'a, Message>
|
||||||
icons: &'a [icon::Handle],
|
{
|
||||||
hovered_option: &'a mut Option<usize>,
|
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>,
|
selected_option: Option<usize>,
|
||||||
on_selected: Box<dyn FnMut(usize) -> Message + 'a>,
|
on_selected: Box<dyn FnMut(usize) -> Message + 'a>,
|
||||||
|
close_on_selected: Option<Message>,
|
||||||
on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
|
on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
|
||||||
padding: Padding,
|
padding: Padding,
|
||||||
text_size: Option<f32>,
|
text_size: Option<f32>,
|
||||||
text_line_height: text::LineHeight,
|
text_line_height: text::LineHeight,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
impl<S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer> for List<'_, S, Message>
|
||||||
for List<'a, S, Message>
|
where
|
||||||
|
[S]: std::borrow::ToOwned,
|
||||||
|
Message: Clone,
|
||||||
{
|
{
|
||||||
fn size(&self) -> Size<Length> {
|
fn size(&self) -> Size<Length> {
|
||||||
Size::new(Length::Fill, Length::Shrink)
|
Size::new(Length::Fill, Length::Shrink)
|
||||||
|
|
@ -330,9 +473,13 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
) -> event::Status {
|
) -> event::Status {
|
||||||
match event {
|
match event {
|
||||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||||
|
let hovered_guard = self.hovered_option.lock().unwrap();
|
||||||
if cursor.is_over(layout.bounds()) {
|
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));
|
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;
|
return event::Status::Captured;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -348,14 +495,15 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
+ self.padding.vertical();
|
+ self.padding.vertical();
|
||||||
|
|
||||||
let new_hovered_option = (cursor_position.y / option_height) as usize;
|
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 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));
|
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 { .. }) => {
|
Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||||
|
|
@ -367,11 +515,15 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
let option_height =
|
let option_height =
|
||||||
f32::from(self.text_line_height.to_absolute(Pixels(text_size)))
|
f32::from(self.text_line_height.to_absolute(Pixels(text_size)))
|
||||||
+ self.padding.vertical();
|
+ 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));
|
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;
|
return event::Status::Captured;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -434,6 +586,8 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
height: option_height,
|
height: option_height,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let hovered_guard = self.hovered_option.lock().unwrap();
|
||||||
|
|
||||||
let (color, font) = if self.selected_option == Some(i) {
|
let (color, font) = if self.selected_option == Some(i) {
|
||||||
let item_x = bounds.x + appearance.border_width;
|
let item_x = bounds.x + appearance.border_width;
|
||||||
let item_width = appearance.border_width.mul_add(-2.0, bounds.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())
|
(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_x = bounds.x + appearance.border_width;
|
||||||
let item_width = appearance.border_width.mul_add(-2.0, bounds.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>>
|
impl<'a, S: AsRef<str>, Message: 'a> From<List<'a, S, Message>>
|
||||||
for Element<'a, Message, crate::Theme, crate::Renderer>
|
for Element<'a, Message, crate::Theme, crate::Renderer>
|
||||||
|
where
|
||||||
|
[S]: std::borrow::ToOwned,
|
||||||
|
Message: Clone,
|
||||||
{
|
{
|
||||||
fn from(list: List<'a, S, Message>) -> Self {
|
fn from(list: List<'a, S, Message>) -> Self {
|
||||||
Element::new(list)
|
Element::new(list)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
//! Displays a list of options in a popover menu on select.
|
//! Displays a list of options in a popover menu on select.
|
||||||
|
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
|
use iced_core::window;
|
||||||
pub use menu::Menu;
|
pub use menu::Menu;
|
||||||
|
|
||||||
pub mod multi;
|
pub mod multi;
|
||||||
|
|
@ -12,11 +13,40 @@ pub mod multi;
|
||||||
mod widget;
|
mod widget;
|
||||||
pub use widget::*;
|
pub use widget::*;
|
||||||
|
|
||||||
|
use crate::surface;
|
||||||
|
|
||||||
/// Displays a list of options in a popover menu on select.
|
/// Displays a list of options in a popover menu on select.
|
||||||
pub fn dropdown<'a, S: AsRef<str>, Message: 'a>(
|
pub fn dropdown<
|
||||||
selections: &'a [S],
|
S: AsRef<str> + std::clone::Clone + Send + Sync + 'static,
|
||||||
|
Message: 'static + Clone,
|
||||||
|
>(
|
||||||
|
selections: &[S],
|
||||||
selected: Option<usize>,
|
selected: Option<usize>,
|
||||||
on_selected: impl Fn(usize) -> Message + 'a,
|
on_selected: impl Fn(usize) -> Message + Send + Sync + 'static,
|
||||||
) -> Dropdown<'a, S, Message> {
|
) -> Dropdown<'_, S, Message, Message> {
|
||||||
Dropdown::new(selections, selected, on_selected)
|
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>
|
impl<Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer> for Overlay<'_, Message> {
|
||||||
for Overlay<'a, Message>
|
|
||||||
{
|
|
||||||
fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node {
|
fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node {
|
||||||
let position = self.position;
|
let position = self.position;
|
||||||
let space_below = bounds.height - (position.y + self.target_height);
|
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,
|
text_line_height: text::LineHeight,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, S, Item, Message> Widget<Message, crate::Theme, crate::Renderer>
|
impl<S, Item, Message> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
for InnerList<'a, S, Item, Message>
|
for InnerList<'_, S, Item, Message>
|
||||||
where
|
where
|
||||||
S: AsRef<str>,
|
S: AsRef<str>,
|
||||||
Item: Clone + PartialEq,
|
Item: Clone + PartialEq,
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ impl<'a, S: AsRef<str>, Message: 'a, Item: Clone + PartialEq + 'static>
|
||||||
cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
) {
|
) {
|
||||||
let font = self.font.unwrap_or_else(|| crate::font::default());
|
let font = self.font.unwrap_or_else(crate::font::default);
|
||||||
|
|
||||||
draw(
|
draw(
|
||||||
renderer,
|
renderer,
|
||||||
|
|
@ -278,7 +278,7 @@ pub fn layout(
|
||||||
bounds: Size::new(f32::MAX, f32::MAX),
|
bounds: Size::new(f32::MAX, f32::MAX),
|
||||||
size: iced::Pixels(text_size),
|
size: iced::Pixels(text_size),
|
||||||
line_height: text_line_height,
|
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,
|
horizontal_alignment: alignment::Horizontal::Left,
|
||||||
vertical_alignment: alignment::Vertical::Top,
|
vertical_alignment: alignment::Vertical::Top,
|
||||||
shaping: text::Shaping::Advanced,
|
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),
|
bounds: Size::new(f32::MAX, f32::MAX),
|
||||||
size: iced::Pixels(text_size),
|
size: iced::Pixels(text_size),
|
||||||
line_height,
|
line_height,
|
||||||
font: font.unwrap_or_else(|| crate::font::default()),
|
font: font.unwrap_or_else(crate::font::default),
|
||||||
horizontal_alignment: alignment::Horizontal::Left,
|
horizontal_alignment: alignment::Horizontal::Left,
|
||||||
vertical_alignment: alignment::Vertical::Top,
|
vertical_alignment: alignment::Vertical::Top,
|
||||||
shaping: text::Shaping::Advanced,
|
shaping: text::Shaping::Advanced,
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0 AND MIT
|
// SPDX-License-Identifier: MPL-2.0 AND MIT
|
||||||
|
|
||||||
use super::menu::{self, Menu};
|
use super::menu::{self, Menu};
|
||||||
use crate::widget::icon;
|
use crate::widget::icon::{self, Handle};
|
||||||
|
use crate::{surface, Element};
|
||||||
use derive_setters::Setters;
|
use derive_setters::Setters;
|
||||||
use iced::Radians;
|
use iced::window;
|
||||||
use iced_core::event::{self, Event};
|
use iced_core::event::{self, Event};
|
||||||
use iced_core::text::{self, Paragraph, Text};
|
use iced_core::text::{self, Paragraph, Text};
|
||||||
use iced_core::widget::tree::{self, Tree};
|
use iced_core::widget::tree::{self, Tree};
|
||||||
|
|
@ -14,14 +15,24 @@ use iced_core::{
|
||||||
Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget,
|
Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget,
|
||||||
};
|
};
|
||||||
use iced_widget::pick_list::{self, Catalog};
|
use iced_widget::pick_list::{self, Catalog};
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
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.
|
/// A widget for selecting a single value from a list of selections.
|
||||||
#[derive(Setters)]
|
#[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)]
|
#[setters(skip)]
|
||||||
on_selected: Box<dyn Fn(usize) -> Message + 'a>,
|
on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync>,
|
||||||
#[setters(skip)]
|
#[setters(skip)]
|
||||||
selections: &'a [S],
|
selections: &'a [S],
|
||||||
#[setters]
|
#[setters]
|
||||||
|
|
@ -38,9 +49,21 @@ pub struct Dropdown<'a, S: AsRef<str>, Message> {
|
||||||
text_line_height: text::LineHeight,
|
text_line_height: text::LineHeight,
|
||||||
#[setters(strip_option)]
|
#[setters(strip_option)]
|
||||||
font: Option<crate::font::Font>,
|
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.
|
/// The default gap.
|
||||||
pub const DEFAULT_GAP: f32 = 4.0;
|
pub const DEFAULT_GAP: f32 = 4.0;
|
||||||
|
|
||||||
|
|
@ -52,10 +75,10 @@ impl<'a, S: AsRef<str>, Message> Dropdown<'a, S, Message> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
selections: &'a [S],
|
selections: &'a [S],
|
||||||
selected: Option<usize>,
|
selected: Option<usize>,
|
||||||
on_selected: impl Fn(usize) -> Message + 'a,
|
on_selected: impl Fn(usize) -> Message + 'static + Send + Sync,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
on_selected: Box::new(on_selected),
|
on_selected: Arc::new(on_selected),
|
||||||
selections,
|
selections,
|
||||||
icons: &[],
|
icons: &[],
|
||||||
selected,
|
selected,
|
||||||
|
|
@ -65,12 +88,73 @@ impl<'a, S: AsRef<str>, Message> Dropdown<'a, S, Message> {
|
||||||
text_size: None,
|
text_size: None,
|
||||||
text_line_height: text::LineHeight::Relative(1.2),
|
text_line_height: text::LineHeight::Relative(1.2),
|
||||||
font: None,
|
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>
|
impl<
|
||||||
for Dropdown<'a, S, Message>
|
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 {
|
fn tag(&self) -> tree::Tag {
|
||||||
tree::Tag::of::<State>()
|
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>,
|
shell: &mut Shell<'_, Message>,
|
||||||
_viewport: &Rectangle,
|
_viewport: &Rectangle,
|
||||||
) -> event::Status {
|
) -> event::Status {
|
||||||
update(
|
update::<S, Message, AppMessage>(
|
||||||
&event,
|
&event,
|
||||||
layout,
|
layout,
|
||||||
cursor,
|
cursor,
|
||||||
shell,
|
shell,
|
||||||
self.on_selected.as_ref(),
|
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||||
|
self.positioner.clone(),
|
||||||
|
self.on_selected.clone(),
|
||||||
self.selected,
|
self.selected,
|
||||||
self.selections,
|
self.selections,
|
||||||
|| tree.state.downcast_mut::<State>(),
|
|| 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,
|
cursor: mouse::Cursor,
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
) {
|
) {
|
||||||
let font = self.font.unwrap_or_else(|| crate::font::default());
|
let font = self.font.unwrap_or_else(crate::font::default);
|
||||||
draw(
|
draw(
|
||||||
renderer,
|
renderer,
|
||||||
theme,
|
theme,
|
||||||
|
|
@ -211,6 +306,11 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
|
||||||
renderer: &crate::Renderer,
|
renderer: &crate::Renderer,
|
||||||
translation: Vector,
|
translation: Vector,
|
||||||
) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
|
) -> 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>();
|
let state = tree.state.downcast_mut::<State>();
|
||||||
|
|
||||||
overlay(
|
overlay(
|
||||||
|
|
@ -225,8 +325,9 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
|
||||||
self.selections,
|
self.selections,
|
||||||
self.icons,
|
self.icons,
|
||||||
self.selected,
|
self.selected,
|
||||||
&self.on_selected,
|
self.on_selected.as_ref(),
|
||||||
translation,
|
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>>
|
impl<
|
||||||
for crate::Element<'a, Message>
|
'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)
|
Self::new(pick_list)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The local state of a [`Dropdown`].
|
/// The local state of a [`Dropdown`].
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
icon: Option<svg::Handle>,
|
icon: Option<svg::Handle>,
|
||||||
menu: menu::State,
|
menu: menu::State,
|
||||||
keyboard_modifiers: keyboard::Modifiers,
|
keyboard_modifiers: keyboard::Modifiers,
|
||||||
is_open: bool,
|
is_open: Arc<AtomicBool>,
|
||||||
hovered_option: Option<usize>,
|
hovered_option: Arc<Mutex<Option<usize>>>,
|
||||||
hashes: Vec<u64>,
|
hashes: Vec<u64>,
|
||||||
selections: Vec<crate::Plain>,
|
selections: Vec<crate::Plain>,
|
||||||
|
popup_id: window::Id,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
|
@ -276,10 +384,11 @@ impl State {
|
||||||
},
|
},
|
||||||
menu: menu::State::default(),
|
menu: menu::State::default(),
|
||||||
keyboard_modifiers: keyboard::Modifiers::default(),
|
keyboard_modifiers: keyboard::Modifiers::default(),
|
||||||
is_open: false,
|
is_open: Arc::new(AtomicBool::new(false)),
|
||||||
hovered_option: None,
|
hovered_option: Arc::new(Mutex::new(None)),
|
||||||
selections: Vec::new(),
|
selections: Vec::new(),
|
||||||
hashes: Vec::new(),
|
hashes: Vec::new(),
|
||||||
|
popup_id: window::Id::unique(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -316,7 +425,7 @@ pub fn layout(
|
||||||
bounds: Size::new(f32::MAX, f32::MAX),
|
bounds: Size::new(f32::MAX, f32::MAX),
|
||||||
size: iced::Pixels(text_size),
|
size: iced::Pixels(text_size),
|
||||||
line_height: text_line_height,
|
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,
|
horizontal_alignment: alignment::Horizontal::Left,
|
||||||
vertical_alignment: alignment::Vertical::Top,
|
vertical_alignment: alignment::Vertical::Top,
|
||||||
shaping: text::Shaping::Advanced,
|
shaping: text::Shaping::Advanced,
|
||||||
|
|
@ -348,32 +457,136 @@ pub fn layout(
|
||||||
|
|
||||||
/// Processes an [`Event`] and updates the [`State`] of a [`Dropdown`]
|
/// Processes an [`Event`] and updates the [`State`] of a [`Dropdown`]
|
||||||
/// accordingly.
|
/// accordingly.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
|
||||||
pub fn update<'a, S: AsRef<str>, Message>(
|
pub fn update<
|
||||||
|
'a,
|
||||||
|
S: AsRef<str> + Send + Sync + Clone + 'static,
|
||||||
|
Message: Clone + 'static,
|
||||||
|
AppMessage: Clone + 'static,
|
||||||
|
>(
|
||||||
event: &Event,
|
event: &Event,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
shell: &mut Shell<'_, Message>,
|
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>,
|
selected: Option<usize>,
|
||||||
selections: &[S],
|
selections: &[S],
|
||||||
state: impl FnOnce() -> &'a mut State,
|
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 {
|
) -> event::Status {
|
||||||
match event {
|
match event {
|
||||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||||
let state = state();
|
let state = state();
|
||||||
|
let is_open = state.is_open.load(Ordering::Relaxed);
|
||||||
if state.is_open {
|
if is_open {
|
||||||
// Event wasn't processed by overlay, so cursor was clicked either outside it's
|
// 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.
|
// 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
|
event::Status::Captured
|
||||||
} else if cursor.is_over(layout.bounds()) {
|
} else if cursor.is_over(layout.bounds()) {
|
||||||
state.is_open = true;
|
state.is_open.store(true, Ordering::Relaxed);
|
||||||
state.hovered_option = selected;
|
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
|
event::Status::Captured
|
||||||
} else {
|
} else {
|
||||||
event::Status::Ignored
|
event::Status::Ignored
|
||||||
|
|
@ -383,11 +596,9 @@ pub fn update<'a, S: AsRef<str>, Message>(
|
||||||
delta: mouse::ScrollDelta::Lines { .. },
|
delta: mouse::ScrollDelta::Lines { .. },
|
||||||
}) => {
|
}) => {
|
||||||
let state = state();
|
let state = state();
|
||||||
|
let is_open = state.is_open.load(Ordering::Relaxed);
|
||||||
|
|
||||||
if state.keyboard_modifiers.command()
|
if state.keyboard_modifiers.command() && cursor.is_over(layout.bounds()) && !is_open {
|
||||||
&& cursor.is_over(layout.bounds())
|
|
||||||
&& !state.is_open
|
|
||||||
{
|
|
||||||
let next_index = selected.map(|index| index + 1).unwrap_or_default();
|
let next_index = selected.map(|index| index + 1).unwrap_or_default();
|
||||||
|
|
||||||
if selections.len() < next_index {
|
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`].
|
/// Returns the current overlay of a [`Dropdown`].
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[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<'_>,
|
layout: Layout<'_>,
|
||||||
_renderer: &crate::Renderer,
|
_renderer: &crate::Renderer,
|
||||||
state: &'a mut State,
|
state: &'a mut State,
|
||||||
|
|
@ -439,22 +713,27 @@ pub fn overlay<'a, S: AsRef<str>, Message: 'a>(
|
||||||
selected_option: Option<usize>,
|
selected_option: Option<usize>,
|
||||||
on_selected: &'a dyn Fn(usize) -> Message,
|
on_selected: &'a dyn Fn(usize) -> Message,
|
||||||
translation: Vector,
|
translation: Vector,
|
||||||
) -> Option<overlay::Element<'a, Message, crate::Theme, crate::Renderer>> {
|
close_on_selected: Option<Message>,
|
||||||
if state.is_open {
|
) -> 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 bounds = layout.bounds();
|
||||||
|
|
||||||
let menu = Menu::new(
|
let menu = Menu::new(
|
||||||
&mut state.menu,
|
state.menu.clone(),
|
||||||
selections,
|
Cow::Borrowed(selections),
|
||||||
icons,
|
Cow::Borrowed(icons),
|
||||||
&mut state.hovered_option,
|
state.hovered_option.clone(),
|
||||||
selected_option,
|
selected_option,
|
||||||
|option| {
|
|option| {
|
||||||
state.is_open = false;
|
state.is_open.store(false, Ordering::Relaxed);
|
||||||
|
|
||||||
(on_selected)(option)
|
(on_selected)(option)
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
|
close_on_selected,
|
||||||
)
|
)
|
||||||
.width({
|
.width({
|
||||||
let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 {
|
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>
|
impl<Message: 'static + Clone> Widget<Message, crate::Theme, Renderer> for FlexRow<'_, Message> {
|
||||||
for FlexRow<'a, Message>
|
|
||||||
{
|
|
||||||
fn children(&self) -> Vec<Tree> {
|
fn children(&self) -> Vec<Tree> {
|
||||||
self.children.iter().map(Tree::new).collect()
|
self.children.iter().map(Tree::new).collect()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,12 @@ pub struct Grid<'a, Message> {
|
||||||
row: u16,
|
row: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<Message> Default for Grid<'_, Message> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a, Message> Grid<'a, Message> {
|
impl<'a, Message> Grid<'a, Message> {
|
||||||
pub const fn new() -> Self {
|
pub const fn new() -> Self {
|
||||||
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> {
|
fn children(&self) -> Vec<Tree> {
|
||||||
self.children.iter().map(Tree::new).collect()
|
self.children.iter().map(Tree::new).collect()
|
||||||
}
|
}
|
||||||
|
|
@ -303,6 +309,12 @@ pub struct Assignment {
|
||||||
pub(super) height: u16,
|
pub(super) height: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Assignment {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Assignment {
|
impl Assignment {
|
||||||
pub const fn new() -> Self {
|
pub const fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
|
||||||
|
|
@ -120,8 +120,8 @@ pub struct HeaderBarWidget<'a, Message> {
|
||||||
header_bar_inner: Element<'a, Message>,
|
header_bar_inner: Element<'a, Message>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer>
|
impl<Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
for HeaderBarWidget<'a, Message>
|
for HeaderBarWidget<'_, Message>
|
||||||
{
|
{
|
||||||
fn diff(&mut self, tree: &mut tree::Tree) {
|
fn diff(&mut self, tree: &mut tree::Tree) {
|
||||||
tree.diff_children(&mut [&mut self.header_bar_inner]);
|
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::Spacious => 48.0,
|
||||||
Density::Standard => 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.
|
// Creates the headerbar widget.
|
||||||
let mut widget = widget::row::with_capacity(3)
|
let mut widget = widget::row::with_capacity(3)
|
||||||
// If elements exist in the start region, append them here.
|
// 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)
|
.align_y(iced::Alignment::Center)
|
||||||
.apply(widget::container)
|
.apply(widget::container)
|
||||||
.align_x(iced::Alignment::Start)
|
.align_x(iced::Alignment::Start)
|
||||||
.width(Length::Shrink),
|
.width(Length::FillPortion(portion)),
|
||||||
)
|
)
|
||||||
// If elements exist in the center region, use them here.
|
// If elements exist in the center region, use them here.
|
||||||
// This will otherwise use the title as a widget if a title was defined.
|
// 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)
|
.align_y(iced::Alignment::Center)
|
||||||
.apply(widget::container)
|
.apply(widget::container)
|
||||||
.align_x(iced::Alignment::End)
|
.align_x(iced::Alignment::End)
|
||||||
.width(Length::Shrink),
|
.width(if center_empty {
|
||||||
|
Length::Fill
|
||||||
|
} else {
|
||||||
|
Length::FillPortion(portion)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.align_y(iced::Alignment::Center)
|
.align_y(iced::Alignment::Center)
|
||||||
.height(Length::Fixed(height))
|
.height(Length::Fixed(height))
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ impl Icon {
|
||||||
self.height
|
self.height
|
||||||
.unwrap_or_else(|| Length::Fixed(f32::from(self.size))),
|
.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)
|
.content_fit(self.content_fit)
|
||||||
.into()
|
.into()
|
||||||
};
|
};
|
||||||
|
|
@ -100,7 +100,7 @@ impl Icon {
|
||||||
self.height
|
self.height
|
||||||
.unwrap_or_else(|| Length::Fixed(f32::from(self.size))),
|
.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)
|
.content_fit(self.content_fit)
|
||||||
.symbolic(self.handle.symbolic)
|
.symbolic(self.handle.symbolic)
|
||||||
.into()
|
.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 {
|
fn from(builder: Named) -> Self {
|
||||||
builder.icon().into()
|
builder.icon().into()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,8 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
for IdContainer<'a, Message, Theme, Renderer>
|
for IdContainer<'_, Message, Theme, Renderer>
|
||||||
where
|
where
|
||||||
Renderer: iced_core::Renderer,
|
Renderer: iced_core::Renderer,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -138,8 +138,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message, Renderer> Widget<Message, Theme, Renderer>
|
impl<Message, Renderer> Widget<Message, Theme, Renderer> for LayerContainer<'_, Message, Renderer>
|
||||||
for LayerContainer<'a, Message, Renderer>
|
|
||||||
where
|
where
|
||||||
Renderer: iced_core::Renderer,
|
Renderer: iced_core::Renderer,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ pub struct ListColumn<'a, Message> {
|
||||||
children: Vec<Element<'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 {
|
fn default() -> Self {
|
||||||
let cosmic_theme::Spacing {
|
let cosmic_theme::Spacing {
|
||||||
space_xxs, space_m, ..
|
space_xxs, space_m, ..
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@
|
||||||
//!
|
//!
|
||||||
|
|
||||||
pub mod action;
|
pub mod action;
|
||||||
|
|
||||||
pub use action::MenuAction as Action;
|
pub use action::MenuAction as Action;
|
||||||
|
|
||||||
mod flex;
|
mod flex;
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ impl KeyBind {
|
||||||
pub fn matches(&self, modifiers: Modifiers, key: &Key) -> bool {
|
pub fn matches(&self, modifiers: Modifiers, key: &Key) -> bool {
|
||||||
let key_eq = match (key, &self.key) {
|
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
|
// 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),
|
(a, b) => a.eq(b),
|
||||||
};
|
};
|
||||||
key_eq
|
key_eq
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,8 @@ impl Default for MenuBarState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn menu_roots_children<'a, Message, Renderer>(
|
pub(crate) fn menu_roots_children<Message, Renderer>(
|
||||||
menu_roots: &Vec<MenuTree<'a, Message, Renderer>>,
|
menu_roots: &Vec<MenuTree<'_, Message, Renderer>>,
|
||||||
) -> Vec<Tree>
|
) -> Vec<Tree>
|
||||||
where
|
where
|
||||||
Renderer: renderer::Renderer,
|
Renderer: renderer::Renderer,
|
||||||
|
|
@ -95,8 +95,8 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(invalid_reference_casting)]
|
#[allow(invalid_reference_casting)]
|
||||||
pub(crate) fn menu_roots_diff<'a, Message, Renderer>(
|
pub(crate) fn menu_roots_diff<Message, Renderer>(
|
||||||
menu_roots: &mut Vec<MenuTree<'a, Message, Renderer>>,
|
menu_roots: &mut Vec<MenuTree<'_, Message, Renderer>>,
|
||||||
tree: &mut Tree,
|
tree: &mut Tree,
|
||||||
) where
|
) where
|
||||||
Renderer: renderer::Renderer,
|
Renderer: renderer::Renderer,
|
||||||
|
|
@ -280,8 +280,7 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<'a, Message, Renderer> Widget<Message, crate::Theme, Renderer>
|
impl<Message, Renderer> Widget<Message, crate::Theme, Renderer> for MenuBar<'_, Message, Renderer>
|
||||||
for MenuBar<'a, Message, Renderer>
|
|
||||||
where
|
where
|
||||||
Renderer: renderer::Renderer,
|
Renderer: renderer::Renderer,
|
||||||
{
|
{
|
||||||
|
|
@ -366,6 +365,8 @@ where
|
||||||
if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) {
|
if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) {
|
||||||
state.view_cursor = view_cursor;
|
state.view_cursor = view_cursor;
|
||||||
state.open = true;
|
state.open = true;
|
||||||
|
// #[cfg(feature = "wayland")]
|
||||||
|
// TODO emit Message to open menu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
|
|
@ -437,6 +438,9 @@ where
|
||||||
_renderer: &Renderer,
|
_renderer: &Renderer,
|
||||||
translation: Vector,
|
translation: Vector,
|
||||||
) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
|
) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
|
||||||
|
// #[cfg(feature = "wayland")]
|
||||||
|
// return None;
|
||||||
|
|
||||||
let state = tree.state.downcast_ref::<MenuBarState>();
|
let state = tree.state.downcast_ref::<MenuBarState>();
|
||||||
if !state.open {
|
if !state.open {
|
||||||
return None;
|
return None;
|
||||||
|
|
|
||||||
|
|
@ -447,7 +447,7 @@ where
|
||||||
pub(crate) style: &'b <crate::Theme as StyleSheet>::Style,
|
pub(crate) style: &'b <crate::Theme as StyleSheet>::Style,
|
||||||
pub(crate) position: Point,
|
pub(crate) position: Point,
|
||||||
}
|
}
|
||||||
impl<'a, 'b, Message, Renderer> Menu<'a, 'b, Message, Renderer>
|
impl<'b, Message, Renderer> Menu<'_, 'b, Message, Renderer>
|
||||||
where
|
where
|
||||||
Renderer: renderer::Renderer,
|
Renderer: renderer::Renderer,
|
||||||
{
|
{
|
||||||
|
|
@ -455,8 +455,8 @@ where
|
||||||
overlay::Element::new(Box::new(self))
|
overlay::Element::new(Box::new(self))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, crate::Theme, Renderer>
|
impl<Message, Renderer> overlay::Overlay<Message, crate::Theme, Renderer>
|
||||||
for Menu<'a, 'b, Message, Renderer>
|
for Menu<'_, '_, Message, Renderer>
|
||||||
where
|
where
|
||||||
Renderer: renderer::Renderer,
|
Renderer: renderer::Renderer,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ use std::rc::Rc;
|
||||||
use iced_widget::core::{renderer, Element};
|
use iced_widget::core::{renderer, Element};
|
||||||
|
|
||||||
use crate::iced_core::{Alignment, Length};
|
use crate::iced_core::{Alignment, Length};
|
||||||
use crate::widget::icon;
|
|
||||||
use crate::widget::menu::action::MenuAction;
|
use crate::widget::menu::action::MenuAction;
|
||||||
use crate::widget::menu::key_bind::KeyBind;
|
use crate::widget::menu::key_bind::KeyBind;
|
||||||
|
use crate::widget::{icon, Button};
|
||||||
use crate::{theme, widget};
|
use crate::{theme, widget};
|
||||||
|
|
||||||
/// Nested menu is essentially a tree of items, a menu is a collection of items
|
/// 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.
|
/// - A button for the root menu item.
|
||||||
pub fn menu_root<'a, Message, Renderer: renderer::Renderer>(
|
pub fn menu_root<'a, Message, Renderer: renderer::Renderer>(
|
||||||
label: impl Into<Cow<'a, str>> + 'a,
|
label: impl Into<Cow<'a, str>> + 'a,
|
||||||
) -> iced::Element<'a, Message, crate::Theme, Renderer>
|
) -> Button<'a, Message>
|
||||||
where
|
where
|
||||||
Element<'a, Message, crate::Theme, Renderer>: From<widget::Button<'a, Message>>,
|
Element<'a, Message, crate::Theme, Renderer>: From<widget::Button<'a, Message>>,
|
||||||
{
|
{
|
||||||
widget::button::custom(widget::text(label))
|
widget::button::custom(widget::text(label))
|
||||||
.padding([4, 12])
|
.padding([4, 12])
|
||||||
.class(theme::Button::MenuRoot)
|
.class(theme::Button::MenuRoot)
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a list of menu items from a vector of `MenuItem`.
|
/// Create a list of menu items from a vector of `MenuItem`.
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,14 @@ pub mod aspect_ratio;
|
||||||
#[cfg(feature = "autosize")]
|
#[cfg(feature = "autosize")]
|
||||||
pub mod 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;
|
pub mod button;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use button::{Button, IconButton, LinkButton, TextButton};
|
pub use button::{Button, IconButton, LinkButton, TextButton};
|
||||||
|
|
@ -335,9 +343,12 @@ pub use toggler::toggler;
|
||||||
|
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use tooltip::{tooltip, Tooltip};
|
pub use tooltip::{tooltip, Tooltip};
|
||||||
|
|
||||||
|
#[cfg(all(feature = "wayland", feature = "winit"))]
|
||||||
|
pub mod wayland;
|
||||||
|
|
||||||
pub mod tooltip {
|
pub mod tooltip {
|
||||||
use crate::Element;
|
use crate::Element;
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
pub use iced::widget::tooltip::Position;
|
pub use iced::widget::tooltip::Position;
|
||||||
|
|
||||||
|
|
@ -362,6 +373,10 @@ pub mod warning;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use warning::*;
|
pub use warning::*;
|
||||||
|
|
||||||
|
pub mod wrapper;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use wrapper::*;
|
||||||
|
|
||||||
#[cfg(feature = "markdown")]
|
#[cfg(feature = "markdown")]
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use iced::widget::markdown;
|
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 {
|
fn from(nav_bar_toggle: NavBarToggle<Message>) -> Self {
|
||||||
let icon = if nav_bar_toggle.active {
|
let icon = if nav_bar_toggle.active {
|
||||||
widget::icon::from_svg_bytes(
|
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
|
// TODO More options for positioning similar to GdkPopup, xdg_popup
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message: Clone, Renderer> Widget<Message, crate::Theme, Renderer>
|
impl<Message: Clone, Renderer> Widget<Message, crate::Theme, Renderer>
|
||||||
for Popover<'a, Message, Renderer>
|
for Popover<'_, Message, Renderer>
|
||||||
where
|
where
|
||||||
Renderer: iced_core::Renderer,
|
Renderer: iced_core::Renderer,
|
||||||
{
|
{
|
||||||
|
|
@ -305,8 +305,8 @@ pub struct Overlay<'a, 'b, Message, Renderer> {
|
||||||
pos: Point,
|
pos: Point,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, crate::Theme, Renderer>
|
impl<Message, Renderer> overlay::Overlay<Message, crate::Theme, Renderer>
|
||||||
for Overlay<'a, 'b, Message, Renderer>
|
for Overlay<'_, '_, Message, Renderer>
|
||||||
where
|
where
|
||||||
Message: Clone,
|
Message: Clone,
|
||||||
Renderer: iced_core::Renderer,
|
Renderer: iced_core::Renderer,
|
||||||
|
|
@ -425,7 +425,7 @@ where
|
||||||
) -> Option<overlay::Element<'c, Message, crate::Theme, Renderer>> {
|
) -> Option<overlay::Element<'c, Message, crate::Theme, Renderer>> {
|
||||||
self.content
|
self.content
|
||||||
.as_widget_mut()
|
.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
|
where
|
||||||
Message: Clone,
|
Message: Clone,
|
||||||
Renderer: iced_core::Renderer,
|
Renderer: iced_core::Renderer,
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ where
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
limits: &layout::Limits,
|
limits: &layout::Limits,
|
||||||
) -> layout::Node {
|
) -> layout::Node {
|
||||||
let layout = self.container.layout(
|
self.container.layout(
|
||||||
tree,
|
tree,
|
||||||
renderer,
|
renderer,
|
||||||
if self.ignore_bounds {
|
if self.ignore_bounds {
|
||||||
|
|
@ -217,9 +217,7 @@ where
|
||||||
} else {
|
} else {
|
||||||
limits
|
limits
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
layout
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn operate(
|
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)
|
SegmentedButton::new(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, SelectionMode, Message> SegmentedVariant
|
impl<SelectionMode, Message> SegmentedVariant
|
||||||
for SegmentedButton<'a, Horizontal, SelectionMode, Message>
|
for SegmentedButton<'_, Horizontal, SelectionMode, Message>
|
||||||
where
|
where
|
||||||
Model<SelectionMode>: Selectable,
|
Model<SelectionMode>: Selectable,
|
||||||
SelectionMode: Default,
|
SelectionMode: Default,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ pub struct EntityMut<'a, SelectionMode: Default> {
|
||||||
pub(super) model: &'a mut Model<SelectionMode>,
|
pub(super) model: &'a mut Model<SelectionMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, SelectionMode: Default> EntityMut<'a, SelectionMode>
|
impl<SelectionMode: Default> EntityMut<'_, SelectionMode>
|
||||||
where
|
where
|
||||||
Model<SelectionMode>: Selectable,
|
Model<SelectionMode>: Selectable,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,8 @@ where
|
||||||
SegmentedButton::new(model)
|
SegmentedButton::new(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, SelectionMode, Message> SegmentedVariant
|
impl<SelectionMode, Message> SegmentedVariant
|
||||||
for SegmentedButton<'a, Vertical, SelectionMode, Message>
|
for SegmentedButton<'_, Vertical, SelectionMode, Message>
|
||||||
where
|
where
|
||||||
Model<SelectionMode>: Selectable,
|
Model<SelectionMode>: Selectable,
|
||||||
SelectionMode: Default,
|
SelectionMode: Default,
|
||||||
|
|
|
||||||
|
|
@ -547,8 +547,8 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Variant, SelectionMode, Message> Widget<Message, crate::Theme, Renderer>
|
impl<Variant, SelectionMode, Message> Widget<Message, crate::Theme, Renderer>
|
||||||
for SegmentedButton<'a, Variant, SelectionMode, Message>
|
for SegmentedButton<'_, Variant, SelectionMode, Message>
|
||||||
where
|
where
|
||||||
Self: SegmentedVariant,
|
Self: SegmentedVariant,
|
||||||
Model<SelectionMode>: Selectable,
|
Model<SelectionMode>: Selectable,
|
||||||
|
|
@ -562,7 +562,7 @@ where
|
||||||
if let Some(ref context_menu) = self.context_menu {
|
if let Some(ref context_menu) = self.context_menu {
|
||||||
let mut tree = Tree::empty();
|
let mut tree = Tree::empty();
|
||||||
tree.state = tree::State::new(MenuBarState::default());
|
tree.state = tree::State::new(MenuBarState::default());
|
||||||
tree.children = menu_roots_children(&context_menu);
|
tree.children = menu_roots_children(context_menu);
|
||||||
children.push(tree);
|
children.push(tree);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -719,7 +719,7 @@ where
|
||||||
let on_dnd_enter =
|
let on_dnd_enter =
|
||||||
self.on_dnd_enter
|
self.on_dnd_enter
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.zip(entity.clone())
|
.zip(entity)
|
||||||
.map(|(on_enter, entity)| {
|
.map(|(on_enter, entity)| {
|
||||||
move |_, _, mime_types| on_enter(entity, mime_types)
|
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.
|
/// A section with a pre-defined list column.
|
||||||
pub fn with_column<'a, Message: 'static>(
|
pub fn with_column<Message: 'static>(children: ListColumn<'_, Message>) -> Section<'_, Message> {
|
||||||
children: ListColumn<'a, Message>,
|
|
||||||
) -> Section<'a, Message> {
|
|
||||||
Section {
|
Section {
|
||||||
title: Cow::Borrowed(""),
|
title: Cow::Borrowed(""),
|
||||||
children,
|
children,
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,10 @@ use crate::{
|
||||||
Element,
|
Element,
|
||||||
};
|
};
|
||||||
use apply::Apply;
|
use apply::Apply;
|
||||||
use derive_setters::Setters;
|
|
||||||
use iced::{alignment::Horizontal, Border, Shadow};
|
|
||||||
use iced::{Alignment, Length};
|
use iced::{Alignment, Length};
|
||||||
use std::marker::PhantomData;
|
use iced::{Border, Shadow};
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::ops::{Add, Sub};
|
use std::ops::{Add, Sub};
|
||||||
use std::{borrow::Cow, fmt::Display};
|
|
||||||
|
|
||||||
/// Horizontal spin button widget.
|
/// Horizontal spin button widget.
|
||||||
pub fn spin_button<'a, T, M>(
|
pub fn spin_button<'a, T, M>(
|
||||||
|
|
@ -153,9 +151,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn horizontal_variant<'a, T, Message>(
|
fn horizontal_variant<T, Message>(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message>
|
||||||
spin_button: SpinButton<'a, T, Message>,
|
|
||||||
) -> Element<'a, Message>
|
|
||||||
where
|
where
|
||||||
Message: Clone + 'static,
|
Message: Clone + 'static,
|
||||||
T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
|
T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
|
||||||
|
|
@ -193,7 +189,7 @@ where
|
||||||
.into()
|
.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
|
where
|
||||||
Message: Clone + 'static,
|
Message: Clone + 'static,
|
||||||
T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
|
T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ use super::style::StyleSheet;
|
||||||
pub use super::value::Value;
|
pub use super::value::Value;
|
||||||
|
|
||||||
use apply::Apply;
|
use apply::Apply;
|
||||||
use cosmic_theme::Theme;
|
|
||||||
use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent};
|
use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent};
|
||||||
use iced::clipboard::mime::AsMimeTypes;
|
use iced::clipboard::mime::AsMimeTypes;
|
||||||
use iced::Limits;
|
use iced::Limits;
|
||||||
|
|
@ -40,10 +39,6 @@ use iced_core::{
|
||||||
Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size,
|
Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size,
|
||||||
Vector, Widget,
|
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};
|
use iced_runtime::{task, Action, Task};
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
|
|
@ -200,7 +195,7 @@ pub struct TextInput<'a, Message> {
|
||||||
error: Option<Cow<'a, str>>,
|
error: Option<Cow<'a, str>>,
|
||||||
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
|
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
|
||||||
on_paste: 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>>,
|
on_toggle_edit: Option<Box<dyn Fn(bool) -> Message + 'a>>,
|
||||||
leading_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>,
|
leading_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>,
|
||||||
trailing_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,
|
line_height: text::LineHeight,
|
||||||
helper_line_height: text::LineHeight,
|
helper_line_height: text::LineHeight,
|
||||||
always_active: bool,
|
always_active: bool,
|
||||||
|
/// The text input tracks and manages the input value in its state.
|
||||||
|
manage_value: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message> TextInput<'a, Message>
|
impl<'a, Message> TextInput<'a, Message>
|
||||||
|
|
@ -255,6 +252,7 @@ where
|
||||||
label: None,
|
label: None,
|
||||||
helper_text: None,
|
helper_text: None,
|
||||||
always_active: false,
|
always_active: false,
|
||||||
|
manage_value: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,14 +338,24 @@ where
|
||||||
|
|
||||||
/// Sets the message that should be produced when the [`TextInput`] is
|
/// Sets the message that should be produced when the [`TextInput`] is
|
||||||
/// focused and the enter key is pressed.
|
/// focused and the enter key is pressed.
|
||||||
pub fn on_submit(self, message: Message) -> Self {
|
pub fn on_submit<F>(self, callback: F) -> Self
|
||||||
self.on_submit_maybe(Some(message))
|
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
|
/// Maybe sets the message that should be produced when the [`TextInput`] is
|
||||||
/// focused and the enter key is pressed.
|
/// focused and the enter key is pressed.
|
||||||
pub fn on_submit_maybe(mut self, message: Option<Message>) -> Self {
|
pub fn on_submit_maybe<F>(mut self, callback: Option<F>) -> Self
|
||||||
self.on_submit = message;
|
where
|
||||||
|
F: 'a + Fn(String) -> Message,
|
||||||
|
{
|
||||||
|
if let Some(callback) = callback {
|
||||||
|
self.on_submit = Some(Box::new(callback));
|
||||||
|
} else {
|
||||||
|
self.on_submit = None;
|
||||||
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -416,6 +424,12 @@ where
|
||||||
self
|
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
|
/// Draws the [`TextInput`] with the given [`Renderer`], overriding its
|
||||||
/// [`Value`] if provided.
|
/// [`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
|
where
|
||||||
Message: Clone + 'static,
|
Message: Clone + 'static,
|
||||||
{
|
{
|
||||||
|
|
@ -526,9 +540,14 @@ where
|
||||||
|
|
||||||
fn diff(&mut self, tree: &mut Tree) {
|
fn diff(&mut self, tree: &mut Tree) {
|
||||||
let state = tree.state.downcast_mut::<State>();
|
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
|
// 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.last_click = None;
|
||||||
state.is_focused = None;
|
state.is_focused = None;
|
||||||
state.is_pasting = None;
|
state.is_pasting = None;
|
||||||
|
|
@ -581,13 +600,10 @@ where
|
||||||
// if the previous state was at the end of the text, keep it there
|
// if the previous state was at the end of the text, keep it there
|
||||||
let old_value = Value::new(&old_value);
|
let old_value = Value::new(&old_value);
|
||||||
if state.is_focused.is_some() {
|
if state.is_focused.is_some() {
|
||||||
match state.cursor.state(&old_value) {
|
if let cursor::State::Index(index) = state.cursor.state(&old_value) {
|
||||||
cursor::State::Index(index) => {
|
if index == old_value.len() {
|
||||||
if index == old_value.len() {
|
state.cursor.move_to(self.value.len());
|
||||||
state.cursor.move_to(self.value.len());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -597,6 +613,11 @@ where
|
||||||
state.is_focused = None;
|
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
|
let mut children: Vec<_> = self
|
||||||
.leading_icon
|
.leading_icon
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
|
|
@ -779,7 +800,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if tree.children.len() > 0 {
|
if !tree.children.is_empty() {
|
||||||
let index = tree.children.len() - 1;
|
let index = tree.children.len() - 1;
|
||||||
if let (Some(trailing_icon), Some(tree)) =
|
if let (Some(trailing_icon), Some(tree)) =
|
||||||
(self.trailing_icon.as_mut(), tree.children.get_mut(index))
|
(self.trailing_icon.as_mut(), tree.children.get_mut(index))
|
||||||
|
|
@ -824,13 +845,14 @@ where
|
||||||
self.is_editable,
|
self.is_editable,
|
||||||
self.on_input.as_deref(),
|
self.on_input.as_deref(),
|
||||||
self.on_paste.as_deref(),
|
self.on_paste.as_deref(),
|
||||||
&self.on_submit,
|
self.on_submit.as_deref(),
|
||||||
self.on_toggle_edit.as_deref(),
|
self.on_toggle_edit.as_deref(),
|
||||||
|| tree.state.downcast_mut::<State>(),
|
|| tree.state.downcast_mut::<State>(),
|
||||||
self.on_create_dnd_source.as_deref(),
|
self.on_create_dnd_source.as_deref(),
|
||||||
dnd_id,
|
dnd_id,
|
||||||
line_height,
|
line_height,
|
||||||
layout,
|
layout,
|
||||||
|
self.manage_value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -856,7 +878,7 @@ where
|
||||||
&self.placeholder,
|
&self.placeholder,
|
||||||
self.size,
|
self.size,
|
||||||
self.font,
|
self.font,
|
||||||
self.on_input.is_none(),
|
self.on_input.is_none() && !self.manage_value,
|
||||||
self.is_secure,
|
self.is_secure,
|
||||||
self.leading_icon.as_ref(),
|
self.leading_icon.as_ref(),
|
||||||
self.trailing_icon.as_ref(),
|
self.trailing_icon.as_ref(),
|
||||||
|
|
@ -925,7 +947,11 @@ where
|
||||||
}
|
}
|
||||||
let mut children = layout.children();
|
let mut children = layout.children();
|
||||||
let layout = children.next().unwrap();
|
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> {
|
fn id(&self) -> Option<Id> {
|
||||||
|
|
@ -1236,13 +1262,14 @@ pub fn update<'a, Message: 'static>(
|
||||||
is_editable: bool,
|
is_editable: bool,
|
||||||
on_input: Option<&dyn Fn(String) -> Message>,
|
on_input: Option<&dyn Fn(String) -> Message>,
|
||||||
on_paste: 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>,
|
on_toggle_edit: Option<&dyn Fn(bool) -> Message>,
|
||||||
state: impl FnOnce() -> &'a mut State,
|
state: impl FnOnce() -> &'a mut State,
|
||||||
#[allow(unused_variables)] on_start_dnd_source: Option<&dyn Fn(State) -> Message>,
|
#[allow(unused_variables)] on_start_dnd_source: Option<&dyn Fn(State) -> Message>,
|
||||||
#[allow(unused_variables)] dnd_id: u128,
|
#[allow(unused_variables)] dnd_id: u128,
|
||||||
line_height: text::LineHeight,
|
line_height: text::LineHeight,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
|
manage_value: bool,
|
||||||
) -> event::Status
|
) -> event::Status
|
||||||
where
|
where
|
||||||
Message: Clone,
|
Message: Clone,
|
||||||
|
|
@ -1264,7 +1291,7 @@ where
|
||||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||||
let state = state();
|
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())
|
cursor.position_over(layout.bounds())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -1299,7 +1326,7 @@ where
|
||||||
// single click that is on top of the selected text
|
// single click that is on top of the selected text
|
||||||
// is the click on 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 left = start.min(end);
|
||||||
let right = end.max(start);
|
let right = end.max(start);
|
||||||
|
|
||||||
|
|
@ -1339,8 +1366,11 @@ where
|
||||||
|
|
||||||
let contents = editor.contents();
|
let contents = editor.contents();
|
||||||
let unsecured_value = Value::new(&contents);
|
let unsecured_value = Value::new(&contents);
|
||||||
let message = (on_input)(contents);
|
state.tracked_value = unsecured_value.clone();
|
||||||
shell.publish(message);
|
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 {
|
if let Some(on_start_dnd) = on_start_dnd_source {
|
||||||
shell.publish(on_start_dnd(state.clone()));
|
shell.publish(on_start_dnd(state.clone()));
|
||||||
}
|
}
|
||||||
|
|
@ -1349,7 +1379,7 @@ where
|
||||||
iced_core::clipboard::start_dnd(
|
iced_core::clipboard::start_dnd(
|
||||||
clipboard,
|
clipboard,
|
||||||
false,
|
false,
|
||||||
id.map(|id| iced_core::clipboard::DndSource::Widget(id)),
|
id.map(iced_core::clipboard::DndSource::Widget),
|
||||||
Some(iced_core::clipboard::IconSurface::new(
|
Some(iced_core::clipboard::IconSurface::new(
|
||||||
Element::from(
|
Element::from(
|
||||||
TextInput::<'static, ()>::new("", input_text.clone())
|
TextInput::<'static, ()>::new("", input_text.clone())
|
||||||
|
|
@ -1531,7 +1561,7 @@ where
|
||||||
let state = state();
|
let state = state();
|
||||||
|
|
||||||
if let Some(focus) = &mut state.is_focused {
|
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;
|
return event::Status::Ignored;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1545,8 +1575,8 @@ where
|
||||||
|
|
||||||
match key {
|
match key {
|
||||||
keyboard::Key::Named(keyboard::key::Named::Enter) => {
|
keyboard::Key::Named(keyboard::key::Named::Enter) => {
|
||||||
if let Some(on_submit) = on_submit.clone() {
|
if let Some(on_submit) = on_submit {
|
||||||
shell.publish(on_submit);
|
shell.publish((on_submit)(unsecured_value.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::Backspace) => {
|
keyboard::Key::Named(keyboard::key::Named::Backspace) => {
|
||||||
|
|
@ -1566,9 +1596,11 @@ where
|
||||||
|
|
||||||
let contents = editor.contents();
|
let contents = editor.contents();
|
||||||
let unsecured_value = Value::new(&contents);
|
let unsecured_value = Value::new(&contents);
|
||||||
let message = (on_input)(editor.contents());
|
state.tracked_value = unsecured_value.clone();
|
||||||
shell.publish(message);
|
if let Some(on_input) = on_input {
|
||||||
|
let message = (on_input)(editor.contents());
|
||||||
|
shell.publish(message);
|
||||||
|
}
|
||||||
let value = if is_secure {
|
let value = if is_secure {
|
||||||
unsecured_value.secure()
|
unsecured_value.secure()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1592,8 +1624,12 @@ where
|
||||||
editor.delete();
|
editor.delete();
|
||||||
let contents = editor.contents();
|
let contents = editor.contents();
|
||||||
let unsecured_value = Value::new(&contents);
|
let unsecured_value = Value::new(&contents);
|
||||||
let message = (on_input)(contents);
|
if let Some(on_input) = on_input {
|
||||||
shell.publish(message);
|
let message = (on_input)(contents);
|
||||||
|
state.tracked_value = unsecured_value.clone();
|
||||||
|
shell.publish(message);
|
||||||
|
}
|
||||||
|
|
||||||
let value = if is_secure {
|
let value = if is_secure {
|
||||||
unsecured_value.secure()
|
unsecured_value.secure()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1671,10 +1707,12 @@ where
|
||||||
|
|
||||||
let mut editor = Editor::new(value, &mut state.cursor);
|
let mut editor = Editor::new(value, &mut state.cursor);
|
||||||
editor.delete();
|
editor.delete();
|
||||||
|
let content = editor.contents();
|
||||||
let message = (on_input)(editor.contents());
|
state.tracked_value = Value::new(&content);
|
||||||
|
if let Some(on_input) = on_input {
|
||||||
shell.publish(message);
|
let message = (on_input)(content);
|
||||||
|
shell.publish(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyboard::Key::Character(c)
|
keyboard::Key::Character(c)
|
||||||
|
|
@ -1699,13 +1737,16 @@ where
|
||||||
|
|
||||||
let contents = editor.contents();
|
let contents = editor.contents();
|
||||||
let unsecured_value = Value::new(&contents);
|
let unsecured_value = Value::new(&contents);
|
||||||
let message = if let Some(paste) = &on_paste {
|
state.tracked_value = unsecured_value.clone();
|
||||||
(paste)(contents)
|
if let Some(on_input) = on_input {
|
||||||
} else {
|
let message = if let Some(paste) = &on_paste {
|
||||||
(on_input)(contents)
|
(paste)(contents)
|
||||||
};
|
} else {
|
||||||
shell.publish(message);
|
(on_input)(contents)
|
||||||
|
};
|
||||||
|
|
||||||
|
shell.publish(message);
|
||||||
|
}
|
||||||
state.is_pasting = Some(content);
|
state.is_pasting = Some(content);
|
||||||
|
|
||||||
let value = if is_secure {
|
let value = if is_secure {
|
||||||
|
|
@ -1750,8 +1791,11 @@ where
|
||||||
}
|
}
|
||||||
let contents = editor.contents();
|
let contents = editor.contents();
|
||||||
let unsecured_value = Value::new(&contents);
|
let unsecured_value = Value::new(&contents);
|
||||||
let message = (on_input)(contents);
|
state.tracked_value = unsecured_value.clone();
|
||||||
shell.publish(message);
|
if let Some(on_input) = on_input {
|
||||||
|
let message = (on_input)(contents);
|
||||||
|
shell.publish(message);
|
||||||
|
}
|
||||||
|
|
||||||
focus.updated_at = Instant::now();
|
focus.updated_at = Instant::now();
|
||||||
LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
|
LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
|
||||||
|
|
@ -1926,7 +1970,7 @@ where
|
||||||
editor.paste(Value::new(content.as_str()));
|
editor.paste(Value::new(content.as_str()));
|
||||||
let contents = editor.contents();
|
let contents = editor.contents();
|
||||||
let unsecured_value = Value::new(&contents);
|
let unsecured_value = Value::new(&contents);
|
||||||
|
state.tracked_value = unsecured_value.clone();
|
||||||
if let Some(on_paste) = on_paste.as_ref() {
|
if let Some(on_paste) = on_paste.as_ref() {
|
||||||
let message = (on_paste)(contents);
|
let message = (on_paste)(contents);
|
||||||
shell.publish(message);
|
shell.publish(message);
|
||||||
|
|
@ -2408,6 +2452,7 @@ pub(crate) struct DndOfferState;
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
|
pub tracked_value: Value,
|
||||||
pub value: crate::Plain,
|
pub value: crate::Plain,
|
||||||
pub placeholder: crate::Plain,
|
pub placeholder: crate::Plain,
|
||||||
pub label: crate::Plain,
|
pub label: crate::Plain,
|
||||||
|
|
@ -2482,6 +2527,7 @@ impl State {
|
||||||
/// Creates a new [`State`], representing a focused [`TextInput`].
|
/// Creates a new [`State`], representing a focused [`TextInput`].
|
||||||
pub fn focused(is_secure: bool, is_read_only: bool) -> Self {
|
pub fn focused(is_secure: bool, is_read_only: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
tracked_value: Value::default(),
|
||||||
is_secure,
|
is_secure,
|
||||||
value: crate::Plain::default(),
|
value: crate::Plain::default(),
|
||||||
placeholder: crate::Plain::default(),
|
placeholder: crate::Plain::default(),
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||||
///
|
///
|
||||||
/// [`TextInput`]: crate::widget::TextInput
|
/// [`TextInput`]: crate::widget::TextInput
|
||||||
// TODO: Reduce allocations, cache results (?)
|
// TODO: Reduce allocations, cache results (?)
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Default, Debug, Clone, PartialEq)]
|
||||||
pub struct Value {
|
pub struct Value {
|
||||||
graphemes: Vec<String>,
|
graphemes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use std::rc::Rc;
|
||||||
|
|
||||||
use crate::widget::container;
|
use crate::widget::container;
|
||||||
use crate::widget::Column;
|
use crate::widget::Column;
|
||||||
use iced::{Padding, Task};
|
use iced::Task;
|
||||||
use iced_core::Element;
|
use iced_core::Element;
|
||||||
use slotmap::new_key_type;
|
use slotmap::new_key_type;
|
||||||
use slotmap::SlotMap;
|
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>
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
for Toaster<'a, Message, Theme, Renderer>
|
for Toaster<'_, Message, Theme, Renderer>
|
||||||
where
|
where
|
||||||
Renderer: iced_core::Renderer,
|
Renderer: iced_core::Renderer,
|
||||||
{
|
{
|
||||||
|
|
@ -191,8 +191,8 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b, Message, Theme, Renderer> Overlay<Message, Theme, Renderer>
|
impl<Message, Theme, Renderer> Overlay<Message, Theme, Renderer>
|
||||||
for ToasterOverlay<'a, 'b, Message, Theme, Renderer>
|
for ToasterOverlay<'_, '_, Message, Theme, Renderer>
|
||||||
where
|
where
|
||||||
Renderer: renderer::Renderer,
|
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