minimize applet (#217)

* chore: add minimize applet skeleton

* fix(minimize): desktop typo

* wip: minimize applet

* feat: include window images, and overlay their icon

* cleanup

* fix: add minimize applet to workspace

* chore: add host wayland display to desktop file for minimize applet

* chore: Cargo.lock

* cleanup: fix typos

* fix: don't hide minimized apps in app-list
This commit is contained in:
Ashley Wulber 2024-03-04 13:08:52 -05:00 committed by GitHub
parent 06634c3348
commit 090bb9653f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1301 additions and 4 deletions

22
Cargo.lock generated
View file

@ -927,6 +927,28 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "cosmic-applet-minimize"
version = "0.1.1"
dependencies = [
"anyhow",
"i18n-embed 0.13.9",
"i18n-embed-fl 0.6.7",
"image",
"libcosmic",
"memmap2 0.9.4",
"once_cell",
"png",
"rust-embed 6.8.1",
"rust-embed-utils 7.8.1",
"rustix 0.38.31",
"tempfile",
"tokio",
"tracing",
"tracing-log",
"tracing-subscriber",
]
[[package]]
name = "cosmic-applet-network"
version = "0.1.0"

View file

@ -5,6 +5,7 @@ members = [
"cosmic-applet-audio",
"cosmic-applet-battery",
"cosmic-applet-bluetooth",
"cosmic-applet-minimize",
"cosmic-applet-network",
"cosmic-applet-notifications",
"cosmic-applet-power",
@ -18,6 +19,7 @@ members = [
resolver = "2"
[workspace.dependencies]
anyhow = "1.0.79"
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "e65fa5e" }
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = [
"client",

View file

@ -0,0 +1,25 @@
[package]
name = "cosmic-applet-minimize"
version = "0.1.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow.workspace = true
image = "0.24"
libcosmic.workspace = true
memmap2 = "0.9.0"
rustix = { version = "0.38.0", features = ["fs"] }
png = "0.17.5"
tokio = { version = "1.17.0", features = ["sync", "macros"] }
tracing = "0.1.40"
tracing-subscriber.workspace = true
tracing-log.workspace = true
tempfile = "3.5.0"
# Application i18n
i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.6"
rust-embed = "6.6"
rust-embed-utils = "7.5.0"
once_cell = "1"

View file

@ -0,0 +1,15 @@
[Desktop Entry]
Name=Cosmic Applet Minimize Windows
Comment=Applet for Cosmic Panel
Type=Application
Exec=cosmic-applet-minimize
Terminal=false
Categories=Cosmic;Iced;
Keywords=Cosmic;Iced;
# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
Icon=com.system76.CosmicAppletMinimize
StartupNotify=true
NoDisplay=true
X-CosmicApplet=true
X-MinimizeApplet=true
X-HostWaylandDisplay=true

View file

@ -0,0 +1,29 @@
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Size=256">
<rect id="Rectangle 418" x="8" y="40" width="240" height="176" rx="16" fill="url(#paint0_linear_2049_378)"/>
<rect id="Rectangle 419" x="24" y="94" width="96" height="68" rx="8" fill="white"/>
<rect id="Rectangle 439" x="136" y="94" width="96" height="68" rx="8" fill="white"/>
<circle id="Ellipse 25" cx="112" cy="102" r="4" fill="#229FAD"/>
<circle id="Ellipse 28" cx="224" cy="102" r="4" fill="#229FAD"/>
<circle id="Ellipse 26" cx="102" cy="102" r="4" fill="#229FAD"/>
<circle id="Ellipse 29" cx="214" cy="102" r="4" fill="#229FAD"/>
<circle id="Ellipse 27" cx="92" cy="102" r="4" fill="#229FAD"/>
<circle id="Ellipse 30" cx="204" cy="102" r="4" fill="#229FAD"/>
<rect id="Rectangle 440" x="92" y="138" width="32" height="32" rx="8" fill="url(#paint1_linear_2049_378)"/>
<rect id="Rectangle 441" x="204" y="139" width="32" height="32" rx="8" fill="url(#paint2_linear_2049_378)"/>
</g>
<defs>
<linearGradient id="paint0_linear_2049_378" x1="248" y1="40" x2="6.11676" y2="213.373" gradientUnits="userSpaceOnUse">
<stop stop-color="#49BAC8"/>
<stop offset="1" stop-color="#229FAD"/>
</linearGradient>
<linearGradient id="paint1_linear_2049_378" x1="92" y1="170" x2="124.659" y2="138.687" gradientUnits="userSpaceOnUse">
<stop stop-color="#DDC74C"/>
<stop offset="1" stop-color="#F7E062"/>
</linearGradient>
<linearGradient id="paint2_linear_2049_378" x1="204" y1="171" x2="236.659" y2="139.687" gradientUnits="userSpaceOnUse">
<stop stop-color="#C48400"/>
<stop offset="1" stop-color="#FFAD00"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,4 @@
fallback_language = "en"
[fluent]
assets_dir = "i18n"

View file

@ -0,0 +1,47 @@
// SPDX-License-Identifier: MPL-2.0-only
use i18n_embed::{
fluent::{fluent_language_loader, FluentLanguageLoader},
DefaultLocalizer, LanguageLoader, Localizer,
};
use once_cell::sync::Lazy;
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "i18n/"]
struct Localizations;
pub static LANGUAGE_LOADER: Lazy<FluentLanguageLoader> = Lazy::new(|| {
let loader: FluentLanguageLoader = fluent_language_loader!();
loader
.load_fallback_language(&Localizations)
.expect("Error while loading fallback language");
loader
});
#[macro_export]
macro_rules! fl {
($message_id:literal) => {{
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id)
}};
($message_id:literal, $($args:expr),*) => {{
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *)
}};
}
// Get the `Localizer` to be used for localizing this library.
pub fn localizer() -> Box<dyn Localizer> {
Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
}
pub fn localize() {
let localizer = localizer();
let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages();
if let Err(error) = localizer.select(&requested_languages) {
eprintln!("Error while loading language for Minimize {}", error);
}
}

