libcosmic updates

This commit is contained in:
Ashley Wulber 2024-10-16 20:36:46 -04:00 committed by Ashley Wulber
parent 9c62f19e4b
commit 0491c4baaa
91 changed files with 3550 additions and 2300 deletions

View file

@ -8,15 +8,22 @@ rust-version = "1.80"
name = "cosmic"
[features]
default = ["clipboard"]
# Accessibility support
a11y = ["iced/a11y", "iced_accessibility"]
# Builds support for animated images
animated-image = ["image", "dep:async-fs", "tokio?/io-util", "tokio?/fs"]
# XXX Use "a11y"; which is causing a panic currently
applet = ["wayland", "tokio", "cosmic-panel-config", "ron"]
# XXX autosize should not be used on winit windows unless dialogs
autosize = []
applet = [
"a11y",
"autosize",
"wayland",
"tokio",
"cosmic-panel-config",
"ron",
"multi-window",
]
applet-token = []
clipboard = ["iced_sctk?/clipboard"]
# Use the cosmic-settings-daemon for config handling
dbus-config = ["cosmic-config/dbus", "dep:zbus", "cosmic-settings-daemon"]
# Debug features
@ -61,7 +68,7 @@ wayland = [
"ashpd?/wayland",
"iced_runtime/wayland",
"iced/wayland",
"iced_sctk",
"iced_winit/wayland",
"cctk",
]
# multi-window support
@ -120,7 +127,7 @@ path = "cosmic-theme"
[dependencies.iced]
path = "./iced"
default-features = false
features = ["advanced", "image", "lazy", "svg", "web-colors"]
features = ["advanced", "image", "lazy", "svg", "web-colors", "tiny-skia"]
[dependencies.iced_runtime]
path = "./iced/runtime"
@ -146,13 +153,6 @@ optional = true
[dependencies.iced_tiny_skia]
path = "./iced/tiny_skia"
[dependencies.iced_style]
path = "./iced/style"
[dependencies.iced_sctk]
path = "./iced/sctk"
optional = true
[dependencies.iced_winit]
path = "./iced/winit"
optional = true
@ -189,3 +189,9 @@ dirs = "5.0.1"
[patch."https://github.com/pop-os/libcosmic"]
libcosmic = { path = "./" }
# FIXME update winit deps where necessary to use this
# [patch.crates-io]
[patch."https://github.com/pop-os/winit.git"]
# winit = { git = "https://github.com/rust-windowing/winit.git", rev = "241b7a80bba96c91fa3901729cd5dec66abb9be4" }
# winit = { path = "../../winit" }

View file

