Skip to content

Commit

Permalink
wip: access ui and event handling
Browse files Browse the repository at this point in the history
  • Loading branch information
wash2 committed Jan 4, 2024
1 parent 2d7dacd commit 5020814
Show file tree
Hide file tree
Showing 10 changed files with 3,775 additions and 508 deletions.
3,770 changes: 3,300 additions & 470 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ license = "GPL-3.0-or-later"
anyhow = "1.0.60"
# ashpd = "0.4.0"
ashpd = { git = "https://github.com/bilelmoussaoui/ashpd" }
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols" }
cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols" }
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", rev = "c1b6516" }
cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols", rev = "c1b6516" }
futures = "0.3"
image = "0.24"
libcosmic = { git = "https://github.com/pop-os/libcosmic", features = ["wayland", "tokio"] }
memmap2 = "0.9.0"
# pipewire = { git = "https://github.com/pop-os/pipewire-rs" }
once_cell = "1.19.0"
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs", features = ["v0_3_33"] }
png = "0.17.5"
rustix = { version = "0.38.0", features = ["fs"] }
Expand All @@ -28,3 +30,7 @@ gbm = "0.14.0"
wayland-protocols = "0.31.0"
env_logger = "0.10.0"
log = "0.4.20"
# i18n
i18n-embed = { version = "0.14.1", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.7.0"
rust-embed = "8.2.0"
4 changes: 4 additions & 0 deletions i18n.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fallback_language = "en"

[fluent]
assets_dir = "./i18n"
2 changes: 2 additions & 0 deletions i18n/en/xdg_desktop_portal_cosmic.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
allow = Allow
cancel = Cancel
235 changes: 222 additions & 13 deletions src/access.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,56 @@
#![allow(dead_code, unused_variables)]

use cosmic::iced::wayland::actions::layer_surface::SctkLayerSurfaceSettings;
use cosmic::iced::wayland::actions::window::SctkWindowSettings;
use cosmic::iced_sctk::commands::layer_surface::{destroy_layer_surface, get_layer_surface};
use cosmic::iced_sctk::commands::window::{close_window, get_window};
use cosmic::widget::{button, container, dropdown, horizontal_space, icon, text, Row};
use cosmic::{
iced::{
widget::{column, row},
window, Length,
},
iced_core::Alignment,
};
use once_cell::sync::Lazy;
use tokio::sync::mpsc::Sender;
use zbus::zvariant;

use crate::wayland::WaylandHelper;
use crate::PortalResponse;
use crate::{app::CosmicPortal, fl};
use crate::{subscription, PortalResponse};

#[derive(zvariant::DeserializeDict, zvariant::Type, Debug)]
#[derive(zvariant::DeserializeDict, zvariant::Type, Debug, Clone)]
#[zvariant(signature = "a{sv}")]
struct AccessDialogOptions {
pub(crate) struct AccessDialogOptions {
modal: Option<bool>,
deny_label: Option<String>,
grant_label: Option<String>,
icon: Option<String>,
//(ID returned with the response, choices (ID, label), label, initial selection or "" meaning the portal should choose)
choices: Option<Vec<(String, String, Vec<(String, String)>, String)>>,
}

#[derive(zvariant::SerializeDict, zvariant::Type)]
pub static ACCESS_ID: Lazy<window::Id> = Lazy::new(|| window::Id::unique());

#[derive(zvariant::SerializeDict, zvariant::Type, Debug, Clone)]
#[zvariant(signature = "a{sv}")]
struct AccessDialogResult {
pub struct AccessDialogResult {
choices: Vec<(String, String)>,
}

pub struct Access {
wayland_helper: WaylandHelper,
tx: Sender<subscription::Event>,
}

impl Access {
pub fn new(wayland_helper: WaylandHelper) -> Self {
Self { wayland_helper }
pub fn new(wayland_helper: WaylandHelper, tx: Sender<subscription::Event>) -> Self {
Self { wayland_helper, tx }
}
}

#[zbus::dbus_interface(name = "org.freedesktop.impl.portal.Access")]
#[zbus::dbus_interface(name = "")]
impl Access {
async fn access_dialog(
&self,
Expand All @@ -41,11 +60,201 @@ impl Access {
title: &str,
subtitle: &str,
body: &str,
option: AccessDialogOptions,
options: AccessDialogOptions,
) -> PortalResponse<AccessDialogResult> {
log::debug!("Access dialog {app_id} {parent_window} {title} {subtitle} {body} {option:?}");
PortalResponse::Success(AccessDialogResult {
choices: Vec::new(),
})
// TODO send event to subscription via channel
// await response via channel
log::debug!("Access dialog {app_id} {parent_window} {title} {subtitle} {body} {options:?}");
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
if let Err(err) = self
.tx
.send(subscription::Event::Access(AccessDialogArgs {
handle: handle.to_owned(),
app_id: app_id.to_string(),
parent_window: parent_window.to_string(),
title: title.to_string(),
subtitle: subtitle.to_string(),
body: body.to_string(),
options,
tx,
}))
.await
{
log::error!("Failed to send access dialog event, {err}");
}
if let Some(res) = rx.recv().await {
res
} else {
PortalResponse::Cancelled::<AccessDialogResult>
}
}
}

#[derive(Debug, Clone)]
pub enum Msg {
Allow,
Cancel,
Choice(usize, usize),
}

#[derive(Clone)]
pub(crate) struct AccessDialogArgs {
pub handle: zvariant::ObjectPath<'static>,
pub app_id: String,
pub parent_window: String,
pub title: String,
pub subtitle: String,
pub body: String,
pub options: AccessDialogOptions,
pub tx: Sender<PortalResponse<AccessDialogResult>>,
}

impl AccessDialogArgs {
pub(crate) fn get_surface(&self) -> cosmic::Command<Msg> {
if self.options.modal.unwrap_or_default() {
// create a modal surface
get_window(SctkWindowSettings {
window_id: *ACCESS_ID,
app_id: Some(crate::DBUS_NAME.to_string()),
title: Some(self.title.clone()),
parent: None, // TODO parse parent window and set parent
autosize: true,
resizable: None,
..Default::default()
})
} else {
// create a layer surface
get_layer_surface(SctkLayerSurfaceSettings {
id: *ACCESS_ID,
layer: cosmic_client_toolkit::sctk::shell::wlr_layer::Layer::Top,
keyboard_interactivity:
cosmic_client_toolkit::sctk::shell::wlr_layer::KeyboardInteractivity::OnDemand,
pointer_interactivity: true,
anchor: cosmic_client_toolkit::sctk::shell::wlr_layer::Anchor::empty(),
output: cosmic::iced::wayland::actions::layer_surface::IcedOutput::Active,
namespace: "access portal".to_string(),
..Default::default()
})
}
}

pub(crate) fn destroy_surface(&self) -> cosmic::Command<Msg> {
if self.options.modal.unwrap_or_default() {
close_window(*ACCESS_ID)
} else {
destroy_layer_surface(*ACCESS_ID)
}
}
}

pub(crate) fn view(portal: &CosmicPortal) -> cosmic::Element<Msg> {
let spacing = portal.core.system_theme().cosmic().spacing;
let Some(args) = portal.access_args.as_ref() else {
return text("Oops, no access dialog args").into();
};

let choices = &portal.access_choices;
let mut options = Vec::with_capacity(choices.len() + 3);
for (i, choice) in choices.iter().enumerate() {
options.push(dropdown(choice.1.as_slice(), choice.0, move |j| Msg::Choice(i, j)).into());
}
options.push(horizontal_space(Length::Fill).into());
options.push(
button::text(
args.options
.deny_label
.clone()
.unwrap_or_else(|| fl!("cancel")),
)
.on_press(Msg::Cancel)
.into(),
);
options.push(
button::text(
args.options
.grant_label
.clone()
.unwrap_or_else(|| fl!("allow")),
)
.on_press(Msg::Allow)
.style(cosmic::theme::Button::Suggested)
.into(),
);

container(
column![
row![
icon::Icon::from(
icon::from_name(
args.options
.icon
.as_ref()
.map_or("image-missing", |name| name.as_str())
)
.size(64)
)
.width(Length::Fixed(64.0))
.height(Length::Fixed(64.0)), // TODO icon for the dialog
text(args.title.as_str()),
text(args.subtitle.as_str()),
text(args.body.as_str()),
],
Row::with_children(options)
.spacing(spacing.space_xxs as f32) // space_l
.align_items(Alignment::Center),
]
.spacing(spacing.space_l as f32), // space_l
)
.into()
}

pub fn update_msg(portal: &mut CosmicPortal, msg: Msg) -> cosmic::Command<crate::app::Msg> {
match msg {
Msg::Allow => {
let args = portal.access_args.take().unwrap();
let tx = args.tx.clone();
tokio::spawn(async move {
tx.send(PortalResponse::Cancelled::<AccessDialogResult>)
.await
});

args.destroy_surface()
}
Msg::Cancel => {
let args = portal.access_args.take().unwrap();
let tx = args.tx.clone();
tokio::spawn(async move {
tx.send(PortalResponse::Cancelled::<AccessDialogResult>)
.await
});

args.destroy_surface()
}
Msg::Choice(i, j) => {
let args = portal.access_args.as_mut().unwrap();
portal.access_choices[i].0 = Some(j);
cosmic::iced::Command::none()
}
}
.map(crate::app::Msg::Access)
}
pub fn update_args(
portal: &mut CosmicPortal,
msg: AccessDialogArgs,
) -> cosmic::Command<crate::app::Msg> {
let mut cmds = Vec::with_capacity(2);
if let Some(args) = portal.access_args.take() {
// destroy surface and recreate
cmds.push(args.destroy_surface());
// send cancelled response
tokio::spawn(async move {
let _ = args
.tx
.send(PortalResponse::Cancelled::<AccessDialogResult>);
});
}

cmds.push(msg.get_surface());
portal.access_args = Some(msg);
cosmic::iced::Command::batch(cmds).map(crate::app::Msg::Access)
}
85 changes: 85 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use cosmic::{app, iced::window};

use crate::{access, screenshot, subscription};

pub(crate) fn run() -> cosmic::iced::Result {
let settings = cosmic::app::Settings::default().no_main_window(true);
cosmic::app::run::<CosmicPortal>(settings, ())
}

#[derive(Default, Clone)]
// run iced app with no main surface
pub struct CosmicPortal {
pub core: app::Core,
pub access_args: Option<access::AccessDialogArgs>,
pub access_choices: Vec<(Option<usize>, Vec<String>)>,
}

#[derive(Debug, Clone)]
pub enum Msg {
Access(access::Msg),
Portal(subscription::Event),
}

impl cosmic::Application for CosmicPortal {
type Executor = cosmic::executor::Default;

type Flags = ();

type Message = Msg;

const APP_ID: &'static str = "org.freedesktop.portal.desktop.cosmic";

fn core(&self) -> &app::Core {
&self.core
}

fn core_mut(&mut self) -> &mut app::Core {
&mut self.core
}

fn init(
core: app::Core,
_flags: Self::Flags,
) -> (Self, cosmic::iced::Command<app::Message<Self::Message>>) {
(
Self {
core,
..Default::default()
},
cosmic::iced::Command::none(),
)
}

fn view(&self) -> cosmic::prelude::Element<Self::Message> {
unimplemented!()
}

fn view_window(&self, id: window::Id) -> cosmic::prelude::Element<Self::Message> {
if id == *access::ACCESS_ID {
access::view(self).map(Msg::Access)
} else if id == *screenshot::SCREENSHOT_ID {
screenshot::view(self)
} else {
panic!("Unknown window id {:?}", id);
}
}

fn update(
&mut self,
message: Self::Message,
) -> cosmic::iced::Command<app::Message<Self::Message>> {
match message {
Msg::Access(m) => access::update_msg(self, m).map(cosmic::app::Message::App),
Msg::Portal(e) => match e {
subscription::Event::Access(args) => {
access::update_args(self, args).map(cosmic::app::Message::App)
}
},
}
}

fn subscription(&self) -> cosmic::iced_futures::Subscription<Self::Message> {
subscription::portal_subscription().map(|e| Msg::Portal(e))
}
}
Loading

0 comments on commit 5020814

Please sign in to comment.