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:
parent
06634c3348
commit
090bb9653f
16 changed files with 1301 additions and 4 deletions
22
Cargo.lock
generated
22
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
25
cosmic-applet-minimize/Cargo.toml
Normal file
25
cosmic-applet-minimize/Cargo.toml
Normal 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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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 |
4
cosmic-applet-minimize/i18n.toml
Normal file
4
cosmic-applet-minimize/i18n.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
fallback_language = "en"
|
||||
|
||||
[fluent]
|
||||
assets_dir = "i18n"
|
||||
47
cosmic-applet-minimize/src/localize.rs
Normal file
47
cosmic-applet-minimize/src/localize.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
187
cosmic-applet-minimize/src/main.rs
Normal file
187
cosmic-applet-minimize/src/main.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
555
cosmic-applet-minimize/src/wayland_handler.rs
Normal file
555
cosmic-applet-minimize/src/wayland_handler.rs
Normal 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(®istry_state, &qh),
|
||||
toplevel_manager_state: ToplevelManagerState::new(®istry_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]);
|
||||
121
cosmic-applet-minimize/src/wayland_subscription.rs
Normal file
121
cosmic-applet-minimize/src/wayland_subscription.rs
Normal 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),
|
||||
}
|
||||
289
cosmic-applet-minimize/src/window_image.rs
Normal file
289
cosmic-applet-minimize/src/window_image.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
3
justfile
3
justfile
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue