1
0
Fork 0
forked from wry/wry

portal: implement workspace capture

This commit is contained in:
Julian Orth 2024-04-20 13:48:12 +02:00
parent c6864a6d85
commit 33a0a40857
23 changed files with 518 additions and 50 deletions

View file

@ -72,6 +72,12 @@ You can change this GPU at runtime.
## Screen Sharing
Jay supports screen sharing via xdg-desktop-portal.
There are three supported modes:
- Window capture
- Output capture
- Workspace capture which is like output capture except that only one workspace will be
shown.
## Screen Locking

View file

@ -1,6 +1,7 @@
# Unreleased
- Screencasts now support window capture.
- Screencasts now support workspace capture.
- Add support for wp-alpha-modifier.
- Add support for per-device keymaps.
- Add support for virtual-keyboard-unstable-v1.

View file

@ -472,6 +472,7 @@ fn create_dummy_output(state: &Rc<State>) {
has_capture: Cell::new(false),
title_texture: Cell::new(None),
attention_requests: Default::default(),
render_highlight: Default::default(),
});
*dummy_workspace.output_link.borrow_mut() =
Some(dummy_output.workspaces.add_last(dummy_workspace.clone()));

View file

@ -17,6 +17,7 @@ pub mod jay_screencast;
pub mod jay_screenshot;
pub mod jay_seat_events;
pub mod jay_select_toplevel;
pub mod jay_select_workspace;
pub mod jay_toplevel;
pub mod jay_workspace;
pub mod jay_workspace_watcher;

View file