View file

@ -0,0 +1,187 @@
mod localize;
pub(crate) mod wayland_handler;
pub(crate) mod wayland_subscription;
pub(crate) mod window_image;
use crate::localize::localize;
use cosmic::app::Command;
use cosmic::applet::cosmic_panel_config::PanelAnchor;
use cosmic::cctk::cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
use cosmic::cctk::sctk::reexports::calloop;
use cosmic::cctk::toplevel_info::ToplevelInfo;
use cosmic::desktop::DesktopEntryData;
use cosmic::iced::{widget::text, Length, Subscription};
use cosmic::iced_style::application;
use cosmic::iced_widget::{Column, Row};
use cosmic::widget::tooltip;
use cosmic::{Element, Theme};
use wayland_subscription::{
ToplevelRequest, ToplevelUpdate, WaylandImage, WaylandRequest, WaylandUpdate,
};
const VERSION: &str = env!("CARGO_PKG_VERSION");
pub fn main() -> cosmic::iced::Result {
tracing_subscriber::fmt::init();
let _ = tracing_log::LogTracer::init();
// Prepare i18n
localize();
tracing::info!("Starting minimize applet with version {VERSION}");
cosmic::applet::run::<Minimize>(true, ())
}
#[derive(Default)]
struct Minimize {
core: cosmic::app::Core,
apps: Vec<(
ZcosmicToplevelHandleV1,
ToplevelInfo,
DesktopEntryData,
Option<WaylandImage>,
)>,
tx: Option<calloop::channel::Sender<WaylandRequest>>,
}
#[derive(Debug, Clone)]
enum Message {
Wayland(WaylandUpdate),
Activate(ZcosmicToplevelHandleV1),
}
impl cosmic::Application for Minimize {
type Message = Message;
type Executor = cosmic::SingleThreadExecutor;
type Flags = ();
const APP_ID: &'static str = "com.system76.CosmicAppletMinimize";
fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command<Message>) {
(
Self {
core,
..Default::default()
},
Command::none(),
)
}
fn core(&self) -> &cosmic::app::Core {
&self.core
}
fn core_mut(&mut self) -> &mut cosmic::app::Core {
&mut self.core
}
fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
Some(cosmic::applet::style())
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::Wayland(update) => match update {
WaylandUpdate::Init(tx) => {
self.tx = Some(tx);
}
WaylandUpdate::Finished => {
panic!("Wayland Subscription ended...")
}
WaylandUpdate::Toplevel(t) => match t {
ToplevelUpdate::Add(handle, info) | ToplevelUpdate::Update(handle, info) => {
let data = |id| {
cosmic::desktop::load_applications_for_app_ids(
None,
std::iter::once(id),
true,
)
.remove(0)
};
if let Some(pos) = self.apps.iter_mut().position(|a| a.0 == handle) {
if self.apps[pos].1.app_id != info.app_id {
self.apps[pos].2 = data(&info.app_id)
}
self.apps[pos].1 = info;
} else {
let data = data(&info.app_id);
self.apps.push((handle, info, data, None));
}
}
ToplevelUpdate::Remove(handle) => self.apps.retain(|a| a.0 != handle),
},
WaylandUpdate::Image(handle, img) => {
if let Some(pos) = self.apps.iter().position(|a| a.0 == handle) {
self.apps[pos].3 = Some(img);
}
}
},
Message::Activate(handle) => {
if let Some(tx) = self.tx.as_ref() {
let _ = tx.send(WaylandRequest::Toplevel(ToplevelRequest::Activate(handle)));
}
}
};
Command::none()
}
fn subscription(&self) -> Subscription<Message> {
wayland_subscription::wayland_subscription().map(Message::Wayland)
}
fn view(&self) -> Element<Message> {
let (width, _) = self.core.applet.suggested_size();
let theme = self.core.system_theme().cosmic();
let space_xxs = theme.space_xxs();
let icon_buttons = self.apps.iter().map(|(handle, _, data, img)| {
tooltip(
Element::from(crate::window_image::WindowImage::new(
img.clone(),
&data.icon,
width as f32,
Message::Activate(handle.clone()),
space_xxs,
)),
data.name.clone(),
// tooltip::Position::FollowCursor,
// FIXME tooltip fails to appear when created as indicated in design
// maybe it should be a subsurface
match self.core.applet.anchor {
PanelAnchor::Left => tooltip::Position::Right,
PanelAnchor::Right => tooltip::Position::Left,
PanelAnchor::Top => tooltip::Position::Bottom,
PanelAnchor::Bottom => tooltip::Position::Top,
},
)
.snap_within_viewport(false)
.text_shaping(text::Shaping::Advanced)
.into()
});
// TODO optional dividers on ends if detects app list neighbor
// not sure the best way to tell if there is an adjacent app-list
if matches!(
self.core.applet.anchor,
PanelAnchor::Top | PanelAnchor::Bottom
) {
Row::with_children(icon_buttons)
.align_items(cosmic::iced_core::Alignment::Center)
.height(Length::Shrink)
.width(Length::Shrink)
.spacing(space_xxs)
.padding([0, space_xxs])
.into()
} else {
Column::with_children(icon_buttons)
.align_items(cosmic::iced_core::Alignment::Center)
.height(Length::Shrink)
.width(Length::Shrink)
.spacing(space_xxs)
.padding([space_xxs, 0])
.into()
}
}
}

