chore: update iced

This commit is contained in:
Ashley Wulber 2026-03-20 16:17:24 -04:00
parent 5c1887fb07
commit 6c401d7030
7 changed files with 1502 additions and 915 deletions

View file

@ -24,7 +24,7 @@ pub fn key_binds() -> HashMap<KeyBind, Action> {
//TODO: key bindings
bind!([], Key::Character("f".into()), Fullscreen);
bind!([Alt], Key::Named(Named::Enter), Fullscreen);
bind!([], Key::Named(Named::Space), PlayPause);
bind!([], Key::Character(" ".into()), PlayPause);
bind!([], Key::Named(Named::ArrowLeft), SeekBackward);
bind!([], Key::Named(Named::ArrowRight), SeekForward);

View file

@ -2,17 +2,19 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{
app::{command, message, Command, Core, Settings},
action::{self, app},
app::{Core, Settings, Task},
command::set_theme,
cosmic_config::{self, CosmicConfigEntry},
cosmic_theme, executor, font,
iced::{
event::{self, Event},
keyboard::{Event as KeyEvent, Key, Modifiers},
mouse::{Event as MouseEvent, ScrollDelta},
subscription::Subscription,
window, Alignment, Background, Border, Color, ContentFit, Length, Limits,
window::{self, set_mode},
Alignment, Background, Border, Color, ContentFit, Length, Limits, Subscription,
},
iced_style, theme,
theme,
widget::{self, menu::action::MenuAction, nav_bar, segmented_button, Slider},
Application, ApplicationExt, Element,
};
@ -26,7 +28,12 @@ use std::{
ffi::{CStr, CString},
fs,
path::{Path, PathBuf},
process, thread,
process,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread,
time::{Duration, Instant},
};
use tokio::sync::mpsc;
@ -34,6 +41,7 @@ use tokio::sync::mpsc;
use crate::{
config::{Config, ConfigState, CONFIG_VERSION, RepeatState},
key_bind::{key_binds, KeyBind},
mpris::subscription,
project::ProjectNode,
};
@ -254,7 +262,6 @@ impl AsRef<str> for TextCode {
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {
None,
Config(Config),
ConfigState(ConfigState),
DropdownToggle(DropdownKind),
@ -351,15 +358,15 @@ impl App {
was_open
}
fn load(&mut self) -> Command<Message> {
fn load(&mut self) -> Task<Message> {
if self.close() {
// Allow a redraw before trying to load again, to prevent deadlock
return Command::perform(async { message::app(Message::Reload) }, |x| x);
return Task::perform(async { action::app(Message::Reload) }, |x| x);
}
let url = match &self.flags.url_opt {
Some(some) => some.clone(),
None => return Command::none(),
None => return Task::none(),
};
log::info!("Loading {}", url);
@ -607,8 +614,8 @@ impl App {
self.update_mpris_state();
}
fn update_config(&mut self) -> Command<Message> {
cosmic::app::command::set_theme(self.flags.config.app_theme.theme())
fn update_config(&mut self) -> Task<Message> {
set_theme(self.flags.config.app_theme.theme())
}
fn update_flags(&mut self) {
@ -800,10 +807,10 @@ impl App {
self.nav_model.activate(active_id);
}
fn update_title(&mut self) -> Command<Message> {
fn update_title(&mut self) -> Task<Message> {
//TODO: filename?
let title = "COSMIC Media Player";
self.set_window_title(title.to_string())
self.set_window_title(title.to_string(), self.core.main_window_id().unwrap())
}
/// Allow screen to dim or turn off if there is no input from the user.
@ -850,7 +857,7 @@ impl Application for App {
}
/// Creates the application, and optionally emits command on initialize.
fn init(mut core: Core, flags: Self::Flags) -> (Self, Command<Self::Message>) {
fn init(mut core: Core, flags: Self::Flags) -> (Self, Task<Self::Message>) {
core.window.content_container = false;
#[cfg(feature = "xdg-portal")]
@ -905,10 +912,13 @@ impl Application for App {
.as_ref()
.and_then(|url| url.to_file_path().ok());
let command = match (app.flags.urls.take(), maybe_path) {
(Some(urls), _) => command::message::app(Message::MultipleLoad(urls)),
(None, Some(path)) if path.is_dir() => command::message::app(Message::FolderLoad(path)),
_ => app.load(), //If there is no url args, we execute load for nothing?
//If only one file is loaded, nothing is added to the navbar.
(Some(urls), _) => {
cosmic::task::message(cosmic::action::app(Message::MultipleLoad(urls)))
}
(None, Some(path)) if path.is_dir() => {
cosmic::task::message(cosmic::action::app(Message::FolderLoad(path)))
}
_ => app.load(),
};
(app, command)
}
@ -917,15 +927,15 @@ impl Application for App {
Some(&self.nav_model)
}
fn on_escape(&mut self) -> Command<Self::Message> {
fn on_escape(&mut self) -> Task<Self::Message> {
if self.fullscreen {
return self.update(Message::Fullscreen);
} else {
Command::none()
Task::none()
}
}
fn on_nav_select(&mut self, id: nav_bar::Id) -> Command<Message> {
fn on_nav_select(&mut self, id: nav_bar::Id) -> Task<Message> {
// Toggle open state and get clone of node data
let node_opt = match self.nav_model.data_mut::<ProjectNode>(id) {
Some(node) => {
@ -966,13 +976,13 @@ impl Application for App {
// folder in condensed mode.
self.core_mut().nav_bar_set_toggled(true);
Command::none()
Task::none()
}
ProjectNode::File { path, .. } => match url::Url::from_file_path(&path) {
Ok(url) => self.update(Message::FileLoad(url)),
Err(()) => {
log::warn!("failed to convert {:?} to url", path);
Command::none()
Task::none()
}
},
}
@ -984,21 +994,21 @@ impl Application for App {
}
}
fn style(&self) -> Option<theme::Application> {
fn style(&self) -> Option<cosmic::iced_core::theme::Style> {
// This ensures we have a solid background color even when using no content container
Some(theme::Application::Custom(Box::new(|theme| {
iced_style::application::Appearance {
background_color: theme.cosmic().bg_color().into(),
icon_color: theme.cosmic().on_bg_color().into(),
text_color: theme.cosmic().on_bg_color().into(),
}
})))
let theme = self.core.system_theme();
Some(cosmic::iced_core::theme::Style {
background_color: theme.cosmic().bg_color().into(),
icon_color: theme.cosmic().on_bg_color().into(),
text_color: theme.cosmic().on_bg_color().into(),
})
}
/// 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::None => {}
Message::Config(config) => {
if config != self.flags.config {
log::info!("update config");
@ -1031,17 +1041,17 @@ impl Application for App {
Message::FileOpen => {
//TODO: embed cosmic-files dialog (after libcosmic rebase works)
#[cfg(feature = "xdg-portal")]
return Command::perform(
return Task::perform(
async move {
let dialog = cosmic::dialog::file_chooser::open::Dialog::new()
.title(fl!("open-media"));
match dialog.open_file().await {
Ok(response) => {
message::app(Message::FileLoad(response.url().to_owned()))
action::app(Message::FileLoad(response.url().to_owned()))
}
Err(err) => {
log::warn!("failed to open file: {}", err);
message::none()
action::none()
}
}
},
@ -1095,7 +1105,7 @@ impl Application for App {
Message::FolderOpen => {
//TODO: embed cosmic-files dialog (after libcosmic rebase works)
#[cfg(feature = "xdg-portal")]
return Command::perform(
return Task::perform(
async move {
let dialog = cosmic::dialog::file_chooser::open::Dialog::new()
.title(fl!("open-media-folder"));
@ -1103,16 +1113,16 @@ impl Application for App {
Ok(response) => {
let url = response.url();
match url.to_file_path() {
Ok(path) => message::app(Message::FolderLoad(path)),
Ok(path) => action::app(Message::FolderLoad(path)),
Err(()) => {
log::warn!("unsupported folder URL {:?}", url);
message::none()
action::none()
}
}
}
Err(err) => {
log::warn!("failed to open folder: {}", err);
message::none()
action::none()
}
}
},
@ -1156,17 +1166,18 @@ impl Application for App {
//TODO: cleanest way to close dropdowns
self.dropdown_opt = None;
self.fullscreen = !self.fullscreen;
self.core.window.show_headerbar = !self.fullscreen;
self.controls = !self.fullscreen;
return window::change_mode(
window::Id::MAIN,
if self.fullscreen {
window::Mode::Fullscreen
} else {
window::Mode::Windowed
},
);
if let Some(window_id) = self.core.main_window_id() {
self.fullscreen = !self.fullscreen;
self.core.window.show_headerbar = !self.fullscreen;
return set_mode(
window_id,
if self.fullscreen {
window::Mode::Fullscreen
} else {
window::Mode::Windowed
},
);
}
}
Message::Key(modifiers, key) => {
for (key_bind, action) in self.key_binds.iter() {
@ -1385,7 +1396,7 @@ impl Application for App {
if let Some(video) = &mut self.video_opt {
video.set_paused(true);
}
return Command::perform(
return Task::perform(
async move {
tokio::task::spawn_blocking(move || {
match gst_pbutils::MissingPluginMessage::parse(&element) {
@ -1419,7 +1430,7 @@ impl Application for App {
"gstreamer registry update: {:?}",
gst::Registry::update()
);
return message::app(Message::Reload);
return action::app(Message::Reload);
},
_ => {
log::warn!("failed to install plugins: {status}");
@ -1433,7 +1444,7 @@ impl Application for App {
log::warn!("failed to parse missing plugin message: {err}");
}
}
message::none()
action::none()
})
.await
.unwrap()
@ -1467,7 +1478,7 @@ impl Application for App {
process::exit(0);
}
}
Command::none()
Task::none()
}
fn header_start(&self) -> Vec<Element<'_, Self::Message>> {
@ -1501,25 +1512,25 @@ impl Application for App {
let Some(video) = &self.video_opt else {
//TODO: use space variables
let column = widget::column::with_capacity(4)
.align_items(Alignment::Center)
.align_x(Alignment::Center)
.spacing(24)
.width(Length::Fill)
.height(Length::Fill)
.push(widget::vertical_space(Length::Fill))
.push(widget::space::vertical())
.push(
widget::column::with_capacity(2)
.align_items(Alignment::Center)
.align_x(Alignment::Center)
.spacing(8)
.push(widget::icon::from_name("folder-symbolic").size(64))
.push(widget::text::body(fl!("no-video-or-audio-file-open"))),
)
.push(widget::button::suggested(fl!("open-file")).on_press(Message::FileOpen))
.push(widget::vertical_space(Length::Fill));
.push(widget::space::vertical());
return widget::container(column)
.width(Length::Fill)
.height(Length::Fill)
.style(theme::Container::WindowBackground)
.class(theme::Container::WindowBackground)
.into();
};
@ -1534,6 +1545,7 @@ impl Application for App {
.on_new_frame(Message::NewFrame)
.width(Length::Fill)
.height(Length::Fill)
.id(cosmic::widget::Id::new("video-player".to_string()))
.into();
let mut background_color = Color::BLACK;
@ -1543,7 +1555,7 @@ impl Application for App {
text_color_opt = Some(Color::from(theme.cosmic().on_bg_component_color()));
let mut col = widget::column();
col = col.push(widget::vertical_space(Length::Fill));
col = col.push(widget::space::vertical());
if let Some(album_art) = &self.album_art_opt {
col = col.push(
widget::image(widget::image::Handle::from_path(album_art.path()))
@ -1553,7 +1565,7 @@ impl Application for App {
} else {
col = col.push(widget::icon::from_name("audio-x-generic-symbolic").size(256));
}
col = col.push(widget::vertical_space(space_s));
col = col.push(widget::space::vertical().height(space_s));
if self.mpris_meta.title.is_empty() {
col = col.push(widget::text::title4(fl!("untitled")));
} else {
@ -1566,7 +1578,7 @@ impl Application for App {
col = col.push(widget::text::body(artist));
}
}
col = col.push(widget::vertical_space(space_s));
col = col.push(widget::space::vertical().height(space_s));
if !self.mpris_meta.album.is_empty() {
col = col.push(widget::text::body(fl!(
"album",
@ -1576,7 +1588,7 @@ impl Application for App {
if let Some(year) = &self.mpris_meta.album_year_opt {
col = col.push(widget::text::body(format!("{}", year)));
}
col = col.push(widget::vertical_space(Length::Fill));
col = col.push(widget::space::vertical());
// Space to keep from going under control overlay
let mut control_height = space_xxs + 32 + space_xxs;
@ -1586,11 +1598,11 @@ impl Application for App {
// This is a hack to have the video player running but not visible (since the controls will cover it as an overlay)
video_player = widget::row::with_children(vec![
widget::horizontal_space(Length::Fill).into(),
widget::space::horizontal().into(),
widget::container(col.push(widget::container(video_player).height(control_height)))
.width(320)
.into(),
widget::horizontal_space(Length::Fill).into(),
widget::space::horizontal().into(),
])
.into();
}
@ -1629,7 +1641,7 @@ impl Application for App {
.step(0.01)
.into(),
])
.align_items(Alignment::Center)
.align_y(Alignment::Center)
.into(),
);
}
@ -1668,14 +1680,14 @@ impl Application for App {
popup_items.push(
widget::row::with_children(vec![
widget::horizontal_space(Length::Fill).into(),
widget::space::horizontal().into(),
widget::container(column)
.padding(1)
//TODO: move style to libcosmic
.style(theme::Container::custom(|theme| {
.class(theme::Container::custom(|theme| {
let cosmic = theme.cosmic();
let component = &cosmic.background.component;
widget::container::Appearance {
widget::container::Style {
icon_color: Some(component.on.into()),
text_color: Some(component.on.into()),
background: Some(Background::Color(component.base.into())),
@ -1694,8 +1706,8 @@ impl Application for App {
);
}
if self.controls {
let mut row = widget::row::with_capacity(8)
.align_items(Alignment::Center)
let mut row = widget::row::with_capacity(7)
.align_y(Alignment::Center)
.spacing(space_xxs)
.push(
widget::button::icon(
@ -1728,7 +1740,7 @@ impl Application for App {
widget::tooltip::Position::Top,
));
if self.core.is_condensed() {
row = row.push(widget::horizontal_space(Length::Fill));
row = row.push(widget::space::horizontal());
} else {
row = row
.push(widget::text(format_time(self.position)).font(font::mono()))
@ -1777,7 +1789,7 @@ impl Application for App {
popup_items.push(
widget::container(row)
.padding([space_xxs, space_xs])
.style(theme::Container::WindowBackground)
.class(theme::Container::WindowBackground)
.into(),
);
@ -1785,7 +1797,7 @@ impl Application for App {
popup_items.push(
widget::container(
widget::row::with_capacity(3)
.align_items(Alignment::Center)
.align_y(Alignment::Center)
.spacing(space_xxs)
.push(widget::text(format_time(self.position)).font(font::mono()))
.push(
@ -1799,7 +1811,7 @@ impl Application for App {
),
)
.padding([space_xxs, space_xs])
.style(theme::Container::WindowBackground)
.class(theme::Container::WindowBackground)
.into(),
);
}
@ -1811,9 +1823,9 @@ impl Application for App {
widget::container(popover)
.width(Length::Fill)
.height(Length::Fill)
.style(theme::Container::Custom(Box::new(move |_theme| {
.class(theme::Container::Custom(Box::new(move |_theme| {
let mut appearance =
widget::container::Appearance::default().with_background(background_color);
widget::container::Style::default().background(background_color);
if let Some(text_color) = text_color_opt {
appearance.text_color = Some(text_color);
}
@ -1828,7 +1840,7 @@ impl Application for App {
struct ThemeSubscription;
let mut subscriptions = vec![
event::listen_with(|event, _status| match event {
event::listen_with(|event, _status, _window_id| match event {
Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => {
Some(Message::Key(modifiers, key))
}

View file

@ -1,8 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{
Element, theme,
widget::menu::{self, ItemHeight, ItemWidth, MenuBar, key_bind::KeyBind},
theme,
widget::{
menu::{self, key_bind::KeyBind, ItemHeight, ItemWidth, MenuBar},
RcElementWrapper,
},
Element,
};
use std::{collections::HashMap, path::PathBuf};
@ -39,6 +43,7 @@ pub fn menu_bar<'a>(
for (i, path) in config_state.recent_files.iter().enumerate() {
recent_files.push(menu::Item::Button(
format_url(path),
None,
Action::FileOpenRecent(i),
));
}
@ -46,6 +51,7 @@ pub fn menu_bar<'a>(
recent_files.push(menu::Item::Divider);
recent_files.push(menu::Item::Button(
fl!("clear-recent"),
None,
Action::FileClearRecents,
));
}
@ -59,6 +65,7 @@ pub fn menu_bar<'a>(
for (i, path) in config_state.recent_projects.iter().enumerate() {
recent_projects.push(menu::Item::Button(
format_path(path),
None,
Action::FolderOpenRecent(i),
));
}
@ -66,6 +73,7 @@ pub fn menu_bar<'a>(
recent_projects.push(menu::Item::Divider);
recent_projects.push(menu::Item::Button(
fl!("clear-recent"),
None,
Action::FolderClearRecents,
));
}
@ -74,24 +82,25 @@ pub fn menu_bar<'a>(
for (folder_i, (name, _path)) in projects.iter().enumerate() {
close_projects.push(menu::Item::Button(
name.clone(),
None,
Action::FolderClose(folder_i),
));
}
MenuBar::new(vec![menu::Tree::with_children(
menu::root(fl!("file")),
RcElementWrapper::new(Element::from(menu::root(fl!("file")))),
menu::items(
key_binds,
vec![
menu::Item::Button(fl!("open-media"), Action::FileOpen),
menu::Item::Button(fl!("open-media"), None, Action::FileOpen),
menu::Item::Folder(fl!("open-recent-media"), recent_files),
menu::Item::Button(fl!("close-file"), Action::FileClose),
menu::Item::Button(fl!("close-file"), None, Action::FileClose),
menu::Item::Divider,
menu::Item::Button(fl!("open-media-folder"), Action::FolderOpen),
menu::Item::Button(fl!("open-media-folder"), None, Action::FolderOpen),
menu::Item::Folder(fl!("open-recent-media-folder"), recent_projects),
menu::Item::Folder(fl!("close-media-folder"), close_projects),
menu::Item::Divider,
menu::Item::Button(fl!("quit"), Action::WindowClose),
menu::Item::Button(fl!("quit"), None, Action::WindowClose),
],
),
)])

View file

@ -1,6 +1,9 @@
use cosmic::iced::{
futures::{self, SinkExt},
subscription::{self, Subscription},
use cosmic::{
iced::{
futures::{self, SinkExt, Stream},
Subscription,
},
iced_futures::stream,
};
use mpris_server::{
LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, Property, RootInterface,
@ -377,15 +380,13 @@ impl PlaylistsInterface for Player {
}
*/
pub fn subscription() -> Subscription<Message> {
struct MprisSubscription;
subscription::channel(
TypeId::of::<MprisSubscription>(),
16,
move |mut msg_tx| async move {
fn watcher_stream() -> impl Stream<Item = Message> {
stream::channel(
5,
move |mut msg_tx: futures::channel::mpsc::Sender<Message>| async move {
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
let meta = MprisMeta::default();
let state = MprisState::default();
let meta: MprisMeta = MprisMeta::default();
let state: MprisState = MprisState::default();
msg_tx
.send(Message::MprisChannel(meta.clone(), state.clone(), event_tx))
.await
@ -455,3 +456,9 @@ pub fn subscription() -> Subscription<Message> {
},
)
}
#[cold]
pub fn subscription() -> Subscription<Message> {
struct MprisSubscription;
Subscription::run_with(TypeId::of::<MprisSubscription>(), |_| watcher_stream())
}

View file

@ -42,11 +42,11 @@ pub fn main(
}
}?;
if let Some((width, height)) = size_opt {
image = image.thumbnail(width, height);
}
// if let Some((width, height)) = size_opt {
// image = image.thumbnail(width, height);
// }
image.save_with_format(output, ImageFormat::Png)?;
// image.save_with_format(output, ImageFormat::Png)?;
Ok(())
}