@ -15,6 +15,7 @@ use {
jay_screenshot::JayScreenshot,
jay_seat_events::JaySeatEvents,
jay_select_toplevel::{JaySelectToplevel, JayToplevelSelector},
jay_select_workspace::{JaySelectWorkspace, JayWorkspaceSelector},
jay_workspace_watcher::JayWorkspaceWatcher,
},
leaks::Tracker,
@ -85,13 +86,14 @@ pub struct Cap;
impl Cap {
pub const NONE: u16 = 0;
pub const WINDOW_CAPTURE: u16 = 1;
pub const SELECT_WORKSPACE: u16 = 2;
}
impl JayCompositor {
fn send_capabilities(&self) {
self.client.event(Capabilities {
self_id: self.id,
cap: &[Cap::NONE, Cap::WINDOW_CAPTURE],
cap: &[Cap::NONE, Cap::WINDOW_CAPTURE, Cap::SELECT_WORKSPACE],
});
}
@ -362,6 +364,24 @@ impl JayCompositorRequestHandler for JayCompositor {
seat.global.select_toplevel(selector);
Ok(())
}
fn select_workspace(&self, req: SelectWorkspace, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let seat = self.client.lookup(req.seat)?;
let obj = Rc::new(JaySelectWorkspace {
id: req.id,
client: self.client.clone(),
tracker: Default::default(),
destroyed: Cell::new(false),
});
track!(self.client, obj);
self.client.add_client_obj(&obj)?;
let selector = JayWorkspaceSelector {
ws: Default::default(),
jsw: obj.clone(),
};
seat.global.select_workspace(selector);
Ok(())
}
}
object_base! {

View file

@ -0,0 +1,105 @@
use {
crate::{
client::{Client, ClientError},
ifs::{jay_workspace::JayWorkspace, wl_seat::WorkspaceSelector},
leaks::Tracker,
object::{Object, Version},
tree::WorkspaceNode,
utils::clonecell::CloneCell,
wire::{jay_select_workspace::*, JaySelectWorkspaceId, JayWorkspaceId},
},
std::{cell::Cell, rc::Rc},
thiserror::Error,
};
pub struct JaySelectWorkspace {
pub id: JaySelectWorkspaceId,
pub client: Rc<Client>,
pub tracker: Tracker<Self>,
pub destroyed: Cell<bool>,
}
pub struct JayWorkspaceSelector {
pub ws: CloneCell<Option<Rc<WorkspaceNode>>>,
pub jsw: Rc<JaySelectWorkspace>,
}
impl WorkspaceSelector for JayWorkspaceSelector {
fn set(&self, ws: Rc<WorkspaceNode>) {
self.ws.set(Some(ws));
}
}
impl Drop for JayWorkspaceSelector {
fn drop(&mut self) {
if self.jsw.destroyed.get() {
return;
}
match self.ws.take() {
None => {
self.jsw.send_cancelled();
}
Some(ws) => {
let id = match self.jsw.client.new_id() {
Ok(id) => id,
Err(e) => {
self.jsw.client.error(e);
return;
}
};
let jw = Rc::new(JayWorkspace {
id,
client: self.jsw.client.clone(),
workspace: CloneCell::new(Some(ws.clone())),
tracker: Default::default(),
});
track!(self.jsw.client, jw);
self.jsw.client.add_server_obj(&jw);
self.jsw
.send_selected(ws.output.get().global.name.raw(), id);
ws.jay_workspaces
.set((self.jsw.client.id, jw.id), jw.clone());
jw.send_initial_properties(&ws);
}
};
let _ = self.jsw.client.remove_obj(&*self.jsw);
}
}
impl JaySelectWorkspace {
fn send_cancelled(&self) {
self.client.event(Cancelled { self_id: self.id });
}
fn send_selected(&self, output: u32, id: JayWorkspaceId) {
self.client.event(Selected {
self_id: self.id,
output,
id,
});
}
}
impl JaySelectWorkspaceRequestHandler for JaySelectWorkspace {
type Error = JaySelectWorkspaceError;
}
object_base! {
self = JaySelectWorkspace;
version = Version(1);
}
impl Object for JaySelectWorkspace {
fn break_loops(&self) {
self.destroyed.set(true);
}
}
simple_add_obj!(JaySelectWorkspace);
#[derive(Debug, Error)]
pub enum JaySelectWorkspaceError {
#[error(transparent)]
ClientError(Box<ClientError>),
}
efrom!(JaySelectWorkspaceError, ClientError);

View file

@ -19,6 +19,14 @@ pub struct JayWorkspace {
}
impl JayWorkspace {
pub fn send_initial_properties(&self, workspace: &WorkspaceNode) {
self.send_linear_id(workspace);
self.send_name(workspace);
self.send_output(&workspace.output.get());
self.send_visible(workspace.visible.get());
self.send_done();
}
pub fn send_linear_id(&self, ws: &WorkspaceNode) {
self.client.event(LinearId {
self_id: self.id,

View file

@ -36,11 +36,7 @@ impl JayWorkspaceWatcher {
id: jw.id,
linear_id: workspace.id.raw(),
});
jw.send_linear_id(workspace);
jw.send_name(workspace);
jw.send_output(&workspace.output.get());
jw.send_visible(workspace.visible.get());
jw.send_done();
jw.send_initial_properties(workspace);
Ok(())
}

View file

@ -83,7 +83,10 @@ use {
thiserror::Error,
uapi::OwnedFd,
};
pub use {event_handling::NodeSeatState, pointer_owner::ToplevelSelector};
pub use {
event_handling::NodeSeatState,
pointer_owner::{ToplevelSelector, WorkspaceSelector},
};
pub const POINTER: u32 = 1;
const KEYBOARD: u32 = 2;
@ -1153,10 +1156,13 @@ impl WlSeatGlobal {
self.forward.set(forward);
}
#[allow(dead_code)]
pub fn select_toplevel(self: &Rc<Self>, selector: impl ToplevelSelector) {
self.pointer_owner.select_toplevel(self, selector);
}
pub fn select_workspace(self: &Rc<Self>, selector: impl WorkspaceSelector) {
self.pointer_owner.select_workspace(self, selector);
}
}
global_base!(WlSeatGlobal, WlSeat, WlSeatError);

View file

@ -14,7 +14,7 @@ use {
xdg_toplevel_drag_v1::XdgToplevelDragV1,
},
state::DeviceHandlerData,
tree::{FindTreeUsecase, FoundNode, Node, ToplevelNode},
tree::{FindTreeUsecase, FoundNode, Node, ToplevelNode, WorkspaceNode},
utils::{clonecell::CloneCell, smallmap::SmallMap},
},
std::{
@ -33,6 +33,10 @@ pub trait ToplevelSelector: 'static {
fn set(&self, toplevel: Rc<dyn ToplevelNode>);
}
pub trait WorkspaceSelector: 'static {
fn set(&self, ws: Rc<WorkspaceNode>);
}
impl Default for PointerOwnerHolder {
fn default() -> Self {
let default = Rc::new(SimplePointerOwner {
@ -157,19 +161,32 @@ impl PointerOwnerHolder {
seat.changes.or_assign(CHANGE_CURSOR_MOVED);
}
pub fn select_toplevel(&self, seat: &Rc<WlSeatGlobal>, selector: impl ToplevelSelector) {
fn select_element(&self, seat: &Rc<WlSeatGlobal>, usecase: impl SimplePointerOwnerUsecase) {
self.revert_to_default(seat);
let usecase = Rc::new(SelectToplevelUsecase {
seat: Rc::downgrade(seat),
selector,
latest: Default::default(),
});
if let Some(node) = seat.pointer_stack.borrow().last() {
usecase.node_focus(seat, node);
}
self.owner.set(Rc::new(SimplePointerOwner { usecase }));
seat.trigger_tree_changed();
}
pub fn select_toplevel(&self, seat: &Rc<WlSeatGlobal>, selector: impl ToplevelSelector) {
let usecase = Rc::new(SelectToplevelUsecase {
seat: Rc::downgrade(seat),
selector,
latest: Default::default(),
});
self.select_element(seat, usecase)
}
pub fn select_workspace(&self, seat: &Rc<WlSeatGlobal>, selector: impl WorkspaceSelector) {
let usecase = Rc::new(SelectWorkspaceUsecase {
seat: Rc::downgrade(seat),
selector,
latest: Default::default(),
});
self.select_element(seat, usecase)
}
}
trait PointerOwner {
@ -221,6 +238,12 @@ struct SelectToplevelUsecase<S: ?Sized> {
selector: S,
}
struct SelectWorkspaceUsecase<S: ?Sized> {
seat: Weak<WlSeatGlobal>,
latest: CloneCell<Option<Rc<WorkspaceNode>>>,
selector: S,
}
impl<T: SimplePointerOwnerUsecase> PointerOwner for SimplePointerOwner<T> {
fn button(&self, seat: &Rc<WlSeatGlobal>, time_usec: u64, button: u32, state: KeyState) {
if state != KeyState::Pressed {
@ -674,8 +697,22 @@ impl SimplePointerOwnerUsecase for DefaultPointerUsecase {
}
}
impl<S: ToplevelSelector + ?Sized> SimplePointerOwnerUsecase for Rc<SelectToplevelUsecase<S>> {
const FIND_TREE_USECASE: FindTreeUsecase = FindTreeUsecase::SelectToplevel;
trait NodeSelectorUsecase: Sized + 'static {
const FIND_TREE_USECASE: FindTreeUsecase;
fn default_button(
self: &Rc<Self>,
spo: &SimplePointerOwner<Rc<Self>>,
seat: &Rc<WlSeatGlobal>,
button: u32,
pn: &Rc<dyn Node>,
) -> bool;
fn node_focus(self: &Rc<Self>, seat: &Rc<WlSeatGlobal>, node: &Rc<dyn Node>);
}
impl<U: NodeSelectorUsecase + ?Sized> SimplePointerOwnerUsecase for Rc<U> {
const FIND_TREE_USECASE: FindTreeUsecase = <U as NodeSelectorUsecase>::FIND_TREE_USECASE;
const IS_DEFAULT: bool = false;
fn default_button(
@ -685,17 +722,7 @@ impl<S: ToplevelSelector + ?Sized> SimplePointerOwnerUsecase for Rc<SelectToplev
button: u32,
pn: &Rc<dyn Node>,
) -> bool {
let Some(tl) = pn.clone().node_into_toplevel() else {
return false;
};
let selected_toplevel =
button == BTN_RIGHT || (button == BTN_LEFT && !tl.tl_admits_children());
if !selected_toplevel {
return false;
}
self.selector.set(tl);
spo.revert_to_default(seat);
true
<U as NodeSelectorUsecase>::default_button(self, spo, seat, button, pn)
}
fn start_drag(
@ -721,6 +748,34 @@ impl<S: ToplevelSelector + ?Sized> SimplePointerOwnerUsecase for Rc<SelectToplev
}
fn node_focus(&self, seat: &Rc<WlSeatGlobal>, node: &Rc<dyn Node>) {
<U as NodeSelectorUsecase>::node_focus(self, seat, node)
}
}
impl<S: ToplevelSelector> NodeSelectorUsecase for SelectToplevelUsecase<S> {
const FIND_TREE_USECASE: FindTreeUsecase = FindTreeUsecase::SelectToplevel;
fn default_button(
self: &Rc<Self>,
spo: &SimplePointerOwner<Rc<Self>>,
seat: &Rc<WlSeatGlobal>,
button: u32,
pn: &Rc<dyn Node>,
) -> bool {
let Some(tl) = pn.clone().node_into_toplevel() else {
return false;
};
let selected_toplevel =
button == BTN_RIGHT || (button == BTN_LEFT && !tl.tl_admits_children());
if !selected_toplevel {
return false;
}
self.selector.set(tl);
spo.revert_to_default(seat);
true
}
fn node_focus(self: &Rc<Self>, seat: &Rc<WlSeatGlobal>, node: &Rc<dyn Node>) {
let mut damage = false;
let tl = node.clone().node_into_toplevel();
if let Some(tl) = &tl {
@ -750,3 +805,50 @@ impl<S: ?Sized> Drop for SelectToplevelUsecase<S> {
}
}
}
impl<S: WorkspaceSelector> NodeSelectorUsecase for SelectWorkspaceUsecase<S> {
const FIND_TREE_USECASE: FindTreeUsecase = FindTreeUsecase::SelectWorkspace;
fn default_button(
self: &Rc<Self>,
spo: &SimplePointerOwner<Rc<Self>>,
seat: &Rc<WlSeatGlobal>,
_button: u32,
pn: &Rc<dyn Node>,
) -> bool {
let Some(ws) = pn.clone().node_into_workspace() else {
return false;
};
self.selector.set(ws);
spo.revert_to_default(seat);
true
}
fn node_focus(self: &Rc<Self>, seat: &Rc<WlSeatGlobal>, node: &Rc<dyn Node>) {
let mut damage = false;
let ws = node.clone().node_into_workspace();
if let Some(ws) = &ws {
ws.render_highlight.fetch_add(1);
seat.set_known_cursor(KnownCursor::Pointer);
damage = true;
}
if let Some(prev) = self.latest.set(ws) {
prev.render_highlight.fetch_sub(1);
damage = true;
}
if damage {
seat.state.damage();
}
}
}
impl<S: ?Sized> Drop for SelectWorkspaceUsecase<S> {
fn drop(&mut self) {
if let Some(prev) = self.latest.take() {
prev.render_highlight.fetch_sub(1);
if let Some(seat) = self.seat.upgrade() {
seat.state.damage();
}
}
}
}

View file

@ -297,7 +297,7 @@ fn finish_display_connect(dpy: Rc<PortalDisplayPrelude>) {
id: dpy.con.id(),
con: dpy.con.clone(),
owner: Default::default(),
window_capture: Cell::new(false),
caps: Default::default(),
});
dpy.con.add_object(jc.clone());
dpy.registry.request_bind(name, version, jc.deref());

View file

@ -37,7 +37,9 @@ use {
wl_usr::usr_ifs::{
usr_jay_screencast::{UsrJayScreencast, UsrJayScreencastOwner},
usr_jay_select_toplevel::UsrJaySelectToplevel,
usr_jay_select_workspace::UsrJaySelectWorkspace,
usr_jay_toplevel::UsrJayToplevel,
usr_jay_workspace::UsrJayWorkspace,
},
},
std::{
@ -64,6 +66,7 @@ pub enum ScreencastPhase {
SourcesSelected,
Selecting(Rc<SelectingScreencast>),
SelectingWindow(Rc<SelectingWindowScreencast>),
SelectingWorkspace(Rc<SelectingWorkspaceScreencast>),
Starting(Rc<StartingScreencast>),
Started(Rc<StartedScreencast>),
Terminated,
@ -89,6 +92,12 @@ pub struct SelectingWindowScreencast {
pub selector: Rc<UsrJaySelectToplevel>,
}
pub struct SelectingWorkspaceScreencast {
pub core: SelectingScreencastCore,
pub dpy: Rc<PortalDisplay>,
pub selector: Rc<UsrJaySelectWorkspace>,
}
pub struct StartingScreencast {
pub session: Rc<ScreencastSession>,
pub request_obj: Rc<DbusObject>,
@ -100,6 +109,7 @@ pub struct StartingScreencast {
pub enum ScreencastTarget {
Output(Rc<PortalOutput>),
Workspace(Rc<PortalOutput>, Rc<UsrJayWorkspace>),
Toplevel(Rc<UsrJayToplevel>),
}
@ -156,15 +166,27 @@ impl PwClientNodeOwner for StartingScreencast {
port.supported_metas.set(SUPPORTED_META_VIDEO_CROP);
let jsc = self.dpy.jc.create_screencast();
match &self.target {
ScreencastTarget::Output(o) => jsc.set_output(&o.jay),
ScreencastTarget::Output(o) => {
jsc.set_output(&o.jay);
jsc.set_allow_all_workspaces(true);
}
ScreencastTarget::Workspace(o, ws) => {
jsc.set_output(&o.jay);
jsc.allow_workspace(ws);
}
ScreencastTarget::Toplevel(t) => jsc.set_toplevel(t),
}
jsc.set_use_linear_buffers(true);
jsc.set_allow_all_workspaces(true);
jsc.configure();
if let ScreencastTarget::Toplevel(t) = &self.target {
match &self.target {
ScreencastTarget::Output(_) => {}
ScreencastTarget::Workspace(_, w) => {
self.dpy.con.remove_obj(&**w);
}
ScreencastTarget::Toplevel(t) => {
self.dpy.con.remove_obj(&**t);
}
}
let started = Rc::new(StartedScreencast {
session: self.session.clone(),
node: self.node.clone(),
@ -253,14 +275,24 @@ impl ScreencastSession {
s.dpy.con.remove_obj(&*s.selector);
s.core.reply.err("Session has been terminated");
}
ScreencastPhase::SelectingWorkspace(s) => {
s.dpy.con.remove_obj(&*s.selector);
s.core.reply.err("Session has been terminated");
}
ScreencastPhase::Starting(s) => {
s.reply.err("Session has been terminated");
s.node.con.destroy_obj(s.node.deref());
s.dpy.screencasts.remove(self.session_obj.path());
if let ScreencastTarget::Toplevel(t) = &s.target {
match &s.target {
ScreencastTarget::Output(_) => {}
ScreencastTarget::Workspace(_, w) => {
s.dpy.con.remove_obj(&**w);
}
ScreencastTarget::Toplevel(t) => {
s.dpy.con.remove_obj(&**t);
}
}
}
ScreencastPhase::Started(s) => {
s.jay_screencast.con.remove_obj(s.jay_screencast.deref());
s.node.con.destroy_obj(s.node.deref());

View file

@ -5,6 +5,7 @@ use {
ptl_display::{PortalDisplay, PortalOutput, PortalSeat},
ptl_screencast::{
ScreencastPhase, ScreencastSession, ScreencastTarget, SelectingWindowScreencast,
SelectingWorkspaceScreencast,
},
ptr_gui::{
Align, Button, ButtonOwner, Flow, GuiElement, Label, Orientation, OverlayWindow,
@ -14,7 +15,9 @@ use {
theme::Color,
utils::copyhashmap::CopyHashMap,
wl_usr::usr_ifs::{
usr_jay_select_toplevel::UsrJaySelectToplevelOwner, usr_jay_toplevel::UsrJayToplevel,
usr_jay_select_toplevel::UsrJaySelectToplevelOwner,
usr_jay_select_workspace::UsrJaySelectWorkspaceOwner, usr_jay_toplevel::UsrJayToplevel,
usr_jay_workspace::UsrJayWorkspace,
},
},
std::rc::Rc,
@ -43,7 +46,8 @@ struct StaticButton {
#[derive(Copy, Clone, Eq, PartialEq)]
enum ButtonRole {
Accept,
Window,
SelectWorkspace,
SelectWindow,
Reject,
}
@ -71,14 +75,20 @@ fn create_accept_gui(surface: &Rc<SelectionGuiSurface>) -> Rc<dyn GuiElement> {
let label = Rc::new(Label::default());
*label.text.borrow_mut() = text;
let accept_button = static_button(surface, ButtonRole::Accept, "Share This Output");
let window_button = static_button(surface, ButtonRole::Window, "Share A Window");
let workspace_button = static_button(surface, ButtonRole::SelectWorkspace, "Share A Workspcae");
let window_button = static_button(surface, ButtonRole::SelectWindow, "Share A Window");
let reject_button = static_button(surface, ButtonRole::Reject, "Reject");
for button in [&accept_button, &window_button, &reject_button] {
for button in [
&accept_button,
&workspace_button,
&window_button,
&reject_button,
] {
button.border_color.set(Color::from_gray(100));
button.border.set(2.0);
button.padding.set(5.0);
}
for button in [&accept_button, &window_button] {
for button in [&accept_button, &workspace_button, &window_button] {
button.bg_color.set(Color::from_rgb(170, 200, 170));
button.bg_hover_color.set(Color::from_rgb(170, 255, 170));
}
@ -92,7 +102,10 @@ fn create_accept_gui(surface: &Rc<SelectionGuiSurface>) -> Rc<dyn GuiElement> {
flow.in_margin.set(V_MARGIN);
flow.cross_margin.set(H_MARGIN);
let mut elements: Vec<Rc<dyn GuiElement>> = vec![label, accept_button];
if surface.gui.dpy.jc.window_capture.get() {
if surface.gui.dpy.jc.caps.select_workspace.get() {
elements.push(workspace_button);
}
if surface.gui.dpy.jc.caps.window_capture.get() {
elements.push(window_button);
}
elements.push(reject_button);
@ -140,7 +153,7 @@ impl ButtonOwner for StaticButton {
return;
}
match self.role {
ButtonRole::Accept | ButtonRole::Window => {
ButtonRole::Accept | ButtonRole::SelectWorkspace | ButtonRole::SelectWindow => {
log::info!("User has accepted the request");
let selecting = match self.surface.gui.screencast_session.phase.get() {
ScreencastPhase::Selecting(selecting) => selecting,
@ -154,6 +167,19 @@ impl ButtonOwner for StaticButton {
selecting
.core
.starting(dpy, ScreencastTarget::Output(self.surface.output.clone()));
} else if self.role == ButtonRole::SelectWorkspace {
let selector = dpy.jc.select_workspace(&seat.wl);
let selecting = Rc::new(SelectingWorkspaceScreencast {
core: selecting.core.clone(),
dpy: dpy.clone(),
selector: selector.clone(),
});
selector.owner.set(Some(selecting.clone()));
self.surface
.gui
.screencast_session
.phase
.set(ScreencastPhase::SelectingWorkspace(selecting));
} else {
let selector = dpy.jc.select_toplevel(&seat.wl);
let selecting = Rc::new(SelectingWindowScreencast {
@ -199,6 +225,37 @@ impl UsrJaySelectToplevelOwner for SelectingWindowScreencast {
}
}
impl UsrJaySelectWorkspaceOwner for SelectingWorkspaceScreencast {
fn done(&self, output: u32, ws: Option<Rc<UsrJayWorkspace>>) {
let Some(ws) = ws else {
log::info!("User has aborted the selection");
self.core.session.kill();
return;
};
match self.core.session.phase.get() {
ScreencastPhase::SelectingWorkspace(s) => {
self.dpy.con.remove_obj(&*s.selector);
}
_ => {
self.dpy.con.remove_obj(&*ws);
return;
}
}
log::info!("User has selected a workspace");
let output = match self.dpy.outputs.get(&output) {
Some(o) => o,
_ => {
log::warn!("Workspace does not belong to any known output");
self.dpy.con.remove_obj(&*ws);
self.core.session.kill();
return;
}
};
self.core
.starting(&self.dpy, ScreencastTarget::Workspace(output, ws));
}
}
fn static_button(surface: &Rc<SelectionGuiSurface>, role: ButtonRole, text: &str) -> Rc<Button> {
let button = Rc::new(Button::default());
let slf = Rc::new(StaticButton {

View file

@ -117,13 +117,13 @@ impl Renderer<'_> {
if let Some(ws) = output.workspace.get() {
fullscreen = ws.fullscreen.get();
}
let theme = &self.state.theme;
let th = theme.sizes.title_height.get();
if let Some(fs) = fullscreen {
fs.tl_as_node().node_render(self, x, y, None);
} else {
render_layer!(output.layers[0]);
render_layer!(output.layers[1]);
let theme = &self.state.theme;
let th = theme.sizes.title_height.get();
{
let c = theme.colors.bar_background.get();
self.base.fill_boxes2(
@ -201,6 +201,13 @@ impl Renderer<'_> {
}
render_layer!(output.layers[2]);
render_layer!(output.layers[3]);
if let Some(ws) = output.workspace.get() {
if ws.render_highlight.get() > 0 {
let color = self.state.theme.colors.highlight.get();
let bounds = ws.position.get().at_point(x, y + th + 1);
self.base.fill_boxes(&[bounds], &color);
}
}
}
pub fn render_workspace(&mut self, workspace: &WorkspaceNode, x: i32, y: i32) {

View file

@ -104,6 +104,7 @@ impl FindTreeResult {
pub enum FindTreeUsecase {
None,
SelectToplevel,
SelectWorkspace,
}
pub trait Node: 'static {

View file

@ -448,6 +448,7 @@ impl OutputNode {
has_capture: Cell::new(false),
title_texture: Default::default(),
attention_requests: Default::default(),
render_highlight: Default::default(),
});
ws.update_has_captures();
*ws.output_link.borrow_mut() = Some(self.workspaces.add_last(ws.clone()));
@ -743,6 +744,20 @@ impl Node for OutputNode {
}
return FindTreeResult::AcceptsInput;
}
let bar_height = self.state.theme.sizes.title_height.get() + 1;
if usecase == FindTreeUsecase::SelectWorkspace {
if y >= bar_height {
y -= bar_height;
if let Some(ws) = self.workspace.get() {
tree.push(FoundNode {
node: ws.clone(),
x,
y,
});
return FindTreeResult::AcceptsInput;
}
}
}
{
let res = self.find_layer_surface_at(x, y, &[OVERLAY, TOP], tree, usecase);
if res.accepts_input() {
@ -791,7 +806,6 @@ impl Node for OutputNode {
});
fs.tl_as_node().node_find_tree_at(x, y, tree, usecase)
} else {
let bar_height = self.state.theme.sizes.title_height.get() + 1;
if y >= bar_height {
y -= bar_height;
let len = tree.len();

View file

@ -20,6 +20,7 @@ use {
clonecell::CloneCell,
copyhashmap::CopyHashMap,
linkedlist::{LinkedList, LinkedNode, NodeRef},
numcell::NumCell,
threshold_counter::ThresholdCounter,
},
wire::JayWorkspaceId,
@ -53,6 +54,7 @@ pub struct WorkspaceNode {
pub has_capture: Cell<bool>,
pub title_texture: Cell<Option<TextTexture>>,
pub attention_requests: ThresholdCounter,
pub render_highlight: NumCell<u32>,
}
impl WorkspaceNode {

View file

@ -4,6 +4,7 @@ pub mod usr_jay_pointer;
pub mod usr_jay_render_ctx;
pub mod usr_jay_screencast;
pub mod usr_jay_select_toplevel;
pub mod usr_jay_select_workspace;
pub mod usr_jay_toplevel;
pub mod usr_jay_workspace;
pub mod usr_jay_workspace_watcher;

View file

@ -11,6 +11,7 @@ use {
usr_jay_output::UsrJayOutput, usr_jay_pointer::UsrJayPointer,
usr_jay_render_ctx::UsrJayRenderCtx, usr_jay_screencast::UsrJayScreencast,
usr_jay_select_toplevel::UsrJaySelectToplevel,
usr_jay_select_workspace::UsrJaySelectWorkspace,
usr_jay_workspace_watcher::UsrJayWorkspaceWatcher, usr_wl_output::UsrWlOutput,
usr_wl_seat::UsrWlSeat,
},
@ -25,7 +26,13 @@ pub struct UsrJayCompositor {
pub id: JayCompositorId,
pub con: Rc<UsrCon>,
pub owner: CloneCell<Option<Rc<dyn UsrJayCompositorOwner>>>,
pub caps: UsrJayCompositorCaps,
}
#[derive(Default)]
pub struct UsrJayCompositorCaps {
pub window_capture: Cell<bool>,
pub select_workspace: Cell<bool>,
}
pub trait UsrJayCompositorOwner {
@ -129,6 +136,21 @@ impl UsrJayCompositor {
sc
}
pub fn select_workspace(&self, seat: &UsrWlSeat) -> Rc<UsrJaySelectWorkspace> {
let sc = Rc::new(UsrJaySelectWorkspace {
id: self.con.id(),
con: self.con.clone(),
owner: Default::default(),
});
self.con.request(SelectWorkspace {
self_id: self.id,
id: sc.id,
seat: seat.id,
});
self.con.add_object(sc.clone());
sc
}
fn client_id(&self, parser: MsgParser<'_, '_>) -> Result<(), MsgParserError> {
let ev: ClientId = self.con.parse(self, parser)?;
if let Some(owner) = self.owner.get() {
@ -150,7 +172,8 @@ impl UsrJayCompositor {
for &cap in ev.cap {
match cap {
Cap::NONE => {}
Cap::WINDOW_CAPTURE => self.window_capture.set(true),
Cap::WINDOW_CAPTURE => self.caps.window_capture.set(true),
Cap::SELECT_WORKSPACE => self.caps.select_workspace.set(true),
_ => {}
}
}

View file

@ -1,7 +1,6 @@
use {
crate::{
format::formats,
ifs::jay_workspace::JayWorkspace,
utils::{
buffd::{MsgParser, MsgParserError},
clonecell::CloneCell,
@ -9,7 +8,10 @@ use {
video::dmabuf::{DmaBuf, DmaBufPlane, PlaneVec},
wire::{jay_screencast::*, JayScreencastId},
wl_usr::{
usr_ifs::{usr_jay_output::UsrJayOutput, usr_jay_toplevel::UsrJayToplevel},
usr_ifs::{
usr_jay_output::UsrJayOutput, usr_jay_toplevel::UsrJayToplevel,
usr_jay_workspace::UsrJayWorkspace,
},
usr_object::UsrObject,
UsrCon,
},
@ -78,8 +80,7 @@ impl UsrJayScreencast {
});
}
#[allow(dead_code)]
pub fn allow_workspace(&self, ws: &JayWorkspace) {
pub fn allow_workspace(&self, ws: &UsrJayWorkspace) {
self.con.request(AllowWorkspace {
self_id: self.id,
workspace: ws.id,

View file

@ -0,0 +1,72 @@
use {
crate::{
utils::{
buffd::{MsgParser, MsgParserError},
clonecell::CloneCell,
},
wire::{jay_select_workspace::*, JaySelectWorkspaceId},
wl_usr::{usr_ifs::usr_jay_workspace::UsrJayWorkspace, usr_object::UsrObject, UsrCon},
},
std::rc::Rc,
thiserror::Error,
};
pub struct UsrJaySelectWorkspace {
pub id: JaySelectWorkspaceId,
pub con: Rc<UsrCon>,
pub owner: CloneCell<Option<Rc<dyn UsrJaySelectWorkspaceOwner>>>,
}
pub trait UsrJaySelectWorkspaceOwner {
fn done(&self, output: u32, ws: Option<Rc<UsrJayWorkspace>>);
}
impl UsrJaySelectWorkspace {
fn cancelled(&self, parser: MsgParser<'_, '_>) -> Result<(), UsrJaySelectWorkspaceError> {
let _ev: Cancelled = self.con.parse(self, parser)?;
if let Some(owner) = self.owner.get() {
owner.done(0, None);
}
self.con.remove_obj(self);
Ok(())
}
fn selected(&self, parser: MsgParser<'_, '_>) -> Result<(), UsrJaySelectWorkspaceError> {
let ev: Selected = self.con.parse(self, parser)?;
let tl = Rc::new(UsrJayWorkspace {
id: ev.id,
con: self.con.clone(),
owner: Default::default(),
});
self.con.add_object(tl.clone());
match self.owner.get() {
Some(owner) => owner.done(ev.output, Some(tl)),
_ => self.con.remove_obj(&*tl),
}
self.con.remove_obj(self);
Ok(())
}
}
usr_object_base! {
UsrJaySelectWorkspace, JaySelectWorkspace;
CANCELLED => cancelled,
SELECTED => selected,
}
impl UsrObject for UsrJaySelectWorkspace {
fn destroy(&self) {
// nothing
}
fn break_loops(&self) {
self.owner.take();
}
}
#[derive(Debug, Error)]
pub enum UsrJaySelectWorkspaceError {
#[error("Parsing failed")]
MsgParserError(#[from] MsgParserError),
}

View file

@ -83,6 +83,11 @@ request select_toplevel {
seat: id(wl_seat),
}
request select_workspace {
id: id(jay_select_workspace),
seat: id(wl_seat),
}
# events
event client_id {

View file

@ -0,0 +1,7 @@
event cancelled {
}
event selected {
output: u32,
id: id(jay_workspace),
}