View file

@ -0,0 +1,555 @@
use crate::wayland_subscription::{
ToplevelRequest, ToplevelUpdate, WaylandImage, WaylandRequest, WaylandUpdate,
};
use std::{
os::{
fd::{AsFd, FromRawFd, RawFd},
unix::net::UnixStream,
},
sync::{Arc, Condvar, Mutex, MutexGuard},
};
use cctk::{
sctk::{
self,
reexports::{calloop, calloop_wayland_source::WaylandSource},
seat::{SeatHandler, SeatState},
},
toplevel_info::{ToplevelInfoHandler, ToplevelInfoState},
toplevel_management::{ToplevelManagerHandler, ToplevelManagerState},
wayland_client::{self, protocol::wl_seat::WlSeat, WEnum},
};
use cosmic::{
cctk::{
self,
cosmic_protocols::{
self,
screencopy::v1::client::{
zcosmic_screencopy_manager_v1, zcosmic_screencopy_session_v1,
},
toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
},
screencopy::{
BufferInfo, ScreencopyHandler, ScreencopySessionData, ScreencopySessionDataExt,
ScreencopyState,
},
sctk::shm::{Shm, ShmHandler},
wayland_client::{
protocol::{
wl_buffer,
wl_shm::{self, WlShm},
wl_shm_pool,
},
Dispatch, Proxy,
},
},
iced_futures::futures,
};
use cosmic_protocols::{
toplevel_info::v1::client::zcosmic_toplevel_handle_v1,
toplevel_management::v1::client::zcosmic_toplevel_manager_v1,
};
use futures::channel::mpsc::UnboundedSender;
use sctk::registry::{ProvidesRegistryState, RegistryState};
use wayland_client::{globals::registry_queue_init, Connection, QueueHandle};
#[derive(Default)]
struct SessionInner {
buffer_infos: Option<Vec<BufferInfo>>,
res: Option<Result<(), WEnum<zcosmic_screencopy_session_v1::FailureReason>>>,
}
// TODO: dmabuf? need to handle modifier negotation
#[derive(Default)]
struct Session {
condvar: Condvar,
inner: Mutex<SessionInner>,
}
#[derive(Default)]
struct SessionData {
session: Arc<Session>,
session_data: ScreencopySessionData,
}
impl Session {
pub fn for_session(
session: &zcosmic_screencopy_session_v1::ZcosmicScreencopySessionV1,
) -> Option<&Self> {
Some(&session.data::<SessionData>()?.session)
}
fn update<F: FnOnce(&mut SessionInner)>(&self, f: F) {
f(&mut self.inner.lock().unwrap());
self.condvar.notify_all();
}
fn wait_while<F: FnMut(&SessionInner) -> bool>(&self, mut f: F) -> MutexGuard<SessionInner> {
self.condvar
.wait_while(self.inner.lock().unwrap(), |data| f(data))
.unwrap()
}
}
impl ScreencopySessionDataExt for SessionData {
fn screencopy_session_data(&self) -> &ScreencopySessionData {
&self.session_data
}
}
struct AppData {
exit: bool,
tx: UnboundedSender<WaylandUpdate>,
queue_handle: QueueHandle<Self>,
conn: Connection,
screencopy_state: ScreencopyState,
shm_state: Shm,
registry_state: RegistryState,
toplevel_info_state: ToplevelInfoState,
toplevel_manager_state: ToplevelManagerState,
seat_state: SeatState,
}
struct CaptureData {
qh: QueueHandle<AppData>,
conn: Connection,
wl_shm: WlShm,
screencopy_manager: zcosmic_screencopy_manager_v1::ZcosmicScreencopyManagerV1,
}
impl CaptureData {
pub fn capture_source_shm_fd<Fd: AsFd>(
&self,
overlay_cursor: bool,
source: ZcosmicToplevelHandleV1,
fd: Fd,
len: Option<u32>,
) -> Option<ShmImage<Fd>> {
// XXX error type?
// TODO: way to get cursor metadata?
#[allow(unused_variables)] // TODO
let overlay_cursor = if overlay_cursor { 1 } else { 0 };
let session = Arc::new(Session::default());
let screencopy_session = self.screencopy_manager.capture_toplevel(
&source,
zcosmic_screencopy_manager_v1::CursorMode::Hidden, // XXX take into account adventised capabilities
&self.qh,
SessionData {
session: session.clone(),
session_data: Default::default(),
},
);
self.conn.flush().unwrap();
let buffer_infos = session
.wait_while(|data| data.buffer_infos.is_none())
.buffer_infos
.take()
.unwrap();
// XXX
let Some(buffer_info) = buffer_infos.iter().find(|x| {
x.type_ == WEnum::Value(zcosmic_screencopy_session_v1::BufferType::WlShm)
&& x.format == wl_shm::Format::Abgr8888.into()
}) else {
tracing::error!("No suitable buffer format found");
tracing::warn!("Available formats: {:#?}", buffer_infos);
return None;
};
let buf_len = buffer_info.stride * buffer_info.height;
if let Some(len) = len {
if len != buf_len {
return None;
}
} else if let Err(_err) = rustix::fs::ftruncate(&fd, buf_len as _) {
};
let pool = self
.wl_shm
.create_pool(fd.as_fd(), buf_len as i32, &self.qh, ());
let buffer = pool.create_buffer(
0,
buffer_info.width as i32,
buffer_info.height as i32,
buffer_info.stride as i32,
wl_shm::Format::Abgr8888,
&self.qh,
(),
);
screencopy_session.attach_buffer(&buffer, None, 0); // XXX age?
screencopy_session.commit(zcosmic_screencopy_session_v1::Options::empty());
self.conn.flush().unwrap();
// TODO: wait for server to release buffer?
let res = session
.wait_while(|data| data.res.is_none())
.res
.take()
.unwrap();
pool.destroy();
buffer.destroy();
//std::thread::sleep(std::time::Duration::from_millis(16));
if res.is_ok() {
Some(ShmImage {
fd,
width: buffer_info.width,
height: buffer_info.height,
})
} else {
None
}
}
}
pub struct ShmImage<T: AsFd> {
fd: T,
pub width: u32,
pub height: u32,
}
impl<T: AsFd> ShmImage<T> {
pub fn image(&self) -> anyhow::Result<image::RgbaImage> {
let mmap = unsafe { memmap2::Mmap::map(&self.fd.as_fd())? };
image::RgbaImage::from_raw(self.width, self.height, mmap.to_vec())
.ok_or_else(|| anyhow::anyhow!("ShmImage had incorrect size"))
}
}
impl ProvidesRegistryState for AppData {
fn registry(&mut self) -> &mut RegistryState {
&mut self.registry_state
}
sctk::registry_handlers!();
}
impl SeatHandler for AppData {
fn seat_state(&mut self) -> &mut sctk::seat::SeatState {
&mut self.seat_state
}
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: WlSeat) {}
fn new_capability(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: WlSeat,
_: sctk::seat::Capability,
) {
}
fn remove_capability(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: WlSeat,
_: sctk::seat::Capability,
) {
}
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: WlSeat) {}
}
impl ToplevelManagerHandler for AppData {
fn toplevel_manager_state(&mut self) -> &mut cctk::toplevel_management::ToplevelManagerState {
&mut self.toplevel_manager_state
}
fn capabilities(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: Vec<WEnum<zcosmic_toplevel_manager_v1::ZcosmicToplelevelManagementCapabilitiesV1>>,
) {
// TODO capabilities could affect the options in the applet
}
}
impl AppData {
fn send_image(&self, handle: ZcosmicToplevelHandleV1) {
let tx = self.tx.clone();
let capure_data = CaptureData {
qh: self.queue_handle.clone(),
conn: self.conn.clone(),
wl_shm: self.shm_state.wl_shm().clone(),
screencopy_manager: self.screencopy_state.screencopy_manager.clone(),
};
std::thread::spawn(move || {
use std::ffi::CStr;
let name =
unsafe { CStr::from_bytes_with_nul_unchecked(b"minimize-applet-screencopy\0") };
let Ok(fd) = rustix::fs::memfd_create(name, rustix::fs::MemfdFlags::CLOEXEC) else {
tracing::error!("Failed to get fd for capture");
return;
};
// XXX is this going to use to much memory?
let img = capure_data.capture_source_shm_fd(false, handle.clone(), fd, None);
if let Some(img) = img {
let Ok(img) = img.image() else {
tracing::error!("Failed to get RgbaImage");
return;
};
// resize to 128x128
let max = img.width().max(img.height());
let ratio = max as f32 / 128.0;
let img = if ratio > 1.0 {
let new_width = (img.width() as f32 / ratio).round();
let new_height = (img.height() as f32 / ratio).round();
image::imageops::resize(
&img,
new_width as u32,
new_height as u32,
image::imageops::FilterType::Lanczos3,
)
} else {
img
};
if let Err(err) =
tx.unbounded_send(WaylandUpdate::Image(handle, WaylandImage::new(img)))
{
tracing::error!("Failed to send image event to subscription {err:?}");
};
} else {
tracing::error!("Failed to capture image");
}
});
}
}
impl ToplevelInfoHandler for AppData {
fn toplevel_info_state(&mut self) -> &mut ToplevelInfoState {
&mut self.toplevel_info_state
}
fn new_toplevel(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
) {
if let Some(info) = self.toplevel_info_state.info(toplevel) {
if info
.state
.contains(&zcosmic_toplevel_handle_v1::State::Minimized)
{
// spawn thread for sending the image
self.send_image(toplevel.clone());
let _ = self
.tx
.unbounded_send(WaylandUpdate::Toplevel(ToplevelUpdate::Add(
toplevel.clone(),
info.clone(),
)));
} else {
let _ = self
.tx
.unbounded_send(WaylandUpdate::Toplevel(ToplevelUpdate::Remove(
toplevel.clone(),
)));
}
}
}
fn update_toplevel(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
) {
if let Some(info) = self.toplevel_info_state.info(toplevel) {
if info
.state
.contains(&zcosmic_toplevel_handle_v1::State::Minimized)
{
self.send_image(toplevel.clone());
let _ = self
.tx
.unbounded_send(WaylandUpdate::Toplevel(ToplevelUpdate::Update(
toplevel.clone(),
info.clone(),
)));
} else {
let _ = self
.tx
.unbounded_send(WaylandUpdate::Toplevel(ToplevelUpdate::Remove(
toplevel.clone(),
)));
}
}
}
fn toplevel_closed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
) {
let _ = self
.tx
.unbounded_send(WaylandUpdate::Toplevel(ToplevelUpdate::Remove(
toplevel.clone(),
)));
}
}
pub(crate) fn wayland_handler(
tx: UnboundedSender<WaylandUpdate>,
rx: calloop::channel::Channel<WaylandRequest>,
) {
let socket = std::env::var("X_PRIVILEGED_WAYLAND_SOCKET")
.ok()
.and_then(|fd| {
fd.parse::<RawFd>()
.ok()
.map(|fd| unsafe { UnixStream::from_raw_fd(fd) })
});
let conn = if let Some(socket) = socket {
Connection::from_socket(socket).unwrap()
} else {
Connection::connect_to_env().unwrap()
};
let (globals, event_queue) = registry_queue_init(&conn).unwrap();
let mut event_loop = calloop::EventLoop::<AppData>::try_new().unwrap();
let qh = event_queue.handle();
let wayland_source = WaylandSource::new(conn.clone(), event_queue);
let handle = event_loop.handle();
wayland_source
.insert(handle.clone())
.expect("Failed to insert wayland source.");
if handle
.insert_source(rx, |event, _, state| match event {
calloop::channel::Event::Msg(req) => match req {
WaylandRequest::Toplevel(req) => match req {
ToplevelRequest::Activate(handle) => {
if let Some(seat) = state.seat_state.seats().next() {
let manager = &state.toplevel_manager_state.manager;
manager.activate(&handle, &seat);
}
}
},
},
calloop::channel::Event::Closed => {
state.exit = true;
}
})
.is_err()
{
return;
}
let registry_state = RegistryState::new(&globals);
let screencopy_state = ScreencopyState::new(&globals, &qh);
let shm_state = Shm::bind(&globals, &qh).expect("Failed to get shm state");
let mut app_data = AppData {
exit: false,
tx,
conn,
queue_handle: qh.clone(),
shm_state,
screencopy_state,
seat_state: SeatState::new(&globals, &qh),
toplevel_info_state: ToplevelInfoState::new(&registry_state, &qh),
toplevel_manager_state: ToplevelManagerState::new(&registry_state, &qh),
registry_state,
};
loop {
if app_data.exit {
break;
}
event_loop.dispatch(None, &mut app_data).unwrap();
}
}
impl ShmHandler for AppData {
fn shm_state(&mut self) -> &mut Shm {
&mut self.shm_state
}
}
impl ScreencopyHandler for AppData {
fn screencopy_state(&mut self) -> &mut ScreencopyState {
&mut self.screencopy_state
}
fn init_done(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
session: &zcosmic_screencopy_session_v1::ZcosmicScreencopySessionV1,
buffer_infos: &[BufferInfo],
) {
Session::for_session(session).unwrap().update(|data| {
data.buffer_infos = Some(buffer_infos.to_vec());
});
}
fn ready(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
session: &zcosmic_screencopy_session_v1::ZcosmicScreencopySessionV1,
) {
Session::for_session(session).unwrap().update(|data| {
data.res = Some(Ok(()));
});
session.destroy();
}
fn failed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
session: &zcosmic_screencopy_session_v1::ZcosmicScreencopySessionV1,
reason: WEnum<zcosmic_screencopy_session_v1::FailureReason>,
) {
// TODO send message to thread
Session::for_session(session).unwrap().update(|data| {
data.res = Some(Err(reason));
});
session.destroy();
}
}
impl Dispatch<wl_shm_pool::WlShmPool, ()> for AppData {
fn event(
_app_data: &mut Self,
_buffer: &wl_shm_pool::WlShmPool,
_event: wl_shm_pool::Event,
_: &(),
_: &Connection,
_qh: &QueueHandle<Self>,
) {
}
}
impl Dispatch<wl_buffer::WlBuffer, ()> for AppData {
fn event(
_app_data: &mut Self,
_buffer: &wl_buffer::WlBuffer,
_event: wl_buffer::Event,
_: &(),
_: &Connection,
_qh: &QueueHandle<Self>,
) {
}
}
sctk::delegate_shm!(AppData);
sctk::delegate_seat!(AppData);
sctk::delegate_registry!(AppData);
cctk::delegate_toplevel_info!(AppData);
cctk::delegate_toplevel_manager!(AppData);
cctk::delegate_screencopy!(AppData, session: [SessionData]);