@ -3,7 +3,10 @@ use std::ops::Deref;
use crate::{CosmicConfigEntry, Update};
use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy};
use futures_util::SinkExt;
use iced_futures::futures::{self, future::pending, StreamExt};
use iced_futures::{
futures::{self, future::pending, Stream, StreamExt},
stream, Subscription,
};
pub async fn settings_daemon_proxy() -> zbus::Result<CosmicSettingsDaemonProxy<'static>> {
let conn = zbus::Connection::session().await?;
@ -58,13 +61,20 @@ pub fn watcher_subscription<T: CosmicConfigEntry + Send + Sync + Default + 'stat
config_id: &'static str,
is_state: bool,
) -> iced_futures::Subscription<Update<T>> {
let id = std::any::TypeId::of::<T>();
Subscription::run_with_id(id, watcher_stream(settings_daemon, config_id, is_state))
}
fn watcher_stream<T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone>(
settings_daemon: CosmicSettingsDaemonProxy<'static>,
config_id: &'static str,
is_state: bool,
) -> impl Stream<Item = Update<T>> {
enum Change {
Changes(Changed),
OwnerChanged(bool),
}
let id = std::any::TypeId::of::<T>();
iced_futures::subscription::channel((is_state, config_id, id), 5, move |mut tx| async move {
stream::channel(5, move |mut tx| async move {
let version = T::VERSION;
let Ok(cosmic_config) = (if is_state {

View file

@ -384,6 +384,7 @@ where
) -> (Vec<crate::Error>, Vec<&'static str>);
}
#[derive(Debug)]
pub struct Update<T> {
pub errors: Vec<crate::Error>,
pub keys: Vec<&'static str>,

View file

@ -1,5 +1,5 @@
use iced_futures::futures::SinkExt;
use iced_futures::{futures::channel::mpsc, subscription};
use iced_futures::futures::{SinkExt, Stream};
use iced_futures::{futures::channel::mpsc, stream};
use notify::RecommendedWatcher;
use std::{borrow::Cow, hash::Hash};
@ -24,17 +24,7 @@ pub fn config_subscription<
config_id: Cow<'static, str>,
config_version: u64,
) -> iced_futures::Subscription<crate::Update<T>> {
subscription::channel(id, 100, move |mut output| {
let config_id = config_id.clone();
async move {
let config_id = config_id.clone();
let mut state = ConfigState::Init(config_id, config_version, false);
loop {
state = start_listening(state, &mut output, id).await;
}
}
})
iced_futures::Subscription::run_with_id(id, watcher_stream(config_id, config_version, false))
}
pub fn config_state_subscription<
@ -45,26 +35,30 @@ pub fn config_state_subscription<
config_id: Cow<'static, str>,
config_version: u64,
) -> iced_futures::Subscription<crate::Update<T>> {
subscription::channel(id, 100, move |mut output| {
iced_futures::Subscription::run_with_id(id, watcher_stream(config_id, config_version, true))
}
fn watcher_stream<T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry>(
config_id: Cow<'static, str>,
config_version: u64,
is_state: bool,
) -> impl Stream<Item = crate::Update<T>> {
stream::channel(100, move |mut output| {
let config_id = config_id.clone();
async move {
let config_id = config_id.clone();
let mut state = ConfigState::Init(config_id, config_version, true);
let mut state = ConfigState::Init(config_id, config_version, is_state);
loop {
state = start_listening(state, &mut output, id).await;
state = start_listening::<T>(state, &mut output).await;
}
}
})
}
async fn start_listening<
I: Copy,
T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry,
>(
async fn start_listening<T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry>(
state: ConfigState<T>,
output: &mut mpsc::Sender<crate::Update<T>>,
id: I,
) -> ConfigState<T> {
use iced_futures::futures::{future::pending, StreamExt};

View file

@ -9,8 +9,20 @@ edition = "2021"
once_cell = "1"
rust-embed = "8.0.0"
tracing = "0.1"
env_logger = "0.10.0"
log = "0.4.17"
[dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic"
default-features = false
features = ["applet", "tokio", "wayland"]
features = [
"applet",
"applet-token",
"multi-window",
"tokio",
"wayland",
"winit",
"desktop",
"dbus-config",
"image",
]

View file

@ -3,5 +3,10 @@ use crate::window::Window;
mod window;
fn main() -> cosmic::iced::Result {
cosmic::applet::run::<Window>(true, ())
let env = env_logger::Env::default()
.filter_or("MY_LOG_LEVEL", "warn")
.write_style_or("MY_LOG_STYLE", "always");
env_logger::init_from_env(env);
cosmic::applet::run::<Window>(())
}

View file

@ -1,9 +1,10 @@
use cosmic::app::Core;
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
use cosmic::iced::application;
use cosmic::iced::platform_specific::shell::commands::popup::{destroy_popup, get_popup};
use cosmic::iced::window::Id;
use cosmic::iced::{Command, Limits};
use cosmic::iced::{Length, Limits, Task};
use cosmic::iced_runtime::core::window;
use cosmic::iced_style::application;
use cosmic::theme::iced;
use cosmic::widget::{list_column, settings, toggler};
use cosmic::{Element, Theme};
@ -37,22 +38,19 @@ impl cosmic::Application for Window {
&mut self.core
}
fn init(
core: Core,
_flags: Self::Flags,
) -> (Self, Command<cosmic::app::Message<Self::Message>>) {
fn init(core: Core, _flags: Self::Flags) -> (Self, Task<cosmic::app::Message<Self::Message>>) {
let window = Window {
core,
..Default::default()
};
(window, Command::none())
(window, Task::none())
}
fn on_close_requested(&self, id: window::Id) -> Option<Message> {
Some(Message::PopupClosed(id))
}
fn update(&mut self, message: Self::Message) -> Command<cosmic::app::Message<Self::Message>> {
fn update(&mut self, message: Self::Message) -> Task<cosmic::app::Message<Self::Message>> {
match message {
Message::TogglePopup => {
return if let Some(p) = self.popup.take() {
@ -60,17 +58,23 @@ impl cosmic::Application for Window {
} else {
let new_id = Id::unique();
self.popup.replace(new_id);
let mut popup_settings =
self.core
.applet
.get_popup_settings(Id::MAIN, new_id, None, None, None);
let mut popup_settings = self.core.applet.get_popup_settings(
self.core.main_window_id().unwrap(),
new_id,
None,
None,
None,
);
popup_settings.positioner.size_limits = Limits::NONE
.max_width(372.0)
.min_width(300.0)
.min_height(200.0)
.max_height(1080.0);
.max_height(1080.0)
.height(500)
.width(500);
popup_settings.positioner.size = Some((500, 500));
get_popup(popup_settings)
}
};
}
Message::PopupClosed(id) => {
if self.popup.as_ref() == Some(&id) {
@ -79,7 +83,7 @@ impl cosmic::Application for Window {
}
Message::ToggleExampleRow(toggled) => self.example_row = toggled,
}
Command::none()
Task::none()
}
fn view(&self) -> Element<Self::Message> {
@ -93,15 +97,16 @@ impl cosmic::Application for Window {
fn view_window(&self, _id: Id) -> Element<Self::Message> {
let content_list = list_column().padding(5).spacing(0).add(settings::item(
"Example row",
toggler(None, self.example_row, |value| {
cosmic::widget::container(toggler(self.example_row, |value| {
Message::ToggleExampleRow(value)
}),
}))
.height(Length::Fixed(50.)),
));
self.core.applet.popup_container(content_list).into()
}
fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
fn style(&self) -> Option<cosmic::iced_runtime::Appearance> {
Some(cosmic::applet::style())
}
}

View file

@ -11,4 +11,15 @@ tracing-log = "0.2.0"
[dependencies.libcosmic]
path = "../../"
default-features = false
features = ["debug", "winit", "tokio", "xdg-portal", "dbus-config", "a11y"]
features = [
"debug",
"winit",
"tokio",
"xdg-portal",
"dbus-config",
"a11y",
"wayland",
"wgpu",
"single-instance",
"multi-window",
]

View file

@ -3,7 +3,8 @@
//! Application API example
use cosmic::app::{Command, Core, Settings};
use cosmic::app::{Core, Settings, Task};
use cosmic::iced::widget::column;
use cosmic::iced_core::Size;
use cosmic::widget::nav_bar;
use cosmic::{executor, iced, ApplicationExt, Element};
@ -50,12 +51,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {}
pub enum Message {
Input1(String),
Input2(String),
}
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
nav_model: nav_bar::Model,
input_1: String,
input_2: String,
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
@ -81,7 +87,7 @@ impl cosmic::Application for App {
}
/// Creates the application, and optionally emits command on initialize.
fn init(core: Core, input: Self::Flags) -> (Self, Command<Self::Message>) {
fn init(core: Core, input: Self::Flags) -> (Self, Task<Self::Message>) {
let mut nav_model = nav_bar::Model::default();
for (title, content) in input {
@ -90,7 +96,12 @@ impl cosmic::Application for App {
nav_model.activate_position(0);
let mut app = App { core, nav_model };
let mut app = App {
core,
nav_model,
input_1: String::new(),
input_2: String::new(),
};
let command = app.update_title();
@ -103,14 +114,22 @@ impl cosmic::Application for App {
}
/// Called when a navigation item is selected.
fn on_nav_select(&mut self, id: nav_bar::Id) -> Command<Self::Message> {
fn on_nav_select(&mut self, id: nav_bar::Id) -> Task<Self::Message> {
self.nav_model.activate(id);
self.update_title()
}
/// Handle application events here.
fn update(&mut self, _message: Self::Message) -> Command<Self::Message> {
Command::none()
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::Input1(v) => {
self.input_1 = v;
}
Message::Input2(v) => {
self.input_2 = v;
}
}
Task::none()
}
/// Creates a view after each update.
@ -122,11 +141,20 @@ impl cosmic::Application for App {
let text = cosmic::widget::text(page_content);
let centered = cosmic::widget::container(text)
let centered = cosmic::widget::container(
column![
text,
cosmic::widget::text_input::text_input("", &self.input_1).on_input(Message::Input1),
cosmic::widget::text_input::text_input("", &self.input_2).on_input(Message::Input2),
]
.width(iced::Length::Fill)
.height(iced::Length::Shrink)
.align_x(iced::alignment::Horizontal::Center)
.align_y(iced::alignment::Vertical::Center);
.align_x(iced::alignment::Horizontal::Center),
)
.width(iced::Length::Fill)
.height(iced::Length::Shrink)
.align_x(iced::alignment::Horizontal::Center)
.align_y(iced::alignment::Vertical::Center);
Element::from(centered)
}
@ -142,10 +170,14 @@ where
.unwrap_or("Unknown Page")
}
fn update_title(&mut self) -> Command<Message> {
fn update_title(&mut self) -> Task<Message> {
let header_title = self.active_page_title().to_owned();
let window_title = format!("{header_title} — COSMIC AppDemo");
self.set_header_title(header_title);
self.set_window_title(window_title)
if let Some(id) = self.core.main_window_id() {
self.set_window_title(window_title, id)
} else {
Task::none()
}
}
}

View file

@ -4,7 +4,7 @@
//! Calendar widget example
use chrono::{Local, NaiveDate};
use cosmic::app::{Command, Core, Settings};
use cosmic::app::{Core, Settings, Task};
use cosmic::{executor, iced, ApplicationExt, Element};
/// Runs application with these settings
@ -50,7 +50,7 @@ impl cosmic::Application for App {
}
/// Creates the application, and optionally emits command on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Command<Self::Message>) {
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) {
let now = Local::now();
let mut app = App {
@ -64,7 +64,7 @@ impl cosmic::Application for App {
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::DateSelected(date) => {
self.date_selected = date;
@ -73,7 +73,7 @@ impl cosmic::Application for App {
println!("Date selected: {:?}", self.date_selected);
Command::none()
Task::none()
}
/// Creates a view after each update.
@ -99,7 +99,7 @@ impl App
where
Self: cosmic::Application,
{
fn update_title(&mut self) -> Command<Message> {
fn update_title(&mut self) -> Task<Message> {
self.set_header_title(String::from("Calendar Demo"));
self.set_window_title(String::from("Calendar Demo"))
}

View file

@ -3,7 +3,7 @@
//! Application API example
use cosmic::app::{Command, Core, Settings};
use cosmic::app::{Task, Core, Settings};
use cosmic::iced_core::Size;
use cosmic::widget::{menu, segmented_button};
use cosmic::{executor, iced, ApplicationExt, Element};
@ -65,7 +65,7 @@ impl cosmic::Application for App {
}
/// Creates the application, and optionally emits command on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Command<Self::Message>) {
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) {
let mut app = App {
core,
button_label: String::from("Right click me"),
@ -80,10 +80,10 @@ impl cosmic::Application for App {
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
self.button_label = format!("Clicked {message:?}");
Command::none()
Task::none()
}
/// Creates a view after each update.

View file

@ -6,7 +6,7 @@ use cosmic::{
ThemeBuilder,
},
font::load_fonts,
iced::{self, Application, Command, Length, Subscription},
iced::{self, Application, Length, Subscription, Task},
iced::{
subscription,
widget::{self, column, container, horizontal_space, row, text},
@ -324,7 +324,7 @@ impl Application for Window {
type Message = Message;
type Theme = Theme;
fn new(_flags: ()) -> (Self, Command<Self::Message>) {
fn new(_flags: ()) -> (Self, Task<Self::Message>) {
let mut window = Window::default()
.nav_bar_toggled(true)
.show_maximize(true)
@ -389,8 +389,8 @@ impl Application for Window {
])
}
fn update(&mut self, message: Message) -> iced::Command<Self::Message> {
let mut ret = Command::none();
fn update(&mut self, message: Message) -> iced::Task<Self::Message> {
let mut ret = Task::none();
match message {
Message::NavBar(key) => {
if let Some(page) = self.nav_id_to_page.get(key).copied() {
@ -437,10 +437,10 @@ impl Application for Window {
Message::ToggleNavBarCondensed => {
self.nav_bar_toggled_condensed = !self.nav_bar_toggled_condensed
}
Message::Drag => return drag(window::Id::MAIN),
Message::Close => return close(window::Id::MAIN),
Message::Minimize => return minimize(window::Id::MAIN, true),
Message::Maximize => return toggle_maximize(window::Id::MAIN),
Message::Drag => return drag(self.core.main_window_id().unwrap()),
Message::Close => return close(self.core.main_window_id().unwrap()),
Message::Minimize => return minimize(self.core.main_window_id().unwrap(), true),
Message::Maximize => return toggle_maximize(self.core.main_window_id().unwrap()),
Message::InputChanged => {}

View file

@ -482,7 +482,7 @@ impl State {
))
.layer(cosmic::cosmic_theme::Layer::Secondary)
.padding(16)
.style(cosmic::theme::Container::Background)
.class(cosmic::theme::Container::Background)
.into(),
cosmic::widget::text_input::secure_input(
"Type to search apps or type “?” for more options...",

View file

@ -3,7 +3,7 @@
//! Application API example
use cosmic::app::{Command, Core, Settings};
use cosmic::app::{Task, Core, Settings};
use cosmic::{executor, iced, ApplicationExt, Element};
/// Runs application with these settings
@ -51,7 +51,7 @@ impl cosmic::Application for App {
}
/// Creates the application, and optionally emits command on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Command<Self::Message>) {
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) {
let mut app = App {
core,
selected: 0,
@ -67,7 +67,7 @@ impl cosmic::Application for App {
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::Clicked(id) => self.selected = id,
Message::Remove(id) => {
@ -75,7 +75,7 @@ impl cosmic::Application for App {
}
}
Command::none()
Task::none()
}
/// Creates a view after each update.
@ -106,7 +106,7 @@ impl App
where
Self: cosmic::Application,
{
fn update_title(&mut self) -> Command<Message> {
fn update_title(&mut self) -> Task<Message> {
self.set_header_title(String::from("Image Button Demo"));
self.set_window_title(String::from("Image Button Demo"))
}

View file

@ -6,7 +6,7 @@
use std::collections::HashMap;
use std::{env, process};
use cosmic::app::{Command, Core, Settings};
use cosmic::app::{Task, Core, Settings};
use cosmic::iced::window;
use cosmic::iced_core::alignment::{Horizontal, Vertical};
use cosmic::iced_core::keyboard::Key;
@ -97,7 +97,7 @@ impl cosmic::Application for App {
}
/// Creates the application, and optionally emits command on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Command<Self::Message>) {
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) {
let app = App {
core,
config: Config {
@ -106,7 +106,7 @@ impl cosmic::Application for App {
key_binds: key_binds(),
};
(app, Command::none())
(app, Task::none())
}
fn header_start(&self) -> Vec<Element<Self::Message>> {
@ -114,13 +114,13 @@ impl cosmic::Application for App {
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::WindowClose => {
return window::close(window::Id::MAIN);
return window::close(self.core.main_window_id().unwrap());
}
Message::WindowNew => match env::current_exe() {
Ok(exe) => match process::Command::new(&exe).spawn() {
Ok(exe) => match process::Task::new(&exe).spawn() {
Ok(_child) => {}
Err(err) => {
eprintln!("failed to execute {:?}: {}", exe, err);
@ -132,7 +132,7 @@ impl cosmic::Application for App {
},
Message::ToggleHideContent => self.config.hide_content = !self.config.hide_content,
}
Command::none()
Task::none()
}
/// Creates a view after each update.

View file

@ -6,7 +6,7 @@ use cosmic::{
iced_core::{id, Alignment, Length, Point},
iced_widget::{column, container, scrollable, text, text_input},
widget::{button, header_bar},
ApplicationExt, Command,
ApplicationExt, Task,
};
#[derive(Debug, Clone, PartialEq)]
@ -42,10 +42,10 @@ impl cosmic::Application for MultiWindow {
&mut self.core
}
fn init(core: Core, _input: Self::Flags) -> (Self, cosmic::app::Command<Self::Message>) {
fn init(core: Core, _input: Self::Flags) -> (Self, cosmic::app::Task<Self::Message>) {
let windows = MultiWindow {
windows: HashMap::from([(
window::Id::MAIN,
self.core.main_window_id().unwrap(),
Window {
input_id: id::Id::new("main"),
input_value: String::new(),
@ -54,12 +54,12 @@ impl cosmic::Application for MultiWindow {
core,
};
(windows, cosmic::app::Command::none())
(windows, cosmic::app::Task::none())
}
fn subscription(&self) -> cosmic::iced_futures::Subscription<Self::Message> {
event::listen_with(|event, _| {
if let iced::Event::Window(id, window_event) = event {
event::listen_with(|event, _, id| {
if let iced::Event::Window(window_event) = event {
match window_event {
window::Event::CloseRequested => Some(Message::CloseWindow(id)),
window::Event::Opened { position, .. } => {
@ -77,18 +77,18 @@ impl cosmic::Application for MultiWindow {
fn update(
&mut self,
message: Self::Message,
) -> iced::Command<cosmic::app::Message<Self::Message>> {
) -> iced::Task<cosmic::app::Message<Self::Message>> {
match message {
Message::CloseWindow(id) => window::close(id),
Message::WindowClosed(id) => {
self.windows.remove(&id);
Command::none()
Task::none()
}
Message::WindowOpened(id, ..) => {
if let Some(window) = self.windows.get(&id) {
text_input::focus(window.input_id.clone())
} else {
Command::none()
Task::none()
}
}
Message::NewWindow => {
@ -113,13 +113,13 @@ impl cosmic::Application for MultiWindow {
spawn_window
}
Message::Input(id, value) => {
if let Some(w) = self.windows.get_mut(&window::Id::MAIN) {
if let Some(w) = self.windows.get_mut(&self.core.main_window_id().unwrap()) {
if id == w.input_id {
w.input_value = value;
}
}
Command::none()
Task::none()
}
}
}
@ -142,17 +142,15 @@ impl cosmic::Application for MultiWindow {
column![input, new_window_button]
.spacing(50)
.width(Length::Fill)
.align_items(Alignment::Center),
.align_x(Alignment::Center),
);
let window_content = container(container(content).width(200).center_x())
let window_content = container(container(content).center_x(Length::Fixed(200.)))
.style(cosmic::style::Container::Background)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y();
.center_x(Length::Fill)
.center_y(Length::Fill);
if id == window::Id::MAIN {
if id == self.core.main_window_id().unwrap() {
window_content.into()
} else {
column![header_bar().focused(focused), window_content].into()
@ -160,6 +158,6 @@ impl cosmic::Application for MultiWindow {
}
fn view(&self) -> cosmic::prelude::Element<Self::Message> {
self.view_window(window::Id::MAIN)
self.view_window(self.core.main_window_id().unwrap())
}
}

View file

@ -5,7 +5,7 @@
use std::collections::HashMap;
use cosmic::app::{Command, Core, Settings};
use cosmic::app::{Core, Settings, Task};
use cosmic::iced_core::Size;
use cosmic::widget::{menu, nav_bar};
use cosmic::{executor, iced, ApplicationExt, Element};
@ -106,7 +106,7 @@ impl cosmic::Application for App {
}
/// Creates the application, and optionally emits command on initialize.
fn init(core: Core, input: Self::Flags) -> (Self, Command<Self::Message>) {
fn init(core: Core, input: Self::Flags) -> (Self, Task<Self::Message>) {
let mut nav_model = nav_bar::Model::default();
for (title, content) in input {
@ -143,13 +143,13 @@ impl cosmic::Application for App {
}
/// Called when a navigation item is selected.
fn on_nav_select(&mut self, id: nav_bar::Id) -> Command<Self::Message> {
fn on_nav_select(&mut self, id: nav_bar::Id) -> Task<Self::Message> {
self.nav_model.activate(id);
self.update_title()
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::NavMenuAction(message) => match message {
NavMenuAction::Delete(id) => self.nav_model.remove(id),
@ -168,7 +168,7 @@ impl cosmic::Application for App {
},
}
Command::none()
Task::none()
}
/// Creates a view after each update.
@ -200,7 +200,7 @@ where
.unwrap_or("Unknown Page")
}
fn update_title(&mut self) -> Command<Message> {
fn update_title(&mut self) -> Task<Message> {
let header_title = self.active_page_title().to_owned();
let window_title = format!("{header_title} — COSMIC AppDemo");
self.set_header_title(header_title);

View file

@ -4,7 +4,7 @@
//! An application which provides an open dialog
use apply::Apply;
use cosmic::app::{Command, Core, Settings};
use cosmic::app::{Task, Core, Settings};
use cosmic::dialog::file_chooser::{self, FileFilter};
use cosmic::iced_core::Length;
use cosmic::widget::button;
@ -66,7 +66,7 @@ impl cosmic::Application for App {
}
/// Creates the application, and optionally emits command on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Command<Self::Message>) {
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) {
let mut app = App {
core,
file_contents: String::new(),
@ -77,7 +77,7 @@ impl cosmic::Application for App {
app.set_header_title("Open a file".into());
let cmd = app.set_window_title(
"COSMIC OpenDialog Demo".into(),
cosmic::iced::window::Id::MAIN,
cosmic::iced::self.core.main_window_id().unwrap(),
);
(app, cmd)
@ -88,7 +88,7 @@ impl cosmic::Application for App {
vec![button::suggested("Open").on_press(Message::OpenFile).into()]
}
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::Cancelled => {
eprintln!("open file dialog cancelled");
@ -198,7 +198,7 @@ impl cosmic::Application for App {
}
}
Command::none()
Task::none()
}
fn view(&self) -> Element<Self::Message> {

View file

@ -3,7 +3,7 @@
//! Application API example
use cosmic::app::{Command, Core, Settings};
use cosmic::app::{Core, Settings, Task};
use cosmic::{executor, iced, ApplicationExt, Element};
/// Runs application with these settings
@ -55,7 +55,7 @@ impl cosmic::Application for App {
}
/// Creates the application, and optionally emits command on initialize.
fn init(core: Core, _input: Self::Flags) -> (Self, Command<Self::Message>) {
fn init(core: Core, _input: Self::Flags) -> (Self, Task<Self::Message>) {
let mut app = App {
core,
editing: false,
@ -63,7 +63,7 @@ impl cosmic::Application for App {
search_id: cosmic::widget::Id::unique(),
};
let commands = Command::batch(vec![
let commands = Task::batch(vec![
cosmic::widget::text_input::focus(app.search_id.clone()),
app.update_title(),
]);
@ -72,7 +72,7 @@ impl cosmic::Application for App {
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
match message {
Message::Input(text) => {
self.input = text;
@ -83,7 +83,7 @@ impl cosmic::Application for App {
}
}
Command::none()
Task::none()
}
/// Creates a view after each update.
@ -115,7 +115,7 @@ impl App
where
Self: cosmic::Application,
{
fn update_title(&mut self) -> Command<Message> {
fn update_title(&mut self) -> Task<Message> {
let window_title = format!("COSMIC TextInputs Demo");
self.set_header_title(window_title.clone());
self.set_window_title(window_title)

2
iced

@ -1 +1 @@
Subproject commit 061995084a5775b4fd7df63dda336be01ddf491c
Subproject commit f2f9dfc6c37e14c4f8ff5dafe70d72d3c40eb692

View file

@ -7,62 +7,88 @@ use iced::window;
use super::Message;
/// Commands for COSMIC applications.
pub type Command<M> = iced::Command<Message<M>>;
pub type Task<M> = iced::Task<Message<M>>;
/// Creates a command which yields a [`crate::app::Message`].
pub fn message<M: Send + 'static>(message: Message<M>) -> Command<M> {
pub fn message<M: Send + 'static>(message: Message<M>) -> Task<M> {
crate::command::message(message)
}
/// Convenience methods for building message-based commands.
pub mod message {
/// Creates a command which yields an application message.
pub fn app<M: Send + 'static>(message: M) -> crate::app::Command<M> {
pub fn app<M: Send + 'static>(message: M) -> crate::app::Task<M> {
super::message(super::Message::App(message))
}
/// Creates a command which yields a cosmic message.
pub fn cosmic<M: Send + 'static>(
message: crate::app::cosmic::Message,
) -> crate::app::Command<M> {
pub fn cosmic<M: Send + 'static>(message: crate::app::cosmic::Message) -> crate::app::Task<M> {
super::message(super::Message::Cosmic(message))
}
}
pub fn drag<M: Send + 'static>(id: Option<window::Id>) -> iced::Command<Message<M>> {
crate::command::drag(id).map(Message::Cosmic)
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.get().cloned()) else {
return iced::Task::none();
};
crate::command::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.get().cloned()) else {
return iced::Task::none();
};
crate::command::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.get().cloned()) else {
return iced::Task::none();
};
crate::command::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.get().cloned()) else {
return iced::Task::none();
};
crate::command::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.get().cloned()) else {
return iced::Task::none();
};
crate::command::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.get().cloned()) else {
return iced::Task::none();
};
crate::command::toggle_maximize(id).map(Message::Cosmic)
}
}
pub fn maximize<M: Send + 'static>(
id: Option<window::Id>,
maximized: bool,
) -> iced::Command<Message<M>> {
crate::command::maximize(id, maximized).map(Message::Cosmic)
}
pub fn minimize<M: Send + 'static>(id: Option<window::Id>) -> iced::Command<Message<M>> {
crate::command::minimize(id).map(Message::Cosmic)
}
pub fn set_scaling_factor<M: Send + 'static>(factor: f32) -> iced::Command<Message<M>> {
message::cosmic(super::cosmic::Message::ScaleFactor(factor))
}
pub fn set_theme<M: Send + 'static>(theme: crate::Theme) -> iced::Command<Message<M>> {
pub fn set_theme<M: Send + 'static>(theme: crate::Theme) -> iced::Task<Message<M>> {
message::cosmic(super::cosmic::Message::AppThemeChange(theme))
}
pub fn set_title<M: Send + 'static>(
id: Option<window::Id>,
title: String,
) -> iced::Command<Message<M>> {
crate::command::set_title(id, title).map(Message::Cosmic)
}
pub fn set_windowed<M: Send + 'static>(id: Option<window::Id>) -> iced::Command<Message<M>> {
crate::command::set_windowed(id).map(Message::Cosmic)
}
pub fn toggle_maximize<M: Send + 'static>(id: Option<window::Id>) -> iced::Command<Message<M>> {
crate::command::toggle_maximize(id).map(Message::Cosmic)
}

View file

@ -1,7 +1,7 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use std::collections::HashMap;
use std::{cell::OnceCell, collections::HashMap};
use crate::widget::nav_bar;
use cosmic_config::CosmicConfigEntry;
@ -40,8 +40,8 @@ pub struct Window {
pub show_close: bool,
pub show_maximize: bool,
pub show_minimize: bool,
height: u32,
width: u32,
height: f32,
width: f32,
}
/// COSMIC-specific application settings
@ -93,6 +93,10 @@ pub struct Core {
#[cfg(feature = "dbus-config")]
pub(crate) settings_daemon: Option<cosmic_settings_daemon::CosmicSettingsDaemonProxy<'static>>,
pub(crate) main_window: OnceCell<window::Id>,
pub(crate) exit_on_main_window_closed: bool,
}
impl Default for Core {
@ -135,10 +139,10 @@ impl Default for Core {
show_maximize: true,
show_minimize: true,
show_window_menu: false,
height: 0,
width: 0,
height: 0.,
width: 0.,
},
focused_window: Some(window::Id::MAIN),
focused_window: None,
#[cfg(feature = "applet")]
applet: crate::applet::Context::default(),
#[cfg(feature = "single-instance")]
@ -148,6 +152,8 @@ impl Default for Core {
portal_is_dark: None,
portal_accent: None,
portal_is_high_contrast: None,
main_window: OnceCell::new(),
exit_on_main_window_closed: true,
}
}
}
@ -297,12 +303,12 @@ impl Core {
}
/// Set the height of the main window.
pub(crate) fn set_window_height(&mut self, new_height: u32) {
pub(crate) fn set_window_height(&mut self, new_height: f32) {
self.window.height = new_height;
}
/// Set the width of the main window.
pub(crate) fn set_window_width(&mut self, new_width: u32) {
pub(crate) fn set_window_width(&mut self, new_width: f32) {
self.window.width = new_width;
self.is_condensed_update();
}
@ -364,4 +370,12 @@ impl Core {
self.portal_is_dark
.unwrap_or(self.system_theme_mode.is_dark)
}
/// The [`Id`] of the main window
pub fn main_window_id(&self) -> Option<window::Id> {
self.main_window
.get()
.filter(|id| iced::window::Id::NONE != **id)
.cloned()
}
}

View file

@ -1,32 +1,23 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use std::borrow::Borrow;
use std::sync::Arc;
use super::{command, Application, ApplicationExt, Core, Subscription};
use super::{Application, ApplicationExt, Core, Subscription};
use crate::config::CosmicTk;
use crate::theme::{self, Theme, ThemeType, THEME};
use crate::theme::{Theme, ThemeType, THEME};
use crate::widget::nav_bar;
use crate::{keyboard_nav, Element};
#[cfg(feature = "wayland")]
use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState};
use cosmic_theme::ThemeMode;
#[cfg(feature = "wayland")]
use iced::event::wayland::{self, WindowEvent};
#[cfg(feature = "wayland")]
use iced::event::PlatformSpecific;
#[cfg(all(feature = "winit", feature = "multi-window"))]
use iced::multi_window::Application as IcedApplication;
#[cfg(feature = "wayland")]
use iced::wayland::Application as IcedApplication;
use iced::event::wayland;
#[cfg(not(any(feature = "multi-window", feature = "wayland")))]
use iced::Application as IcedApplication;
use iced::{window, Command};
use iced::{window, Task};
use iced_futures::event::listen_with;
#[cfg(not(feature = "wayland"))]
use iced_runtime::command::Action;
#[cfg(not(feature = "wayland"))]
use iced_runtime::window::Action as WindowAction;
use palette::color_difference::EuclideanDistance;
/// A message managed internally by COSMIC.
@ -65,7 +56,7 @@ pub enum Message {
/// Updates the window maximized state
WindowMaximized(window::Id, bool),
/// Updates the tracked window geometry.
WindowResize(window::Id, u32, u32),
WindowResize(window::Id, f32, f32),
/// Tracks updates to window state.
#[cfg(feature = "wayland")]
WindowState(window::Id, WindowState),
@ -86,7 +77,9 @@ pub enum Message {
Unfocus(window::Id),
/// Tracks updates to window suggested size.
#[cfg(feature = "applet")]
Configure(cctk::sctk::shell::xdg::window::WindowConfigure),
SuggestedBounds(Option<iced::Size>),
/// Window Created
MainWindowCreated(window::Id),
}
#[derive(Default)]
@ -94,109 +87,104 @@ pub struct Cosmic<App> {
pub app: App,
}
impl<T: Application> IcedApplication for Cosmic<T>
impl<T: Application> Cosmic<T>
where
T::Message: Send + 'static,
{
type Executor = T::Executor;
type Flags = (Core, T::Flags);
type Message = super::Message<T::Message>;
type Theme = Theme;
fn new((mut core, flags): Self::Flags) -> (Self, iced::Command<Self::Message>) {
pub fn init(
(mut core, flags, window_settings): (Core, T::Flags, iced::window::Settings),
) -> (Self, iced::Task<super::Message<T::Message>>) {
#[cfg(feature = "dbus-config")]
{
use iced_futures::futures::executor::block_on;
core.settings_daemon = block_on(cosmic_config::dbus::settings_daemon_proxy()).ok();
}
let (model, command) = T::init(core, flags);
let (model, mut command) = T::init(core, flags);
(Self::new(model), command)
}
#[cfg(not(any(feature = "multi-window", feature = "wayland")))]
fn title(&self) -> String {
#[cfg(not(feature = "multi-window"))]
pub fn title(&self) -> String {
self.app.title().to_string()
}
#[cfg(any(feature = "multi-window", feature = "wayland"))]
fn title(&self, id: window::Id) -> String {
#[cfg(feature = "multi-window")]
pub fn title(&self, id: window::Id) -> String {
self.app.title(id).to_string()
}
fn update(&mut self, message: Self::Message) -> iced::Command<Self::Message> {
pub fn update(
&mut self,
message: super::Message<T::Message>,
) -> iced::Task<super::Message<T::Message>> {
match message {
super::Message::App(message) => self.app.update(message),
super::Message::Cosmic(message) => self.cosmic_update(message),
super::Message::None => iced::Command::none(),
super::Message::None => iced::Task::none(),
#[cfg(feature = "single-instance")]
super::Message::DbusActivation(message) => self.app.dbus_activation(message),
}
}
#[cfg(not(any(feature = "multi-window", feature = "wayland")))]
fn scale_factor(&self) -> f64 {
#[cfg(not(feature = "multi-window"))]
pub fn scale_factor(&self) -> f64 {
f64::from(self.app.core().scale_factor())
}
#[cfg(any(feature = "multi-window", feature = "wayland"))]
fn scale_factor(&self, _id: window::Id) -> f64 {
#[cfg(feature = "multi-window")]
pub fn scale_factor(&self, _id: window::Id) -> f64 {
f64::from(self.app.core().scale_factor())
}
fn style(&self) -> <Self::Theme as iced_style::application::StyleSheet>::Style {
pub fn style(&self, theme: &Theme) -> iced_runtime::Appearance {
if let Some(style) = self.app.style() {
style
} else if self.app.core().window.sharp_corners {
theme::Application::default()
let theme = THEME.lock().unwrap();
crate::style::iced::application::appearance(theme.borrow())
} else {
theme::Application::Custom(Box::new(|theme| iced_style::application::Appearance {
let theme = THEME.lock().unwrap();
iced_runtime::Appearance {
background_color: iced_core::Color::TRANSPARENT,
icon_color: theme.cosmic().on_bg_color().into(),
text_color: theme.cosmic().on_bg_color().into(),
}))
}
}
}
#[allow(clippy::too_many_lines)]
fn subscription(&self) -> Subscription<Self::Message> {
let window_events = listen_with(|event, _| {
pub fn subscription(&self) -> Subscription<super::Message<T::Message>> {
let window_events = listen_with(|event, _, id| {
match event {
iced::Event::Window(id, window::Event::Resized { width, height }) => {
iced::Event::Window(window::Event::Resized(iced::Size { width, height })) => {
return Some(Message::WindowResize(id, width, height));
}
iced::Event::Window(id, window::Event::Closed) => {
iced::Event::Window(window::Event::Closed) => {
return Some(Message::SurfaceClosed(id))
}
iced::Event::Window(id, window::Event::Focused) => return Some(Message::Focus(id)),
iced::Event::Window(id, window::Event::Unfocused) => {
return Some(Message::Unfocus(id))
iced::Event::Window(window::Event::Focused) => return Some(Message::Focus(id)),
iced::Event::Window(window::Event::Unfocused) => return Some(Message::Unfocus(id)),
iced::Event::Window(window::Event::Opened { .. }) => {
return Some(Message::MainWindowCreated(id));
}
#[cfg(feature = "wayland")]
iced::Event::PlatformSpecific(PlatformSpecific::Wayland(event)) => match event {
wayland::Event::Window(WindowEvent::State(state), _surface, id) => {
return Some(Message::WindowState(id, state));
iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => {
match event {
wayland::Event::Popup(wayland::PopupEvent::Done, _, id)
| wayland::Event::Layer(wayland::LayerEvent::Done, _, id) => {
return Some(Message::SurfaceClosed(id));
}
#[cfg(feature = "applet")]
wayland::Event::Window(
iced::event::wayland::WindowEvent::SuggestedBounds(b),
) => {
return Some(Message::SuggestedBounds(b));
}
_ => (),
}
wayland::Event::Window(
WindowEvent::WmCapabilities(capabilities),
_surface,
id,
) => {
return Some(Message::WmCapabilities(id, capabilities));
}
wayland::Event::Popup(wayland::PopupEvent::Done, _, id)
| wayland::Event::Layer(wayland::LayerEvent::Done, _, id) => {
return Some(Message::SurfaceClosed(id));
}
#[cfg(feature = "applet")]
wayland::Event::Window(WindowEvent::Configure(conf), _surface, id)
if id == window::Id::MAIN =>
{
return Some(Message::Configure(conf));
}
_ => (),
},
}
_ => (),
}
@ -275,19 +263,24 @@ where
Subscription::batch(subscriptions)
}
#[cfg(not(any(feature = "multi-window", feature = "wayland")))]
fn theme(&self) -> Self::Theme {
#[cfg(not(feature = "multi-window"))]
pub fn theme(&self) -> Theme {
crate::theme::active()
}
#[cfg(any(feature = "multi-window", feature = "wayland"))]
fn theme(&self, _id: window::Id) -> Self::Theme {
#[cfg(feature = "multi-window")]
pub fn theme(&self, _id: window::Id) -> Theme {
crate::theme::active()
}
#[cfg(any(feature = "multi-window", feature = "wayland"))]
fn view(&self, id: window::Id) -> Element<Self::Message> {
if id != self.app.main_window_id() {
#[cfg(feature = "multi-window")]
pub fn view(&self, id: window::Id) -> Element<super::Message<T::Message>> {
if !self
.app
.core()
.main_window_id()
.is_some_and(|main_id| main_id == id)
{
return self.app.view_window(id).map(super::Message::App);
}
@ -298,37 +291,43 @@ where
}
}
#[cfg(not(any(feature = "multi-window", feature = "wayland")))]
fn view(&self) -> Element<Self::Message> {
#[cfg(not(feature = "multi-window"))]
pub fn view(&self) -> Element<super::Message<T::Message>> {
self.app.view_main()
}
}
impl<T: Application> Cosmic<T> {
#[cfg(feature = "wayland")]
pub fn close(&mut self) -> iced::Command<super::Message<T::Message>> {
iced_sctk::commands::window::close_window(self.app.main_window_id())
}
#[cfg(not(feature = "wayland"))]
#[allow(clippy::unused_self)]
pub fn close(&mut self) -> iced::Command<super::Message<T::Message>> {
iced::Command::single(Action::Window(WindowAction::Close(
self.app.main_window_id(),
)))
pub fn close(&mut self) -> iced::Task<super::Message<T::Message>> {
if let Some(id) = self.app.core().main_window_id() {
iced::window::close(id)
} else {
iced::Task::none()
}
}
#[allow(clippy::too_many_lines)]
fn cosmic_update(&mut self, message: Message) -> iced::Command<super::Message<T::Message>> {
fn cosmic_update(&mut self, message: Message) -> iced::Task<super::Message<T::Message>> {
match message {
Message::WindowMaximized(id, maximized) => {
if self.app.main_window_id() == id {
if self
.app
.core()
.main_window_id()
.is_some_and(|main_id| main_id == id)
{
self.app.core_mut().window.sharp_corners = maximized;
}
}
Message::WindowResize(id, width, height) => {
if self.app.main_window_id() == id {
if self
.app
.core()
.main_window_id()
.is_some_and(|main_id| main_id == id)
{
self.app.core_mut().set_window_width(width);
self.app.core_mut().set_window_height(height);
}
@ -336,15 +335,19 @@ impl<T: Application> Cosmic<T> {
self.app.on_window_resize(id, width, height);
//TODO: more efficient test of maximized (winit has no event for maximize if set by the OS)
#[cfg(not(feature = "wayland"))]
return iced::window::fetch_maximized(id, move |maximized| {
return iced::window::get_maximized(id).map(move |maximized| {
super::Message::Cosmic(Message::WindowMaximized(id, maximized))
});
}
#[cfg(feature = "wayland")]
Message::WindowState(id, state) => {
if self.app.main_window_id() == id {
if self
.app
.core()
.main_window_id()
.is_some_and(|main_id| main_id == id)
{
self.app.core_mut().window.sharp_corners = state.intersects(
WindowState::MAXIMIZED
| WindowState::FULLSCREEN
@ -359,7 +362,12 @@ impl<T: Application> Cosmic<T> {
#[cfg(feature = "wayland")]
Message::WmCapabilities(id, capabilities) => {
if self.app.main_window_id() == id {
if self
.app
.core()
.main_window_id()
.is_some_and(|main_id| main_id == id)
{
self.app.core_mut().window.show_maximize =
capabilities.contains(WindowManagerCapabilities::MAXIMIZE);
self.app.core_mut().window.show_minimize =
@ -379,9 +387,7 @@ impl<T: Application> Cosmic<T> {
keyboard_nav::Message::Escape => return self.app.on_escape(),
keyboard_nav::Message::Search => return self.app.on_search(),
keyboard_nav::Message::Fullscreen => {
return command::toggle_maximize(Some(self.app.main_window_id()))
}
keyboard_nav::Message::Fullscreen => return self.app.core().toggle_maximize(None),
},
Message::ContextDrawer(show) => {
@ -389,11 +395,11 @@ impl<T: Application> Cosmic<T> {
return self.app.on_context_drawer();
}
Message::Drag => return command::drag(Some(self.app.main_window_id())),
Message::Drag => return self.app.core().drag(None),
Message::Minimize => return command::minimize(Some(self.app.main_window_id())),
Message::Minimize => return self.app.core().minimize(None),
Message::Maximize => return command::toggle_maximize(Some(self.app.main_window_id())),
Message::Maximize => return self.app.core().toggle_maximize(None),
Message::NavBar(key) => {
self.app.core_mut().nav_bar_set_toggled_condensed(false);
@ -433,7 +439,7 @@ impl<T: Application> Cosmic<T> {
let cur_is_dark = THEME.lock().unwrap().theme_type.is_dark();
// Ignore updates if the current theme mode does not match.
if cur_is_dark != theme.cosmic().is_dark {
return iced::Command::none();
return iced::Task::none();
}
let cmd = self.app.system_theme_update(&keys, theme.cosmic());
// Record the last-known system theme in event that the current theme is custom.
@ -479,7 +485,7 @@ impl<T: Application> Cosmic<T> {
}
Message::SystemThemeModeChange(keys, mode) => {
if !keys.contains(&"is_dark") {
return iced::Command::none();
return iced::Task::none();
}
if match THEME.lock().unwrap().theme_type {
ThemeType::System {
@ -488,7 +494,7 @@ impl<T: Application> Cosmic<T> {
} => prefer_dark.is_some(),
_ => false,
} {
return iced::Command::none();
return iced::Task::none();
}
let mut cmds = vec![self.app.system_theme_mode_update(&keys, &mode)];
@ -527,23 +533,37 @@ impl<T: Application> Cosmic<T> {
}
}
}
return Command::batch(cmds);
return Task::batch(cmds);
}
Message::Activate(_token) => {
Message::Activate(_token) =>
{
#[cfg(feature = "wayland")]
return iced_sctk::commands::activation::activate(
self.app.main_window_id(),
#[allow(clippy::used_underscore_binding)]
_token,
);
}
Message::SurfaceClosed(id) => {
if let Some(msg) = self.app.on_close_requested(id) {
return self.app.update(msg);
if let Some(id) = self.app.core().main_window_id() {
return iced_winit::platform_specific::commands::activation::activate(
id,
#[allow(clippy::used_underscore_binding)]
_token,
);
}
}
Message::SurfaceClosed(id) => {
let mut ret = if let Some(msg) = self.app.on_close_requested(id) {
self.app.update(msg)
} else {
Task::none()
};
let core = self.app.core();
if core.exit_on_main_window_closed
&& core.main_window_id().is_some_and(|m_id| id == m_id)
{
ret = Task::batch(vec![iced::exit::<super::Message<T::Message>>()]);
}
return ret;
}
Message::ShowWindowMenu => {
return window::show_window_menu(window::Id::MAIN);
if let Some(id) = self.app.core().main_window_id() {
return iced::window::show_system_menu(id);
}
}
#[cfg(feature = "xdg-portal")]
Message::DesktopSettings(crate::theme::portal::Desktop::ColorScheme(s)) => {
@ -555,7 +575,7 @@ impl<T: Application> Cosmic<T> {
} => prefer_dark.is_some(),
_ => false,
} {
return iced::Command::none();
return iced::Task::none();
}
let is_dark = match s {
ColorScheme::NoPreference => None,
@ -595,7 +615,7 @@ impl<T: Application> Cosmic<T> {
if cur_accent.distance_squared(*c) < 0.00001 {
// skip calculations if we already have the same color
return iced::Command::none();
return iced::Task::none();
}
{
@ -640,19 +660,20 @@ impl<T: Application> Cosmic<T> {
core.focused_window = None;
}
}
#[cfg(feature = "applet")]
Message::Configure(configure) => {
if let Some(w) = configure.new_size.0 {
self.app.core_mut().set_window_width(w.get());
}
if let Some(h) = configure.new_size.1 {
self.app.core_mut().set_window_height(h.get());
}
self.app.core_mut().applet.configure = Some(configure);
Message::MainWindowCreated(id) => {
let core = self.app.core_mut();
_ = core.main_window.set(id);
}
#[cfg(feature = "applet")]
Message::SuggestedBounds(b) => {
tracing::info!("Suggested bounds: {b:?}");
let core = self.app.core_mut();
core.applet.suggested_bounds = b;
}
_ => {}
}
iced::Command::none()
iced::Task::none()
}
}

View file

@ -9,6 +9,8 @@
pub mod command;
mod core;
pub mod cosmic;
#[cfg(all(feature = "winit", feature = "multi-window"))]
pub(crate) mod multi_window;
pub mod settings;
pub mod message {
@ -47,17 +49,14 @@ pub mod message {
use std::borrow::Cow;
pub use self::command::Command;
pub use self::command::Task;
pub use self::core::Core;
pub use self::settings::Settings;
use crate::prelude::*;
use crate::theme::THEME;
use crate::widget::{context_drawer, horizontal_space, id_container, menu, nav_bar, popover};
use apply::Apply;
#[cfg(all(feature = "winit", feature = "multi-window"))]
use iced::{multi_window::Application as IcedApplication, window};
#[cfg(any(not(feature = "winit"), not(feature = "multi-window")))]
use iced::{window, Application as IcedApplication};
use iced::window;
use iced::{Length, Subscription};
pub use message::Message;
use url::Url;
@ -73,15 +72,15 @@ use {
pub(crate) fn iced_settings<App: Application>(
settings: Settings,
flags: App::Flags,
) -> iced::Settings<(Core, App::Flags)> {
) -> (iced::Settings, (Core, App::Flags, iced::window::Settings)) {
preload_fonts();
let mut core = Core::default();
core.debug = settings.debug;
core.icon_theme_override = settings.default_icon_theme.is_some();
core.set_scale_factor(settings.scale_factor);
core.set_window_width(settings.size.width as u32);
core.set_window_height(settings.size.height as u32);
core.set_window_width(settings.size.width);
core.set_window_height(settings.size.height);
if let Some(icon_theme) = settings.default_icon_theme {
crate::icon_theme::set_default(icon_theme);
@ -91,65 +90,40 @@ pub(crate) fn iced_settings<App: Application>(
THEME.lock().unwrap().set_theme(settings.theme.theme_type);
let mut iced = iced::Settings::with_flags((core, flags));
if settings.no_main_window {
core.main_window.set(iced::window::Id::NONE).unwrap();
}
let mut iced = iced::Settings::default();
iced.antialiasing = settings.antialiasing;
iced.default_font = settings.default_font;
iced.default_text_size = iced::Pixels(settings.default_text_size);
iced.exit_on_close_request = settings.exit_on_close;
#[cfg(not(feature = "wayland"))]
{
let exit_on_close = settings.exit_on_close;
iced.window.exit_on_close_request = exit_on_close;
}
let exit_on_close = settings.exit_on_close;
iced.exit_on_close_request = exit_on_close;
let mut window_settings = iced::window::Settings::default();
window_settings.exit_on_close_request = exit_on_close;
iced.id = Some(App::APP_ID.to_owned());
#[cfg(all(not(feature = "wayland"), target_os = "linux"))]
{
iced.window.platform_specific.application_id = App::APP_ID.to_string();
window_settings.platform_specific.application_id = App::APP_ID.to_string();
core.exit_on_main_window_closed = exit_on_close;
if let Some(border_size) = settings.resizable {
window_settings.resize_border = border_size as u32;
window_settings.resizable = true;
}
window_settings.decorations = !settings.client_decorations;
window_settings.size = settings.size;
let min_size = settings.size_limits.min();
if min_size != iced::Size::ZERO {
window_settings.min_size = Some(min_size);
}
let max_size = settings.size_limits.max();
if max_size != iced::Size::INFINITY {
window_settings.max_size = Some(max_size);
}
#[cfg(feature = "wayland")]
{
use iced::wayland::actions::window::SctkWindowSettings;
use iced_sctk::settings::InitialSurface;
iced.initial_surface = if settings.no_main_window {
InitialSurface::None
} else {
InitialSurface::XdgWindow(SctkWindowSettings {
app_id: Some(App::APP_ID.to_owned()),
autosize: settings.autosize,
client_decorations: settings.client_decorations,
resizable: settings.resizable,
size: (settings.size.width as u32, settings.size.height as u32).into(),
size_limits: settings.size_limits,
title: None,
transparent: settings.transparent,
xdg_activation_token: std::env::var("XDG_ACTIVATION_TOKEN").ok(),
..SctkWindowSettings::default()
})
};
}
#[cfg(not(feature = "wayland"))]
{
if let Some(border_size) = settings.resizable {
iced.window.resize_border = border_size as u32;
iced.window.resizable = true;
}
iced.window.decorations = !settings.client_decorations;
iced.window.size = settings.size;
let min_size = settings.size_limits.min();
if min_size != iced::Size::ZERO {
iced.window.min_size = Some(min_size);
}
let max_size = settings.size_limits.max();
if max_size != iced::Size::INFINITY {
iced.window.max_size = Some(max_size);
}
iced.window.transparent = settings.transparent;
}
iced
window_settings.transparent = settings.transparent;
(iced, (core, flags, window_settings))
}
/// Launch a COSMIC application with the given [`Settings`].
@ -158,9 +132,40 @@ pub(crate) fn iced_settings<App: Application>(
///
/// Returns error on application failure.
pub fn run<App: Application>(settings: Settings, flags: App::Flags) -> iced::Result {
let settings = iced_settings::<App>(settings, flags);
cosmic::Cosmic::<App>::run(settings)
let default_font = settings.default_font;
let (settings, flags) = iced_settings::<App>(settings, flags);
#[cfg(not(feature = "multi-window"))]
{
iced::application(
cosmic::Cosmic::title,
cosmic::Cosmic::update,
cosmic::Cosmic::view,
)
.subscription(cosmic::Cosmic::subscription)
.style(cosmic::Cosmic::style)
.theme(cosmic::Cosmic::theme)
.window_size((500.0, 800.0))
.settings(settings)
.window(flags.2.clone())
.run_with(move || cosmic::Cosmic::<App>::init(flags))
}
#[cfg(feature = "multi-window")]
{
let mut app = multi_window::multi_window(
cosmic::Cosmic::title,
cosmic::Cosmic::update,
cosmic::Cosmic::view,
);
if flags.0.main_window.get().is_none() {
app = app.window(flags.2.clone());
_ = flags.0.main_window.set(iced_core::window::Id::RESERVED);
}
app.subscription(cosmic::Cosmic::subscription)
.style(cosmic::Cosmic::style)
.theme(cosmic::Cosmic::theme)
.settings(settings)
.run_with(move || cosmic::Cosmic::<App>::init(flags))
}
}
#[cfg(feature = "single-instance")]
@ -362,9 +367,41 @@ where
tracing::info!("Another instance is running");
Ok(())
} else {
let mut settings = iced_settings::<App>(settings, flags);
settings.flags.0.single_instance = true;
cosmic::Cosmic::<App>::run(settings)
let (settings, mut flags) = iced_settings::<App>(settings, flags);
flags.0.single_instance = true;
#[cfg(not(feature = "multi-window"))]
{
iced::application(
cosmic::Cosmic::title,
cosmic::Cosmic::update,
cosmic::Cosmic::view,
)
.subscription(cosmic::Cosmic::subscription)
.style(cosmic::Cosmic::style)
.theme(cosmic::Cosmic::theme)
.window_size((500.0, 800.0))
.settings(settings)
.window(flags.2.clone())
.run_with(move || cosmic::Cosmic::<App>::init(flags))
}
#[cfg(feature = "multi-window")]
{
let mut app = multi_window::multi_window(
cosmic::Cosmic::title,
cosmic::Cosmic::update,
cosmic::Cosmic::view,
);
if flags.0.main_window.get().is_none() {
app = app.window(flags.2.clone());
_ = flags.0.main_window.set(iced_core::window::Id::RESERVED);
}
app.subscription(cosmic::Cosmic::subscription)
.style(cosmic::Cosmic::style)
.theme(cosmic::Cosmic::theme)
.settings(settings)
.run_with(move || cosmic::Cosmic::<App>::init(flags))
}
}
}
@ -409,7 +446,7 @@ where
fn core_mut(&mut self) -> &mut Core;
/// Creates the application, and optionally emits command on initialize.
fn init(core: Core, flags: Self::Flags) -> (Self, Command<Self::Message>);
fn init(core: Core, flags: Self::Flags) -> (Self, Task<Self::Message>);
/// Displays a context drawer on the side of the application window when `Some`.
fn context_drawer(&self) -> Option<Element<Self::Message>> {
@ -441,11 +478,6 @@ where
Vec::new()
}
/// Get the main [`window::Id`], which is [`window::Id::MAIN`] by default
fn main_window_id(&self) -> window::Id {
window::Id::MAIN
}
/// Allows overriding the default nav bar widget.
fn nav_bar(&self) -> Option<Element<Message<Self::Message>>> {
if !self.core().nav_bar_active() {
@ -491,32 +523,32 @@ where
}
// Called when context drawer is toggled
fn on_context_drawer(&mut self) -> Command<Self::Message> {
Command::none()
fn on_context_drawer(&mut self) -> Task<Self::Message> {
Task::none()
}
/// Called when the escape key is pressed.
fn on_escape(&mut self) -> Command<Self::Message> {
Command::none()
fn on_escape(&mut self) -> Task<Self::Message> {
Task::none()
}
/// Called when a navigation item is selected.
fn on_nav_select(&mut self, id: nav_bar::Id) -> Command<Self::Message> {
Command::none()
fn on_nav_select(&mut self, id: nav_bar::Id) -> Task<Self::Message> {
Task::none()
}
/// Called when a context menu is requested for a navigation item.
fn on_nav_context(&mut self, id: nav_bar::Id) -> Command<Self::Message> {
Command::none()
fn on_nav_context(&mut self, id: nav_bar::Id) -> Task<Self::Message> {
Task::none()
}
/// Called when the search function is requested.
fn on_search(&mut self) -> Command<Self::Message> {
Command::none()
fn on_search(&mut self) -> Task<Self::Message> {
Task::none()
}
/// Called when a window is resized.
fn on_window_resize(&mut self, id: window::Id, width: u32, height: u32) {}
fn on_window_resize(&mut self, id: window::Id, width: f32, height: f32) {}
/// Event sources that are to be listened to.
fn subscription(&self) -> Subscription<Self::Message> {
@ -524,8 +556,8 @@ where
}
/// Respond to an application-specific message.
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
Command::none()
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
Task::none()
}
/// Respond to a system theme change
@ -533,8 +565,8 @@ where
&mut self,
keys: &[&'static str],
new_theme: &cosmic_theme::Theme,
) -> Command<Self::Message> {
Command::none()
) -> Task<Self::Message> {
Task::none()
}
/// Respond to a system theme mode change
@ -542,8 +574,8 @@ where
&mut self,
keys: &[&'static str],
new_theme: &cosmic_theme::ThemeMode,
) -> Command<Self::Message> {
Command::none()
) -> Task<Self::Message> {
Task::none()
}
/// Constructs the view for the main window.
@ -555,33 +587,33 @@ where
}
/// Overrides the default style for applications
fn style(&self) -> Option<<crate::Theme as iced_style::application::StyleSheet>::Style> {
fn style(&self) -> Option<iced_runtime::Appearance> {
None
}
/// Handles dbus activation messages
#[cfg(feature = "single-instance")]
fn dbus_activation(&mut self, msg: DbusActivationMessage) -> Command<Self::Message> {
Command::none()
fn dbus_activation(&mut self, msg: DbusActivationMessage) -> Task<Self::Message> {
Task::none()
}
}
/// Methods automatically derived for all types implementing [`Application`].
pub trait ApplicationExt: Application {
/// Initiates a window drag.
fn drag(&mut self) -> Command<Self::Message>;
fn drag(&mut self) -> Task<Self::Message>;
/// Maximizes the window.
fn maximize(&mut self) -> Command<Self::Message>;
fn maximize(&mut self) -> Task<Self::Message>;
/// Minimizes the window.
fn minimize(&mut self) -> Command<Self::Message>;
fn minimize(&mut self) -> Task<Self::Message>;
/// Get the title of the main window.
#[cfg(not(any(feature = "multi-window", feature = "wayland")))]
#[cfg(not(feature = "multi-window"))]
fn title(&self) -> &str;
#[cfg(any(feature = "multi-window", feature = "wayland"))]
#[cfg(feature = "multi-window")]
/// Get the title of a window.
fn title(&self, id: window::Id) -> &str;
@ -600,56 +632,58 @@ pub trait ApplicationExt: Application {
self.core_mut().set_header_title(title);
}
#[cfg(not(any(feature = "multi-window", feature = "wayland")))]
#[cfg(not(feature = "multi-window"))]
/// Set the title of the main window.
fn set_window_title(&mut self, title: String) -> Command<Self::Message>;
fn set_window_title(&mut self, title: String) -> Task<Self::Message>;
#[cfg(any(feature = "multi-window", feature = "wayland"))]
#[cfg(feature = "multi-window")]
/// Set the title of a window.
fn set_window_title(&mut self, title: String, id: window::Id) -> Command<Self::Message>;
fn set_window_title(&mut self, title: String, id: window::Id) -> Task<Self::Message>;
/// View template for the main window.
fn view_main(&self) -> Element<Message<Self::Message>>;
}
impl<App: Application> ApplicationExt for App {
fn drag(&mut self) -> Command<Self::Message> {
command::drag(Some(self.main_window_id()))
fn drag(&mut self) -> Task<Self::Message> {
self.core().drag(None)
}
fn maximize(&mut self) -> Command<Self::Message> {
command::maximize(Some(self.main_window_id()), true)
fn maximize(&mut self) -> Task<Self::Message> {
self.core().maximize(None, true)
}
fn minimize(&mut self) -> Command<Self::Message> {
command::minimize(Some(self.main_window_id()))
fn minimize(&mut self) -> Task<Self::Message> {
self.core().minimize(None)
}
#[cfg(any(feature = "multi-window", feature = "wayland"))]
#[cfg(feature = "multi-window")]
fn title(&self, id: window::Id) -> &str {
self.core().title.get(&id).map_or("", |s| s.as_str())
}
#[cfg(not(any(feature = "multi-window", feature = "wayland")))]
#[cfg(not(feature = "multi-window"))]
fn title(&self) -> &str {
self.core()
.title
.get(&self.main_window_id())
.map_or("", |s| s.as_str())
.main_window_id()
.and_then(|id| self.core().title.get(&id).map(std::string::String::as_str))
.unwrap_or("")
}
#[cfg(any(feature = "multi-window", feature = "wayland"))]
fn set_window_title(&mut self, title: String, id: window::Id) -> Command<Self::Message> {
#[cfg(feature = "multi-window")]
fn set_window_title(&mut self, title: String, id: window::Id) -> Task<Self::Message> {
self.core_mut().title.insert(id, title.clone());
command::set_title(Some(id), title)
self.core().set_title(Some(id), title)
}
#[cfg(not(any(feature = "multi-window", feature = "wayland")))]
fn set_window_title(&mut self, title: String) -> Command<Self::Message> {
let id = self.main_window_id();
#[cfg(not(feature = "multi-window"))]
fn set_window_title(&mut self, title: String) -> Task<Self::Message> {
let Some(id) = self.core().main_window_id() else {
return Task::none();
};
self.core_mut().title.insert(id, title.clone());
Command::none()
Task::none()
}
#[allow(clippy::too_many_lines)]
@ -659,7 +693,7 @@ impl<App: Application> ApplicationExt for App {
let is_condensed = core.is_condensed();
let focused = core
.focused_window()
.is_some_and(|i| i == self.main_window_id());
.is_some_and(|i| Some(i) == self.core().main_window_id());
let content_row = crate::widget::row::with_children({
let mut widgets = Vec::with_capacity(4);
@ -678,7 +712,7 @@ impl<App: Application> ApplicationExt for App {
if self.nav_model().is_none() || core.show_content() {
// Manual spacing must be used due to state workarounds below
if has_nav {
widgets.push(horizontal_space(Length::Fixed(8.0)).into());
widgets.push(horizontal_space().width(Length::Fixed(8.0)).into());
}
let main_content = self.view().map(Message::App);
@ -727,7 +761,7 @@ impl<App: Application> ApplicationExt for App {
);
} else {
//TODO: this element is added to workaround state issues
widgets.push(horizontal_space(Length::Shrink).into());
widgets.push(horizontal_space().width(Length::Shrink).into());
}
}
}
@ -744,7 +778,7 @@ impl<App: Application> ApplicationExt for App {
.padding([0, 8, 8, 8])
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.style(crate::theme::Container::WindowBackground)
.class(crate::theme::Container::WindowBackground)
.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container")))
.into()
} else {
@ -770,7 +804,7 @@ impl<App: Application> ApplicationExt for App {
} else {
Message::Cosmic(cosmic::Message::ToggleNavBar)
})
.style(crate::theme::Button::HeaderBar);
.class(crate::theme::Button::HeaderBar);
header = header.start(toggle);
}
@ -825,11 +859,9 @@ impl<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::subscription::channel(
iced_futures::Subscription::run_with_id(
TypeId::of::<DbusActivation>(),
10,
move |mut output| async move {
iced::stream::channel(10, move |mut output| async move {
let mut single_instance: DbusActivation = DbusActivation::new();
let mut rx = single_instance.rx();
if let Ok(builder) = zbus::ConnectionBuilder::session() {
@ -888,7 +920,7 @@ fn single_instance_subscription<App: ApplicationExt>() -> Subscription<Message<A
loop {
iced::futures::pending!();
}
},
}),
)
}

238
src/app/multi_window.rs Normal file
View file

@ -0,0 +1,238 @@
//! Create and run daemons that run in the background.
//! Copied from iced 0.13, but adds optional initial window
use iced::application;
use iced::window;
use iced::{
self,
program::{self, with_style, with_subscription, with_theme, with_title},
runtime::{Appearance, DefaultStyle},
Program,
};
use iced::{Element, Result, Settings, Subscription, Task};
use std::marker::PhantomData;
pub(crate) struct Instance<State, Message, Theme, Renderer, Update, View> {
update: Update,
view: View,
_state: PhantomData<State>,
_message: PhantomData<Message>,
_theme: PhantomData<Theme>,
_renderer: PhantomData<Renderer>,
}
/// Creates an iced [`MultiWindow`] given its title, update, and view logic.
pub fn multi_window<State, Message, Theme, Renderer>(
title: impl Title<State>,
update: impl application::Update<State, Message>,
view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>,
) -> MultiWindow<impl Program<State = State, Message = Message, Theme = Theme>>
where
State: 'static,
Message: Send + std::fmt::Debug + 'static,
Theme: Default + DefaultStyle,
Renderer: program::Renderer,
{
use std::marker::PhantomData;
impl<State, Message, Theme, Renderer, Update, View> Program
for Instance<State, Message, Theme, Renderer, Update, View>
where
Message: Send + std::fmt::Debug + 'static,
Theme: Default + DefaultStyle,
Renderer: program::Renderer,
Update: application::Update<State, Message>,
View: for<'a> self::View<'a, State, Message, Theme, Renderer>,
{
type State = State;
type Message = Message;
type Theme = Theme;
type Renderer = Renderer;
type Executor = iced_futures::backend::default::Executor;
fn update(&self, state: &mut Self::State, message: Self::Message) -> Task<Self::Message> {
self.update.update(state, message).into()
}
fn view<'a>(
&self,
state: &'a Self::State,
window: window::Id,
) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> {
self.view.view(state, window).into()
}
}
MultiWindow {
raw: Instance {
update,
view,
_state: PhantomData,
_message: PhantomData,
_theme: PhantomData,
_renderer: PhantomData,
},
settings: Settings::default(),
window: None,
}
.title(title)
}
/// The underlying definition and configuration of an iced daemon.
///
/// You can use this API to create and run iced applications
/// step by step—without coupling your logic to a trait
/// or a specific type.
///
/// You can create a [`MultiWindow`] with the [`daemon`] helper.
#[derive(Debug)]
pub struct MultiWindow<P: Program> {
raw: P,
settings: Settings,
window: Option<window::Settings>,
}
impl<P: Program> MultiWindow<P> {
#[cfg(any(feature = "winit", feature = "wayland"))]
/// Runs the [`MultiWindow`].
///
/// The state of the [`MultiWindow`] must implement [`Default`].
/// If your state does not implement [`Default`], use [`run_with`]
/// instead.
///
/// [`run_with`]: Self::run_with
pub fn run(self) -> Result
where
Self: 'static,
P::State: Default,
{
self.raw.run(self.settings, self.window)
}
#[cfg(any(feature = "winit", feature = "wayland"))]
/// Runs the [`MultiWindow`] with a closure that creates the initial state.
pub fn run_with<I>(self, initialize: I) -> Result
where
Self: 'static,
I: FnOnce() -> (P::State, Task<P::Message>) + 'static,
{
self.raw.run_with(self.settings, self.window, initialize)
}
/// Sets the [`Settings`] that will be used to run the [`MultiWindow`].
pub fn settings(self, settings: Settings) -> Self {
Self { settings, ..self }
}
/// Sets the [`Title`] of the [`MultiWindow`].
pub(crate) fn title(
self,
title: impl Title<P::State>,
) -> MultiWindow<impl Program<State = P::State, Message = P::Message, Theme = P::Theme>> {
MultiWindow {
raw: with_title(self.raw, move |state, window| title.title(state, window)),
settings: self.settings,
window: self.window,
}
}
/// Sets the subscription logic of the [`MultiWindow`].
pub fn subscription(
self,
f: impl Fn(&P::State) -> Subscription<P::Message>,
) -> MultiWindow<impl Program<State = P::State, Message = P::Message, Theme = P::Theme>> {
MultiWindow {
raw: with_subscription(self.raw, f),
settings: self.settings,
window: self.window,
}
}
/// Sets the theme logic of the [`MultiWindow`].
pub fn theme(
self,
f: impl Fn(&P::State, window::Id) -> P::Theme,
) -> MultiWindow<impl Program<State = P::State, Message = P::Message, Theme = P::Theme>> {
MultiWindow {
raw: with_theme(self.raw, f),
settings: self.settings,
window: self.window,
}
}
/// Sets the style logic of the [`MultiWindow`].
pub fn style(
self,
f: impl Fn(&P::State, &P::Theme) -> Appearance,
) -> MultiWindow<impl Program<State = P::State, Message = P::Message, Theme = P::Theme>> {
MultiWindow {
raw: with_style(self.raw, f),
settings: self.settings,
window: self.window,
}
}
/// Sets the window settings of the [`MultiWindow`].
pub fn window(self, window: window::Settings) -> Self {
Self {
raw: self.raw,
settings: self.settings,
window: Some(window),
}
}
}
/// The title logic of some [`MultiWindow`].
///
/// This trait is implemented both for `&static str` and
/// any closure `Fn(&State, window::Id) -> String`.
///
/// This trait allows the [`daemon`] builder to take any of them.
pub trait Title<State> {
/// Produces the title of the [`MultiWindow`].
fn title(&self, state: &State, window: window::Id) -> String;
}
impl<State> Title<State> for &'static str {
fn title(&self, _state: &State, _window: window::Id) -> String {
(*self).to_string()
}
}
impl<T, State> Title<State> for T
where
T: Fn(&State, window::Id) -> String,
{
fn title(&self, state: &State, window: window::Id) -> String {
self(state, window)
}
}
/// The view logic of some [`MultiWindow`].
///
/// This trait allows the [`daemon`] builder to take any closure that
/// returns any `Into<Element<'_, Message>>`.
pub trait View<'a, State, Message, Theme, Renderer> {
/// Produces the widget of the [`MultiWindow`].
fn view(
&self,
state: &'a State,
window: window::Id,
) -> impl Into<Element<'a, Message, Theme, Renderer>>;
}
impl<'a, T, State, Message, Theme, Renderer, Widget> View<'a, State, Message, Theme, Renderer> for T
where
T: Fn(&'a State, window::Id) -> Widget,
State: 'static,
Widget: Into<Element<'a, Message, Theme, Renderer>>,
{
fn view(
&self,
state: &'a State,
window: window::Id,
) -> impl Into<Element<'a, Message, Theme, Renderer>> {
self(state, window)
}
}

View file

@ -20,7 +20,6 @@ pub struct Settings {
pub(crate) autosize: bool,
/// Set the application to not create a main window
#[cfg(feature = "wayland")]
pub(crate) no_main_window: bool,
/// Whether the window should have a border, a title bar, etc. or not.
@ -77,7 +76,6 @@ impl Default for Settings {
antialiasing: true,
#[cfg(feature = "wayland")]
autosize: false,
#[cfg(feature = "wayland")]
no_main_window: false,
client_decorations: true,
debug: false,

View file

@ -2,7 +2,7 @@
pub mod token;
use crate::{
app::Core,
app::{self, iced_settings, Core},
cctk::sctk,
iced::{
self,
@ -10,9 +10,13 @@ use crate::{
widget::Container,
window, Color, Length, Limits, Rectangle,
},
iced_style, iced_widget,
iced_widget,
theme::{self, system_dark, system_light, Button, THEME},
widget::{self, layer_container},
widget::{
self,
autosize::{autosize, Autosize},
layer_container,
},
Application, Element, Renderer,
};
use cctk::sctk::shell::xdg::window::WindowConfigure;
@ -21,14 +25,16 @@ use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize};
use cosmic_theme::Theme;
use iced::Pixels;
use iced_core::{Padding, Shadow};
use iced_style::container::Appearance;
use iced_widget::runtime::command::platform_specific::wayland::popup::{
SctkPopupSettings, SctkPositioner,
};
use iced_widget::runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner};
use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity};
use std::{borrow::Cow, num::NonZeroU32, rc::Rc};
use std::{borrow::Cow, num::NonZeroU32, rc::Rc, sync::LazyLock};
use tracing::info;
use crate::app::cosmic;
static AUTOSIZE_ID: LazyLock<iced::id::Id> =
LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize"));
static AUTOSIZE_MAIN_ID: LazyLock<iced::id::Id> =
LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize-main"));
#[derive(Debug, Clone)]
pub struct Context {
@ -37,9 +43,9 @@ pub struct Context {
pub background: CosmicPanelBackground,
pub output_name: String,
pub panel_type: PanelType,
/// Includes the suggested size of the window.
/// Includes the configured size of the window.
/// This can be used by apples to handle overflow themselves.
pub configure: Option<WindowConfigure>,
pub suggested_bounds: Option<(iced::Size)>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -94,7 +100,7 @@ impl Default for Context {
.unwrap_or(CosmicPanelBackground::ThemeDefault),
output_name: std::env::var("COSMIC_PANEL_OUTPUT").unwrap_or_default(),
panel_type: PanelType::from(std::env::var("COSMIC_PANEL_NAME").unwrap_or_default()),
configure: None,
suggested_bounds: None,
}
}
}
@ -116,19 +122,21 @@ impl Context {
let suggested = self.suggested_size(true);
let applet_padding = self.suggested_padding(true);
let configured_width = self
.configure
.suggested_bounds
.as_ref()
.and_then(|c| c.new_size.0.map(|w| w))
.and_then(|c| NonZeroU32::new(c.width as u32)) // TODO: should this be physical size instead of logical?
.unwrap_or_else(|| {
NonZeroU32::new(suggested.0 as u32 + applet_padding as u32 * 2).unwrap()
});
let configured_height = self
.configure
.suggested_bounds
.as_ref()
.and_then(|c| c.new_size.1.map(|h| h))
.and_then(|c| NonZeroU32::new(c.height as u32))
.unwrap_or_else(|| {
NonZeroU32::new(suggested.1 as u32 + applet_padding as u32 * 2).unwrap()
});
info!("{configured_height:?}");
(configured_width, configured_height)
}
@ -149,18 +157,15 @@ impl Context {
#[allow(clippy::cast_precision_loss)]
pub fn window_settings(&self) -> crate::app::Settings {
let (width, height) = self.suggested_size(true);
let width = f32::from(width);
let height = f32::from(height);
let applet_padding = self.suggested_padding(true);
let width = f32::from(width) + applet_padding as f32 * 2.;
let height = f32::from(height) + applet_padding as f32 * 2.;
let mut settings = crate::app::Settings::default()
.size(iced_core::Size::new(
width + applet_padding as f32 * 2.,
height + applet_padding as f32 * 2.,
))
.size(iced_core::Size::new(width, height))
.size_limits(
Limits::NONE
.min_height(height as f32 + applet_padding as f32 * 2.0)
.min_width(width as f32 + applet_padding as f32 * 2.0),
.min_height(height as f32)
.min_width(width as f32),
)
.resizable(None)
.default_text_size(14.0)
@ -169,6 +174,7 @@ impl Context {
if let Some(theme) = self.theme() {
settings = settings.theme(theme);
}
settings.exit_on_close = true;
settings
}
@ -182,26 +188,18 @@ impl Context {
&self,
icon: widget::icon::Handle,
) -> crate::widget::Button<'a, Message> {
let suggested = self.suggested_size(icon.symbolic);
let mut suggested = self.suggested_size(icon.symbolic);
let applet_padding = self.suggested_padding(icon.symbolic);
let (mut configured_width, mut configured_height) = self.suggested_window_size();
// Adjust the width to include padding and force the crosswise dim to match the window size
let is_horizontal = self.is_horizontal();
if is_horizontal {
configured_width =
NonZeroU32::new(suggested.0 as u32 + applet_padding as u32 * 2).unwrap();
} else {
configured_height =
NonZeroU32::new(suggested.1 as u32 + applet_padding as u32 * 2).unwrap();
}
let symbolic = icon.symbolic;
crate::widget::button::custom(
layer_container(
widget::icon(icon)
.style(if symbolic {
theme::Svg::Custom(Rc::new(|theme| crate::iced_style::svg::Appearance {
.class(if symbolic {
theme::Svg::Custom(Rc::new(|theme| crate::iced_widget::svg::Style {
color: Some(theme.cosmic().background.on.into()),
}))
} else {
@ -215,9 +213,9 @@ impl Context {
.width(Length::Fill)
.height(Length::Fill),
)
.width(Length::Fixed(configured_width.get() as f32))
.height(Length::Fixed(configured_height.get() as f32))
.style(Button::AppletIcon)
.width(Length::Fixed((suggested.0 + 2 * applet_padding) as f32))
.height(Length::Fixed((suggested.1 + 2 * applet_padding) as f32))
.class(Button::AppletIcon)
}
#[must_use]
@ -225,10 +223,11 @@ impl Context {
&self,
icon_name: &'a str,
) -> crate::widget::Button<'a, Message> {
let suggested_size = self.suggested_size(true);
self.icon_button_from_handle(
widget::icon::from_name(icon_name)
.symbolic(true)
.size(self.suggested_size(true).0)
.size(suggested_size.0)
.into(),
)
}
@ -237,7 +236,7 @@ impl Context {
pub fn popup_container<'a, Message: 'static>(
&self,
content: impl Into<Element<'a, Message>>,
) -> Container<'a, Message, crate::Theme, Renderer> {
) -> Autosize<'a, Message, crate::Theme, Renderer> {
let (vertical_align, horizontal_align) = match self.anchor {
PanelAnchor::Left => (Vertical::Center, Horizontal::Left),
PanelAnchor::Right => (Vertical::Center, Horizontal::Right),
@ -245,12 +244,12 @@ impl Context {
PanelAnchor::Bottom => (Vertical::Bottom, Horizontal::Center),
};
Container::<Message, _, Renderer>::new(
Container::<Message, _, Renderer>::new(content).style(theme::Container::custom(
|theme| {
autosize(
Container::<Message, _, Renderer>::new(
Container::<Message, _, Renderer>::new(content).style(|theme| {
let cosmic = theme.cosmic();
let corners = cosmic.corner_radii.clone();
Appearance {
iced_widget::container::Style {
text_color: Some(cosmic.background.on.into()),
background: Some(Color::from(cosmic.background.base).into()),
border: iced::Border {
@ -261,13 +260,21 @@ impl Context {
shadow: Shadow::default(),
icon_color: Some(cosmic.background.on.into()),
}
},
)),
}),
)
.width(Length::Shrink)
.height(Length::Shrink)
.align_x(horizontal_align)
.align_y(vertical_align),
AUTOSIZE_ID.clone(),
)
.limits(
Limits::NONE
.min_width(1.)
.min_height(1.)
.max_width(500.)
.max_height(1000.),
)
.width(Length::Shrink)
.height(Length::Shrink)
.align_x(horizontal_align)
.align_y(vertical_align)
}
#[must_use]
@ -282,7 +289,7 @@ impl Context {
) -> SctkPopupSettings {
let (width, height) = self.suggested_size(true);
let applet_padding = self.suggested_padding(true);
let pixel_offset = 8;
let pixel_offset = 4;
let (offset, anchor, gravity) = match self.anchor {
PanelAnchor::Left => ((pixel_offset, 0), Anchor::Right, Gravity::Right),
PanelAnchor::Right => ((-pixel_offset, 0), Anchor::Left, Gravity::Left),
@ -312,6 +319,35 @@ impl Context {
}
}
pub fn autosize_window<'a, Message: 'static>(
&self,
content: impl Into<Element<'a, Message>>,
) -> Autosize<'a, Message, crate::Theme, crate::Renderer> {
let force_configured = matches!(&self.panel_type, &PanelType::Other(ref n) if n.is_empty());
let w = autosize(content, AUTOSIZE_MAIN_ID.clone());
let mut limits = Limits::NONE;
let suggested_window_size = self.suggested_window_size();
if let Some(width) = self
.suggested_bounds
.as_ref()
.filter(|c| c.width as i32 > 0)
.map(|c| c.width)
{
limits = limits.width(width as f32);
}
if let Some(height) = self
.suggested_bounds
.as_ref()
.filter(|c| c.height as i32 > 0)
.map(|c| c.height)
{
limits = limits.height(height as f32);
}
w.limits(limits)
}
#[must_use]
pub fn theme(&self) -> Option<theme::Theme> {
match self.background {
@ -348,77 +384,67 @@ impl Context {
/// # Errors
///
/// Returns error on application failure.
pub fn run<App: Application>(autosize: bool, flags: App::Flags) -> iced::Result {
pub fn run<App: Application>(flags: App::Flags) -> iced::Result {
let helper = Context::default();
let mut settings = helper.window_settings();
settings.autosize = autosize;
if autosize {
settings.size_limits = Limits::NONE;
}
if let Some(icon_theme) = settings.default_icon_theme {
let mut settings = helper.window_settings();
settings.resizable = None;
if let Some(icon_theme) = settings.default_icon_theme.clone() {
crate::icon_theme::set_default(icon_theme);
}
let (width, height) = (settings.size.width as u32, settings.size.height as u32);
THEME
.lock()
.unwrap()
.set_theme(settings.theme.theme_type.clone());
let mut core = Core::default();
core.window.show_window_menu = false;
let (iced_settings, (mut core, flags, mut window_settings)) =
iced_settings::<App>(settings, flags);
core.window.show_headerbar = false;
core.window.sharp_corners = true;
core.window.show_maximize = false;
core.window.show_minimize = false;
core.window.use_template = false;
core.debug = settings.debug;
core.set_scale_factor(settings.scale_factor);
core.set_window_width(width);
core.set_window_height(height);
window_settings.decorations = false;
window_settings.exit_on_close_request = true;
window_settings.resizable = false;
window_settings.resize_border = 0;
THEME.lock().unwrap().set_theme(settings.theme.theme_type);
// TODO make multi-window not mandatory
let mut iced = iced::Settings::with_flags((core, flags));
iced.antialiasing = settings.antialiasing;
iced.default_font = settings.default_font;
iced.default_text_size = settings.default_text_size.into();
iced.id = Some(App::APP_ID.to_owned());
{
use iced::wayland::actions::window::SctkWindowSettings;
use iced_sctk::settings::InitialSurface;
iced.initial_surface = InitialSurface::XdgWindow(SctkWindowSettings {
app_id: Some(App::APP_ID.to_owned()),
autosize: settings.autosize,
client_decorations: settings.client_decorations,
resizable: settings.resizable,
size: (width, height),
size_limits: settings.size_limits,
title: None,
transparent: settings.transparent,
..SctkWindowSettings::default()
});
let mut app = super::app::multi_window::multi_window(
cosmic::Cosmic::title,
cosmic::Cosmic::update,
cosmic::Cosmic::view,
);
if core.main_window.get().is_none() {
app = app.window(window_settings.clone());
_ = core.main_window.set(iced_core::window::Id::RESERVED);
}
<cosmic::Cosmic<App> as iced::Application>::run(iced)
app.subscription(cosmic::Cosmic::subscription)
.style(cosmic::Cosmic::style)
.theme(cosmic::Cosmic::theme)
.settings(iced_settings)
.run_with(move || cosmic::Cosmic::<App>::init((core, flags, window_settings)))
}
#[must_use]
pub fn style() -> <crate::Theme as iced_style::application::StyleSheet>::Style {
<crate::Theme as iced_style::application::StyleSheet>::Style::Custom(Box::new(|theme| {
iced_style::application::Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
icon_color: theme.cosmic().on_bg_color().into(),
}
}))
pub fn style() -> iced_runtime::Appearance {
let theme = crate::theme::THEME.lock().unwrap();
iced_runtime::Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
icon_color: theme.cosmic().on_bg_color().into(),
}
}
pub fn menu_button<'a, Message>(
content: impl Into<Element<'a, Message>>,
) -> crate::widget::Button<'a, Message> {
crate::widget::button::custom(content)
.style(Button::AppletMenu)
.class(Button::AppletMenu)
.padding(menu_control_padding())
.width(Length::Fill)
}

View file

@ -1,11 +1,12 @@
use crate::iced;
use crate::iced::subscription;
use crate::iced_futures::futures;
use cctk::sctk::reexports::calloop;
use futures::{
channel::mpsc::{unbounded, UnboundedReceiver},
SinkExt, StreamExt,
};
use iced::Subscription;
use iced_futures::stream;
use std::{fmt::Debug, hash::Hash, thread::JoinHandle};
use super::wayland_handler::wayland_handler;
@ -13,13 +14,16 @@ use super::wayland_handler::wayland_handler;
pub fn activation_token_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
) -> iced::Subscription<TokenUpdate> {
subscription::channel(id, 50, move |mut output| async move {
let mut state = State::Ready;
Subscription::run_with_id(
id,
stream::channel(50, move |mut output| async move {
let mut state = State::Ready;
loop {
state = start_listening(state, &mut output).await;
}
})
loop {
state = start_listening(state, &mut output).await;
}
}),
)
}
pub enum State {

View file

@ -4,114 +4,55 @@
//! Create asynchronous actions to be performed in the background.
use iced::window;
use iced::Command;
use iced::Task;
use iced_core::window::Mode;
#[cfg(feature = "wayland")]
use iced_runtime::command::platform_specific::wayland::window::Action as WindowAction;
#[cfg(feature = "wayland")]
use iced_runtime::command::platform_specific::wayland::Action as WaylandAction;
#[cfg(feature = "wayland")]
use iced_runtime::command::platform_specific::Action as PlatformAction;
use iced_runtime::command::Action;
use iced_runtime::{task, Action};
use std::future::Future;
/// Yields a command which contains a batch of commands.
pub fn batch<X: 'static + Into<Y>, Y: 'static>(
commands: impl IntoIterator<Item = Command<X>>,
) -> Command<Y> {
Command::batch(commands).map(Into::into)
pub fn batch<X: Send + 'static + Into<Y>, Y: Send + 'static>(
commands: impl IntoIterator<Item = Task<X>>,
) -> Task<Y> {
Task::batch(commands).map(Into::into)
}
/// Yields a command which will run the future on the runtime executor.
pub fn future<X: Into<Y>, Y>(future: impl Future<Output = X> + Send + 'static) -> Command<Y> {
Command::single(Action::Future(Box::pin(async move { future.await.into() })))
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 command which will return a message.
pub fn message<X: Send + 'static + Into<Y>, Y>(message: X) -> Command<Y> {
pub fn message<X: Send + 'static + Into<Y>, Y: 'static>(message: X) -> Task<Y> {
future(async move { message.into() })
}
/// Initiates a window drag.
#[cfg(feature = "wayland")]
pub fn drag<M>(id: Option<window::Id>) -> Command<M> {
iced_sctk::commands::window::start_drag_window(id.unwrap_or(window::Id::MAIN))
}
/// Initiates a window drag.
#[cfg(not(feature = "wayland"))]
pub fn drag<M>(id: Option<window::Id>) -> Command<M> {
iced_runtime::window::drag(id.unwrap_or(window::Id::MAIN))
pub fn drag<M>(id: window::Id) -> Task<M> {
iced_runtime::window::drag(id)
}
/// Maximizes the window.
#[cfg(feature = "wayland")]
pub fn maximize<M>(id: Option<window::Id>, maximized: bool) -> Command<M> {
iced_sctk::commands::window::maximize(id.unwrap_or(window::Id::MAIN), maximized)
}
/// Maximizes the window.
#[cfg(not(feature = "wayland"))]
pub fn maximize<M>(id: Option<window::Id>, maximized: bool) -> Command<M> {
iced_runtime::window::maximize(id.unwrap_or(window::Id::MAIN), maximized)
pub fn maximize<M>(id: window::Id, maximized: bool) -> Task<M> {
iced_runtime::window::maximize(id, maximized)
}
/// Minimizes the window.
#[cfg(feature = "wayland")]
pub fn minimize<M>(id: Option<window::Id>) -> Command<M> {
iced_sctk::commands::window::set_mode_window(id.unwrap_or(window::Id::MAIN), Mode::Hidden)
}
/// Minimizes the window.
#[cfg(not(feature = "wayland"))]
pub fn minimize<M>(id: Option<window::Id>) -> Command<M> {
iced_runtime::window::minimize(id.unwrap_or(window::Id::MAIN), true)
pub fn minimize<M>(id: window::Id) -> Task<M> {
iced_runtime::window::minimize(id, true)
}
/// Sets the title of a window.
#[cfg(feature = "wayland")]
pub fn set_title<M>(id: Option<window::Id>, title: String) -> Command<M> {
window_action(WindowAction::Title {
id: id.unwrap_or(window::Id::MAIN),
title,
})
}
/// Sets the title of a window.
#[cfg(not(feature = "wayland"))]
#[allow(unused_variables, clippy::needless_pass_by_value)]
pub fn set_title<M>(id: Option<window::Id>, title: String) -> Command<M> {
Command::none()
pub fn set_title<M>(id: window::Id, title: String) -> Task<M> {
Task::none()
}
/// Sets the window mode to windowed.
#[cfg(feature = "wayland")]
pub fn set_windowed<M>(id: Option<window::Id>) -> Command<M> {
iced_sctk::commands::window::set_mode_window(id.unwrap_or(window::Id::MAIN), Mode::Windowed)
}
/// Sets the window mode to windowed.
#[cfg(not(feature = "wayland"))]
pub fn set_windowed<M>(id: Option<window::Id>) -> Command<M> {
iced_runtime::window::change_mode(id.unwrap_or(window::Id::MAIN), Mode::Windowed)
pub fn set_windowed<M>(id: window::Id) -> Task<M> {
iced_runtime::window::change_mode(id, Mode::Windowed)
}
/// Toggles the windows' maximize state.
#[cfg(feature = "wayland")]
pub fn toggle_maximize<M>(id: Option<window::Id>) -> Command<M> {
iced_sctk::commands::window::toggle_maximize(id.unwrap_or(window::Id::MAIN))
}
/// Toggles the windows' maximize state.
#[cfg(not(feature = "wayland"))]
pub fn toggle_maximize<M>(id: Option<window::Id>) -> Command<M> {
iced_runtime::window::toggle_maximize(id.unwrap_or(window::Id::MAIN))
}
/// Creates a command to apply an action to a window.
#[cfg(feature = "wayland")]
pub fn window_action<M>(action: WindowAction<M>) -> Command<M> {
Command::single(Action::PlatformSpecific(PlatformAction::Wayland(
WaylandAction::Window(action),
)))
pub fn toggle_maximize<M>(id: window::Id) -> Task<M> {
iced_runtime::window::toggle_maximize(id)
}

View file

@ -64,7 +64,7 @@ impl<'a, Message: 'a> CollectionWidget<'a, Message> for crate::widget::Column<'a
where
E: Into<crate::Element<'a, Message>>,
{
self.extend(other.drain(..))
self.extend(other.drain(..).map(Into::into))
}
fn push(self, element: impl Into<crate::Element<'a, Message>>) -> Self {
@ -77,7 +77,7 @@ impl<'a, Message: 'a> CollectionWidget<'a, Message> for crate::widget::Row<'a, M
where
E: Into<crate::Element<'a, Message>>,
{
self.extend(other.drain(..))
self.extend(other.drain(..).map(Into::into))
}
fn push(self, element: impl Into<crate::Element<'a, Message>>) -> Self {

View file

@ -4,7 +4,7 @@
//! Select preferred fonts.
pub use iced::Font;
use iced_core::font::{Family, Weight};
use iced_core::font::Weight;
pub fn default() -> Font {
Font::from(crate::config::interface_font())

View file

@ -17,7 +17,7 @@ pub enum Message {
}
pub fn subscription() -> Subscription<Message> {
listen_raw(|event, status| {
listen_raw(|event, status, _| {
if event::Status::Ignored != status {
return None;
}

View file

@ -4,28 +4,25 @@
#![allow(clippy::module_name_repetitions)]
#![cfg_attr(target_os = "redox", feature(lazy_cell))]
#[cfg(all(feature = "wayland", feature = "winit"))]
compile_error!("cannot use `wayland` feature with `winit`");
/// Recommended default imports.
pub mod prelude {
pub use crate::ext::*;
#[cfg(any(feature = "winit", feature = "wayland"))]
#[cfg(feature = "winit")]
pub use crate::ApplicationExt;
pub use crate::{Also, Apply, Element, Renderer, Theme};
}
pub use apply::{Also, Apply};
#[cfg(any(feature = "winit", feature = "wayland"))]
#[cfg(feature = "winit")]
pub mod app;
#[cfg(any(feature = "winit", feature = "wayland"))]
#[cfg(feature = "winit")]
pub use app::{Application, ApplicationExt};
#[cfg(feature = "applet")]
pub mod applet;
pub use iced::Command;
pub use iced::Task;
pub mod command;
pub mod config;
@ -62,12 +59,6 @@ pub use iced_renderer;
#[doc(inline)]
pub use iced_runtime;
#[cfg(feature = "wayland")]
pub use iced_sctk;
#[doc(inline)]
pub use iced_style;
#[doc(inline)]
pub use iced_widget;
@ -96,7 +87,7 @@ pub mod theme;
pub use theme::{style, Theme};
pub mod widget;
type Plain = iced_core::text::paragraph::Plain<<Renderer as iced_core::text::Renderer>::Paragraph>;
type Paragraph = <Renderer as iced_core::text::Renderer>::Paragraph;
pub type Renderer = iced::Renderer;
pub type Element<'a, Message> = iced::Element<'a, Message, crate::Theme, Renderer>;

View file

@ -14,6 +14,7 @@ use cosmic_config::CosmicConfigEntry;
use cosmic_theme::Component;
use cosmic_theme::LayeredTheme;
use iced_futures::Subscription;
use iced_runtime::{Appearance, DefaultStyle};
use std::sync::{Arc, Mutex};
@ -273,3 +274,14 @@ impl LayeredTheme for Theme {
self.layer = layer;
}
}
impl DefaultStyle for Theme {
fn default_style(&self) -> Appearance {
let cosmic = self.cosmic();
Appearance {
icon_color: cosmic.bg_color().into(),
background_color: cosmic.bg_color().into(),
text_color: cosmic.on_bg_color().into(),
}
}
}

View file

@ -1,7 +1,7 @@
use ashpd::desktop::settings::{ColorScheme, Contrast};
use ashpd::desktop::Color;
use iced::futures::{self, select, FutureExt, SinkExt, StreamExt};
use iced_futures::subscription;
use iced_futures::{stream, subscription};
use tracing::error;
#[derive(Debug, Clone)]
@ -12,86 +12,92 @@ pub enum Desktop {
}
pub fn desktop_settings() -> iced_futures::Subscription<Desktop> {
subscription::channel(std::any::TypeId::of::<Desktop>(), 10, |mut tx| {
async move {
let mut attempts = 0;
loop {
let Ok(settings) = ashpd::desktop::settings::Settings::new().await else {
error!("Failed to create the settings proxy");
#[cfg(feature = "tokio")]
::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts)))
.await;
#[cfg(not(feature = "tokio"))]
{
pending::<()>().await;
unreachable!();
}
attempts += 1;
continue;
};
match settings.color_scheme().await {
Ok(color_scheme) => {
let _ = tx.send(Desktop::ColorScheme(color_scheme)).await;
}
Err(err) => error!("Failed to get the color scheme {err:?}"),
};
match settings.contrast().await {
Ok(contrast) => {
let _ = tx.send(Desktop::Contrast(contrast)).await;
}
Err(err) => error!("Failed to get the contrast {err:?}"),
};
let mut color_scheme_stream = settings.receive_color_scheme_changed().await.ok();
if color_scheme_stream.is_none() {
error!("Failed to receive color scheme changes");
}
let mut contrast_stream = settings.receive_contrast_changed().await.ok();
if contrast_stream.is_none() {
error!("Failed to receive contrast changes");
}
iced_futures::Subscription::run_with_id(
std::any::TypeId::of::<Desktop>(),
stream::channel(10, |mut tx| {
async move {
let mut attempts = 0;
loop {
if color_scheme_stream.is_none() && contrast_stream.is_none() {
break;
let Ok(settings) = ashpd::desktop::settings::Settings::new().await else {
error!("Failed to create the settings proxy");
#[cfg(feature = "tokio")]
::tokio::time::sleep(::tokio::time::Duration::from_secs(
2_u64.pow(attempts),
))
.await;
#[cfg(not(feature = "tokio"))]
{
pending::<()>().await;
unreachable!();
}
attempts += 1;
continue;
};
match settings.color_scheme().await {
Ok(color_scheme) => {
let _ = tx.send(Desktop::ColorScheme(color_scheme)).await;
}
Err(err) => error!("Failed to get the color scheme {err:?}"),
};
match settings.contrast().await {
Ok(contrast) => {
let _ = tx.send(Desktop::Contrast(contrast)).await;
}
Err(err) => error!("Failed to get the contrast {err:?}"),
};
let mut color_scheme_stream =
settings.receive_color_scheme_changed().await.ok();
if color_scheme_stream.is_none() {
error!("Failed to receive color scheme changes");
}
let next_color_scheme = async {
if let Some(s) = color_scheme_stream.as_mut() {
return s.next().await;
}
futures::future::pending().await
};
let next_contrast = async {
if let Some(s) = contrast_stream.as_mut() {
return s.next().await;
}
futures::future::pending().await
};
let mut contrast_stream = settings.receive_contrast_changed().await.ok();
if contrast_stream.is_none() {
error!("Failed to receive contrast changes");
}
select! {
s = next_color_scheme.fuse() => {
if let Some(s) = s {
_ = tx.send(Desktop::ColorScheme(s)).await;
} else {
color_scheme_stream = None;
loop {
if color_scheme_stream.is_none() && contrast_stream.is_none() {
break;
}
let next_color_scheme = async {
if let Some(s) = color_scheme_stream.as_mut() {
return s.next().await;
}
},
futures::future::pending().await
};
c = next_contrast.fuse() => {
if let Some(c) = c {
_ = tx.send(Desktop::Contrast(c)).await;
} else {
contrast_stream = None;
let next_contrast = async {
if let Some(s) = contrast_stream.as_mut() {
return s.next().await;
}
}
};
// Reset the attempts counter if we successfully received a change
attempts = 0;
futures::future::pending().await
};
select! {
s = next_color_scheme.fuse() => {
if let Some(s) = s {
_ = tx.send(Desktop::ColorScheme(s)).await;
} else {
color_scheme_stream = None;
}
},
c = next_contrast.fuse() => {
if let Some(c) = c {
_ = tx.send(Desktop::Contrast(c)).await;
} else {
contrast_stream = None;
}
}
};
// Reset the attempts counter if we successfully received a change
attempts = 0;
}
}
}
}
})
}),
)
}

View file

@ -8,17 +8,17 @@ use iced_core::{Background, Color};
use crate::{
theme::TRANSPARENT_COMPONENT,
widget::button::{Appearance, StyleSheet},
widget::button::{Catalog, Style},
};
#[derive(Default)]
pub enum Button {
AppletIcon,
Custom {
active: Box<dyn Fn(bool, &crate::Theme) -> Appearance>,
disabled: Box<dyn Fn(&crate::Theme) -> Appearance>,
hovered: Box<dyn Fn(bool, &crate::Theme) -> Appearance>,
pressed: Box<dyn Fn(bool, &crate::Theme) -> Appearance>,
active: Box<dyn Fn(bool, &crate::Theme) -> Style>,
disabled: Box<dyn Fn(&crate::Theme) -> Style>,
hovered: Box<dyn Fn(bool, &crate::Theme) -> Style>,
pressed: Box<dyn Fn(bool, &crate::Theme) -> Style>,
},
AppletMenu,
Destructive,
@ -44,10 +44,10 @@ pub fn appearance(
disabled: bool,
style: &Button,
color: impl Fn(&Component) -> (Color, Option<Color>, Option<Color>),
) -> Appearance {
) -> Style {
let cosmic = theme.cosmic();
let mut corner_radii = &cosmic.corner_radii.radius_xl;
let mut appearance = Appearance::new();
let mut appearance = Style::new();
match style {
Button::Standard
@ -163,10 +163,10 @@ pub fn appearance(
appearance
}
impl StyleSheet for crate::Theme {
type Style = Button;
impl Catalog for crate::Theme {
type Class = Button;
fn active(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance {
fn active(&self, focused: bool, selected: bool, style: &Self::Class) -> Style {
if let Button::Custom { active, .. } = style {
return active(focused, self);
}
@ -186,7 +186,7 @@ impl StyleSheet for crate::Theme {
})
}
fn disabled(&self, style: &Self::Style) -> Appearance {
fn disabled(&self, style: &Self::Class) -> Style {
if let Button::Custom { disabled, .. } = style {
return disabled(self);
}
@ -202,11 +202,11 @@ impl StyleSheet for crate::Theme {
})
}
fn drop_target(&self, style: &Self::Style) -> Appearance {
fn drop_target(&self, style: &Self::Class) -> Style {
self.active(false, false, style)
}
fn hovered(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance {
fn hovered(&self, focused: bool, selected: bool, style: &Self::Class) -> Style {
if let Button::Custom { hovered, .. } = style {
return hovered(focused, self);
}
@ -233,7 +233,7 @@ impl StyleSheet for crate::Theme {
)
}
fn pressed(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance {
fn pressed(&self, focused: bool, selected: bool, style: &Self::Class) -> Style {
if let Button::Custom { pressed, .. } = style {
return pressed(focused, self);
}

File diff suppressed because it is too large Load diff

View file

@ -10,8 +10,6 @@ mod dropdown;
pub mod iced;
#[doc(inline)]
pub use self::iced::Application;
#[doc(inline)]
pub use self::iced::Checkbox;
#[doc(inline)]
pub use self::iced::Container;

View file

@ -9,9 +9,10 @@ use iced_core::mouse;
use iced_core::overlay;
use iced_core::renderer;
use iced_core::widget::Tree;
use iced_core::{Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget};
use iced_core::{Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget};
pub use iced_style::container::{Appearance, StyleSheet};
use iced_widget::container;
pub use iced_widget::container::{Catalog, Style};
pub fn aspect_ratio_container<'a, Message: 'static, T>(
content: T,
@ -117,22 +118,22 @@ where
/// Centers the contents in the horizontal axis of the [`Container`].
#[must_use]
pub fn center_x(mut self) -> Self {
self.container = self.container.center_x();
pub fn center_x(mut self, width: Length) -> Self {
self.container = self.container.center_x(width);
self
}
/// Centers the contents in the vertical axis of the [`Container`].
#[must_use]
pub fn center_y(mut self) -> Self {
self.container = self.container.center_y();
pub fn center_y(mut self, height: Length) -> Self {
self.container = self.container.center_y(height);
self
}
/// Sets the style of the [`Container`].
#[must_use]
pub fn style(mut self, style: impl Into<<crate::Theme as StyleSheet>::Style>) -> Self {
self.container = self.container.style(style);
pub fn class(mut self, style: impl Into<crate::style::Container<'a>>) -> Self {
self.container = self.container.class(style);
self
}
}
@ -173,9 +174,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn iced_core::widget::Operation<
iced_core::widget::OperationOutputWrapper<Message>,
>,
operation: &mut dyn iced_core::widget::Operation<()>,
) {
self.container.operate(tree, layout, renderer, operation);
}
@ -241,8 +240,9 @@ where
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
self.container.overlay(tree, layout, renderer)
self.container.overlay(tree, layout, renderer, translation)
}
}

276
src/widget/autosize.rs Normal file
View file

@ -0,0 +1,276 @@
//! Autosize Container, which will resize the window to its contents.
use cctk::sctk::shell::xdg::window;
use iced_core::event::{self, Event};
use iced_core::layout;
use iced_core::mouse;
use iced_core::overlay;
use iced_core::renderer;
use iced_core::widget::{Id, Tree};
use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget};
pub use iced_widget::container::{Catalog, Style};
pub fn autosize<'a, Message: 'static, Theme, E>(
content: E,
id: Id,
) -> Autosize<'a, Message, Theme, crate::Renderer>
where
E: Into<Element<'a, Message, Theme, crate::Renderer>>,
Theme: iced_widget::container::Catalog,
<Theme as iced_widget::container::Catalog>::Class<'a>: From<crate::theme::Container<'a>>,
{
Autosize::new(content, id)
}
/// An element decorating some content.
///
/// It is normally used for alignment purposes.
#[allow(missing_debug_implementations)]
pub struct Autosize<'a, Message, Theme, Renderer>
where
Renderer: iced_core::Renderer,
{
content: Element<'a, Message, Theme, Renderer>,
id: Id,
limits: layout::Limits,
auto_width: bool,
auto_height: bool,
}
impl<'a, Message, Theme, Renderer> Autosize<'a, Message, Theme, Renderer>
where
Renderer: iced_core::Renderer,
{
/// Creates an empty [`IdContainer`].
pub(crate) fn new<T>(content: T, id: Id) -> Self
where
T: Into<Element<'a, Message, Theme, Renderer>>,
{
Autosize {
content: content.into(),
id,
limits: layout::Limits::NONE,
auto_width: true,
auto_height: true,
}
}
pub fn limits(mut self, limits: layout::Limits) -> Self {
self.limits = limits;
self
}
pub fn auto_width(mut self, auto_width: bool) -> Self {
self.auto_width = auto_width;
self
}
pub fn auto_height(mut self, auto_height: bool) -> Self {
self.auto_height = auto_height;
self
}
pub fn max_width(mut self, v: f32) -> Self {
self.limits = self.limits.max_width(v);
self
}
pub fn max_height(mut self, v: f32) -> Self {
self.limits = self.limits.max_height(v);
self
}
pub fn min_width(mut self, v: f32) -> Self {
self.limits = self.limits.min_width(v);
self
}
pub fn min_height(mut self, v: f32) -> Self {
self.limits = self.limits.min_height(v);
self
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Autosize<'a, Message, Theme, Renderer>
where
Renderer: iced_core::Renderer,
{
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 mut limits = self.limits;
let min = self.limits.min();
let max = self.limits.max();
if self.auto_width {
limits.min_width(min.width);
limits.max_width(max.width);
}
if self.auto_height {
limits.min_height(min.height);
limits.max_height(max.height);
}
let node = self
.content
.as_widget()
.layout(&mut tree.children[0], renderer, &self.limits);
let size = node.size();
layout::Node::with_children(size, vec![node])
}
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn iced_core::widget::Operation<()>,
) {
operation.container(Some(&self.id), layout.bounds(), &mut |operation| {
self.content.as_widget().operate(
&mut tree.children[0],
layout.children().next().unwrap(),
renderer,
operation,
);
});
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
#[cfg(feature = "wayland")]
if matches!(
event,
Event::PlatformSpecific(event::PlatformSpecific::Wayland(
event::wayland::Event::RequestResize
))
) {
let bounds = layout.bounds().size();
clipboard.request_logical_window_size(bounds.width.max(1.), bounds.height.max(1.));
}
self.content.as_widget_mut().on_event(
&mut tree.children[0],
event.clone(),
layout.children().next().unwrap(),
cursor_position,
renderer,
clipboard,
shell,
viewport,
)
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
let content_layout = layout.children().next().unwrap();
self.content.as_widget().mouse_interaction(
&tree.children[0],
content_layout,
cursor_position,
viewport,
renderer,
)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
renderer_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
viewport: &Rectangle,
) {
let content_layout = layout.children().next().unwrap();
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
renderer_style,
content_layout,
cursor_position,
viewport,
);
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<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;
}
}
impl<'a, Message, Theme, Renderer> From<Autosize<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Renderer: 'a + iced_core::Renderer,
Theme: 'a,
{
fn from(c: Autosize<'a, Message, Theme, Renderer>) -> Element<'a, Message, Theme, Renderer> {
Element::new(c)
}
}

View file

@ -1,7 +1,7 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use super::{Builder, Style};
use super::{Builder, ButtonClass};
use crate::widget::{
icon::{self, Handle},
tooltip,
@ -48,7 +48,7 @@ impl<'a, Message> Button<'a, Message> {
line_height: 20,
font_size: 14,
font_weight: Weight::Normal,
style: Style::Icon,
class: ButtonClass::Icon,
variant: icon,
}
}
@ -121,7 +121,7 @@ impl<'a, Message> Button<'a, Message> {
pub fn vertical(mut self, vertical: bool) -> Self {
self.variant.vertical = vertical;
self.style = Style::IconVertical;
self.class = ButtonClass::IconVertical;
self
}
}
@ -157,7 +157,7 @@ impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Mes
crate::widget::column::with_children(content)
.padding(builder.padding)
.spacing(builder.spacing)
.align_items(Alignment::Center)
.align_x(Alignment::Center)
.apply(super::custom)
} else {
crate::widget::row::with_children(content)
@ -165,7 +165,7 @@ impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Mes
.width(builder.width)
.height(builder.height)
.spacing(builder.spacing)
.align_items(Alignment::Center)
.align_y(Alignment::Center)
.apply(super::custom)
};
@ -174,18 +174,22 @@ impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Mes
.id(builder.id)
.on_press_maybe(builder.on_press)
.selected(builder.variant.selected)
.style(builder.style);
.class(builder.class);
if builder.tooltip.is_empty() {
button.into()
} else {
tooltip(button, builder.tooltip, tooltip::Position::Top)
.size(builder.font_size)
.font(crate::font::Font {
weight: builder.font_weight,
..crate::font::default()
})
.into()
tooltip(
button,
crate::widget::text(builder.tooltip)
.size(builder.font_size)
.font(crate::font::Font {
weight: builder.font_weight,
..crate::font::default()
}),
tooltip::Position::Top,
)
.into()
}
}
}

View file

@ -42,7 +42,7 @@ impl<'a, Message> Button<'a, Message> {
line_height: 20,
font_size: 14,
font_weight: Weight::Normal,
style: Style::Image,
class: crate::theme::style::Button::Image,
variant,
}
}
@ -80,7 +80,7 @@ where
.selected(builder.variant.selected)
.id(builder.id)
.on_press_maybe(builder.on_press)
.style(builder.style)
.class(builder.class)
.into()
}
}

View file

@ -4,7 +4,7 @@
//! Hyperlink button widget
use super::Builder;
use super::Style;
use super::ButtonClass;
use crate::prelude::*;
use crate::widget::icon::{self, Handle};
use crate::widget::{button, row, tooltip};
@ -44,7 +44,7 @@ impl<'a, Message> Button<'a, Message> {
line_height: 20,
font_size: 14,
font_weight: Weight::Normal,
style: Style::Link,
class: ButtonClass::Link,
variant: link,
}
}
@ -81,23 +81,27 @@ impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Mes
.width(builder.width)
.height(builder.height)
.spacing(builder.spacing)
.align_items(Alignment::Center)
.align_y(Alignment::Center)
.apply(button::custom)
.padding(0)
.id(builder.id)
.on_press_maybe(builder.on_press.take())
.style(builder.style);
.class(builder.class);
if builder.tooltip.is_empty() {
button.into()
} else {
tooltip(button, builder.tooltip, tooltip::Position::Top)
.size(builder.font_size)
.font(crate::font::Font {
weight: builder.font_weight,
..crate::font::default()
})
.into()
tooltip(
button,
crate::widget::text(builder.tooltip)
.size(builder.font_size)
.font(crate::font::Font {
weight: builder.font_weight,
..crate::font::default()
}),
tooltip::Position::Top,
)
.into()
}
}
}

View file

@ -3,7 +3,7 @@
//! Button widgets for COSMIC applications.
pub use crate::theme::Button as Style;
pub use crate::theme::Button as ButtonClass;
pub mod link;
use derive_setters::Setters;
@ -26,7 +26,7 @@ pub use image::Button as ImageButton;
mod style;
#[doc(inline)]
pub use style::{Appearance, StyleSheet};
pub use style::{Catalog, Style};
mod text;
#[doc(inline)]
@ -105,7 +105,7 @@ pub struct Builder<'a, Message, Variant> {
font_weight: Weight,
/// The preferred style of the button.
style: Style,
class: ButtonClass,
#[setters(skip)]
variant: Variant,

View file

@ -9,7 +9,7 @@ use crate::theme::THEME;
/// The appearance of a button.
#[must_use]
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
pub struct Style {
/// The amount of offset to apply to the shadow of the button.
pub shadow_offset: Vector,
@ -41,7 +41,7 @@ pub struct Appearance {
pub text_color: Option<Color>,
}
impl Appearance {
impl Style {
// TODO: `Radius` is not `const fn` compatible.
pub fn new() -> Self {
let rad_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0;
@ -60,33 +60,34 @@ impl Appearance {
}
}
impl std::default::Default for Appearance {
impl std::default::Default for Style {
fn default() -> Self {
Self::new()
}
}
// TODO update to match other styles
/// A set of rules that dictate the style of a button.
pub trait StyleSheet {
pub trait Catalog {
/// The supported style of the [`StyleSheet`].
type Style: Default;
type Class: Default;
/// Produces the active [`Appearance`] of a button.
fn active(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance;
fn active(&self, focused: bool, selected: bool, style: &Self::Class) -> Style;
/// Produces the disabled [`Appearance`] of a button.
fn disabled(&self, style: &Self::Style) -> Appearance;
fn disabled(&self, style: &Self::Class) -> Style;
/// [`Appearance`] when the button is the target of a DND operation.
fn drop_target(&self, style: &Self::Style) -> Appearance {
fn drop_target(&self, style: &Self::Class) -> Style {
self.hovered(false, false, style)
}
/// Produces the hovered [`Appearance`] of a button.
fn hovered(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance;
fn hovered(&self, focused: bool, selected: bool, style: &Self::Class) -> Style;
/// Produces the pressed [`Appearance`] of a button.
fn pressed(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance;
fn pressed(&self, focused: bool, selected: bool, style: &Self::Class) -> Style;
/// Background color of the selection indicator
fn selection_background(&self) -> Background;

View file

@ -1,7 +1,7 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use super::{Builder, Style};
use super::{Builder, ButtonClass, Style};
use crate::widget::{icon, row, tooltip};
use crate::{ext::CollectionWidget, Element};
use apply::Apply;
@ -14,14 +14,14 @@ pub type Button<'a, Message> = Builder<'a, Message, Text>;
pub fn destructive<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> {
Button::new(Text::new())
.label(label)
.style(Style::Destructive)
.class(ButtonClass::Destructive)
}
/// A text button with the suggested style
pub fn suggested<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> {
Button::new(Text::new())
.label(label)
.style(Style::Suggested)
.class(ButtonClass::Suggested)
}
/// A text button with the standard style
@ -31,7 +31,9 @@ pub fn standard<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Messa
/// A text button with the text style
pub fn text<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> {
Button::new(Text::new()).label(label).style(Style::Text)
Button::new(Text::new())
.label(label)
.class(ButtonClass::Text)
}
/// The text variant of a button.
@ -66,7 +68,7 @@ impl<'a, Message> Button<'a, Message> {
line_height: 20,
font_size: 14,
font_weight: Weight::Normal,
style: Style::Standard,
class: ButtonClass::Standard,
variant: text,
}
}
@ -125,23 +127,27 @@ impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Mes
.width(builder.width)
.height(builder.height)
.spacing(builder.spacing)
.align_items(Alignment::Center)
.align_y(Alignment::Center)
.apply(super::custom)
.padding(0)
.id(builder.id)
.on_press_maybe(builder.on_press.take())
.style(builder.style);
.class(builder.class);
if builder.tooltip.is_empty() {
button.into()
} else {
tooltip(button, builder.tooltip, tooltip::Position::Top)
.size(builder.font_size)
.font(crate::font::Font {
weight: builder.font_weight,
..crate::font::default()
})
.into()
tooltip(
button,
crate::widget::text(builder.tooltip)
.size(builder.font_size)
.font(crate::font::Font {
weight: builder.font_weight,
..crate::font::default()
}),
tooltip::Position::Top,
)
.into()
}
}
}

View file

@ -7,7 +7,7 @@
//! A [`Button`] has some local [`State`].
use iced_runtime::core::widget::Id;
use iced_runtime::{keyboard, Command};
use iced_runtime::{keyboard, task, Action, Task};
use iced_core::event::{self, Event};
use iced_core::renderer::{self, Quad, Renderer};
@ -20,11 +20,11 @@ use iced_core::{overlay, Shadow};
use iced_core::{
Background, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget,
};
use iced_renderer::core::widget::{operation, OperationOutputWrapper};
use iced_renderer::core::widget::operation;
use crate::theme::THEME;
pub use super::style::{Appearance, StyleSheet};
pub use super::style::{Catalog, Style};
/// Internally defines different button widget variants.
enum Variant<Message> {
@ -173,7 +173,7 @@ impl<'a, Message> Button<'a, Message> {
}
/// Sets the style variant of this [`Button`].
pub fn style(mut self, style: crate::theme::Button) -> Self {
pub fn class(mut self, style: crate::theme::Button) -> Self {
self.style = style;
self
}
@ -257,7 +257,7 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
tree: &mut Tree,
layout: Layout<'_>,
renderer: &crate::Renderer,
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
operation: &mut dyn Operation<()>,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.content.as_widget().operate(
@ -470,10 +470,11 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
selection_background,
);
iced_core::svg::Renderer::draw(
let svg_handle = svg::Svg::new(crate::widget::common::object_select().clone())
.color(icon_color);
iced_core::svg::Renderer::draw_svg(
renderer,
crate::widget::common::object_select().clone(),
Some(icon_color),
svg_handle,
Rectangle {
width: 16.0,
height: 16.0,
@ -498,11 +499,10 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
},
selection_background,
);
iced_core::svg::Renderer::draw(
let svg_handle = svg::Svg::new(close_icon.clone()).color(icon_color);
iced_core::svg::Renderer::draw_svg(
renderer,
close_icon.clone(),
Some(icon_color),
svg_handle,
Rectangle {
width: 16.0,
height: 16.0,
@ -533,11 +533,16 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &crate::Renderer,
mut translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
let mut position = layout.bounds().position();
translation.x += position.x;
translation.y += position.y;
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout.children().next().unwrap(),
renderer,
translation,
)
}
@ -748,11 +753,11 @@ pub fn update<'a, Message: Clone>(
pub fn draw<Renderer: iced_core::Renderer, Theme>(
renderer: &mut Renderer,
bounds: Rectangle,
styling: &super::style::Appearance,
draw_contents: impl FnOnce(&mut Renderer, &Appearance),
styling: &super::style::Style,
draw_contents: impl FnOnce(&mut Renderer, &Style),
is_image: bool,
) where
Theme: super::style::StyleSheet,
Theme: super::style::Catalog,
{
let doubled_border_width = styling.border_width * 2.0;
let doubled_outline_width = styling.outline_width * 2.0;
@ -909,9 +914,9 @@ pub fn mouse_interaction(
}
}
/// Produces a [`Command`] that focuses the [`Button`] with the given [`Id`].
pub fn focus<Message: 'static>(id: Id) -> Command<Message> {
Command::widget(operation::focusable::focus(id))
/// Produces a [`Task`] that focuses the [`Button`] with the given [`Id`].
pub fn focus<Message: 'static>(id: Id) -> Task<Message> {
task::effect(Action::Widget(Box::new(operation::focusable::focus(id))))
}
impl operation::Focusable for State {

View file

@ -86,7 +86,7 @@ where
text(first_day_of_week.to_string())
.size(12)
.width(Length::Fixed(36.0))
.horizontal_alignment(Horizontal::Center),
.align_x(Horizontal::Center),
);
first_day_of_week = first_day_of_week.succ();
@ -138,17 +138,17 @@ fn date_button<Message>(
on_select: &dyn Fn(NaiveDate) -> Message,
) -> crate::widget::Button<'static, Message> {
let style = if is_day {
button::Style::Suggested
button::ButtonClass::Suggested
} else {
button::Style::Text
button::ButtonClass::Text
};
let button = button::custom(
text(format!("{}", date.day()))
.horizontal_alignment(Horizontal::Center)
.vertical_alignment(Vertical::Center),
.align_x(Horizontal::Center)
.align_y(Vertical::Center),
)
.style(style)
.class(style)
.height(Length::Fixed(36.0))
.width(Length::Fixed(36.0));

View file

@ -5,12 +5,12 @@ use iced_core::{Background, Color};
/// Appearance of the cards.
#[derive(Clone, Copy)]
pub struct Appearance {
pub struct Style {
pub card_1: Background,
pub card_2: Background,
}
impl Default for Appearance {
impl Default for Style {
fn default() -> Self {
Self {
card_1: Background::Color(Color::WHITE),
@ -20,7 +20,7 @@ impl Default for Appearance {
}
/// Defines the [`Appearance`] of a cards.
pub trait StyleSheet {
pub trait Catalog {
/// The default [`Appearance`] of the cards.
fn default(&self) -> Appearance;
fn default(&self) -> Style;
}

View file

@ -10,10 +10,10 @@ use std::time::{Duration, Instant};
use crate::theme::iced::Slider;
use crate::theme::{Button, THEME};
use crate::widget::{container, segmented_button::Entity, slider};
use crate::widget::{button::Catalog, container, segmented_button::Entity, slider};
use crate::Element;
use derive_setters::Setters;
use iced::Command;
use iced::Task;
use iced_core::event::{self, Event};
use iced_core::gradient::{ColorStop, Linear};
use iced_core::renderer::Quad;
@ -23,12 +23,11 @@ use iced_core::{
Rectangle, Renderer, Shadow, Shell, Size, Vector, Widget,
};
use iced_style::slider::{HandleShape, RailBackground};
use iced_widget::slider::{HandleShape, RailBackground};
use iced_widget::{canvas, column, horizontal_space, row, scrollable, vertical_space, Row};
use lazy_static::lazy_static;
use palette::{FromColor, RgbHue};
use super::button::StyleSheet;
use super::divider::horizontal;
use super::icon::{self, from_name};
use super::segmented_button::{self, SingleSelect};
@ -135,7 +134,7 @@ impl ColorPickerModel {
)
}
pub fn update<Message>(&mut self, update: ColorPickerUpdate) -> Command<Message> {
pub fn update<Message>(&mut self, update: ColorPickerUpdate) -> Task<Message> {
match update {
ColorPickerUpdate::ActiveColor(c) => {
self.must_clear_cache.store(true, Ordering::SeqCst);
@ -206,7 +205,7 @@ impl ColorPickerModel {
self.copied_at = None;
}
};
Command::none()
Task::none()
}
#[must_use]
@ -298,7 +297,7 @@ where
.width(self.width),
// canvas with gradient for the current color
// still needs the canvas and the handle to be drawn on it
container(vertical_space(self.height))
container(vertical_space().height(self.height))
.width(self.width)
.height(self.height),
slider(
@ -310,16 +309,21 @@ where
on_update(ColorPickerUpdate::ActiveColor(new))
}
)
.style(Slider::Custom {
.class(Slider::Custom {
active: Rc::new(|t| {
let cosmic = t.cosmic();
let mut a = slider::StyleSheet::active(t, &Slider::default());
a.rail.colors = RailBackground::Gradient {
gradient: Linear::new(Radians(0.0)).add_stops(HSV_RAINBOW.clone()),
auto_angle: true,
};
let mut a =
slider::Catalog::style(t, &Slider::default(), slider::Status::Active);
// a.rail.colors = RailBackground::Gradient {
// gradient: Linear::new(Radians(0.0)).add_stops(HSV_RAINBOW.clone()),
// auto_angle: true,
// };
a.rail.backgrounds = (
Background::Color(Color::TRANSPARENT),
Background::Color(Color::TRANSPARENT),
);
a.rail.width = 8.0;
a.handle.color = Color::TRANSPARENT;
a.handle.background = Color::TRANSPARENT.into();
a.handle.shape = HandleShape::Circle { radius: 8.0 };
a.handle.border_color = cosmic.palette.neutral_10.into();
a.handle.border_width = 4.0;
@ -327,13 +331,15 @@ where
}),
hovered: Rc::new(|t| {
let cosmic = t.cosmic();
let mut a = slider::StyleSheet::active(t, &Slider::default());
a.rail.colors = RailBackground::Gradient {
gradient: Linear::new(Radians(0.0)).add_stops(HSV_RAINBOW.clone()),
auto_angle: true,
};
let mut a =
slider::Catalog::style(t, &Slider::default(), slider::Status::Active);
// a.rail.colors = RailBackground::Gradient {
// gradient: Linear::new(Radians(0.0)).add_stops(HSV_RAINBOW.clone()),
// auto_angle: true,
// };
a.rail.backgrounds = (Color::TRANSPARENT.into(), Color::TRANSPARENT.into());
a.rail.width = 8.0;
a.handle.color = Color::TRANSPARENT;
a.handle.background = Color::TRANSPARENT.into();
a.handle.shape = HandleShape::Circle { radius: 8.0 };
a.handle.border_color = cosmic.palette.neutral_10.into();
a.handle.border_width = 4.0;
@ -341,13 +347,22 @@ where
}),
dragging: Rc::new(|t| {
let cosmic = t.cosmic();
let mut a = slider::StyleSheet::active(t, &Slider::default());
a.rail.colors = RailBackground::Gradient {
gradient: Linear::new(Radians(0.0)).add_stops(HSV_RAINBOW.clone()),
auto_angle: true,
};
let mut a =
slider::Catalog::style(t, &Slider::default(), slider::Status::Active);
// a.rail.backgrounds = (
// RailBackground::Gradient(Gradient {
// gradient: Linear::new(Radians(0.0)).add_stops(HSV_RAINBOW.clone()),
// auto_angle: true,
// }),
// RailBackground::Gradient {
// gradient: Linear::new(Radians(0.0)).add_stops(HSV_RAINBOW.clone()),
// auto_angle: true,
// },
// );
a.rail.backgrounds =
(iced::Color::TRANSPARENT.into(), Color::TRANSPARENT.into());
a.rail.width = 8.0;
a.handle.color = Color::TRANSPARENT;
a.handle.background = Color::TRANSPARENT.into();
a.handle.shape = HandleShape::Circle { radius: 8.0 };
a.handle.border_color = cosmic.palette.neutral_10.into();
a.handle.border_width = 4.0;
@ -373,7 +388,7 @@ where
from_name("edit-copy-symbolic").size(spacing.space_s).into(),
))
.on_press(on_update(ColorPickerUpdate::Copied(Instant::now())))
.style(Button::Text);
.class(Button::Text);
match self.copied_at.take() {
Some(t) if Instant::now().duration_since(t) > Duration::from_secs(2) => {
@ -381,13 +396,13 @@ where
}
Some(_) => tooltip(
button,
copied_to_clipboard_label,
text(copied_to_clipboard_label),
iced_widget::tooltip::Position::Bottom,
)
.into(),
None => tooltip(
button,
copy_to_clipboard_label,
text(copy_to_clipboard_label),
iced_widget::tooltip::Position::Bottom,
)
.into(),
@ -431,7 +446,7 @@ where
)
.width(self.width)
.direction(iced_widget::scrollable::Direction::Horizontal(
scrollable::Properties::new().alignment(scrollable::Alignment::End),
scrollable::Scrollbar::new().anchor(scrollable::Anchor::End),
))
}]
.spacing(spacing.space_xxs),
@ -445,7 +460,7 @@ where
button::custom(
text(reset_to_default)
.width(self.width)
.horizontal_alignment(iced_core::alignment::Horizontal::Center)
.align_x(iced_core::alignment::Horizontal::Center)
)
.width(self.width)
.on_press(on_update(ColorPickerUpdate::Reset))
@ -461,18 +476,18 @@ where
button::custom(
text(cancel)
.width(self.width)
.horizontal_alignment(iced_core::alignment::Horizontal::Center)
.align_x(iced_core::alignment::Horizontal::Center)
)
.width(self.width)
.on_press(on_update(ColorPickerUpdate::Cancel)),
button::custom(
text(save)
.width(self.width)
.horizontal_alignment(iced_core::alignment::Horizontal::Center)
.align_x(iced_core::alignment::Horizontal::Center)
)
.width(self.width)
.on_press(on_update(ColorPickerUpdate::AppliedColor))
.style(Button::Suggested)
.class(Button::Suggested)
]
.spacing(spacing.space_xs)
.width(self.width),
@ -589,7 +604,7 @@ where
let translation = Vector::new(canvas_layout.bounds().x, canvas_layout.bounds().y);
iced_core::Renderer::with_translation(renderer, translation, |renderer| {
canvas::Renderer::draw(renderer, vec![geo]);
iced_renderer::geometry::Renderer::draw_geometry(renderer, geo);
});
let bounds = canvas_layout.bounds();
@ -657,10 +672,11 @@ where
state: &'b mut Tree,
layout: Layout<'_>,
renderer: &crate::Renderer,
translation: Vector,
) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
self.inner
.as_widget_mut()
.overlay(&mut state.children[0], layout, renderer)
.overlay(&mut state.children[0], layout, renderer, translation)
}
fn on_event(
@ -782,12 +798,12 @@ pub fn color_button<'a, Message: 'static>(
let spacing = THEME.lock().unwrap().cosmic().spacing;
button::custom(if color.is_some() {
Element::from(vertical_space(Length::Fixed(f32::from(spacing.space_s))))
Element::from(vertical_space().height(Length::Fixed(f32::from(spacing.space_s))))
} else {
Element::from(column![
vertical_space(Length::FillPortion(6)),
vertical_space().height(Length::FillPortion(6)),
row![
horizontal_space(Length::FillPortion(6)),
horizontal_space().width(Length::FillPortion(6)),
Icon::from(
icon::from_name("list-add-symbolic")
.prefer_svg(true)
@ -797,17 +813,17 @@ pub fn color_button<'a, Message: 'static>(
.width(icon_portion)
.height(Length::Fill)
.content_fit(iced_core::ContentFit::Contain),
horizontal_space(Length::FillPortion(6)),
horizontal_space().width(Length::FillPortion(6)),
]
.height(icon_portion)
.width(Length::Fill),
vertical_space(Length::FillPortion(6)),
vertical_space().height(Length::FillPortion(6)),
])
})
.width(Length::Fixed(f32::from(spacing.space_s)))
.height(Length::Fixed(f32::from(spacing.space_s)))
.on_press_maybe(on_press)
.style(crate::theme::Button::Custom {
.class(crate::theme::Button::Custom {
active: Box::new(move |focused, theme| {
let cosmic = theme.cosmic();
@ -817,7 +833,7 @@ pub fn color_button<'a, Message: 'static>(
(0.0, Color::TRANSPARENT)
};
let standard = theme.active(focused, false, &Button::Standard);
button::Appearance {
button::Style {
shadow_offset: Vector::default(),
background: color.map(Background::from).or(standard.background),
border_radius: cosmic.radius_xs().into(),
@ -834,7 +850,7 @@ pub fn color_button<'a, Message: 'static>(
let cosmic = theme.cosmic();
let standard = theme.disabled(&Button::Standard);
button::Appearance {
button::Style {
shadow_offset: Vector::default(),
background: color.map(Background::from).or(standard.background),
border_radius: cosmic.radius_xs().into(),
@ -857,7 +873,7 @@ pub fn color_button<'a, Message: 'static>(
};
let standard = theme.hovered(focused, false, &Button::Standard);
button::Appearance {
button::Style {
shadow_offset: Vector::default(),
background: color.map(Background::from).or(standard.background),
border_radius: cosmic.radius_xs().into(),
@ -880,7 +896,7 @@ pub fn color_button<'a, Message: 'static>(
};
let standard = theme.pressed(focused, false, &Button::Standard);
button::Appearance {
button::Style {
shadow_offset: Vector::default(),
background: color.map(Background::from).or(standard.background),
border_radius: cosmic.radius_xs().into(),

View file

@ -4,13 +4,14 @@
use crate::Element;
use iced::advanced::layout::{self, Layout};
use iced::advanced::widget::{self, Operation, OperationOutputWrapper};
use iced::advanced::widget::{self, Operation};
use iced::advanced::{overlay, renderer};
use iced::advanced::{Clipboard, Shell};
use iced::{event, mouse, Event, Point, Rectangle, Size};
use iced_core::Renderer;
pub(super) struct Overlay<'a, 'b, Message> {
pub(crate) position: Point,
pub(super) content: &'b mut Element<'a, Message>,
pub(super) tree: &'b mut widget::Tree,
pub(super) width: f32,
@ -21,13 +22,8 @@ impl<'a, 'b, Message> overlay::Overlay<Message, crate::Theme, crate::Renderer>
where
Message: Clone,
{
fn layout(
&mut self,
renderer: &crate::Renderer,
bounds: Size,
position: Point,
_translation: iced::Vector,
) -> layout::Node {
fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node {
let position = self.position;
let limits = layout::Limits::new(Size::ZERO, bounds)
.width(self.width)
.height(bounds.height - 8.0 - position.y);
@ -98,7 +94,7 @@ where
&mut self,
layout: Layout<'_>,
renderer: &crate::Renderer,
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
operation: &mut dyn Operation<()>,
) {
self.content
.as_widget_mut()
@ -122,8 +118,9 @@ where
layout: Layout<'_>,
renderer: &crate::Renderer,
) -> Option<overlay::Element<'c, Message, crate::Theme, crate::Renderer>> {
let translation = iced::Vector::new(self.position.x, self.position.y);
self.content
.as_widget_mut()
.overlay(self.tree, layout, renderer)
.overlay(self.tree, layout, renderer, translation)
}
}

View file

@ -13,11 +13,9 @@ use iced_core::event::{self, Event};
use iced_core::widget::{Operation, Tree};
use iced_core::{
layout, mouse, overlay as iced_overlay, renderer, Clipboard, Layout, Length, Padding,
Rectangle, Shell, Widget,
Rectangle, Shell, Vector, Widget,
};
use iced_renderer::core::widget::OperationOutputWrapper;
#[must_use]
pub struct ContextDrawer<'a, Message> {
id: Option<iced_core::widget::Id>,
@ -48,19 +46,19 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> {
text::heading(header)
.width(Length::FillPortion(1))
.height(Length::Fill)
.horizontal_alignment(alignment::Horizontal::Center)
.vertical_alignment(alignment::Vertical::Center),
.align_x(alignment::Horizontal::Center)
.align_y(alignment::Vertical::Center),
)
.push(
button::text("Close")
.trailing_icon(icon::from_name("go-next-symbolic"))
.on_press(on_close)
.style(crate::theme::Button::Link)
.class(crate::theme::Button::Link)
.apply(container)
.width(Length::FillPortion(1))
.height(Length::Fill)
.align_x(alignment::Horizontal::Right)
.center_y(),
.center_y(Length::Fill),
)
// XXX must be done after pushing elements or it may be overwritten by size hints from contents
.height(Length::Fixed(80.0))
@ -84,7 +82,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> {
container(
LayerContainer::new(pane)
.layer(cosmic_theme::Layer::Primary)
.style(crate::style::Container::ContextDrawer)
.class(crate::style::Container::ContextDrawer)
.width(Length::Fill)
.height(Length::Fill)
.max_width(max_width),
@ -159,7 +157,7 @@ impl<'a, Message: Clone> Widget<Message, crate::Theme, Renderer> for ContextDraw
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
operation: &mut dyn Operation<()>,
) {
self.content
.as_widget()
@ -232,17 +230,20 @@ impl<'a, Message: Clone> Widget<Message, crate::Theme, Renderer> for ContextDraw
tree: &'b mut Tree,
layout: Layout<'_>,
_renderer: &Renderer,
translation: Vector,
) -> Option<iced_overlay::Element<'b, Message, crate::Theme, Renderer>> {
let bounds = layout.bounds();
Some(iced_overlay::Element::new(
layout.position(),
Box::new(Overlay {
content: &mut self.drawer,
tree: &mut tree.children[1],
width: bounds.width,
}),
))
let mut position = layout.position();
position.x += translation.x;
position.y += translation.y;
Some(iced_overlay::Element::new(Box::new(Overlay {
content: &mut self.drawer,
tree: &mut tree.children[1],
width: bounds.width,
position,
})))
}
#[cfg(feature = "a11y")]

View file

@ -8,7 +8,7 @@ use crate::widget::menu::{
};
use derive_setters::Setters;
use iced::touch::Finger;
use iced::Event;
use iced::{Event, Vector};
use iced_core::widget::{tree, Tree, Widget};
use iced_core::{event, mouse, touch, Length, Point, Size};
use std::collections::HashSet;
@ -144,9 +144,7 @@ impl<'a, Message: Clone> Widget<Message, crate::Theme, crate::Renderer>
tree: &mut Tree,
layout: iced_core::Layout<'_>,
renderer: &crate::Renderer,
operation: &mut dyn iced_core::widget::Operation<
iced_core::widget::OperationOutputWrapper<Message>,
>,
operation: &mut dyn iced_core::widget::Operation<()>,
) {
self.content
.as_widget()
@ -213,6 +211,7 @@ impl<'a, Message: Clone> Widget<Message, crate::Theme, crate::Renderer>
tree: &'b mut Tree,
layout: iced_core::Layout<'_>,
_renderer: &crate::Renderer,
translation: Vector,
) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
let state = tree.state.downcast_ref::<LocalState>();
@ -247,6 +246,7 @@ impl<'a, Message: Clone> Widget<Message, crate::Theme, crate::Renderer>
root_bounds_list: vec![bounds],
path_highlight: Some(PathHighlight::MenuActive),
style: &crate::theme::menu_bar::MenuBarStyle::Default,
position: Point::new(translation.x, translation.y),
}
.overlay(),
)

View file

@ -72,11 +72,13 @@ impl<'a, Message: Clone + 'static> From<Dialog<'a, Message>> for Element<'a, Mes
let mut content_col = widget::column::with_capacity(3 + dialog.controls.len() * 2);
content_col = content_col.push(widget::text::title3(dialog.title));
if let Some(body) = dialog.body {
content_col = content_col.push(widget::vertical_space(Length::Fixed(space_xxs.into())));
content_col =
content_col.push(widget::vertical_space().height(Length::Fixed(space_xxs.into())));
content_col = content_col.push(widget::text::body(body));
}
for control in dialog.controls {
content_col = content_col.push(widget::vertical_space(Length::Fixed(space_s.into())));
content_col =
content_col.push(widget::vertical_space().height(Length::Fixed(space_s.into())));
content_col = content_col.push(control);
}
@ -90,7 +92,7 @@ impl<'a, Message: Clone + 'static> From<Dialog<'a, Message>> for Element<'a, Mes
if let Some(button) = dialog.tertiary_action {
button_row = button_row.push(button);
}
button_row = button_row.push(widget::horizontal_space(Length::Fill));
button_row = button_row.push(widget::horizontal_space().width(Length::Fill));
if let Some(button) = dialog.secondary_action {
button_row = button_row.push(button);
}
@ -103,7 +105,7 @@ impl<'a, Message: Clone + 'static> From<Dialog<'a, Message>> for Element<'a, Mes
widget::column::with_children(vec![content_row.into(), button_row.into()])
.spacing(space_l),
)
.style(style::Container::Dialog)
.class(style::Container::Dialog)
.padding(space_m)
.width(Length::Fixed(570.0)),
)

View file

@ -3,6 +3,8 @@ use std::{
sync::atomic::{AtomicU64, Ordering},
};
use iced::Vector;
use crate::{
iced::{
clipboard::{
@ -280,9 +282,7 @@ impl<'a, Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
tree: &mut Tree,
layout: layout::Layout<'_>,
renderer: &crate::Renderer,
operation: &mut dyn iced_core::widget::Operation<
iced_core::widget::OperationOutputWrapper<Message>,
>,
operation: &mut dyn iced_core::widget::Operation<()>,
) {
self.container
.as_widget()
@ -496,10 +496,11 @@ impl<'a, Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
tree: &'b mut Tree,
layout: layout::Layout<'_>,
renderer: &crate::Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
self.container
.as_widget_mut()
.overlay(&mut tree.children[0], layout, renderer)
.overlay(&mut tree.children[0], layout, renderer, translation)
}
fn drag_destinations(

View file

@ -1,69 +1,77 @@
use std::any::Any;
use iced_core::window;
use crate::{
iced::{
clipboard::dnd::{DndAction, DndEvent, SourceEvent},
event, mouse, overlay, Event, Length, Point, Rectangle,
event, mouse, overlay, Event, Length, Point, Rectangle, Vector,
},
iced_core::{
self, layout, renderer,
widget::{tree, Tree},
Clipboard, Shell,
},
iced_style,
widget::{container, Id, Widget},
Element,
};
pub fn dnd_source<
'a,
Message: 'static,
AppMessage: 'static,
Message: Clone + 'static,
D: iced::clipboard::mime::AsMimeTypes + Send + 'static,
>(
child: impl Into<Element<'a, Message>>,
) -> DndSource<'a, Message, AppMessage, D> {
) -> DndSource<'a, Message, D> {
DndSource::new(child)
}
pub struct DndSource<'a, Message, AppMessage, D> {
pub struct DndSource<'a, Message, D> {
id: Id,
action: DndAction,
container: Element<'a, Message>,
window: Option<window::Id>,
drag_content: Option<Box<dyn Fn() -> D>>,
drag_icon: Option<Box<dyn Fn() -> (Element<'static, AppMessage>, tree::State)>>,
drag_icon: Option<Box<dyn Fn() -> (Element<'static, ()>, tree::State)>>,
on_start: Option<Message>,
on_cancelled: Option<Message>,
on_finish: Option<Message>,
drag_threshold: f32,
_phantom: std::marker::PhantomData<AppMessage>,
}
impl<
'a,
Message: 'static,
AppMessage: 'static,
Message: Clone + 'static,
D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static,
> DndSource<'a, Message, AppMessage, D>
> DndSource<'a, Message, D>
{
pub fn new(child: impl Into<Element<'a, Message>>) -> Self {
Self {
id: Id::unique(),
window: None,
action: DndAction::Copy | DndAction::Move,
container: container(child).into(),
drag_content: None,
drag_icon: None,
drag_threshold: 8.0,
_phantom: std::marker::PhantomData,
on_start: None,
on_cancelled: None,
on_finish: None,
}
}
pub fn with_id(child: impl Into<Element<'a, Message>>, id: Id) -> Self {
Self {
id,
window: None,
action: DndAction::Copy | DndAction::Move,
container: container(child).into(),
drag_content: None,
drag_icon: None,
drag_threshold: 8.0,
_phantom: std::marker::PhantomData,
on_start: None,
on_cancelled: None,
on_finish: None,
}
}
@ -82,7 +90,7 @@ impl<
#[must_use]
pub fn drag_icon(
mut self,
f: impl Fn() -> (Element<'static, AppMessage>, tree::State) + 'static,
f: impl Fn() -> (Element<'static, ()>, tree::State) + 'static,
) -> Self {
self.drag_icon = Some(Box::new(f));
self
@ -98,10 +106,15 @@ impl<
let Some(content) = self.drag_content.as_ref().map(|f| f()) else {
return;
};
iced_core::clipboard::start_dnd(
clipboard,
false,
Some(iced_core::clipboard::DndSource::Widget(self.id.clone())),
if let Some(window) = self.window.as_ref() {
Some(iced_core::clipboard::DndSource::Surface(window.clone()))
} else {
Some(iced_core::clipboard::DndSource::Widget(self.id.clone()))
},
self.drag_icon.as_ref().map(|f| {
let (icon, state) = f();
(
@ -116,14 +129,33 @@ impl<
self.action,
);
}
pub fn on_start(mut self, on_start: Option<Message>) -> Self {
self.on_start = on_start;
self
}
pub fn on_cancel(mut self, on_cancelled: Option<Message>) -> Self {
self.on_cancelled = on_cancelled;
self
}
pub fn on_finish(mut self, on_finish: Option<Message>) -> Self {
self.on_finish = on_finish;
self
}
pub fn window(mut self, window: window::Id) -> Self {
self.window = Some(window);
self
}
}
impl<
'a,
Message: 'static,
AppMessage: 'static,
Message: Clone + 'static,
D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static,
> Widget<Message, crate::Theme, crate::Renderer> for DndSource<'a, Message, AppMessage, D>
> Widget<Message, crate::Theme, crate::Renderer> for DndSource<'a, Message, D>
{
fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.container)]
@ -165,9 +197,7 @@ impl<
tree: &mut Tree,
layout: layout::Layout<'_>,
renderer: &crate::Renderer,
operation: &mut dyn iced_core::widget::Operation<
iced_core::widget::OperationOutputWrapper<Message>,
>,
operation: &mut dyn iced_core::widget::Operation<()>,
) {
operation.custom((&mut tree.state) as &mut dyn Any, Some(&self.id));
operation.container(Some(&self.id), layout.bounds(), &mut |operation| {
@ -210,7 +240,6 @@ impl<
}
state.left_pressed_position = Some(position);
// dbg!(&state, &self.id);
return event::Status::Captured;
}
}
@ -229,8 +258,10 @@ impl<
return ret;
}
if let Some(left_pressed_position) = state.left_pressed_position {
// dbg!(&state);
if position.distance(left_pressed_position) > self.drag_threshold {
if let Some(on_start) = self.on_start.as_ref() {
shell.publish(on_start.clone())
}
self.start_dnd(clipboard, state.cached_bounds);
state.is_dragging = true;
state.left_pressed_position = None;
@ -249,8 +280,21 @@ impl<
}
_ => return ret,
},
Event::Dnd(DndEvent::Source(SourceEvent::Cancelled | SourceEvent::Finished)) => {
Event::Dnd(DndEvent::Source(SourceEvent::Cancelled)) => {
if state.is_dragging {
if let Some(m) = self.on_cancelled.as_ref() {
shell.publish(m.clone());
}
state.is_dragging = false;
return event::Status::Captured;
}
return ret;
}
Event::Dnd(DndEvent::Source(SourceEvent::Finished)) => {
if state.is_dragging {
if let Some(m) = self.on_finish.as_ref() {
shell.publish(m.clone());
}
state.is_dragging = false;
return event::Status::Captured;
}
@ -308,10 +352,11 @@ impl<
tree: &'b mut Tree,
layout: layout::Layout<'_>,
renderer: &crate::Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
self.container
.as_widget_mut()
.overlay(&mut tree.children[0], layout, renderer)
.overlay(&mut tree.children[0], layout, renderer, translation)
}
fn drag_destinations(
@ -319,7 +364,7 @@ impl<
state: &Tree,
layout: layout::Layout<'_>,
renderer: &crate::Renderer,
dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) {
self.container.as_widget().drag_destinations(
&state.children[0],
@ -340,12 +385,11 @@ impl<
impl<
'a,
Message: 'static,
AppMessage: 'static,
Message: Clone + 'static,
D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static,
> From<DndSource<'a, Message, AppMessage, D>> for Element<'a, Message>
> From<DndSource<'a, Message, D>> for Element<'a, Message>
{
fn from(e: DndSource<'a, Message, AppMessage, D>) -> Element<'a, Message> {
fn from(e: DndSource<'a, Message, D>) -> Element<'a, Message> {
Element::new(e)
}
}

View file

@ -97,7 +97,7 @@ impl<'a, S: AsRef<str>, Message: 'a> Menu<'a, S, Message> {
position: Point,
target_height: f32,
) -> overlay::Element<'a, Message, crate::Theme, crate::Renderer> {
overlay::Element::new(position, Box::new(Overlay::new(self, target_height)))
overlay::Element::new(Box::new(Overlay::new(self, target_height, position)))
}
}
@ -129,10 +129,15 @@ struct Overlay<'a, Message> {
width: f32,
target_height: f32,
style: (),
position: Point,
}
impl<'a, Message: 'a> Overlay<'a, Message> {
pub fn new<S: AsRef<str>>(menu: Menu<'a, S, Message>, target_height: f32) -> Self {
pub fn new<S: AsRef<str>>(
menu: Menu<'a, S, Message>,
target_height: f32,
position: Point,
) -> Self {
let Menu {
state,
options,
@ -160,7 +165,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
container = container
.padding(padding)
.style(crate::style::Container::Dropdown);
.class(crate::style::Container::Dropdown);
state.tree.diff(&mut container as &mut dyn Widget<_, _, _>);
@ -170,6 +175,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
width,
target_height,
style,
position,
}
}
}
@ -177,13 +183,8 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
for Overlay<'a, Message>
{
fn layout(
&mut self,
renderer: &crate::Renderer,
bounds: Size,
position: Point,
_translation: iced::Vector,
) -> layout::Node {
fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node {
let position = self.position;
let space_below = bounds.height - (position.y + self.target_height);
let space_above = position.y;
@ -447,10 +448,13 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
appearance.selected_background,
);
svg::Renderer::draw(
let svg_handle =
iced_core::Svg::new(crate::widget::common::object_select().clone())
.color(appearance.selected_text_color)
.border_radius(appearance.border_radius);
svg::Renderer::draw_svg(
renderer,
crate::widget::common::object_select().clone(),
Some(appearance.selected_text_color),
svg_handle,
Rectangle {
x: item_x + item_width - 16.0 - 8.0,
y: bounds.y + (bounds.height / 2.0 - 8.0),
@ -494,7 +498,7 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
text::Renderer::fill_text(
renderer,
Text {
content: option.as_ref(),
content: option.as_ref().to_string(),
bounds: bounds.size(),
size: Pixels(text_size),
line_height: self.text_line_height,
@ -502,7 +506,7 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
},
bounds.position(),
color,

View file

@ -97,7 +97,7 @@ where
position: Point,
target_height: f32,
) -> overlay::Element<'a, Message, crate::Theme, crate::Renderer> {
overlay::Element::new(position, Box::new(Overlay::new(self, target_height)))
overlay::Element::new(Box::new(Overlay::new(self, target_height, position)))
}
}
@ -129,12 +129,14 @@ struct Overlay<'a, Message> {
width: f32,
target_height: f32,
style: (),
position: Point,
}
impl<'a, Message: 'a> Overlay<'a, Message> {
pub fn new<S: AsRef<str>, Item: Clone + PartialEq>(
menu: Menu<'a, S, Item, Message>,
target_height: f32,
position: Point,
) -> Self {
let Menu {
state,
@ -163,7 +165,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
container = container
.padding(padding)
.style(crate::style::Container::Dropdown);
.class(crate::style::Container::Dropdown);
state.tree.diff(&mut container as &mut dyn Widget<_, _, _>);
@ -173,6 +175,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
width,
target_height,
style,
position,
}
}
}
@ -180,13 +183,8 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
for Overlay<'a, Message>
{
fn layout(
&mut self,
renderer: &crate::Renderer,
bounds: Size,
position: Point,
_translation: iced::Vector,
) -> layout::Node {
fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node {
let position = self.position;
let space_below = bounds.height - (position.y + self.target_height);
let space_above = position.y;
@ -537,10 +535,13 @@ where
appearance.selected_background,
);
svg::Renderer::draw(
let svg_handle =
svg::Svg::new(crate::widget::common::object_select().clone())
.color(appearance.selected_text_color)
.border_radius(appearance.border_radius);
svg::Renderer::draw_svg(
renderer,
crate::widget::common::object_select().clone(),
Some(appearance.selected_text_color),
svg_handle,
Rectangle {
x: item_x + item_width - 16.0 - 8.0,
y: bounds.y + (bounds.height / 2.0 - 8.0),
@ -587,7 +588,7 @@ where
text::Renderer::fill_text(
renderer,
Text {
content: option.as_ref(),
content: option.as_ref().to_string(),
bounds: bounds.size(),
size: iced::Pixels(text_size),
line_height: self.text_line_height,
@ -595,7 +596,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
},
bounds.position(),
color,
@ -636,7 +637,7 @@ where
text::Renderer::fill_text(
renderer,
Text {
content: description.as_ref(),
content: description.as_ref().to_string(),
bounds: bounds.size(),
size: iced::Pixels(text_size),
line_height: text::LineHeight::Absolute(Pixels(text_line_height + 4.0)),
@ -644,7 +645,7 @@ where
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
},
bounds.position(),
appearance.description_color,

View file

@ -9,7 +9,7 @@ pub mod menu;
pub use menu::Menu;
mod widget;
pub use widget::{Appearance, Dropdown, StyleSheet};
pub use widget::{Catalog, Dropdown, Style};
pub fn dropdown<'a, S: AsRef<str>, Message: 'a, Item: Clone + PartialEq + 'static>(
model: &'a Model<S, Item>,

View file

@ -9,10 +9,13 @@ use iced_core::event::{self, Event};
use iced_core::text::{self, Paragraph, Text};
use iced_core::widget::tree::{self, Tree};
use iced_core::{alignment, keyboard, layout, mouse, overlay, renderer, svg, touch, Shadow};
use iced_core::{Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Widget};
use iced_core::{
Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget,
};
use iced_widget::pick_list;
use std::ffi::OsStr;
pub use iced_widget::style::pick_list::{Appearance, StyleSheet};
pub use iced_widget::pick_list::{Catalog, Style};
/// A widget for selecting a single value from a list of selections.
#[derive(Setters)]
@ -98,7 +101,7 @@ impl<'a, S: AsRef<str>, Message: 'a, Item: Clone + PartialEq + 'static>
for (_, item) in &list.options {
state
.selections
.push((item.clone(), crate::Paragraph::new()));
.push((item.clone(), crate::Plain::default()));
}
}
}
@ -182,6 +185,7 @@ impl<'a, S: AsRef<str>, Message: 'a, Item: Clone + PartialEq + 'static>
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &crate::Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
let state = tree.state.downcast_mut::<State<Item>>();
@ -216,8 +220,8 @@ pub struct State<Item: Clone + PartialEq + 'static> {
keyboard_modifiers: keyboard::Modifiers,
is_open: bool,
hovered_option: Option<Item>,
selections: Vec<(Item, crate::Paragraph)>,
descriptions: Vec<crate::Paragraph>,
selections: Vec<(Item, crate::Plain)>,
descriptions: Vec<crate::Plain>,
}
impl<Item: Clone + PartialEq + 'static> State<Item> {
@ -259,7 +263,7 @@ pub fn layout(
text_size: f32,
text_line_height: text::LineHeight,
font: Option<crate::font::Font>,
selection: Option<(&str, &mut crate::Paragraph)>,
selection: Option<(&str, &mut crate::Plain)>,
) -> layout::Node {
use std::f32;
@ -267,7 +271,7 @@ pub fn layout(
let max_width = match width {
Length::Shrink => {
let measure = move |(label, paragraph): (_, &mut crate::Paragraph)| -> f32 {
let measure = move |(label, paragraph): (_, &mut crate::Plain)| -> f32 {
paragraph.update(Text {
content: label,
bounds: Size::new(f32::MAX, f32::MAX),
@ -277,7 +281,7 @@ pub fn layout(
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
});
paragraph.min_width().round()
};
@ -410,7 +414,7 @@ pub fn overlay<'a, S: AsRef<str>, Message: 'a, Item: Clone + PartialEq + 'static
)
.width({
let measure =
|label: &str, paragraph: &mut crate::Paragraph, line_height: text::LineHeight| {
|label: &str, paragraph: &mut crate::Plain, line_height: text::LineHeight| {
paragraph.update(Text {
content: label,
bounds: Size::new(f32::MAX, f32::MAX),
@ -420,7 +424,7 @@ pub fn overlay<'a, S: AsRef<str>, Message: 'a, Item: Clone + PartialEq + 'static
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
});
paragraph.min_width().round()
};
@ -433,7 +437,7 @@ pub fn overlay<'a, S: AsRef<str>, Message: 'a, Item: Clone + PartialEq + 'static
let paragraph = if state.descriptions.len() > desc_count {
&mut state.descriptions[desc_count]
} else {
state.descriptions.push(crate::Paragraph::new());
state.descriptions.push(crate::Plain::default());
state.descriptions.last_mut().unwrap()
};
desc_count += 1;
@ -448,7 +452,7 @@ pub fn overlay<'a, S: AsRef<str>, Message: 'a, Item: Clone + PartialEq + 'static
None => {
state
.selections
.push((item.clone(), crate::Paragraph::new()));
.push((item.clone(), crate::Plain::default()));
state.selections.len() - 1
}
};
@ -499,9 +503,9 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>(
let is_mouse_over = cursor.is_over(bounds);
let style = if is_mouse_over {
theme.hovered(&())
theme.style(&(), pick_list::Status::Hovered)
} else {
theme.active(&())
theme.style(&(), pick_list::Status::Active)
};
iced_core::Renderer::fill_quad(
@ -515,10 +519,10 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>(
);
if let Some(handle) = state.icon.clone() {
svg::Renderer::draw(
let svg_handle = iced_core::Svg::new(handle).color(style.text_color);
svg::Renderer::draw_svg(
renderer,
handle,
Some(style.text_color),
svg_handle,
Rectangle {
x: bounds.x + bounds.width - gap - 16.0,
y: bounds.center_y() - 8.0,
@ -541,7 +545,7 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>(
text::Renderer::fill_text(
renderer,
Text {
content,
content: content.to_string(),
size: iced::Pixels(text_size),
line_height: text_line_height,
font,
@ -549,7 +553,7 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>(
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
},
bounds.position(),
style.text_color,

View file

@ -5,16 +5,18 @@
use super::menu::{self, Menu};
use crate::widget::icon;
use derive_setters::Setters;
use iced::Radians;
use iced_core::event::{self, Event};
use iced_core::text::{self, Paragraph, Text};
use iced_core::widget::tree::{self, Tree};
use iced_core::{alignment, keyboard, layout, mouse, overlay, renderer, svg, touch, Shadow};
use iced_core::{Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Widget};
use iced_core::{
Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget,
};
use iced_widget::pick_list::{self, Catalog};
use std::ffi::OsStr;
use std::hash::{DefaultHasher, Hash, Hasher};
pub use iced_widget::style::pick_list::{Appearance, StyleSheet};
/// A widget for selecting a single value from a list of selections.
#[derive(Setters)]
pub struct Dropdown<'a, S: AsRef<str>, Message> {
@ -62,6 +64,29 @@ impl<'a, S: AsRef<str>, Message> Dropdown<'a, S, Message> {
font: None,
}
}
fn update_paragraphs(&self, state: &mut tree::State) {
let state = state.downcast_mut::<State>();
state
.selections
.resize_with(self.selections.len(), crate::Plain::default);
for (i, selection) in self.selections.iter().enumerate() {
state.selections[i].update(Text {
content: selection.as_ref(),
bounds: Size::INFINITY,
// TODO use the renderer default size
size: iced::Pixels(self.text_size.unwrap_or(14.0)),
line_height: self.text_line_height,
font: self.font.unwrap_or(crate::font::default()),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: text::Shaping::Advanced,
wrapping: text::Wrapping::default(),
});
}
}
}
impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Renderer>
@ -80,7 +105,7 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
state
.selections
.resize_with(self.selections.len(), crate::Paragraph::new);
.resize_with(self.selections.len(), crate::Plain::default);
state.hashes.resize(self.selections.len(), 0);
for (i, selection) in self.selections.iter().enumerate() {
@ -103,7 +128,7 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
});
}
}
@ -202,6 +227,7 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &crate::Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
let state = tree.state.downcast_mut::<State>();
@ -237,8 +263,8 @@ pub struct State {
keyboard_modifiers: keyboard::Modifiers,
is_open: bool,
hovered_option: Option<usize>,
selections: Vec<crate::Paragraph>,
hashes: Vec<u64>,
selections: Vec<crate::Plain>,
}
impl State {
@ -280,7 +306,7 @@ pub fn layout(
text_size: f32,
text_line_height: text::LineHeight,
font: Option<crate::font::Font>,
selection: Option<(&str, &mut crate::Paragraph)>,
selection: Option<(&str, &mut crate::Plain)>,
) -> layout::Node {
use std::f32;
@ -288,7 +314,7 @@ pub fn layout(
let max_width = match width {
Length::Shrink => {
let measure = move |(label, paragraph): (_, &mut crate::Paragraph)| -> f32 {
let measure = move |(label, paragraph): (_, &mut crate::Plain)| -> f32 {
paragraph.update(Text {
content: label,
bounds: Size::new(f32::MAX, f32::MAX),
@ -298,7 +324,7 @@ pub fn layout(
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
});
paragraph.min_width().round()
};
@ -430,14 +456,14 @@ pub fn overlay<'a, S: AsRef<str>, Message: 'a>(
None,
)
.width({
let measure = |_label: &str, selection_paragraph: &mut crate::Paragraph| -> f32 {
let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 {
selection_paragraph.min_width().round()
};
selections
.iter()
.zip(state.selections.iter_mut())
.map(|(label, selection)| measure(label.as_ref(), selection))
.map(|(label, selection)| measure(label.as_ref(), selection.raw()))
.fold(0.0, |next, current| current.max(next))
+ gap
+ 16.0
@ -477,9 +503,9 @@ pub fn draw<'a, S>(
let is_mouse_over = cursor.is_over(bounds);
let style = if is_mouse_over {
theme.hovered(&())
theme.style(&(), pick_list::Status::Hovered)
} else {
theme.active(&())
theme.style(&(), pick_list::Status::Active)
};
iced_core::Renderer::fill_quad(
@ -493,10 +519,11 @@ pub fn draw<'a, S>(
);
if let Some(handle) = state.icon.clone() {
svg::Renderer::draw(
let svg_handle = svg::Svg::new(handle).color(style.text_color);
svg::Renderer::draw_svg(
renderer,
handle,
Some(style.text_color),
svg_handle,
Rectangle {
x: bounds.x + bounds.width - gap - 16.0,
y: bounds.center_y() - 8.0,
@ -517,7 +544,7 @@ pub fn draw<'a, S>(
text::Renderer::fill_text(
renderer,
Text {
content,
content: content.to_string(),
size: iced::Pixels(text_size),
line_height: text_line_height,
font,
@ -525,7 +552,7 @@ pub fn draw<'a, S>(
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
},
bounds.position(),
style.text_color,

View file

@ -6,9 +6,9 @@ use derive_setters::Setters;
use iced_core::event::{self, Event};
use iced_core::widget::{Operation, Tree};
use iced_core::{
layout, mouse, overlay, renderer, Clipboard, Layout, Length, Padding, Rectangle, Shell, Widget,
layout, mouse, overlay, renderer, Clipboard, Layout, Length, Padding, Rectangle, Shell, Vector,
Widget,
};
use iced_renderer::core::widget::OperationOutputWrapper;
/// Responsively generates rows and columns of widgets based on its dimmensions.
#[derive(Setters)]
@ -132,7 +132,7 @@ impl<'a, Message: 'static + Clone> Widget<Message, crate::Theme, Renderer>
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
operation: &mut dyn Operation<()>,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
@ -225,8 +225,9 @@ impl<'a, Message: 'static + Clone> Widget<Message, crate::Theme, Renderer>
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
overlay::from_children(&mut self.children, tree, layout, renderer)
overlay::from_children(&mut self.children, tree, layout, renderer, translation)
}
#[cfg(feature = "a11y")]
@ -252,7 +253,7 @@ impl<'a, Message: 'static + Clone> Widget<Message, crate::Theme, Renderer>
state: &Tree,
layout: Layout<'_>,
renderer: &Renderer,
dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) {
for ((e, layout), state) in self
.children

View file

@ -7,9 +7,8 @@ use iced_core::event::{self, Event};
use iced_core::widget::{Operation, Tree};
use iced_core::{
layout, mouse, overlay, renderer, Alignment, Clipboard, Layout, Length, Padding, Rectangle,
Shell, Widget,
Shell, Vector, Widget,
};
use iced_renderer::core::widget::OperationOutputWrapper;
/// Responsively generates rows and columns of widgets based on its dimmensions.
#[must_use]
@ -154,7 +153,7 @@ impl<'a, Message: 'static + Clone> Widget<Message, crate::Theme, Renderer> for G
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
operation: &mut dyn Operation<()>,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
@ -247,8 +246,9 @@ impl<'a, Message: 'static + Clone> Widget<Message, crate::Theme, Renderer> for G
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
overlay::from_children(&mut self.children, tree, layout, renderer)
overlay::from_children(&mut self.children, tree, layout, renderer, translation)
}
#[cfg(feature = "a11y")]
@ -274,7 +274,7 @@ impl<'a, Message: 'static + Clone> Widget<Message, crate::Theme, Renderer> for G
state: &Tree,
layout: Layout<'_>,
renderer: &Renderer,
dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) {
for ((e, layout), state) in self
.children

View file

@ -6,7 +6,7 @@ use crate::{ext::CollectionWidget, widget, Element};
use apply::Apply;
use derive_setters::Setters;
use iced::Length;
use iced_core::{widget::tree, Widget};
use iced_core::{widget::tree, Vector, Widget};
use std::borrow::Cow;
#[must_use]
@ -221,9 +221,7 @@ impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer
state: &mut tree::Tree,
layout: iced_core::Layout<'_>,
renderer: &crate::Renderer,
operation: &mut dyn iced_core::widget::Operation<
iced_core::widget::OperationOutputWrapper<Message>,
>,
operation: &mut dyn iced_core::widget::Operation<()>,
) {
let child_tree = &mut state.children[0];
let child_layout = layout.children().next().unwrap();
@ -237,12 +235,16 @@ impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer
state: &'b mut tree::Tree,
layout: iced_core::Layout<'_>,
renderer: &crate::Renderer,
translation: Vector,
) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
let child_tree = &mut state.children[0];
let child_layout = layout.children().next().unwrap();
self.header_bar_inner
.as_widget_mut()
.overlay(child_tree, child_layout, renderer)
self.header_bar_inner.as_widget_mut().overlay(
child_tree,
child_layout,
renderer,
translation,
)
}
fn drag_destinations(
@ -250,7 +252,7 @@ impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer
state: &tree::Tree,
layout: iced_core::Layout<'_>,
renderer: &crate::Renderer,
dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) {
if let Some((child_tree, child_layout)) =
state.children.iter().zip(layout.children()).next()
@ -274,7 +276,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
let mut end = std::mem::take(&mut self.end);
// Also packs the window controls at the very end.
end.push(widget::horizontal_space(Length::Fixed(12.0)).into());
end.push(widget::horizontal_space().width(Length::Fixed(12.0)).into());
end.push(self.window_controls());
let height = match self.density.unwrap_or_else(crate::config::header_size) {
@ -288,7 +290,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
// If elements exist in the start region, append them here.
.push(
widget::row::with_children(start)
.align_items(iced::Alignment::Center)
.align_y(iced::Alignment::Center)
.apply(widget::container)
.align_x(iced::alignment::Horizontal::Left)
.width(Length::Shrink),
@ -297,32 +299,32 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
// This will otherwise use the title as a widget if a title was defined.
.push(if !center.is_empty() {
widget::row::with_children(center)
.align_items(iced::Alignment::Center)
.align_y(iced::Alignment::Center)
.apply(widget::container)
.align_x(iced::alignment::Horizontal::Center)
.width(Length::Fill)
.into()
} else if self.title.is_empty() {
widget::horizontal_space(Length::Fill).into()
widget::horizontal_space().width(Length::Fill).into()
} else {
self.title_widget()
})
.push(
widget::row::with_children(end)
.align_items(iced::Alignment::Center)
.align_y(iced::Alignment::Center)
.apply(widget::container)
.align_x(iced::alignment::Horizontal::Right)
.width(Length::Shrink),
)
.align_items(iced::Alignment::Center)
.align_y(iced::Alignment::Center)
.height(Length::Fixed(height))
.padding([0, 8])
.spacing(8)
.apply(widget::container)
.style(crate::theme::Container::HeaderBar {
.class(crate::theme::Container::HeaderBar {
focused: self.focused,
})
.center_y()
.center_y(Length::Shrink)
.apply(widget::mouse_area);
// Assigns a message to emit when the headerbar is dragged.
@ -350,10 +352,8 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
widget::text::heading(title)
.apply(widget::container)
.center_x()
.center_y()
.width(Length::Fill)
.height(Length::Fill)
.center_x(Length::Fill)
.center_y(Length::Fill)
.into()
}
@ -380,7 +380,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
.padding(8)
};
icon.style(crate::theme::Button::HeaderBar)
icon.class(crate::theme::Button::HeaderBar)
.selected(self.focused)
.icon_size($size)
.on_press($on_press)
@ -405,8 +405,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
)
.spacing(8)
.apply(widget::container)
.height(Length::Fill)
.center_y()
.center_y(Length::Fill)
.into()
}
}

View file

@ -55,7 +55,10 @@ pub fn from_raster_bytes(
) -> Handle {
Handle {
symbolic: false,
data: Data::Image(image::Handle::from_memory(bytes)),
data: match bytes.into() {
Cow::Owned(b) => Data::Image(image::Handle::from_bytes(b)),
Cow::Borrowed(b) => Data::Image(image::Handle::from_bytes(b)),
},
}
}
@ -66,12 +69,14 @@ pub fn from_raster_pixels(
pixels: impl Into<Cow<'static, [u8]>>
+ std::convert::AsRef<[u8]>
+ std::marker::Send
+ std::marker::Sync
+ 'static,
+ std::marker::Sync,
) -> Handle {
Handle {
symbolic: false,
data: Data::Image(image::Handle::from_pixels(width, height, pixels)),
data: match pixels.into() {
Cow::Owned(pixels) => Data::Image(image::Handle::from_bytes(pixels)),
Cow::Borrowed(pixels) => Data::Image(image::Handle::from_bytes(pixels)),
},
}
}

View file

@ -24,7 +24,7 @@ pub fn icon(handle: Handle) -> Icon {
handle,
height: None,
size: 16,
style: crate::theme::Svg::default(),
class: crate::theme::Svg::default(),
width: None,
}
}
@ -40,7 +40,7 @@ pub fn from_name(name: impl Into<Arc<str>>) -> Named {
pub struct Icon {
#[setters(skip)]
handle: Handle,
style: crate::theme::Svg,
class: crate::theme::Svg,
pub(super) size: u16,
content_fit: ContentFit,
#[setters(strip_option)]
@ -86,7 +86,7 @@ impl Icon {
let from_svg = |handle| {
Svg::<crate::Theme>::new(handle)
.style(self.style.clone())
.class(self.class.clone())
.width(
self.width
.unwrap_or_else(|| Length::Fixed(f32::from(self.size))),

View file

@ -4,8 +4,8 @@ use iced_core::mouse;
use iced_core::overlay;
use iced_core::renderer;
use iced_core::widget::{Id, Tree};
use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Widget};
pub use iced_style::container::{Appearance, StyleSheet};
use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget};
pub use iced_widget::container::{Catalog, Style};
pub fn id_container<'a, Message: 'static, Theme, E>(
content: E,
@ -13,8 +13,8 @@ pub fn id_container<'a, Message: 'static, Theme, E>(
) -> IdContainer<'a, Message, Theme, crate::Renderer>
where
E: Into<Element<'a, Message, Theme, crate::Renderer>>,
Theme: iced_style::container::StyleSheet,
<Theme as iced_widget::container::StyleSheet>::Style: From<crate::theme::Container>,
Theme: iced_widget::container::Catalog,
<Theme as iced_widget::container::Catalog>::Class<'a>: From<crate::theme::Container<'a>>,
{
IdContainer::new(content, id)
}
@ -83,9 +83,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn iced_core::widget::Operation<
iced_core::widget::OperationOutputWrapper<Message>,
>,
operation: &mut dyn iced_core::widget::Operation<()>,
) {
operation.container(Some(&self.id), layout.bounds(), &mut |operation| {
self.content.as_widget().operate(
@ -165,11 +163,13 @@ where
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout.children().next().unwrap(),
renderer,
translation,
)
}
@ -178,7 +178,7 @@ where
state: &Tree,
layout: Layout<'_>,
renderer: &Renderer,
dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) {
let content_layout = layout.children().next().unwrap();
self.content.as_widget().drag_destinations(

View file

@ -1,3 +1,4 @@
use crate::Theme;
use cosmic_theme::LayeredTheme;
use iced::widget::Container;
use iced_core::alignment;
@ -7,16 +8,14 @@ use iced_core::mouse;
use iced_core::overlay;
use iced_core::renderer;
use iced_core::widget::Tree;
use iced_core::{Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget};
pub use iced_style::container::{Appearance, StyleSheet};
use iced_core::{Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget};
pub use iced_widget::container::{Catalog, Style};
pub fn layer_container<'a, Message: 'static, Theme, E>(
pub fn layer_container<'a, Message: 'static, E>(
content: E,
) -> LayerContainer<'a, Message, Theme, crate::Renderer>
) -> LayerContainer<'a, Message, crate::Renderer>
where
E: Into<Element<'a, Message, Theme, crate::Renderer>>,
Theme: iced_style::container::StyleSheet + LayeredTheme,
<Theme as iced_widget::container::StyleSheet>::Style: From<crate::theme::Container>,
{
LayerContainer::new(content)
}
@ -25,20 +24,18 @@ where
///
/// It is normally used for alignment purposes.
#[allow(missing_debug_implementations)]
pub struct LayerContainer<'a, Message, Theme, Renderer>
pub struct LayerContainer<'a, Message, Renderer>
where
Renderer: iced_core::Renderer,
Theme: iced_style::container::StyleSheet + LayeredTheme,
{
layer: Option<cosmic_theme::Layer>,
container: Container<'a, Message, Theme, Renderer>,
}
impl<'a, Message, Theme, Renderer> LayerContainer<'a, Message, Theme, Renderer>
impl<'a, Message, Renderer> LayerContainer<'a, Message, Renderer>
where
Renderer: iced_core::Renderer,
Theme: iced_style::container::StyleSheet + LayeredTheme,
<Theme as iced_style::container::StyleSheet>::Style: From<crate::theme::Container>,
// iced_widget::container::Style: From<crate::theme::Container>,
{
/// Creates an empty [`Container`].
pub(crate) fn new<T>(content: T) -> Self
@ -55,7 +52,7 @@ where
#[must_use]
pub fn layer(mut self, layer: cosmic_theme::Layer) -> Self {
self.layer = Some(layer);
self.style(match layer {
self.class(match layer {
cosmic_theme::Layer::Background => crate::theme::Container::Background,
cosmic_theme::Layer::Primary => crate::theme::Container::Primary,
cosmic_theme::Layer::Secondary => crate::theme::Container::Secondary,
@ -113,31 +110,30 @@ where
/// Centers the contents in the horizontal axis of the [`LayerContainer`].
#[must_use]
pub fn center_x(mut self) -> Self {
self.container = self.container.center_x();
pub fn center_x(mut self, width: Length) -> Self {
self.container = self.container.center_x(width);
self
}
/// Centers the contents in the vertical axis of the [`LayerContainer`].
#[must_use]
pub fn center_y(mut self) -> Self {
self.container = self.container.center_y();
pub fn center_y(mut self, height: Length) -> Self {
self.container = self.container.center_y(height);
self
}
/// Sets the style of the [`LayerContainer`].
#[must_use]
pub fn style(mut self, style: impl Into<<Theme as StyleSheet>::Style>) -> Self {
self.container = self.container.style(style);
pub fn class(mut self, style: impl Into<crate::style::iced::Container<'a>>) -> Self {
self.container = self.container.class(style);
self
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for LayerContainer<'a, Message, Theme, Renderer>
impl<'a, Message, Renderer> Widget<Message, Theme, Renderer>
for LayerContainer<'a, Message, Renderer>
where
Renderer: iced_core::Renderer,
Theme: iced_style::container::StyleSheet + LayeredTheme + Clone,
{
fn children(&self) -> Vec<Tree> {
self.container.children()
@ -173,9 +169,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn iced_core::widget::Operation<
iced_core::widget::OperationOutputWrapper<Message>,
>,
operation: &mut dyn iced_core::widget::Operation<()>,
) {
self.container.operate(tree, layout, renderer, operation);
}
@ -232,6 +226,7 @@ where
} else {
theme.clone()
};
self.container.draw(
tree,
renderer,
@ -248,8 +243,9 @@ where
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.container.overlay(tree, layout, renderer)
self.container.overlay(tree, layout, renderer, translation)
}
fn drag_destinations(
@ -257,7 +253,7 @@ where
state: &Tree,
layout: Layout<'_>,
renderer: &Renderer,
dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) {
self.container
.drag_destinations(state, layout, renderer, dnd_rectangles);
@ -272,15 +268,14 @@ where
}
}
impl<'a, Message, Theme, Renderer> From<LayerContainer<'a, Message, Theme, Renderer>>
impl<'a, Message, Renderer> From<LayerContainer<'a, Message, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Renderer: 'a + iced_core::Renderer,
Theme: iced_style::container::StyleSheet + LayeredTheme + 'a + Clone,
{
fn from(
column: LayerContainer<'a, Message, Theme, Renderer>,
column: LayerContainer<'a, Message, Renderer>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(column)
}

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: MPL-2.0
use iced_core::Padding;
use iced_style::container::StyleSheet;
use iced_widget::container::Catalog;
use crate::{theme, widget::divider, Apply, Element};
@ -14,7 +14,7 @@ pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> {
pub struct ListColumn<'a, Message> {
spacing: u16,
padding: Padding,
style: <crate::Theme as StyleSheet>::Style,
style: crate::theme::Container<'a>,
children: Vec<Element<'a, Message>>,
}
@ -23,7 +23,7 @@ impl<'a, Message: 'static> Default for ListColumn<'a, Message> {
Self {
spacing: theme::THEME.lock().unwrap().cosmic().spacing.space_xxs,
padding: Padding::from(0),
style: <crate::Theme as StyleSheet>::Style::List,
style: crate::theme::Container::List,
children: Vec::with_capacity(4),
}
}
@ -41,9 +41,11 @@ impl<'a, Message: 'static> ListColumn<'a, Message> {
}
// Ensure a minimum height of 32.
let container = crate::widget::container(item)
.min_height(32)
.align_y(iced::alignment::Vertical::Center);
let container = iced::widget::row![
crate::widget::container(item).align_y(iced::alignment::Vertical::Center),
crate::widget::vertical_space().height(iced::Length::Fixed(32.))
]
.align_y(iced::alignment::Vertical::Center);
self.children.push(container.into());
self
@ -55,7 +57,7 @@ impl<'a, Message: 'static> ListColumn<'a, Message> {
}
/// Sets the style variant of this [`Circular`].
pub fn style(mut self, style: <crate::Theme as StyleSheet>::Style) -> Self {
pub fn style(mut self, style: <crate::Theme as Catalog>::Class<'a>) -> Self {
self.style = style;
self
}
@ -72,7 +74,7 @@ impl<'a, Message: 'static> ListColumn<'a, Message> {
.padding(self.padding)
.apply(super::container)
.padding([self.spacing, 8])
.style(self.style)
.class(self.style)
.into()
}
}

View file

@ -13,6 +13,6 @@ pub fn container<'a, Message>(
) -> Container<'a, Message, crate::Theme, crate::Renderer> {
super::container(content)
.padding([16, 6])
.style(crate::theme::Container::List)
.class(crate::theme::Container::List)
.width(iced::Length::Fill)
}

View file

@ -9,6 +9,7 @@ use super::{
};
use crate::style::menu_bar::StyleSheet;
use iced::{Point, Vector};
use iced_core::Border;
use iced_widget::core::{
event,
@ -434,6 +435,7 @@ where
tree: &'b mut Tree,
layout: Layout<'_>,
_renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
let state = tree.state.downcast_ref::<MenuBarState>();
if !state.open {
@ -455,6 +457,7 @@ where
root_bounds_list: layout.children().map(|lo| lo.bounds()).collect(),
path_highlight: self.path_highlight,
style: &self.style,
position: Point::new(translation.x, translation.y),
}
.overlay(),
)

View file

@ -276,7 +276,7 @@ impl MenuBounds {
let offset_bounds = Rectangle::new(offset_position, offset_size);
let children_bounds = Rectangle::new(children_position, children_size);
let check_bounds = pad_rectangle(children_bounds, [bounds_expand; 4].into());
let check_bounds = pad_rectangle(children_bounds, bounds_expand.into());
Self {
child_positions,
@ -445,13 +445,14 @@ where
pub(crate) root_bounds_list: Vec<Rectangle>,
pub(crate) path_highlight: Option<PathHighlight>,
pub(crate) style: &'b <crate::Theme as StyleSheet>::Style,
pub(crate) position: Point,
}
impl<'a, 'b, Message, Renderer> Menu<'a, 'b, Message, Renderer>
where
Renderer: renderer::Renderer,
{
pub(crate) fn overlay(self) -> overlay::Element<'b, Message, crate::Theme, Renderer> {
overlay::Element::new(Point::ORIGIN, Box::new(self))
overlay::Element::new(Box::new(self))
}
}
impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, crate::Theme, Renderer>
@ -459,14 +460,9 @@ impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, crate::Theme, Renderer
where
Renderer: renderer::Renderer,
{
fn layout(
&mut self,
renderer: &Renderer,
bounds: Size,
position: Point,
_translation: iced::Vector,
) -> Node {
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> Node {
// layout children
let position = self.position;
let state = self.tree.state.downcast_mut::<MenuBarState>();
let overlay_offset = Point::ORIGIN - position;
let tree_children = &mut self.tree.children;

View file

@ -146,14 +146,14 @@ pub fn menu_button<'a, Message: 'a>(
) -> crate::widget::Button<'a, Message> {
widget::button::custom(
widget::Row::with_children(children)
.align_items(Alignment::Center)
.align_y(Alignment::Center)
.height(Length::Fill)
.width(Length::Fill),
)
.height(Length::Fixed(36.0))
.padding([4, 16])
.width(Length::Fill)
.style(theme::Button::MenuItem)
.class(theme::Button::MenuItem)
}
/// Represents a menu item that performs an action when selected or a separator between menu items.
@ -197,7 +197,7 @@ where
{
widget::button::custom(widget::text(label))
.padding([4, 12])
.style(theme::Button::MenuRoot)
.class(theme::Button::MenuRoot)
.into()
}
@ -245,7 +245,7 @@ where
let key = find_key(&action, key_binds);
let menu_button = menu_button(vec![
widget::text(label).into(),
widget::horizontal_space(Length::Fill).into(),
widget::horizontal_space().width(Length::Fill).into(),
widget::text(key).into(),
])
.on_press(action.message());
@ -256,7 +256,7 @@ where
let key = find_key(&action, key_binds);
let menu_button = menu_button(vec![
widget::text(label).into(),
widget::horizontal_space(Length::Fill).into(),
widget::horizontal_space().width(Length::Fill).into(),
widget::text(key).into(),
]);
@ -270,8 +270,8 @@ where
widget::icon::from_name("object-select-symbolic")
.size(16)
.icon()
.style(theme::Svg::Custom(Rc::new(|theme| {
crate::iced_style::svg::Appearance {
.class(theme::Svg::Custom(Rc::new(|theme| {
iced_widget::svg::Style {
color: Some(theme.cosmic().accent_color().into()),
}
})))
@ -282,9 +282,9 @@ where
},
widget::Space::with_width(Length::Fixed(8.0)).into(),
widget::text(label)
.horizontal_alignment(iced::alignment::Horizontal::Left)
.align_x(iced::alignment::Horizontal::Left)
.into(),
widget::horizontal_space(Length::Fill).into(),
widget::horizontal_space().width(Length::Fill).into(),
widget::text(key).into(),
])
.on_press(action.message()),
@ -294,13 +294,13 @@ where
trees.push(MenuTree::<Message, Renderer>::with_children(
menu_button(vec![
widget::text(label).into(),
widget::horizontal_space(Length::Fill).into(),
widget::horizontal_space().width(Length::Fill).into(),
widget::icon::from_name("pan-end-symbolic")
.size(16)
.icon()
.into(),
])
.style(
.class(
// Menu folders have no on_press so they take on the disabled style by default
if children.is_empty() {
// This will make the folder use the disabled style if it has no children

View file

@ -0,0 +1,319 @@
mod subscription;
use iced::futures::channel::mpsc::UnboundedSender;
use iced::widget::Container;
use iced::Vector;
pub use subscription::*;
use iced_core::alignment;
use iced_core::event::{self, Event};
use iced_core::layout;
use iced_core::mouse;
use iced_core::overlay;
use iced_core::renderer;
use iced_core::widget::Tree;
use iced_core::{Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget};
use std::{fmt::Debug, hash::Hash};
pub use iced_widget::container::{Catalog, Style};
pub fn min_size_tracker<'a, Message, I, T>(
content: T,
id: I,
tx: UnboundedSender<(I, Rectangle)>,
) -> MinSizeTrackerContainer<'a, Message, crate::Renderer, I>
where
I: Hash + Copy + Send + Sync + Debug + 'a,
T: Into<Element<'a, Message, crate::Theme, crate::Renderer>>,
{
MinSizeTrackerContainer::new(content, id, tx)
}
pub fn subscription<
I: 'static + Hash + Copy + Send + Sync + Debug,
R: 'static + Hash + Copy + Send + Sync + Debug + Eq,
>(
id: I,
) -> iced::Subscription<(I, RectangleUpdate<R>)> {
subscription::rectangle_tracker_subscription(id)
}
#[derive(Clone, Debug)]
pub struct MinSizeTracker<I> {
tx: UnboundedSender<(I, Rectangle)>,
}
impl<I> MinSizeTracker<I>
where
I: Hash + Copy + Send + Sync + Debug,
{
pub fn container<'a, Message: 'static, T>(
&self,
id: I,
content: T,
) -> MinSizeTracker<'a, Message, crate::Renderer, I>
where
I: 'a,
T: Into<Element<'a, Message, crate::Theme, crate::Renderer>>,
{
MinSizeTracker::new(content, id, self.tx.clone())
}
}
/// An element decorating some content.
///
/// It is normally used for alignment purposes.
#[allow(missing_debug_implementations)]
pub struct MinSizeTrackerContainer<'a, Message, Renderer, I>
where
Renderer: iced_core::Renderer,
{
tx: UnboundedSender<(I, Rectangle)>,
id: I,
container: Container<'a, Message, crate::Theme, Renderer>,
ignore_bounds: bool,
}
impl<'a, Message, Renderer, I> MinSizeTrackerContainer<'a, Message, Renderer, I>
where
Renderer: iced_core::Renderer,
I: 'a + Hash + Copy + Send + Sync + Debug,
{
/// Creates an empty [`Container`].
pub(crate) fn new<T>(content: T, id: I, tx: UnboundedSender<(I, Rectangle)>) -> Self
where
T: Into<Element<'a, Message, crate::Theme, Renderer>>,
{
MinSizeTrackerContainer {
id,
tx,
container: Container::new(content),
ignore_bounds: false,
}
}
pub fn diff(&mut self, tree: &mut Tree) {
self.container.diff(tree);
}
/// Sets the [`Padding`] of the [`Container`].
#[must_use]
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.container = self.container.padding(padding);
self
}
/// Sets the width of the [`self.`].
#[must_use]
pub fn width(mut self, width: Length) -> Self {
self.container = self.container.width(width);
self
}
/// Sets the height of the [`Container`].
#[must_use]
pub fn height(mut self, height: Length) -> Self {
self.container = self.container.height(height);
self
}
/// Sets the maximum width of the [`Container`].
#[must_use]
pub fn max_width(mut self, max_width: f32) -> Self {
self.container = self.container.max_width(max_width);
self
}
/// Sets the maximum height of the [`Container`] in pixels.
#[must_use]
pub fn max_height(mut self, max_height: f32) -> Self {
self.container = self.container.max_height(max_height);
self
}
/// Sets the content alignment for the horizontal axis of the [`Container`].
#[must_use]
pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self {
self.container = self.container.align_x(alignment);
self
}
/// Sets the content alignment for the vertical axis of the [`Container`].
#[must_use]
pub fn align_y(mut self, alignment: alignment::Vertical) -> Self {
self.container = self.container.align_y(alignment);
self
}
/// Centers the contents in the horizontal axis of the [`Container`].
#[must_use]
pub fn center_x(mut self, width: Length) -> Self {
self.container = self.container.center_x(width);
self
}
/// Centers the contents in the vertical axis of the [`Container`].
#[must_use]
pub fn center_y(mut self, height: Length) -> Self {
self.container = self.container.center_y(height);
self
}
/// Sets the style of the [`Container`].
#[must_use]
pub fn style(mut self, style: impl Into<<crate::Theme as Catalog>::Class<'a>>) -> Self {
self.container = self.container.class(style);
self
}
/// Set to true to ignore parent container bounds when performing layout.
/// This can be useful for widgets that are in auto-sized surfaces.
#[must_use]
pub fn ignore_bounds(mut self, ignore_bounds: bool) -> Self {
self.ignore_bounds = ignore_bounds;
self
}
}
impl<'a, Message, Renderer, I> Widget<Message, crate::Theme, Renderer>
for MinSizeTrackerContainer<'a, Message, Renderer, I>
where
Renderer: iced_core::Renderer,
I: 'a + Hash + Copy + Send + Sync + Debug,
{
fn children(&self) -> Vec<Tree> {
self.container.children()
}
fn state(&self) -> iced_core::widget::tree::State {
self.container.state()
}
fn diff(&mut self, tree: &mut Tree) {
self.container.diff(tree);
}
fn size(&self) -> iced_core::Size<Length> {
self.container.size()
}
fn layout(
&self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.container.layout(
tree,
renderer,
if self.ignore_bounds {
&layout::Limits::NONE
} else {
limits
},
)
}
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn iced_core::widget::Operation<()>,
) {
self.container.operate(tree, layout, renderer, operation);
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &iced_core::Rectangle,
) -> event::Status {
self.container.on_event(
tree,
event,
layout,
cursor_position,
renderer,
clipboard,
shell,
viewport,
)
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.container
.mouse_interaction(tree, layout, cursor_position, viewport, renderer)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &crate::Theme,
renderer_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
viewport: &Rectangle,
) {
let _ = self.tx.unbounded_send((self.id, layout.bounds()));
self.container.draw(
tree,
renderer,
theme,
renderer_style,
layout,
cursor_position,
viewport,
);
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
self.container.overlay(tree, layout, renderer, translation)
}
fn drag_destinations(
&self,
state: &Tree,
layout: Layout<'_>,
renderer: &Renderer,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) {
self.container
.drag_destinations(state, layout, renderer, dnd_rectangles);
}
}
impl<'a, Message, Renderer, I> From<MinSizeTrackerContainer<'a, Message, Renderer, I>>
for Element<'a, Message, crate::Theme, Renderer>
where
Message: 'a,
Renderer: 'a + iced_core::Renderer,
I: 'a + Hash + Copy + Send + Sync + Debug,
{
fn from(
column: MinSizeTrackerContainer<'a, Message, Renderer, I>,
) -> Element<'a, Message, crate::Theme, Renderer> {
Element::new(column)
}
}

View file

@ -0,0 +1,88 @@
use iced::{
futures::{
channel::mpsc::{unbounded, UnboundedReceiver},
stream, StreamExt,
},
Rectangle,
};
use iced_futures::Subscription;
use std::{collections::HashMap, fmt::Debug, hash::Hash};
use super::MinSizeTrackerContainer;
pub fn rectangle_tracker_subscription<
I: 'static + Hash + Copy + Send + Sync + Debug,
R: 'static + Hash + Copy + Send + Sync + Debug + Eq,
>(
id: I,
) -> Subscription<(I, RectangleUpdate<R>)> {
Subscription::run_with_id(
id,
stream::unfold(State::Ready, move |state| start_listening(id, state)),
)
}
pub enum State<I> {
Ready,
Waiting(UnboundedReceiver<(I, Rectangle)>, HashMap<I, Rectangle>),
Finished,
}
async fn start_listening<I: Copy, R: 'static + Hash + Copy + Send + Sync + Debug + Eq>(
id: I,
mut state: State<R>,
) -> Option<((I, RectangleUpdate<R>), State<R>)> {
loop {
let (update, new_state) = match state {
State::Ready => {
let (tx, rx) = unbounded();
(
Some((id, RectangleUpdate::Init(MinSizeTracker { tx }))),
State::Waiting(rx, HashMap::new()),
)
}
State::Waiting(mut rx, mut map) => match rx.next().await {
Some(u) => {
if let Some(prev) = map.get(&u.0) {
let new = u.1;
if (prev.width - new.width).abs() > 0.1
|| (prev.height - new.height).abs() > 0.1
|| (prev.x - new.x).abs() > 0.1
|| (prev.y - new.y).abs() > 0.1
{
map.insert(u.0, new);
(
Some((id, RectangleUpdate::Rectangle(u))),
State::Waiting(rx, map),
)
} else {
(None, State::Waiting(rx, map))
}
} else {
map.insert(u.0, u.1);
(
Some((id, RectangleUpdate::Rectangle(u))),
State::Waiting(rx, map),
)
}
}
None => (None, State::Finished),
},
State::Finished => return None,
};
state = new_state;
if let Some(u) = update {
return Some((u, state));
}
}
}
#[derive(Clone, Debug)]
pub enum RectangleUpdate<I>
where
I: 'static + Hash + Copy + Send + Sync + Debug,
{
Rectangle((I, Rectangle)),
Init(MinSizeTrackerContainer<I>),
}

View file

@ -60,7 +60,7 @@ pub use iced::widget::{combo_box, ComboBox};
pub use iced::widget::{container, Container};
#[doc(inline)]
pub use iced::widget::{horizontal_space, space, vertical_space, Space};
pub use iced::widget::{horizontal_space, vertical_space, Space};
#[doc(inline)]
pub use iced::widget::{image, Image};
@ -94,6 +94,9 @@ pub use iced_core::widget::{Id, Operation, Widget};
pub mod aspect_ratio;
#[cfg(feature = "autosize")]
pub mod autosize;
pub mod button;
#[doc(inline)]
pub use button::{Button, IconButton, LinkButton, TextButton};
@ -166,20 +169,20 @@ pub mod divider {
/// Horizontal divider with default thickness
#[must_use]
pub fn default() -> Rule<crate::Theme> {
horizontal_rule(1).style(crate::theme::Rule::Default)
pub fn default<'a>() -> Rule<'a, crate::Theme> {
horizontal_rule(1).class(crate::theme::Rule::Default)
}
/// Horizontal divider with light thickness
#[must_use]
pub fn light() -> Rule<crate::Theme> {
horizontal_rule(1).style(crate::theme::Rule::LightDivider)
pub fn light<'a>() -> Rule<'a, crate::Theme> {
horizontal_rule(1).class(crate::theme::Rule::LightDivider)
}
/// Horizontal divider with heavy thickness.
#[must_use]
pub fn heavy() -> Rule<crate::Theme> {
horizontal_rule(4).style(crate::theme::Rule::HeavyDivider)
pub fn heavy<'a>() -> Rule<'a, crate::Theme> {
horizontal_rule(4).class(crate::theme::Rule::HeavyDivider)
}
}
@ -189,20 +192,20 @@ pub mod divider {
/// Vertical divider with default thickness
#[must_use]
pub fn default() -> Rule<crate::Theme> {
vertical_rule(1).style(crate::theme::Rule::Default)
pub fn default<'a>() -> Rule<'a, crate::Theme> {
vertical_rule(1).class(crate::theme::Rule::Default)
}
/// Vertical divider with light thickness
#[must_use]
pub fn light() -> Rule<crate::Theme> {
vertical_rule(4).style(crate::theme::Rule::LightDivider)
pub fn light<'a>() -> Rule<'a, crate::Theme> {
vertical_rule(4).class(crate::theme::Rule::LightDivider)
}
/// Vertical divider with heavy thickness.
#[must_use]
pub fn heavy() -> Rule<crate::Theme> {
vertical_rule(10).style(crate::theme::Rule::HeavyDivider)
pub fn heavy<'a>() -> Rule<'a, crate::Theme> {
vertical_rule(10).class(crate::theme::Rule::HeavyDivider)
}
}
}
@ -268,7 +271,7 @@ pub use radio::{radio, Radio};
pub mod rectangle_tracker;
#[doc(inline)]
pub use rectangle_tracker::{rectangle_tracker, RectangleTracker};
pub use rectangle_tracker::{rectangle_tracking_container, RectangleTracker};
#[doc(inline)]
pub use row::{row, Row};
@ -343,13 +346,13 @@ pub mod tooltip {
pub fn tooltip<'a, Message>(
content: impl Into<Element<'a, Message>>,
tooltip: impl Into<Cow<'a, str>>,
tooltip: impl Into<Element<'a, Message>>,
position: Position,
) -> Tooltip<'a, Message> {
let xxs = crate::theme::active().cosmic().space_xxs();
Tooltip::new(content, tooltip, position)
.style(crate::theme::Container::Tooltip)
.class(crate::theme::Container::Tooltip)
.padding(xxs)
.gap(1)
}

View file

@ -147,11 +147,12 @@ impl<'a, Message: Clone + 'static> From<NavBar<'a, Message>>
.spacing(space_xxs)
.style(crate::theme::SegmentedButton::TabBar)
.apply(scrollable)
.class(crate::style::iced::Scrollable::Minimal)
.height(Length::Fill)
.apply(container)
.padding(space_xxs)
.height(Length::Fill)
.style(theme::Container::custom(nav_bar_style))
.class(theme::Container::custom(nav_bar_style))
}
}
@ -162,9 +163,9 @@ impl<'a, Message: Clone + 'static> From<NavBar<'a, Message>> for crate::Element<
}
#[must_use]
pub fn nav_bar_style(theme: &Theme) -> iced_style::container::Appearance {
pub fn nav_bar_style(theme: &Theme) -> iced_widget::container::Style {
let cosmic = &theme.cosmic();
iced_style::container::Appearance {
iced_widget::container::Style {
icon_color: Some(cosmic.on_bg_color().into()),
text_color: Some(cosmic.on_bg_color().into()),
background: Some(Background::Color(cosmic.primary.base.into())),

View file

@ -11,7 +11,7 @@ pub struct NavBarToggle<Message> {
active: bool,
#[setters(strip_option)]
on_toggle: Option<Message>,
style: crate::theme::Button,
class: crate::theme::Button,
selected: bool,
}
@ -20,7 +20,7 @@ pub fn nav_bar_toggle<Message>() -> NavBarToggle<Message> {
NavBarToggle {
active: false,
on_toggle: None,
style: crate::theme::Button::Text,
class: crate::theme::Button::Text,
selected: false,
}
}
@ -43,7 +43,7 @@ impl<'a, Message: 'static + Clone> From<NavBarToggle<Message>> for Element<'a, M
.padding([8, 16])
.on_press_maybe(nav_bar_toggle.on_toggle)
.selected(nav_bar_toggle.selected)
.style(nav_bar_toggle.style)
.class(nav_bar_toggle.class)
.into()
}
}

View file

@ -9,12 +9,12 @@ use iced_core::mouse;
use iced_core::overlay;
use iced_core::renderer;
use iced_core::touch;
use iced_core::widget::{tree, Operation, OperationOutputWrapper, Tree};
use iced_core::widget::{tree, Operation, Tree};
use iced_core::{
Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget,
};
pub use iced_style::container::{Appearance, StyleSheet};
pub use iced_widget::container::{Catalog, Style};
pub fn popover<'a, Message, Renderer>(
content: impl Into<Element<'a, Message, crate::Theme, Renderer>>,
@ -123,7 +123,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
operation: &mut dyn Operation<()>,
) {
self.content
.as_widget()
@ -214,6 +214,7 @@ where
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
mut translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
if !tree.state.downcast_mut::<State>().is_open {
return None;
@ -239,19 +240,22 @@ where
// Round position to prevent rendering issues
overlay_position.x = overlay_position.x.round();
overlay_position.y = overlay_position.y.round();
translation.x += overlay_position.x;
translation.y += overlay_position.y;
Some(overlay::Element::new(
overlay_position,
Box::new(Overlay {
tree: &mut tree.children[1],
content: popup,
position: self.position,
}),
))
Some(overlay::Element::new(Box::new(Overlay {
tree: &mut tree.children[1],
content: popup,
position: self.position,
pos: Point::new(translation.x, translation.y),
})))
} else {
self.content
.as_widget_mut()
.overlay(&mut tree.children[0], layout, renderer)
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
translation,
)
}
}
@ -286,6 +290,7 @@ pub struct Overlay<'a, 'b, Message, Renderer> {
tree: &'a mut Tree,
content: &'a mut Element<'b, Message, crate::Theme, Renderer>,
position: Position,
pos: Point,
}
impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, crate::Theme, Renderer>
@ -294,13 +299,8 @@ where
Message: Clone,
Renderer: iced_core::Renderer,
{
fn layout(
&mut self,
renderer: &Renderer,
bounds: Size,
mut position: Point,
_translation: iced::Vector,
) -> layout::Node {
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
let mut position = self.pos;
let limits = layout::Limits::new(Size::UNIT, bounds);
let node = self
.content
@ -342,7 +342,7 @@ where
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
operation: &mut dyn Operation<()>,
) {
self.content
.as_widget()
@ -413,7 +413,7 @@ where
) -> Option<overlay::Element<'c, Message, crate::Theme, Renderer>> {
self.content
.as_widget_mut()
.overlay(&mut self.tree, layout, renderer)
.overlay(&mut self.tree, layout, renderer, Default::default())
}
}

View file

@ -1,4 +1,6 @@
//! Create choices using radio buttons.
use crate::Theme;
use iced::border;
use iced_core::event::{self, Event};
use iced_core::layout;
use iced_core::mouse;
@ -7,17 +9,18 @@ use iced_core::renderer;
use iced_core::touch;
use iced_core::widget::tree::Tree;
use iced_core::{
Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Widget,
Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, Widget,
};
pub use iced_style::radio::{Appearance, StyleSheet};
use iced_widget::radio as iced_radio;
pub use iced_widget::radio::Catalog;
pub fn radio<'a, Message: Clone, Theme: StyleSheet, V, F>(
pub fn radio<'a, Message: Clone, V, F>(
label: impl Into<Element<'a, Message, Theme, crate::Renderer>>,
value: V,
selected: Option<V>,
f: F,
) -> Radio<'a, Message, Theme, crate::Renderer>
) -> Radio<'a, Message, crate::Renderer>
where
V: Eq + Copy,
F: FnOnce(V) -> Message,
@ -83,9 +86,8 @@ where
/// let content = column![a, b, c, all];
/// ```
#[allow(missing_debug_implementations)]
pub struct Radio<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
pub struct Radio<'a, Message, Renderer = crate::Renderer>
where
Theme: StyleSheet,
Renderer: iced_core::Renderer,
{
is_selected: bool,
@ -94,13 +96,11 @@ where
width: Length,
size: f32,
spacing: f32,
style: Theme::Style,
}
impl<'a, Message, Theme, Renderer> Radio<'a, Message, Theme, Renderer>
impl<'a, Message, Renderer> Radio<'a, Message, Renderer>
where
Message: Clone,
Theme: StyleSheet,
Renderer: iced_core::Renderer,
{
/// The default size of a [`Radio`] button.
@ -130,7 +130,6 @@ where
width: Length::Shrink,
size: Self::DEFAULT_SIZE,
spacing: Self::DEFAULT_SPACING,
style: Default::default(),
}
}
@ -154,20 +153,11 @@ where
self.spacing = spacing.into().0;
self
}
#[must_use]
/// Sets the style of the [`Radio`] button.
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
self.style = style.into();
self
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Radio<'a, Message, Theme, Renderer>
impl<'a, Message, Renderer> Widget<Message, Theme, Renderer> for Radio<'a, Message, Renderer>
where
Message: Clone,
Theme: StyleSheet,
Renderer: iced_core::Renderer,
{
fn children(&self) -> Vec<Tree> {
@ -207,9 +197,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn iced_core::widget::Operation<
iced_core::widget::OperationOutputWrapper<Message>,
>,
operation: &mut dyn iced_core::widget::Operation<()>,
) {
self.label.as_widget().operate(
&mut tree.children[0],
@ -302,9 +290,19 @@ where
let mut children = layout.children();
let custom_style = if is_mouse_over {
theme.hovered(&self.style, self.is_selected)
theme.style(
&(),
iced_radio::Status::Hovered {
is_selected: self.is_selected,
},
)
} else {
theme.active(&self.style, self.is_selected)
theme.style(
&(),
iced_radio::Status::Active {
is_selected: self.is_selected,
},
)
};
{
@ -336,7 +334,7 @@ where
width: dot_size,
height: dot_size,
},
border: Border::with_radius(dot_size / 2.0),
border: border::rounded(dot_size / 2.0),
..renderer::Quad::default()
},
custom_style.dot_color,
@ -363,11 +361,13 @@ where
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.label.as_widget_mut().overlay(
&mut tree.children[0],
layout.children().nth(1).unwrap(),
renderer,
translation,
)
}
@ -376,7 +376,7 @@ where
state: &Tree,
layout: Layout<'_>,
renderer: &Renderer,
dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) {
self.label.as_widget().drag_destinations(
&state.children[0],
@ -387,14 +387,13 @@ where
}
}
impl<'a, Message, Theme, Renderer> From<Radio<'a, Message, Theme, Renderer>>
impl<'a, Message, Renderer> From<Radio<'a, Message, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a + Clone,
Theme: 'a + StyleSheet,
Renderer: 'a + iced_core::Renderer,
{
fn from(radio: Radio<'a, Message, Theme, Renderer>) -> Element<'a, Message, Theme, Renderer> {
fn from(radio: Radio<'a, Message, Renderer>) -> Element<'a, Message, Theme, Renderer> {
Element::new(radio)
}
}

View file

@ -2,6 +2,7 @@ mod subscription;
use iced::futures::channel::mpsc::UnboundedSender;
use iced::widget::Container;
use iced::Vector;
pub use subscription::*;
use iced_core::alignment;
@ -14,9 +15,9 @@ use iced_core::widget::Tree;
use iced_core::{Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget};
use std::{fmt::Debug, hash::Hash};
pub use iced_style::container::{Appearance, StyleSheet};
pub use iced_widget::container::{Catalog, Style};
pub fn rectangle_tracker<'a, Message, I, T>(
pub fn rectangle_tracking_container<'a, Message, I, T>(
content: T,
id: I,
tx: UnboundedSender<(I, Rectangle)>,
@ -71,6 +72,7 @@ where
id: I,
container: Container<'a, Message, crate::Theme, Renderer>,
ignore_bounds: bool,
request_size: bool,
}
impl<'a, Message, Renderer, I> RectangleTrackingContainer<'a, Message, Renderer, I>
@ -88,6 +90,7 @@ where
tx,
container: Container::new(content),
ignore_bounds: false,
request_size: true,
}
}
@ -146,22 +149,22 @@ where
/// Centers the contents in the horizontal axis of the [`Container`].
#[must_use]
pub fn center_x(mut self) -> Self {
self.container = self.container.center_x();
pub fn center_x(mut self, width: Length) -> Self {
self.container = self.container.center_x(width);
self
}
/// Centers the contents in the vertical axis of the [`Container`].
#[must_use]
pub fn center_y(mut self) -> Self {
self.container = self.container.center_y();
pub fn center_y(mut self, height: Length) -> Self {
self.container = self.container.center_y(height);
self
}
/// Sets the style of the [`Container`].
#[must_use]
pub fn style(mut self, style: impl Into<<crate::Theme as StyleSheet>::Style>) -> Self {
self.container = self.container.style(style);
pub fn style(mut self, style: impl Into<<crate::Theme as Catalog>::Class<'a>>) -> Self {
self.container = self.container.class(style);
self
}
@ -202,7 +205,7 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.container.layout(
let layout = self.container.layout(
tree,
renderer,
if self.ignore_bounds {
@ -210,7 +213,10 @@ where
} else {
limits
},
)
);
if self.request_size {}
layout
}
fn operate(
@ -218,9 +224,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn iced_core::widget::Operation<
iced_core::widget::OperationOutputWrapper<Message>,
>,
operation: &mut dyn iced_core::widget::Operation<()>,
) {
self.container.operate(tree, layout, renderer, operation);
}
@ -271,7 +275,6 @@ where
viewport: &Rectangle,
) {
let _ = self.tx.unbounded_send((self.id, layout.bounds()));
self.container.draw(
tree,
renderer,
@ -288,8 +291,9 @@ where
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
self.container.overlay(tree, layout, renderer)
self.container.overlay(tree, layout, renderer, translation)
}
fn drag_destinations(
@ -297,7 +301,7 @@ where
state: &Tree,
layout: Layout<'_>,
renderer: &Renderer,
dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) {
self.container
.drag_destinations(state, layout, renderer, dnd_rectangles);

View file

@ -1,10 +1,11 @@
use iced::{
futures::{
channel::mpsc::{unbounded, UnboundedReceiver},
StreamExt,
stream, StreamExt,
},
subscription, Rectangle,
Rectangle,
};
use iced_futures::Subscription;
use std::{collections::HashMap, fmt::Debug, hash::Hash};
use super::RectangleTracker;
@ -14,8 +15,11 @@ pub fn rectangle_tracker_subscription<
R: 'static + Hash + Copy + Send + Sync + Debug + Eq,
>(
id: I,
) -> iced::Subscription<(I, RectangleUpdate<R>)> {
subscription::unfold(id, State::Ready, move |state| start_listening(id, state))
) -> Subscription<(I, RectangleUpdate<R>)> {
Subscription::run_with_id(
id,
stream::unfold(State::Ready, move |state| start_listening(id, state)),
)
}
pub enum State<I> {
@ -27,7 +31,7 @@ pub enum State<I> {
async fn start_listening<I: Copy, R: 'static + Hash + Copy + Send + Sync + Debug + Eq>(
id: I,
mut state: State<R>,
) -> ((I, RectangleUpdate<R>), State<R>) {
) -> Option<((I, RectangleUpdate<R>), State<R>)> {
loop {
let (update, new_state) = match state {
State::Ready => {
@ -65,11 +69,11 @@ async fn start_listening<I: Copy, R: 'static + Hash + Copy + Send + Sync + Debug
}
None => (None, State::Finished),
},
State::Finished => iced::futures::future::pending().await,
State::Finished => return None,
};
state = new_state;
if let Some(u) = update {
return (u, state);
return Some((u, state));
}
}
}

View file

@ -16,14 +16,15 @@ use iced::clipboard::dnd::{self, DndAction, DndDestinationRectangle, DndEvent, O
use iced::clipboard::mime::AllowedMimeTypes;
use iced::touch::Finger;
use iced::{
alignment, event, keyboard, mouse, touch, Alignment, Background, Color, Command, Event, Length,
Padding, Rectangle, Size,
alignment, event, keyboard, mouse, touch, Alignment, Background, Color, Event, Length, Padding,
Rectangle, Size, Task, Vector,
};
use iced_core::mouse::ScrollDelta;
use iced_core::text::{LineHeight, Paragraph, Renderer as TextRenderer, Shaping, Wrap};
use iced_core::text::{LineHeight, Paragraph, Renderer as TextRenderer, Shaping, Wrapping};
use iced_core::widget::{self, operation, tree};
use iced_core::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget};
use iced_core::{Border, Gradient, Point, Renderer as IcedRenderer, Shadow, Text};
use iced_runtime::{task, Action};
use slotmap::{Key, SecondaryMap};
use std::borrow::Cow;
use std::collections::hash_map::DefaultHasher;
@ -34,8 +35,8 @@ use std::mem;
use std::time::{Duration, Instant};
/// A command that focuses a segmented item stored in a widget.
pub fn focus<Message: 'static>(id: Id) -> Command<Message> {
Command::widget(operation::focusable::focus(id.0))
pub fn focus<Message: 'static>(id: Id) -> Task<Message> {
task::effect(Action::Widget(Box::new(operation::focusable::focus(id.0))))
}
pub enum ItemBounds {
@ -436,15 +437,15 @@ where
if !text.is_empty() {
icon_spacing = f32::from(self.button_spacing);
let paragraph = entry.or_insert_with(|| {
crate::Paragraph::with_text(Text {
content: text,
crate::Plain::new(Text {
content: text.as_ref(),
size: iced::Pixels(self.font_size),
bounds: Size::INFINITY,
font,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: Shaping::Advanced,
wrap: Wrap::default(),
wrapping: Wrapping::default(),
line_height: self.line_height,
})
});
@ -622,23 +623,21 @@ where
}
let text = Text {
content: text,
content: text.as_ref(),
size: iced::Pixels(self.font_size),
bounds: Size::INFINITY,
font,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: Shaping::Advanced,
wrap: Wrap::default(),
wrapping: Wrapping::default(),
line_height: self.line_height,
};
if let Some(paragraph) = state.paragraphs.get_mut(key) {
paragraph.update(text);
} else {
state
.paragraphs
.insert(key, crate::Paragraph::with_text(text));
state.paragraphs.insert(key, crate::Plain::new(text));
}
}
}
@ -1079,9 +1078,7 @@ where
tree: &mut Tree,
_layout: Layout<'_>,
_renderer: &Renderer,
operation: &mut dyn iced_core::widget::Operation<
iced_core::widget::OperationOutputWrapper<Message>,
>,
operation: &mut dyn iced_core::widget::Operation<()>,
) {
let state = tree.state.downcast_mut::<LocalState>();
operation.focusable(state, Some(&self.id.0));
@ -1492,7 +1489,7 @@ where
if self.model.text(key).is_some_and(|text| !text.is_empty()) {
// Draw the text for this segmented button or tab.
renderer.fill_paragraph(
&state.paragraphs[key],
state.paragraphs[key].raw(),
bounds.position(),
apply_alpha(status_appearance.text_color),
Rectangle {
@ -1528,6 +1525,7 @@ where
tree: &'b mut Tree,
layout: iced_core::Layout<'_>,
_renderer: &Renderer,
translation: Vector,
) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, Renderer>> {
let state = tree.state.downcast_ref::<LocalState>();
@ -1575,6 +1573,7 @@ where
root_bounds_list: vec![bounds],
path_highlight: Some(PathHighlight::MenuActive),
style: &crate::theme::menu_bar::MenuBarStyle::Default,
position: Point::new(translation.x, translation.y),
}
.overlay(),
)
@ -1645,7 +1644,7 @@ pub struct LocalState {
/// Dimensions of internal buttons when shrinking
pub(super) internal_layout: Vec<(Size, Size)>,
/// The paragraphs for each text.
paragraphs: SecondaryMap<Entity, crate::Paragraph>,
paragraphs: SecondaryMap<Entity, crate::Plain>,
/// Used to detect changes in text.
text_hashes: SecondaryMap<Entity, u64>,
/// Location of cursor when context menu was opened.

View file

@ -9,7 +9,7 @@ use crate::{
Element,
};
use derive_setters::Setters;
use iced_core::{text::Wrap, Length};
use iced_core::{text::Wrapping, Length};
use taffy::AlignContent;
/// A settings item aligned in a row
@ -20,8 +20,8 @@ pub fn item<'a, Message: 'static>(
widget: impl Into<Element<'a, Message>> + 'a,
) -> Row<'a, Message> {
item_row(vec![
text(title).wrap(Wrap::Word).into(),
horizontal_space(iced::Length::Fill).into(),
text(title).wrapping(Wrapping::Word).into(),
horizontal_space().width(iced::Length::Fill).into(),
widget.into(),
])
}
@ -35,7 +35,7 @@ pub fn item_row<Message>(children: Vec<Element<Message>>) -> Row<Message> {
} = theme::THEME.lock().unwrap().cosmic().spacing;
row::with_children(children)
.spacing(space_xs)
.align_items(iced::Alignment::Center)
.align_y(iced::Alignment::Center)
.padding([0, space_s])
}
@ -46,7 +46,10 @@ pub fn flex_item<'a, Message: 'static>(
widget: impl Into<Element<'a, Message>> + 'a,
) -> FlexRow<'a, Message> {
flex_item_row(vec![
text(title).wrap(Wrap::Word).width(Length::Fill).into(),
text(title)
.wrapping(Wrapping::Word)
.width(Length::Fill)
.into(),
container(widget).into(),
])
}
@ -111,8 +114,8 @@ impl<'a, Message: 'static> Item<'a, Message> {
if let Some(description) = self.description {
let column = column::with_capacity(2)
.spacing(2)
.push(text(self.title).wrap(Wrap::Word))
.push(text(description).wrap(Wrap::Word).size(10))
.push(text(self.title).wrapping(Wrapping::Word))
.push(text(description).wrapping(Wrapping::Word).size(10))
.width(Length::Fill);
contents.push(column.into());
@ -129,6 +132,6 @@ impl<'a, Message: 'static> Item<'a, Message> {
is_checked: bool,
message: impl Fn(bool) -> Message + 'static,
) -> Row<'a, Message> {
self.control(crate::widget::toggler(None, is_checked, message))
self.control(crate::widget::toggler(is_checked, message))
}
}

View file

@ -56,11 +56,10 @@ impl<'a, Message: 'static> SpinButton<'a, Message> {
.apply(button::custom)
.width(Length::Fixed(32.0))
.height(Length::Fixed(32.0))
.style(theme::Button::Text)
.class(theme::Button::Text)
.on_press(model::Message::Decrement)
.into(),
text::title4(label)
.vertical_alignment(Vertical::Center)
.apply(container)
.width(Length::Fixed(48.0))
.align_x(Horizontal::Center)
@ -76,18 +75,18 @@ impl<'a, Message: 'static> SpinButton<'a, Message> {
.apply(button::custom)
.width(Length::Fixed(32.0))
.height(Length::Fixed(32.0))
.style(theme::Button::Text)
.class(theme::Button::Text)
.on_press(model::Message::Increment)
.into(),
])
.width(Length::Shrink)
.height(Length::Fixed(32.0))
.align_items(Alignment::Center),
.align_y(Alignment::Center),
)
.align_y(Vertical::Center)
.width(Length::Shrink)
.height(Length::Fixed(32.0))
.style(theme::Container::custom(container_style))
.class(theme::Container::custom(container_style))
.apply(Element::from)
.map(on_change)
}
@ -100,13 +99,13 @@ impl<'a, Message: 'static> From<SpinButton<'a, Message>> for Element<'a, Message
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn container_style(theme: &crate::Theme) -> iced_style::container::Appearance {
fn container_style(theme: &crate::Theme) -> iced_widget::container::Style {
let basic = &theme.cosmic();
let mut neutral_10 = basic.palette.neutral_10;
neutral_10.alpha = 0.1;
let accent = &basic.accent;
let corners = &basic.corner_radii;
iced_style::container::Appearance {
iced_widget::container::Style {
icon_color: Some(basic.palette.neutral_10.into()),
text_color: Some(basic.palette.neutral_10.into()),
background: None,

View file

@ -7,7 +7,7 @@ use std::borrow::Cow;
///
/// [`Text`]: widget::Text
pub fn text<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme, Renderer> {
Text::new(text).font(crate::font::default())
Text::new(text.into()).font(crate::font::default())
}
/// Available presets for text typography
@ -26,7 +26,7 @@ pub enum Typography {
/// [`Text`] widget with the Title 1 typography preset.
pub fn title1<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme, Renderer> {
Text::new(text)
Text::new(text.into())
.size(32.0)
.line_height(LineHeight::Absolute(44.0.into()))
.font(crate::font::semibold())
@ -34,7 +34,7 @@ pub fn title1<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme,
/// [`Text`] widget with the Title 2 typography preset.
pub fn title2<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme, Renderer> {
Text::new(text)
Text::new(text.into())
.size(28.0)
.line_height(LineHeight::Absolute(36.0.into()))
.font(crate::font::default())
@ -42,7 +42,7 @@ pub fn title2<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme,
/// [`Text`] widget with the Title 3 typography preset.
pub fn title3<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme, Renderer> {
Text::new(text)
Text::new(text.into())
.size(24.0)
.line_height(LineHeight::Absolute(32.0.into()))
.font(crate::font::default())
@ -50,7 +50,7 @@ pub fn title3<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme,
/// [`Text`] widget with the Title 4 typography preset.
pub fn title4<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme, Renderer> {
Text::new(text)
Text::new(text.into())
.size(20.0)
.line_height(LineHeight::Absolute(28.0.into()))
.font(crate::font::default())
@ -58,7 +58,7 @@ pub fn title4<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme,
/// [`Text`] widget with the Heading typography preset.
pub fn heading<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme, Renderer> {
Text::new(text)
Text::new(text.into())
.size(14.0)
.line_height(LineHeight::Absolute(iced::Pixels(20.0)))
.font(crate::font::semibold())
@ -66,7 +66,7 @@ pub fn heading<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme,
/// [`Text`] widget with the Caption Heading typography preset.
pub fn caption_heading<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme, Renderer> {
Text::new(text)
Text::new(text.into())
.size(10.0)
.line_height(LineHeight::Absolute(iced::Pixels(14.0)))
.font(crate::font::semibold())
@ -74,7 +74,7 @@ pub fn caption_heading<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate
/// [`Text`] widget with the Body typography preset.
pub fn body<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme, Renderer> {
Text::new(text)
Text::new(text.into())
.size(14.0)
.line_height(LineHeight::Absolute(20.0.into()))
.font(crate::font::default())
@ -82,7 +82,7 @@ pub fn body<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme, Re
/// [`Text`] widget with the Caption typography preset.
pub fn caption<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme, Renderer> {
Text::new(text)
Text::new(text.into())
.size(10.0)
.line_height(LineHeight::Absolute(14.0.into()))
.font(crate::font::default())
@ -90,7 +90,7 @@ pub fn caption<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme,
/// [`Text`] widget with the Monotext typography preset.
pub fn monotext<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme, Renderer> {
Text::new(text)
Text::new(text.into())
.size(14.0)
.line_height(LineHeight::Absolute(20.0.into()))
.font(crate::font::mono())

View file

@ -18,6 +18,9 @@ use super::style::StyleSheet;
pub use super::value::Value;
use apply::Apply;
use cosmic_theme::Theme;
use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent};
use iced::clipboard::mime::AsMimeTypes;
use iced::Limits;
use iced_core::event::{self, Event};
use iced_core::mouse::{self, click};
@ -39,15 +42,9 @@ use iced_core::{
};
#[cfg(feature = "wayland")]
use iced_renderer::core::event::{wayland, PlatformSpecific};
use iced_renderer::core::widget::OperationOutputWrapper;
#[cfg(feature = "wayland")]
use iced_runtime::command::platform_specific;
use iced_runtime::Command;
#[cfg(feature = "wayland")]
use cctk::sctk::reexports::client::protocol::wl_data_device_manager::DndAction;
#[cfg(feature = "wayland")]
use iced_runtime::command::platform_specific::wayland::data_device::{DataFromMimeType, DndIcon};
use iced_runtime::platform_specific;
use iced_runtime::{task, Action, Task};
thread_local! {
// Prevents two inputs from being focused at the same time.
@ -146,7 +143,7 @@ where
})
.size(16)
.apply(crate::widget::button::custom)
.style(crate::theme::Button::Icon)
.class(crate::theme::Button::Icon)
.on_press(msg)
.padding(8)
.into(),
@ -173,7 +170,6 @@ where
.padding(spacing)
}
#[cfg(feature = "wayland")]
pub(crate) const SUPPORTED_TEXT_MIME_TYPES: &[&str; 6] = &[
"text/plain;charset=utf-8",
"text/plain;charset=UTF-8",
@ -182,17 +178,12 @@ pub(crate) const SUPPORTED_TEXT_MIME_TYPES: &[&str; 6] = &[
"text/plain",
"TEXT",
];
#[cfg(feature = "wayland")]
pub type DnDCommand =
Box<dyn Send + Sync + Fn() -> platform_specific::wayland::data_device::ActionInner>;
#[cfg(not(feature = "wayland"))]
pub type DnDCommand = ();
/// A field that can be filled with text.
#[allow(missing_debug_implementations)]
#[must_use]
pub struct TextInput<'a, Message> {
id: Option<Id>,
id: Id,
placeholder: Cow<'a, str>,
value: Value,
is_secure: bool,
@ -215,7 +206,6 @@ pub struct TextInput<'a, Message> {
trailing_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>,
style: <crate::Theme as StyleSheet>::Style,
on_create_dnd_source: Option<Box<dyn Fn(State) -> Message + 'a>>,
on_dnd_command_produced: Option<Box<dyn Fn(DnDCommand) -> Message + 'a>>,
surface_ids: Option<(window::Id, window::Id)>,
dnd_icon: bool,
line_height: text::LineHeight,
@ -237,7 +227,7 @@ where
let v: Cow<'a, str> = value.into();
TextInput {
id: None,
id: Id::unique(),
placeholder: placeholder.into(),
value: Value::new(v.as_ref()),
is_secure: false,
@ -258,7 +248,6 @@ where
trailing_icon: None,
error: None,
style: crate::theme::TextInput::default(),
on_dnd_command_produced: None,
on_create_dnd_source: None,
surface_ids: None,
dnd_icon: false,
@ -269,6 +258,15 @@ where
}
}
fn dnd_id(&self) -> u128 {
match &self.id.0 {
iced_core::id::Internal::Custom(id, _) | iced_core::id::Internal::Unique(id) => {
*id as u128
}
_ => unreachable!(),
}
}
/// Sets the input to be always active.
/// This makes it behave as if it was always focused.
pub fn always_active(mut self) -> Self {
@ -290,7 +288,7 @@ where
/// Sets the [`Id`] of the [`TextInput`].
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self.id = id;
self
}
@ -433,10 +431,12 @@ where
value: Option<&Value>,
style: &renderer::Style,
) {
let text_layout = self.text_layout(layout.clone());
draw(
renderer,
theme,
layout,
text_layout,
cursor_position,
tree,
value.unwrap_or(&self.value),
@ -467,20 +467,6 @@ where
self
}
/// Sets the dnd command produced handler of the [`TextInput`].
/// Commands should be returned in the update function of the application.
#[cfg(feature = "wayland")]
pub fn on_dnd_command_produced(
mut self,
on_dnd_command_produced: impl Fn(
Box<dyn Send + Sync + Fn() -> platform_specific::wayland::data_device::ActionInner>,
) -> Message
+ 'a,
) -> Self {
self.on_dnd_command_produced = Some(Box::new(on_dnd_command_produced));
self
}
/// Sets the window id of the [`TextInput`] and the window id of the drag icon.
/// Both ids are required to be unique.
/// This is required for the dnd to work.
@ -500,7 +486,7 @@ where
crate::widget::icon::from_name("edit-clear-symbolic")
.size(16)
.apply(crate::widget::button::custom)
.style(crate::theme::Button::Icon)
.class(crate::theme::Button::Icon)
.on_press(on_clear)
.padding(8)
.into(),
@ -550,6 +536,7 @@ where
}
let old_value = state
.value
.raw()
.buffer()
.lines
.iter()
@ -559,6 +546,7 @@ where
|| old_value != self.value.to_string()
|| state
.label
.raw()
.buffer()
.lines
.iter()
@ -567,6 +555,7 @@ where
!= self.label.unwrap_or_default()
|| state
.helper_text
.raw()
.buffer()
.lines
.iter()
@ -651,7 +640,7 @@ where
let v = self.value.to_string();
value_paragraph.update(Text {
content: if self.value.is_empty() {
&self.placeholder
self.placeholder.as_ref()
} else {
&v
},
@ -662,7 +651,7 @@ where
vertical_alignment: alignment::Vertical::Center,
line_height: text::LineHeight::default(),
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
});
let Size { width, height } =
@ -717,12 +706,13 @@ where
tree: &mut Tree,
_layout: Layout<'_>,
_renderer: &crate::Renderer,
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
operation: &mut dyn Operation<()>,
) {
let state = tree.state.downcast_mut::<State>();
operation.focusable(state, self.id.as_ref());
operation.text_input(state, self.id.as_ref());
operation.custom(state, Some(&self.id));
operation.focusable(state, Some(&self.id));
operation.text_input(state, Some(&self.id));
}
fn overlay<'b>(
@ -730,6 +720,7 @@ where
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &crate::Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
let mut layout_ = Vec::with_capacity(2);
if self.leading_icon.is_some() {
@ -752,7 +743,9 @@ where
.zip(&mut tree.children)
.zip(layout_)
.filter_map(|((child, state), layout)| {
child.as_widget_mut().overlay(state, layout, renderer)
child
.as_widget_mut()
.overlay(state, layout, renderer, translation)
})
.collect::<Vec<_>>();
@ -814,8 +807,10 @@ where
}
}
}
let dnd_id = self.dnd_id();
let id = Widget::id(self);
update(
id,
event,
text_layout.children().next().unwrap(),
trailing_icon_layout,
@ -833,9 +828,7 @@ where
self.on_toggle_edit.as_deref(),
|| tree.state.downcast_mut::<State>(),
self.on_create_dnd_source.as_deref(),
self.dnd_icon,
self.on_dnd_command_produced.as_deref(),
self.surface_ids,
dnd_id,
line_height,
layout,
)
@ -851,10 +844,12 @@ where
cursor_position: mouse::Cursor,
viewport: &Rectangle,
) {
let text_layout = self.text_layout(layout.clone());
draw(
renderer,
theme,
layout,
text_layout,
cursor_position,
tree,
&self.value,
@ -934,11 +929,43 @@ where
}
fn id(&self) -> Option<Id> {
self.id.clone()
Some(self.id.clone())
}
fn set_id(&mut self, id: Id) {
self.id = Some(id);
self.id = id;
}
fn drag_destinations(
&self,
state: &Tree,
layout: Layout<'_>,
renderer: &crate::Renderer,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) {
if let Some(input) = layout.children().last() {
let Rectangle {
x,
y,
width,
height,
} = input.bounds();
dnd_rectangles.push(iced::clipboard::dnd::DndDestinationRectangle {
id: self.dnd_id(),
rectangle: iced::clipboard::dnd::Rectangle {
x: x as f64,
y: y as f64,
width: width as f64,
height: height as f64,
},
mime_types: SUPPORTED_TEXT_MIME_TYPES
.iter()
.map(|s| Cow::Borrowed(*s))
.collect(),
actions: DndAction::Move,
preferred: DndAction::Move,
});
}
}
}
@ -954,32 +981,38 @@ where
}
}
/// Produces a [`Command`] that focuses the [`TextInput`] with the given [`Id`].
pub fn focus<Message: 'static>(id: Id) -> Command<Message> {
Command::widget(operation::focusable::focus(id))
/// Produces a [`Task`] that focuses the [`TextInput`] with the given [`Id`].
pub fn focus<Message: 'static>(id: Id) -> Task<Message> {
task::effect(Action::widget(operation::focusable::focus(id)))
}
/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// end.
pub fn move_cursor_to_end<Message: 'static>(id: Id) -> Command<Message> {
Command::widget(operation::text_input::move_cursor_to_end(id))
pub fn move_cursor_to_end<Message: 'static>(id: Id) -> Task<Message> {
task::effect(Action::widget(operation::text_input::move_cursor_to_end(
id,
)))
}
/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// front.
pub fn move_cursor_to_front<Message: 'static>(id: Id) -> Command<Message> {
Command::widget(operation::text_input::move_cursor_to_front(id))
pub fn move_cursor_to_front<Message: 'static>(id: Id) -> Task<Message> {
task::effect(Action::widget(operation::text_input::move_cursor_to_front(
id,
)))
}
/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// provided position.
pub fn move_cursor_to<Message: 'static>(id: Id, position: usize) -> Command<Message> {
Command::widget(operation::text_input::move_cursor_to(id, position))
pub fn move_cursor_to<Message: 'static>(id: Id, position: usize) -> Task<Message> {
task::effect(Action::widget(operation::text_input::move_cursor_to(
id, position,
)))
}
/// Produces a [`Command`] that selects all the content of the [`TextInput`] with the given [`Id`].
pub fn select_all<Message: 'static>(id: Id) -> Command<Message> {
Command::widget(operation::text_input::select_all(id))
/// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`Id`].
pub fn select_all<Message: 'static>(id: Id) -> Task<Message> {
task::effect(Action::widget(operation::text_input::select_all(id)))
}
/// Computes the layout of a [`TextInput`].
@ -1019,7 +1052,7 @@ pub fn layout<Message>(
vertical_alignment: alignment::Vertical::Center,
line_height,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
});
let label_size = label_paragraph.min_bounds();
@ -1158,7 +1191,7 @@ pub fn layout<Message>(
vertical_alignment: alignment::Vertical::Center,
line_height: helper_text_line_height,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
});
let helper_text_size = helper_text_paragraph.min_bounds();
let helper_text_node = layout::Node::new(helper_text_size).translate(helper_pos);
@ -1188,7 +1221,8 @@ pub fn layout<Message>(
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::cast_lossless)]
#[allow(clippy::cast_possible_truncation)]
pub fn update<'a, Message>(
pub fn update<'a, Message: 'static>(
id: Option<Id>,
event: Event,
text_layout: Layout<'_>,
trailing_icon_layout: Option<Layout<'_>>,
@ -1206,9 +1240,7 @@ pub fn update<'a, Message>(
on_toggle_edit: Option<&dyn Fn(bool) -> Message>,
state: impl FnOnce() -> &'a mut State,
#[allow(unused_variables)] on_start_dnd_source: Option<&dyn Fn(State) -> Message>,
#[allow(unused_variables)] dnd_icon: bool,
#[allow(unused_variables)] on_dnd_command_produced: Option<&dyn Fn(DnDCommand) -> Message>,
#[allow(unused_variables)] surface_ids: Option<(window::Id, window::Id)>,
#[allow(unused_variables)] dnd_id: u128,
line_height: text::LineHeight,
layout: Layout<'_>,
) -> event::Status
@ -1253,7 +1285,8 @@ where
let text_layout = layout.children().next().unwrap();
let target = cursor_position.x - text_layout.bounds().x;
let click = mouse::Click::new(cursor_position, state.last_click);
let click =
mouse::Click::new(cursor_position, mouse::Button::Left, state.last_click);
match (
&state.dragging_state,
@ -1266,28 +1299,18 @@ where
// single click that is on top of the selected text
// is the click on selected text?
if let (
Some(on_start_dnd),
Some(on_dnd_command_produced),
Some((window_id, icon_id)),
Some(on_input),
) = (
on_start_dnd_source,
on_dnd_command_produced,
surface_ids,
on_input,
) {
if let Some(on_input) = on_input {
let left = start.min(end);
let right = end.max(start);
let (left_position, _left_offset) = measure_cursor_and_scroll_offset(
&state.value,
state.value.raw(),
text_layout.bounds(),
left,
);
let (right_position, _right_offset) = measure_cursor_and_scroll_offset(
&state.value,
state.value.raw(),
text_layout.bounds(),
right,
);
@ -1305,10 +1328,12 @@ where
if is_secure {
return event::Status::Ignored;
}
let text =
let input_text =
state.selected_text(&value.to_string()).unwrap_or_default();
state.dragging_state =
Some(DraggingState::Dnd(DndAction::empty(), text.clone()));
state.dragging_state = Some(DraggingState::Dnd(
DndAction::empty(),
input_text.clone(),
));
let mut editor = Editor::new(unsecured_value, &mut state.cursor);
editor.delete();
@ -1316,23 +1341,25 @@ where
let unsecured_value = Value::new(&contents);
let message = (on_input)(contents);
shell.publish(message);
shell.publish(on_start_dnd(state.clone()));
if let Some(on_start_dnd) = on_start_dnd_source {
shell.publish(on_start_dnd(state.clone()));
}
let state_clone = state.clone();
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::StartDnd {
mime_types: SUPPORTED_TEXT_MIME_TYPES
.iter()
.map(std::string::ToString::to_string)
.collect(),
actions: DndAction::Move,
origin_id: window_id,
icon_id: Some((
DndIcon::Widget(icon_id, Box::new(state_clone.clone())),
iced::Vector::ZERO,
)),
data: Box::new(TextInputString(text.clone())),
}
})));
iced_core::clipboard::start_dnd(
clipboard,
false,
id.map(|id| iced_core::clipboard::DndSource::Widget(id)),
Some((
Element::from(
TextInput::<'static, ()>::new("", input_text.clone())
.dnd_icon(true),
),
iced_core::widget::tree::State::new(state_clone),
)),
Box::new(TextInputString(input_text)),
DndAction::Move,
);
update_cache(state, &unsecured_value);
} else {
@ -1435,7 +1462,7 @@ where
return event::Status::Ignored;
};
let click = mouse::Click::new(pos, state.last_click);
let click = mouse::Click::new(pos, mouse::Button::Left, state.last_click);
match (
&state.dragging_state,
@ -1621,7 +1648,10 @@ where
{
if !is_secure {
if let Some((start, end)) = state.cursor.selection(value) {
clipboard.write(value.select(start, end).to_string());
clipboard.write(
iced_core::clipboard::Kind::Primary,
value.select(start, end).to_string(),
);
}
}
}
@ -1632,7 +1662,10 @@ where
{
if !is_secure {
if let Some((start, end)) = state.cursor.selection(value) {
clipboard.write(value.select(start, end).to_string());
clipboard.write(
iced_core::clipboard::Kind::Primary,
value.select(start, end).to_string(),
);
}
let mut editor = Editor::new(value, &mut state.cursor);
@ -1650,7 +1683,7 @@ where
content
} else {
let content: String = clipboard
.read()
.read(iced_core::clipboard::Kind::Primary)
.unwrap_or_default()
.chars()
.filter(|c| !c.is_control())
@ -1760,7 +1793,7 @@ where
state.keyboard_modifiers = modifiers;
}
Event::Window(_, window::Event::RedrawRequested(now)) => {
Event::Window(window::Event::RedrawRequested(now)) => {
let state = state();
if let Some(focus) = &mut state.is_focused {
@ -1775,9 +1808,7 @@ where
}
}
#[cfg(feature = "wayland")]
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DataSource(
wayland::DataSourceEvent::DndFinished | wayland::DataSourceEvent::Cancelled,
))) => {
Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => {
let state = state();
if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) {
state.dragging_state = None;
@ -1785,54 +1816,32 @@ where
}
}
#[cfg(feature = "wayland")]
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DataSource(
wayland::DataSourceEvent::DndActionAccepted(action),
))) => {
let state = state();
if let Some(DraggingState::Dnd(_, text)) = state.dragging_state.as_ref() {
state.dragging_state = Some(DraggingState::Dnd(action, text.clone()));
return event::Status::Captured;
}
}
#[cfg(feature = "wayland")]
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer(
wayland::DndOfferEvent::Enter { x, y, mime_types },
))) => {
let Some(on_dnd_command_produced) = on_dnd_command_produced else {
return event::Status::Ignored;
};
Event::Dnd(DndEvent::Offer(
rectangle,
OfferEvent::Enter {
x,
y,
mime_types,
surface,
},
)) if rectangle == Some(dnd_id) => {
let state = state();
let is_clicked = text_layout.bounds().contains(Point {
x: x as f32,
y: y as f32,
});
if !is_clicked {
state.dnd_offer = DndOfferState::OutsideWidget(mime_types, DndAction::None);
return event::Status::Captured;
}
let mut accepted = false;
for m in &mime_types {
if SUPPORTED_TEXT_MIME_TYPES.contains(&m.as_str()) {
let clone = m.clone();
accepted = true;
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::Accept(Some(
clone.clone(),
))
})));
}
}
if accepted {
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::SetActions {
preferred: DndAction::Move,
accepted: DndAction::Move.union(DndAction::Copy),
}
})));
let target = x as f32 - text_layout.bounds().x;
state.dnd_offer = DndOfferState::HandlingOffer(mime_types.clone(), DndAction::None);
state.dnd_offer =
DndOfferState::HandlingOffer(mime_types.clone(), DndAction::empty());
// existing logic for setting the selection
let position = if target > 0.0 {
update_cache(state, value);
@ -1846,57 +1855,11 @@ where
}
}
#[cfg(feature = "wayland")]
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer(
wayland::DndOfferEvent::Motion { x, y },
))) => {
let Some(on_dnd_command_produced) = on_dnd_command_produced else {
return event::Status::Ignored;
};
Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y }))
if rectangle == Some(dnd_id) =>
{
let state = state();
let is_clicked = text_layout.bounds().contains(Point {
x: x as f32,
y: y as f32,
});
if !is_clicked {
if let DndOfferState::HandlingOffer(mime_types, action) = state.dnd_offer.clone() {
state.dnd_offer = DndOfferState::OutsideWidget(mime_types, action);
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::SetActions {
preferred: DndAction::None,
accepted: DndAction::None,
}
})));
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::Accept(None)
})));
}
return event::Status::Captured;
} else if let DndOfferState::OutsideWidget(mime_types, action) = state.dnd_offer.clone()
{
let mut accepted = false;
for m in &mime_types {
if SUPPORTED_TEXT_MIME_TYPES.contains(&m.as_str()) {
accepted = true;
let clone = m.clone();
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::Accept(Some(
clone.clone(),
))
})));
}
}
if accepted {
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::SetActions {
preferred: DndAction::Move,
accepted: DndAction::Move.union(DndAction::Copy),
}
})));
state.dnd_offer = DndOfferState::HandlingOffer(mime_types.clone(), action);
}
};
let target = x as f32 - text_layout.bounds().x;
// existing logic for setting the selection
let position = if target > 0.0 {
@ -1910,13 +1873,7 @@ where
return event::Status::Captured;
}
#[cfg(feature = "wayland")]
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer(
wayland::DndOfferEvent::DropPerformed,
))) => {
let Some(on_dnd_command_produced) = on_dnd_command_produced else {
return event::Status::Ignored;
};
Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if rectangle == Some(dnd_id) => {
let state = state();
if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() {
let Some(mime_type) = SUPPORTED_TEXT_MIME_TYPES
@ -1927,21 +1884,15 @@ where
return event::Status::Captured;
};
state.dnd_offer = DndOfferState::Dropped;
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::RequestDndData(
(*mime_type).to_string(),
)
})));
} else if let DndOfferState::OutsideWidget(..) = &state.dnd_offer {
state.dnd_offer = DndOfferState::None;
return event::Status::Captured;
}
return event::Status::Ignored;
}
#[cfg(feature = "wayland")]
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer(
wayland::DndOfferEvent::Leave,
))) => {
Event::Dnd(DndEvent::Offer(
rectangle,
OfferEvent::Leave | OfferEvent::LeaveDestination,
)) if rectangle == Some(dnd_id) => {
let state = state();
// ASHLEY TODO we should be able to reset but for now we don't if we are handling a
// drop
@ -1954,13 +1905,9 @@ where
return event::Status::Captured;
}
#[cfg(feature = "wayland")]
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer(
wayland::DndOfferEvent::DndData { mime_type, data },
))) => {
let Some(on_dnd_command_produced) = on_dnd_command_produced else {
return event::Status::Ignored;
};
Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type }))
if rectangle == Some(dnd_id) =>
{
let state = state();
if let DndOfferState::Dropped = state.dnd_offer.clone() {
state.dnd_offer = DndOfferState::None;
@ -1982,9 +1929,6 @@ where
shell.publish(message);
}
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::DndFinished
})));
let value = if is_secure {
unsecured_value.secure()
} else {
@ -1995,26 +1939,6 @@ where
}
return event::Status::Ignored;
}
#[cfg(feature = "wayland")]
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer(
wayland::DndOfferEvent::SourceActions(actions),
))) => {
let Some(on_dnd_command_produced) = on_dnd_command_produced else {
return event::Status::Ignored;
};
let state = state();
if let DndOfferState::HandlingOffer(..) = state.dnd_offer.clone() {
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::SetActions {
preferred: actions.intersection(DndAction::Move),
accepted: actions,
}
})));
return event::Status::Captured;
}
return event::Status::Ignored;
}
_ => {}
}
@ -2032,6 +1956,7 @@ pub fn draw<'a, Message>(
renderer: &mut crate::Renderer,
theme: &crate::Theme,
layout: Layout<'_>,
text_layout: Layout<'_>,
cursor_position: mouse::Cursor,
tree: &Tree,
value: &Value,
@ -2083,7 +2008,7 @@ pub fn draw<'a, Message>(
let mut children_layout = layout.children();
let bounds = layout.bounds();
let text_bounds = children_layout.next().unwrap().bounds();
let text_bounds = text_layout.bounds();
let is_mouse_over = cursor_position.is_over(bounds);
@ -2172,7 +2097,7 @@ pub fn draw<'a, Message>(
if let (Some(label_layout), Some(label)) = (label_layout, label) {
renderer.fill_text(
Text {
content: label,
content: label.to_string(),
size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)),
font: font.unwrap_or_else(|| renderer.default_font()),
bounds: label_layout.bounds().size(),
@ -2180,7 +2105,7 @@ pub fn draw<'a, Message>(
vertical_alignment: alignment::Vertical::Top,
line_height,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
},
label_layout.bounds().position(),
appearance.label_color,
@ -2214,14 +2139,24 @@ pub fn draw<'a, Message>(
let size = size.unwrap_or_else(|| renderer.default_size().0);
let radius_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0.into();
let (cursor, offset) = if let Some(focus) = &state.is_focused {
#[cfg(feature = "wayland")]
let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None);
#[cfg(not(feature = "wayland"))]
let handling_dnd_offer = false;
let (cursor, offset) = if let Some(focus) = &state.is_focused.or_else(|| {
handling_dnd_offer.then(|| Focus {
updated_at: Instant::now(),
now: Instant::now(),
})
}) {
match state.cursor.state(value) {
cursor::State::Index(position) => {
let (text_value_width, offset) =
measure_cursor_and_scroll_offset(&state.value, text_bounds, position);
measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, position);
let is_cursor_visible =
((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS) % 2
let is_cursor_visible = handling_dnd_offer
|| ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS)
% 2
== 0;
if is_cursor_visible {
@ -2263,10 +2198,10 @@ pub fn draw<'a, Message>(
let value_paragraph = &state.value;
let (left_position, left_offset) =
measure_cursor_and_scroll_offset(value_paragraph, text_bounds, left);
measure_cursor_and_scroll_offset(value_paragraph.raw(), text_bounds, left);
let (right_position, right_offset) =
measure_cursor_and_scroll_offset(value_paragraph, text_bounds, right);
measure_cursor_and_scroll_offset(value_paragraph.raw(), text_bounds, right);
let width = right_position - left_position;
@ -2331,7 +2266,11 @@ pub fn draw<'a, Message>(
renderer.fill_text(
Text {
content: if text.is_empty() { placeholder } else { &text },
content: if text.is_empty() {
placeholder.to_string()
} else {
text.clone()
},
font,
bounds: bounds.size(),
size: iced::Pixels(size),
@ -2339,7 +2278,7 @@ pub fn draw<'a, Message>(
vertical_alignment: alignment::Vertical::Center,
line_height: text::LineHeight::default(),
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
},
bounds.position(),
color,
@ -2374,7 +2313,7 @@ pub fn draw<'a, Message>(
if let (Some(helper_text_layout), Some(helper_text)) = (helper_text_layout, helper_text) {
renderer.fill_text(
Text {
content: helper_text,
content: helper_text.to_string(), // TODO remove to_string?
size: iced::Pixels(helper_text_size),
font,
bounds: helper_text_layout.bounds().size(),
@ -2382,7 +2321,7 @@ pub fn draw<'a, Message>(
vertical_alignment: alignment::Vertical::Top,
line_height: helper_line_height,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
},
helper_text_layout.bounds().position(),
text_color,
@ -2414,11 +2353,23 @@ pub fn mouse_interaction(
pub struct TextInputString(pub String);
#[cfg(feature = "wayland")]
impl DataFromMimeType for TextInputString {
fn from_mime_type(&self, mime_type: &str) -> Option<Vec<u8>> {
SUPPORTED_TEXT_MIME_TYPES
.contains(&mime_type)
.then(|| self.0.as_bytes().to_vec())
impl AsMimeTypes for TextInputString {
fn available(&self) -> Cow<'static, [String]> {
Cow::Owned(
SUPPORTED_TEXT_MIME_TYPES
.iter()
.cloned()
.map(String::from)
.collect::<Vec<_>>(),
)
}
fn as_bytes(&self, mime_type: &str) -> Option<Cow<'static, [u8]>> {
if SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type) {
Some(Cow::Owned(self.0.clone().as_bytes().to_vec()))
} else {
None
}
}
}
@ -2434,7 +2385,6 @@ pub(crate) enum DraggingState {
pub(crate) enum DndOfferState {
#[default]
None,
OutsideWidget(Vec<String>, DndAction),
HandlingOffer(Vec<String>, DndAction),
Dropped,
}
@ -2446,17 +2396,16 @@ pub(crate) struct DndOfferState;
#[derive(Debug, Default, Clone)]
#[must_use]
pub struct State {
pub value: crate::Paragraph,
pub placeholder: crate::Paragraph,
pub label: crate::Paragraph,
pub helper_text: crate::Paragraph,
pub value: crate::Plain,
pub placeholder: crate::Plain,
pub label: crate::Plain,
pub helper_text: crate::Plain,
pub dirty: bool,
pub is_secure: bool,
pub is_read_only: bool,
select_on_focus: bool,
is_focused: Option<Focus>,
dragging_state: Option<DraggingState>,
#[cfg(feature = "wayland")]
dnd_offer: DndOfferState,
is_pasting: Option<Value>,
last_click: Option<mouse::Click>,
@ -2522,15 +2471,14 @@ impl State {
pub fn focused(is_secure: bool, is_read_only: bool) -> Self {
Self {
is_secure,
value: crate::Paragraph::new(),
placeholder: crate::Paragraph::new(),
label: crate::Paragraph::new(),
helper_text: crate::Paragraph::new(),
value: crate::Plain::default(),
placeholder: crate::Plain::default(),
label: crate::Plain::default(),
helper_text: crate::Plain::default(),
is_read_only,
is_focused: None,
select_on_focus: false,
dragging_state: None,
#[cfg(feature = "wayland")]
dnd_offer: DndOfferState::default(),
is_pasting: None,
last_click: None,
@ -2654,6 +2602,7 @@ fn find_cursor_position(
let char_offset = state
.value
.raw()
.hit_test(Point::new(x + offset, text_bounds.height / 2.0))
.map(text::Hit::cursor)?;
@ -2677,7 +2626,7 @@ fn replace_paragraph(
let mut children_layout = layout.children();
let text_bounds = children_layout.next().unwrap().bounds();
state.value = crate::Paragraph::with_text(Text {
state.value = crate::Plain::new(Text {
font,
line_height,
content: &value.to_string(),
@ -2686,7 +2635,7 @@ fn replace_paragraph(
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
});
}
@ -2714,7 +2663,7 @@ fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 {
};
let (_, offset) =
measure_cursor_and_scroll_offset(&state.value, text_bounds, focus_position);
measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, focus_position);
offset
} else {

View file

@ -8,7 +8,7 @@ use std::rc::Rc;
use crate::widget::container;
use crate::widget::Column;
use crate::Command;
use iced::{Padding, Task};
use iced_core::Element;
use slotmap::new_key_type;
use slotmap::SlotMap;
@ -47,15 +47,15 @@ pub fn toaster<'a, Message: Clone + 'static>(
button::icon(icon::from_name("window-close-symbolic"))
.on_press((toasts.on_close)(id)),
)
.align_items(iced::Alignment::Center)
.align_y(iced::Alignment::Center)
.spacing(space_xxs),
)
.align_items(iced::Alignment::Center)
.align_y(iced::Alignment::Center)
.spacing(space_s);
container(row)
.padding([space_xxs, space_s, space_xxs, space_m])
.style(crate::style::Container::Tooltip)
.class(crate::style::Container::Tooltip)
};
let col = toasts
@ -175,7 +175,7 @@ impl<Message: Clone + Send + 'static> Toasts<Message> {
}
/// Add a new [`Toast`]
pub fn push(&mut self, toast: Toast<Message>) -> Command<Message> {
pub fn push(&mut self, toast: Toast<Message>) -> Task<Message> {
while self.toasts.len() >= self.limit {
self.toasts.remove(
self.queue
@ -200,7 +200,7 @@ impl<Message: Clone + Send + 'static> Toasts<Message> {
}
#[cfg(not(feature = "tokio"))]
{
Command::none()
Task::none()
}
}

View file

@ -14,7 +14,6 @@ use iced_core::widget::Operation;
use iced_core::Element;
use iced_core::Overlay;
use iced_core::{Clipboard, Layout, Length, Point, Rectangle, Shell, Vector, Widget};
use iced_renderer::core::widget::OperationOutputWrapper;
pub struct Toaster<'a, Message, Theme, Renderer> {
toasts: Element<'a, Message, Theme, Renderer>,
@ -90,7 +89,7 @@ where
state: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
operation: &mut dyn Operation<()>,
) {
self.content
.as_widget()
@ -142,22 +141,23 @@ where
state: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
//TODO: this hides the overlay of the content during the toast
if self.is_empty {
self.content
.as_widget_mut()
.overlay(&mut state.children[0], layout, renderer)
self.content.as_widget_mut().overlay(
&mut state.children[0],
layout,
renderer,
translation,
)
} else {
let bounds = layout.bounds();
Some(overlay::Element::new(
bounds.position(),
Box::new(ToasterOverlay::new(
&mut state.children[1],
&mut self.toasts,
)),
))
Some(overlay::Element::new(Box::new(ToasterOverlay::new(
&mut state.children[1],
&mut self.toasts,
))))
}
}
@ -166,7 +166,7 @@ where
state: &Tree,
layout: Layout<'_>,
renderer: &Renderer,
dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) {
self.content.as_widget().drag_destinations(
&state.children[0],
@ -196,13 +196,7 @@ impl<'a, 'b, Message, Theme, Renderer> Overlay<Message, Theme, Renderer>
where
Renderer: renderer::Renderer,
{
fn layout(
&mut self,
renderer: &Renderer,
bounds: Size,
position: Point,
_translation: Vector,
) -> Node {
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> Node {
let limits = Limits::new(Size::ZERO, bounds);
let mut node = self
@ -276,7 +270,7 @@ where
) -> Option<overlay::Element<'c, Message, Theme, Renderer>> {
self.element
.as_widget_mut()
.overlay(self.state, layout, renderer)
.overlay(self.state, layout, renderer, Default::default())
}
}

View file

@ -4,15 +4,15 @@
use iced::{widget, Length};
use iced_core::text;
pub fn toggler<'a, Message, Theme: iced_widget::toggler::StyleSheet, Renderer>(
label: impl Into<Option<String>>,
pub fn toggler<'a, Message, Theme: iced_widget::toggler::Catalog, Renderer>(
is_checked: bool,
f: impl Fn(bool) -> Message + 'a,
) -> widget::Toggler<'a, Message, Theme, Renderer>
where
Renderer: iced_core::Renderer + text::Renderer,
{
widget::Toggler::new(label, is_checked, f)
widget::Toggler::new(is_checked)
.on_toggle(f)
.size(24)
.width(Length::Shrink)
}

View file

@ -41,9 +41,9 @@ impl<'a, Message: 'static + Clone> Warning<'a, Message> {
widget::row::with_capacity(2)
.push(label)
.push(close_button)
.align_items(Alignment::Center)
.align_y(Alignment::Center)
.apply(widget::container)
.style(theme::Container::custom(warning_container))
.class(theme::Container::custom(warning_container))
.padding(10)
.align_y(alignment::Vertical::Center)
.width(Length::Fill)
@ -57,9 +57,9 @@ impl<'a, Message: 'static + Clone> From<Warning<'a, Message>> for Element<'a, Me
}
#[must_use]
pub fn warning_container(theme: &Theme) -> widget::container::Appearance {
pub fn warning_container(theme: &Theme) -> widget::container::Style {
let cosmic = theme.cosmic();
widget::container::Appearance {
widget::container::Style {
icon_color: Some(theme.cosmic().warning.on.into()),
text_color: Some(theme.cosmic().warning.on.into()),
background: Some(Background::Color(theme.cosmic().warning_color().into())),