View file

@ -0,0 +1,121 @@
//! # DBus interface proxy for: `org.freedesktop.UPower.KbdBacklight`
//!
//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data.
//! Source: `Interface '/org/freedesktop/UPower/KbdBacklight' from service 'org.freedesktop.UPower' on system bus`.
use cctk::sctk::reexports::calloop;
use cctk::toplevel_info::ToplevelInfo;
use cosmic::cctk::cosmic_protocols;
use cosmic::iced::subscription;
use cosmic::iced_futures::futures;
use cosmic::{cctk, iced};
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
use futures::{
channel::mpsc::{unbounded, UnboundedReceiver},
SinkExt, StreamExt,
};
use image::EncodableLayout;
use once_cell::sync::Lazy;
use std::fmt::Debug;
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::wayland_handler::wayland_handler;
pub static WAYLAND_RX: Lazy<Mutex<Option<UnboundedReceiver<WaylandUpdate>>>> =
Lazy::new(|| Mutex::new(None));
pub fn wayland_subscription() -> iced::Subscription<WaylandUpdate> {
subscription::channel(
std::any::TypeId::of::<WaylandUpdate>(),
50,
move |mut output| async move {
let mut state = State::Waiting;
loop {
state = start_listening(state, &mut output).await;
}
},
)
}
pub enum State {
Waiting,
Finished,
}
async fn start_listening(
state: State,
output: &mut futures::channel::mpsc::Sender<WaylandUpdate>,
) -> State {
match state {
State::Waiting => {
let mut guard = WAYLAND_RX.lock().await;
let rx = {
if guard.is_none() {
let (calloop_tx, calloop_rx) = calloop::channel::channel();
let (toplevel_tx, toplevel_rx) = unbounded();
let _ = std::thread::spawn(move || {
wayland_handler(toplevel_tx, calloop_rx);
});
*guard = Some(toplevel_rx);
_ = output.send(WaylandUpdate::Init(calloop_tx)).await;
}
guard.as_mut().unwrap()
};
match rx.next().await {
Some(u) => {
_ = output.send(u).await;
State::Waiting
}
None => {
_ = output.send(WaylandUpdate::Finished).await;
tracing::error!("Wayland handler thread died");
State::Finished
}
}
}
State::Finished => iced::futures::future::pending().await,
}
}
#[derive(Clone, Debug)]
pub enum WaylandUpdate {
Init(calloop::channel::Sender<WaylandRequest>),
Finished,
Toplevel(ToplevelUpdate),
Image(ZcosmicToplevelHandleV1, WaylandImage),
}
#[derive(Debug, Clone)]
pub struct WaylandImage {
pub img: Arc<image::RgbaImage>,
}
impl WaylandImage {
pub fn new(img: image::RgbaImage) -> Self {
Self { img: Arc::new(img) }
}
}
impl AsRef<[u8]> for WaylandImage {
fn as_ref(&self) -> &[u8] {
self.img.as_bytes()
}
}
#[derive(Clone, Debug)]
pub enum ToplevelUpdate {
Add(ZcosmicToplevelHandleV1, ToplevelInfo),
Update(ZcosmicToplevelHandleV1, ToplevelInfo),
Remove(ZcosmicToplevelHandleV1),
}
#[derive(Clone, Debug)]
pub enum WaylandRequest {
Toplevel(ToplevelRequest),
}
#[derive(Debug, Clone)]
pub enum ToplevelRequest {
Activate(ZcosmicToplevelHandleV1),
}

View file

@ -0,0 +1,289 @@
use cosmic::{
desktop::IconSource,
iced::Limits,
iced_core::{layout, overlay, widget::Tree, Border, Layout, Length, Size, Vector},
theme::{Button, Container},
widget::{button, container, image::Handle, Image, Widget},
Element,
};
use crate::wayland_subscription::WaylandImage;
pub struct WindowImage<'a, Msg> {
image_button: Element<'a, Msg>,
icon: Element<'a, Msg>,
}
impl<'a, Msg> WindowImage<'a, Msg>
where
Msg: 'static + Clone,
{
pub fn new(
img: Option<WaylandImage>,
icon: &IconSource,
size: f32,
on_press: Msg,
padding: u16,
) -> Self {
let border = 1.0;
Self {
image_button: button(
container(
container(if let Some(img) = img {
let max_dim = img.img.width().max(img.img.height()).max(1);
let ratio = max_dim as f32 / (size - border * 2.0).max(1.0);
let adjusted_width = img.img.width() as f32 / ratio;
let adjusted_height = img.img.height() as f32 / ratio;
Element::from(
Image::new(Handle::from_pixels(
img.img.width(),
img.img.height(),
img.clone(),
))
.width(Length::Fixed(adjusted_width))
.height(Length::Fixed(adjusted_height))
.content_fit(cosmic::iced_core::ContentFit::Contain),
)
} else {
Element::from(
icon.as_cosmic_icon()
.width(Length::Fixed(size))
.height(Length::Fixed(size)),
)
})
.style(Container::Custom(Box::new(move |theme| {
container::Appearance {
border: Border {
color: theme.cosmic().bg_divider().into(),
width: border,
radius: 0.0.into(),
},
..Default::default()
}
})))
.padding(border as u16)
.height(Length::Shrink)
.width(Length::Shrink),
)
.align_x(cosmic::iced_core::alignment::Horizontal::Center)
.align_y(cosmic::iced_core::alignment::Vertical::Center)
.height(Length::Fixed(size))
.width(Length::Fixed(size)),
)
.on_press(on_press)
.width(Length::Shrink)
.height(Length::Shrink)
.style(Button::AppletIcon)
.padding(padding)
.into(),
icon: icon
.as_cosmic_icon()
.width(Length::Fixed(size / 3.0))
.height(Length::Fixed(size / 3.0))
.into(),
}
}
}
impl<'a, Msg> Widget<Msg, cosmic::Theme, cosmic::Renderer> for WindowImage<'a, Msg> {
fn children(&self) -> Vec<cosmic::iced_core::widget::Tree> {
vec![Tree::new(&self.image_button), Tree::new(&self.icon)]
}
fn diff(&mut self, tree: &mut cosmic::iced_core::widget::Tree) {
tree.diff_children(&mut [&mut self.image_button, &mut self.icon])
}
fn overlay<'b>(
&'b mut self,
state: &'b mut Tree,
layout: Layout<'_>,
renderer: &cosmic::Renderer,
) -> Option<cosmic::iced_core::overlay::Element<'b, Msg, cosmic::Theme, cosmic::Renderer>> {
let children = [&mut self.image_button, &mut self.icon]
.into_iter()
.zip(&mut state.children)
.zip(layout.children())
.filter_map(|((child, state), layout)| {
child.as_widget_mut().overlay(state, layout, renderer)
})
.collect::<Vec<_>>();
(!children.is_empty()).then(|| overlay::Group::with_children(children).overlay())
}
fn size(&self) -> Size<Length> {
Size::new(Length::Shrink, Length::Shrink)
}
fn layout(
&self,
tree: &mut cosmic::iced_core::widget::Tree,
renderer: &cosmic::Renderer,
limits: &cosmic::iced_core::layout::Limits,
) -> cosmic::iced_core::layout::Node {
let children = &mut tree.children;
let button = &mut children[0];
let button_node = self
.image_button
.as_widget()
.layout(button, renderer, limits);
let button_bounds = button_node.size();
let icon_width = button_bounds.width / 3.0;
let icon_height = button_bounds.height / 3.0;
let icon = &mut children[1];
let icon_node = self
.icon
.as_widget()
.layout(
icon,
renderer,
&Limits::NONE.width(icon_width).height(icon_height),
)
.translate(Vector::new(2. * icon_width, 2. * icon_height));
layout::Node::with_children(
limits.resolve(Length::Shrink, Length::Shrink, button_node.size()),
vec![button_node, icon_node],
)
}
fn draw(
&self,
tree: &cosmic::iced_core::widget::Tree,
renderer: &mut cosmic::Renderer,
theme: &cosmic::Theme,
style: &cosmic::iced_core::renderer::Style,
layout: cosmic::iced_core::Layout<'_>,
cursor: cosmic::iced_core::mouse::Cursor,
viewport: &cosmic::iced_core::Rectangle,
) {
let children = &[&self.image_button, &self.icon];
// draw children in order
for (i, (layout, child)) in layout.children().zip(children).enumerate() {
let tree = &tree.children[i];
child
.as_widget()
.draw(tree, renderer, theme, style, layout, cursor, viewport);
}
}
fn size_hint(&self) -> Size<Length> {
self.size()
}
fn tag(&self) -> cosmic::iced_core::widget::tree::Tag {
cosmic::iced_core::widget::tree::Tag::stateless()
}
fn state(&self) -> cosmic::iced_core::widget::tree::State {
cosmic::iced_core::widget::tree::State::None
}
fn operate(
&self,
tree: &mut cosmic::iced_core::widget::Tree,
layout: cosmic::iced_core::Layout<'_>,
renderer: &cosmic::Renderer,
operation: &mut dyn cosmic::widget::Operation<
cosmic::iced_core::widget::OperationOutputWrapper<Msg>,
>,
) {
let layout = layout.children().collect::<Vec<_>>();
let children = [&self.image_button, &self.icon];
for (i, (layout, child)) in layout
.into_iter()
.zip(children.into_iter())
.enumerate()
.rev()
{
let tree = &mut tree.children[i];
child.as_widget().operate(tree, layout, renderer, operation);
}
}
fn on_event(
&mut self,
state: &mut cosmic::iced_core::widget::Tree,
event: cosmic::iced_core::Event,
layout: cosmic::iced_core::Layout<'_>,
cursor: cosmic::iced_core::mouse::Cursor,
renderer: &cosmic::Renderer,
clipboard: &mut dyn cosmic::iced_core::Clipboard,
shell: &mut cosmic::iced_core::Shell<'_, Msg>,
viewport: &cosmic::iced_core::Rectangle,
) -> cosmic::iced_core::event::Status {
let children = [&mut self.image_button, &mut self.icon];
let layout = layout.children().collect::<Vec<_>>();
// draw children in order
let mut status = cosmic::iced_core::event::Status::Ignored;
for (i, (layout, child)) in layout
.into_iter()
.zip(children.into_iter())
.enumerate()
.rev()
{
let tree = &mut state.children[i];
status = child.as_widget_mut().on_event(
tree,
event.clone(),
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
if matches!(status, cosmic::iced_core::event::Status::Captured) {
return status;
}
}
status
}
fn mouse_interaction(
&self,
state: &cosmic::iced_core::widget::Tree,
layout: cosmic::iced_core::Layout<'_>,
cursor: cosmic::iced_core::mouse::Cursor,
viewport: &cosmic::iced_core::Rectangle,
renderer: &cosmic::Renderer,
) -> cosmic::iced_core::mouse::Interaction {
let children = [&self.image_button, &self.icon];
let layout = layout.children().collect::<Vec<_>>();
for (i, (layout, child)) in layout
.into_iter()
.zip(children.into_iter())
.enumerate()
.rev()
{
let tree = &state.children[i];
let interaction = child
.as_widget()
.mouse_interaction(tree, layout, cursor, viewport, renderer);
if cursor.is_over(layout.bounds()) {
return interaction;
}
}
cosmic::iced_core::mouse::Interaction::Idle
}
fn id(&self) -> Option<cosmic::widget::Id> {
None
}
fn set_id(&mut self, _id: cosmic::widget::Id) {}
}
impl<'a, Message> From<WindowImage<'a, Message>> for cosmic::Element<'a, Message>
where
Message: 'static + Clone,
{
fn from(w: WindowImage<'a, Message>) -> cosmic::Element<'a, Message> {
Element::new(w)
}
}

View file

@ -18,7 +18,7 @@ tracing-log.workspace = true
itertools = "0.10.3"
slotmap = "1.0.6"
tokio = { version = "1.15.0", features = ["full"] }
anyhow = "1.0"
anyhow.workspace = true
# Application i18n
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.6.4"

View file

@ -5,7 +5,7 @@ edition = "2021"
license = "GPL-3.0-or-later"
[dependencies]
anyhow = "1"
anyhow.workspace = true
libcosmic.workspace = true
cosmic-time.workspace = true
nix = "0.26"

View file

@ -15,7 +15,7 @@ tracing-log.workspace = true
once_cell = "1.9"
futures = "0.3.21"
xdg = "2.4.0"
anyhow = "1.0"
anyhow.workspace = true
tokio = "1.35"
# Application i18n
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }

View file

@ -39,6 +39,7 @@ _install_app_list: (_install 'com.system76.CosmicAppList' 'cosmic-app-list')
_install_audio: (_install 'com.system76.CosmicAppletAudio' 'cosmic-applet-audio')
_install_battery: (_install 'com.system76.CosmicAppletBattery' 'cosmic-applet-battery')
_install_bluetooth: (_install 'com.system76.CosmicAppletBluetooth' 'cosmic-applet-bluetooth')
_install_minimize: (_install 'com.system76.CosmicAppletMinimize' 'cosmic-applet-minimize')
_install_network: (_install 'com.system76.CosmicAppletNetwork' 'cosmic-applet-network')
_install_notifications: (_install 'com.system76.CosmicAppletNotifications' 'cosmic-applet-notifications')
_install_power: (_install 'com.system76.CosmicAppletPower' 'cosmic-applet-power')
@ -54,7 +55,7 @@ _install_app_button: (_install_button 'com.system76.CosmicPanelAppButton' 'cosmi
_install_workspaces_button: (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button')
# Installs files into the system
install: _install_app_list _install_audio _install_battery _install_bluetooth _install_network _install_notifications _install_power _install_workspace _install_time _install_tiling _install_panel_button _install_app_button _install_workspaces_button _install_status_area
install: _install_app_list _install_audio _install_battery _install_bluetooth _install_minimize _install_network _install_notifications _install_power _install_workspace _install_time _install_tiling _install_panel_button _install_app_button _install_workspaces_button _install_status_area
# Extracts vendored dependencies if vendor=1
_extract_vendor: