1
0
Fork 0
forked from wry/wry

Merge pull request #692 from mahkoh/jorth/jay-popup-ext

xdg-popup: implement jay-popup-ext-v1
This commit is contained in:
mahkoh 2025-12-21 15:19:24 +01:00 committed by GitHub
commit 12ec0e6502
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 768 additions and 57 deletions

View file

@ -163,6 +163,7 @@ Jay supports the following wayland protocols:
| ext_session_lock_manager_v1 | 1 | Yes |
| ext_transient_seat_manager_v1 | 1[^ts_rejected] | Yes |
| ext_workspace_manager_v1 | 1 | Yes |
| jay_popup_ext_manager_v1 | 1 | |
| jay_tray_v1 | 1 | |
| org_kde_kwin_server_decoration_manager | 1 | |
| wl_compositor | 6 | |

View file

@ -21,6 +21,7 @@ use {
},
jay_compositor::JayCompositorGlobal,
jay_damage_tracking::JayDamageTrackingGlobal,
jay_popup_ext_manager_v1::JayPopupExtManagerV1Global,
org_kde_kwin_server_decoration_manager::OrgKdeKwinServerDecorationManagerGlobal,
wl_compositor::WlCompositorGlobal,
wl_fixes::WlFixesGlobal,
@ -231,6 +232,7 @@ impl Globals {
add_singleton!(XdgToplevelTagManagerV1Global);
add_singleton!(JayHeadManagerV1Global);
add_singleton!(WpPointerWarpV1Global);
add_singleton!(JayPopupExtManagerV1Global);
}
pub fn add_backend_singletons(&self, backend: &Rc<dyn Backend>) {

View file

@ -22,6 +22,7 @@ pub mod jay_input;
pub mod jay_log_file;
pub mod jay_output;
pub mod jay_pointer;
pub mod jay_popup_ext_manager_v1;
pub mod jay_randr;
pub mod jay_reexec;
pub mod jay_render_ctx;

View file

@ -0,0 +1,107 @@
use {
crate::{
client::{Client, ClientError},
globals::{Global, GlobalName},
ifs::wl_surface::xdg_surface::xdg_popup::jay_popup_ext_v1::{
JayPopupExtV1, JayPopupExtV1Error,
},
leaks::Tracker,
object::{Object, Version},
wire::{JayPopupExtManagerV1Id, jay_popup_ext_manager_v1::*},
},
std::rc::Rc,
thiserror::Error,
};
pub struct JayPopupExtManagerV1Global {
pub name: GlobalName,
}
pub struct JayPopupExtManagerV1 {
pub id: JayPopupExtManagerV1Id,
pub client: Rc<Client>,
pub tracker: Tracker<Self>,
pub version: Version,
}
impl JayPopupExtManagerV1Global {
pub fn new(name: GlobalName) -> Self {
Self { name }
}
fn bind_(
self: Rc<Self>,
id: JayPopupExtManagerV1Id,
client: &Rc<Client>,
version: Version,
) -> Result<(), JayPopupExtManagerV1Error> {
let obj = Rc::new(JayPopupExtManagerV1 {
id,
client: client.clone(),
tracker: Default::default(),
version,
});
track!(client, obj);
client.add_client_obj(&obj)?;
Ok(())
}
}
global_base!(
JayPopupExtManagerV1Global,
JayPopupExtManagerV1,
JayPopupExtManagerV1Error
);
impl Global for JayPopupExtManagerV1Global {
fn singleton(&self) -> bool {
true
}
fn version(&self) -> u32 {
1
}
}
simple_add_global!(JayPopupExtManagerV1Global);
impl JayPopupExtManagerV1RequestHandler for JayPopupExtManagerV1 {
type Error = JayPopupExtManagerV1Error;
fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.client.remove_obj(self)?;
Ok(())
}
fn get_ext(&self, req: GetExt, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let popup = self.client.lookup(req.popup)?;
let obj = Rc::new(JayPopupExtV1::new(
req.id,
&self.client,
self.version,
&popup,
));
track!(self.client, obj);
self.client.add_client_obj(&obj)?;
obj.install()?;
Ok(())
}
}
object_base! {
self = JayPopupExtManagerV1;
version = self.version;
}
impl Object for JayPopupExtManagerV1 {}
simple_add_obj!(JayPopupExtManagerV1);
#[derive(Debug, Error)]
pub enum JayPopupExtManagerV1Error {
#[error(transparent)]
ClientError(Box<ClientError>),
#[error(transparent)]
JayPopupExtV1Error(#[from] JayPopupExtV1Error),
}
efrom!(JayPopupExtManagerV1Error, ClientError);

View file

@ -69,7 +69,7 @@ use {
WlSurface,
dnd_icon::DndIcon,
tray::{DynTrayItem, TrayItemId},
xdg_surface::xdg_popup::XdgPopup,
xdg_surface::{xdg_popup::XdgPopup, xdg_toplevel::ResizeEdges},
zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
},
xdg_toplevel_drag_v1::XdgToplevelDragV1,
@ -1146,6 +1146,24 @@ impl WlSeatGlobal {
}
}
pub fn start_popup_move(self: &Rc<Self>, popup: &Rc<XdgPopup>, serial: u64) {
self.pointer_owner.start_popup_move(self, popup, serial);
}
pub fn start_popup_resize(
self: &Rc<Self>,
popup: &Rc<XdgPopup>,
edges: ResizeEdges,
serial: u64,
) {
self.pointer_owner
.start_popup_resize(self, popup, edges, serial);
}
pub fn cancel_popup_move(self: &Rc<Self>) {
self.pointer_owner.grab_node_removed(self);
}
pub fn cancel_dnd(self: &Rc<Self>) {
self.pointer_owner.cancel_dnd(self);
}

View file

@ -103,6 +103,14 @@ impl NodeSeatState {
self.pointer_foci.remove(&seat.id);
}
pub fn pointer_inside(&self, seat: &WlSeatGlobal) -> bool {
self.pointer_foci.contains(&seat.id)
}
pub fn pointer_not_inside(&self, seat: &WlSeatGlobal) -> bool {
!self.pointer_inside(seat)
}
pub fn disable_focus_history(&self) {
self.no_focus_history.set(true);
}

View file

@ -10,7 +10,11 @@ use {
BTN_LEFT, BTN_RIGHT, CHANGE_CURSOR_MOVED, CHANGE_TREE, Dnd, DroppedDnd,
NodeSeatState, WlSeatError, WlSeatGlobal, wl_pointer::PendingScroll,
},
wl_surface::{WlSurface, dnd_icon::DndIcon},
wl_surface::{
WlSurface,
dnd_icon::DndIcon,
xdg_surface::{xdg_popup::XdgPopup, xdg_toplevel::ResizeEdges},
},
xdg_toplevel_drag_v1::XdgToplevelDragV1,
},
rect::Rect,
@ -20,7 +24,7 @@ use {
PlaceholderNode, TddType, ToplevelNode, WorkspaceDragDestination, WorkspaceNode,
WsMoveConfig, move_ws_to_output, toplevel_set_workspace,
},
utils::{clonecell::CloneCell, smallmap::SmallMap},
utils::{bitflags::BitflagsExt, clonecell::CloneCell, smallmap::SmallMap},
},
linearize::LinearizeExt,
std::{
@ -218,6 +222,22 @@ impl PointerOwnerHolder {
pub fn start_workspace_drag(&self, seat: &Rc<WlSeatGlobal>, ws: &Rc<WorkspaceNode>) {
self.owner.get().start_workspace_drag(seat, ws);
}
pub fn start_popup_move(&self, seat: &Rc<WlSeatGlobal>, popup: &Rc<XdgPopup>, serial: u64) {
self.owner.get().start_popup_move(seat, popup, serial);
}
pub fn start_popup_resize(
&self,
seat: &Rc<WlSeatGlobal>,
popup: &Rc<XdgPopup>,
edges: ResizeEdges,
serial: u64,
) {
self.owner
.get()
.start_popup_resize(seat, popup, edges, serial);
}
}
trait PointerOwner {
@ -277,6 +297,25 @@ trait PointerOwner {
let _ = seat;
let _ = ws;
}
fn start_popup_move(&self, seat: &Rc<WlSeatGlobal>, popup: &Rc<XdgPopup>, serial: u64) {
let _ = seat;
let _ = popup;
let _ = serial;
}
fn start_popup_resize(
&self,
seat: &Rc<WlSeatGlobal>,
popup: &Rc<XdgPopup>,
edges: ResizeEdges,
serial: u64,
) {
let _ = seat;
let _ = popup;
let _ = edges;
let _ = serial;
}
}
struct SimplePointerOwner<T> {
@ -285,7 +324,7 @@ struct SimplePointerOwner<T> {
struct SimpleGrabPointerOwner<T> {
usecase: T,
buttons: SmallMap<u32, (), 1>,
buttons: SmallMap<u32, u64, 1>,
node: Rc<dyn Node>,
serial: u64,
}
@ -334,7 +373,7 @@ impl<T: SimplePointerOwnerUsecase> PointerOwner for SimplePointerOwner<T> {
.owner
.set(Rc::new(SimpleGrabPointerOwner {
usecase: self.usecase.clone(),
buttons: SmallMap::new_with(button, ()),
buttons: SmallMap::new_with(button, serial),
node: pn.clone(),
serial,
}));
@ -435,8 +474,20 @@ impl<T: SimplePointerOwnerUsecase> PointerOwner for SimplePointerOwner<T> {
}
}
impl<T: SimplePointerOwnerUsecase> SimpleGrabPointerOwner<T> {
fn find_button(&self, serial: u64) -> Option<u32> {
for (button, s) in self.buttons.iter() {
if s == serial {
return Some(button);
}
}
None
}
}
impl<T: SimplePointerOwnerUsecase> PointerOwner for SimpleGrabPointerOwner<T> {
fn button(&self, seat: &Rc<WlSeatGlobal>, time_usec: u64, button: u32, state: ButtonState) {
let serial = seat.state.next_serial(self.node.node_client().as_deref());
match state {
ButtonState::Released => {
if self.buttons.remove(&button).is_none() {
@ -450,12 +501,11 @@ impl<T: SimplePointerOwnerUsecase> PointerOwner for SimpleGrabPointerOwner<T> {
}
}
ButtonState::Pressed => {
if self.buttons.insert(button, ()).is_some() {
if self.buttons.insert(button, serial).is_some() {
return;
}
}
}
let serial = seat.state.next_serial(self.node.node_client().as_deref());
seat.handle_node_button(self.node.clone(), time_usec, button, state, serial);
}
@ -508,6 +558,27 @@ impl<T: SimplePointerOwnerUsecase> PointerOwner for SimpleGrabPointerOwner<T> {
fn start_workspace_drag(&self, seat: &Rc<WlSeatGlobal>, ws: &Rc<WorkspaceNode>) {
self.usecase.start_workspace_drag(self, seat, ws);
}
fn start_popup_move(&self, seat: &Rc<WlSeatGlobal>, popup: &Rc<XdgPopup>, serial: u64) {
let Some(button) = self.find_button(serial) else {
return;
};
self.usecase.start_popup_move(self, seat, popup, button);
}
fn start_popup_resize(
&self,
seat: &Rc<WlSeatGlobal>,
popup: &Rc<XdgPopup>,
edges: ResizeEdges,
serial: u64,
) {
let Some(button) = self.find_button(serial) else {
return;
};
self.usecase
.start_popup_resize(self, seat, popup, edges, button);
}
}
impl PointerOwner for DndPointerOwner {
@ -676,6 +747,34 @@ trait SimplePointerOwnerUsecase: Sized + Clone + 'static {
let _ = seat;
let _ = ws;
}
fn start_popup_move(
&self,
grab: &SimpleGrabPointerOwner<Self>,
seat: &Rc<WlSeatGlobal>,
popup: &Rc<XdgPopup>,
button: u32,
) {
let _ = grab;
let _ = seat;
let _ = popup;
let _ = button;
}
fn start_popup_resize(
&self,
grab: &SimpleGrabPointerOwner<Self>,
seat: &Rc<WlSeatGlobal>,
popup: &Rc<XdgPopup>,
edges: ResizeEdges,
button: u32,
) {
let _ = grab;
let _ = seat;
let _ = popup;
let _ = edges;
let _ = button;
}
}
impl DefaultPointerUsecase {
@ -702,6 +801,25 @@ impl DefaultPointerUsecase {
seat.pointer_owner.owner.set(pointer_owner.clone());
pointer_owner.apply_changes(seat);
}
fn start_popup_usecase<U: PopupPointerOwnerUsecase>(
&self,
grab: &SimpleGrabPointerOwner<Self>,
seat: &Rc<WlSeatGlobal>,
popup: &Rc<XdgPopup>,
button: u32,
usecase: U,
) {
self.prepare_new_usecase(grab, seat);
seat.pointer_owner.owner.set(Rc::new(PopupPointerOwner {
popup: popup.clone(),
window_management: false,
button,
usecase,
}));
popup.node_seat_state().add_pointer_grab(seat);
popup.add_interactive_move(seat);
}
}
impl SimplePointerOwnerUsecase for DefaultPointerUsecase {
@ -808,6 +926,65 @@ impl SimplePointerOwnerUsecase for DefaultPointerUsecase {
},
);
}
fn start_popup_move(
&self,
grab: &SimpleGrabPointerOwner<Self>,
seat: &Rc<WlSeatGlobal>,
popup: &Rc<XdgPopup>,
button: u32,
) {
let (x, y) = seat.pointer_cursor.position();
let (dx, dy) = popup
.node_absolute_position()
.translate(x.round_down(), y.round_down());
self.start_popup_usecase(
grab,
seat,
popup,
button,
PopupPointerOwnerMoveUsecase { dx, dy },
);
seat.pointer_cursor.set_known(KnownCursor::Move);
}
fn start_popup_resize(
&self,
grab: &SimpleGrabPointerOwner<Self>,
seat: &Rc<WlSeatGlobal>,
popup: &Rc<XdgPopup>,
edges: ResizeEdges,
button: u32,
) {
let cursor = match (edges.top, edges.left, edges.right, edges.bottom) {
(true, false, false, false) => KnownCursor::NsResize,
(false, false, false, true) => KnownCursor::NsResize,
(false, true, false, false) => KnownCursor::EwResize,
(false, false, true, false) => KnownCursor::EwResize,
(true, true, false, false) => KnownCursor::NwseResize,
(false, false, true, true) => KnownCursor::NwseResize,
(false, true, false, true) => KnownCursor::NeswResize,
(true, false, true, false) => KnownCursor::NeswResize,
_ => return,
};
let (x, y) = seat.pointer_cursor.position();
let pos = popup.node_absolute_position();
let (mut dx, mut dy) = pos.translate(x.round_down(), y.round_down());
if edges.right {
dx = pos.width() - dx;
}
if edges.bottom {
dy = pos.height() - dy;
}
self.start_popup_usecase(
grab,
seat,
popup,
button,
PopupPointerOwnerResizeUsecase { edges, dx, dy },
);
seat.pointer_cursor.set_known(cursor);
}
}
trait NodeSelectorUsecase: Sized + 'static {
@ -944,7 +1121,7 @@ impl<S: ?Sized> Drop for SelectWorkspaceUsecase<S> {
}
impl SimplePointerOwnerUsecase for WindowManagementUsecase {
const FIND_TREE_USECASE: FindTreeUsecase = FindTreeUsecase::SelectToplevel;
const FIND_TREE_USECASE: FindTreeUsecase = FindTreeUsecase::SelectToplevelOrPopup;
const IS_DEFAULT: bool = false;
fn default_button(
@ -954,33 +1131,43 @@ impl SimplePointerOwnerUsecase for WindowManagementUsecase {
button: u32,
pn: &Rc<dyn Node>,
) -> bool {
let Some(tl) = pn.clone().node_into_toplevel() else {
return false;
};
let pos = tl.node_absolute_position();
let pos = pn.node_absolute_position();
let (x, y) = seat.pointer_cursor.position();
let (x, y) = (x.round_down(), y.round_down());
let (mut dx, mut dy) = pos.translate(x, y);
let owner: Rc<dyn PointerOwner> = if button == BTN_LEFT {
seat.pointer_cursor.set_known(KnownCursor::Move);
if tl.tl_data().is_fullscreen.get() {
Rc::new(ToplevelGrabPointerOwner {
tl,
usecase: MoveFullscreenToplevelGrabPointerOwner,
})
} else if tl.tl_data().float.is_none() {
Rc::new(ToplevelGrabPointerOwner {
tl: tl.clone(),
usecase: TileDragUsecase {
if let Some(tl) = pn.clone().node_into_toplevel() {
seat.pointer_cursor.set_known(KnownCursor::Move);
if tl.tl_data().is_fullscreen.get() {
Rc::new(ToplevelGrabPointerOwner {
tl,
destination: Default::default(),
},
usecase: MoveFullscreenToplevelGrabPointerOwner,
})
} else if tl.tl_data().float.is_none() {
Rc::new(ToplevelGrabPointerOwner {
tl: tl.clone(),
usecase: TileDragUsecase {
tl,
destination: Default::default(),
},
})
} else {
Rc::new(ToplevelGrabPointerOwner {
tl,
usecase: MoveToplevelGrabPointerOwner { dx, dy },
})
}
} else if let Some(popup) = pn.clone().node_into_popup() {
popup.add_interactive_move(seat);
seat.pointer_cursor.set_known(KnownCursor::Move);
Rc::new(PopupPointerOwner {
popup,
window_management: true,
button,
usecase: PopupPointerOwnerMoveUsecase { dx, dy },
})
} else {
Rc::new(ToplevelGrabPointerOwner {
tl,
usecase: MoveToplevelGrabPointerOwner { dx, dy },
})
return false;
}
} else if button == BTN_RIGHT {
let mut top = false;
@ -1006,18 +1193,40 @@ impl SimplePointerOwnerUsecase for WindowManagementUsecase {
(true, false, false, true) => KnownCursor::NwResize,
_ => KnownCursor::Move,
};
seat.pointer_cursor.set_known(cursor);
Rc::new(ToplevelGrabPointerOwner {
tl,
usecase: ResizeToplevelGrabPointerOwner {
top,
right,
bottom,
left,
dx,
dy,
},
})
if let Some(tl) = pn.clone().node_into_toplevel() {
seat.pointer_cursor.set_known(cursor);
Rc::new(ToplevelGrabPointerOwner {
tl,
usecase: ResizeToplevelGrabPointerOwner {
top,
right,
bottom,
left,
dx,
dy,
},
})
} else if let Some(popup) = pn.clone().node_into_popup() {
popup.add_interactive_move(seat);
seat.pointer_cursor.set_known(cursor);
Rc::new(PopupPointerOwner {
popup,
window_management: true,
button,
usecase: PopupPointerOwnerResizeUsecase {
edges: ResizeEdges {
top,
left,
right,
bottom,
},
dx,
dy,
},
})
} else {
return false;
}
} else {
return false;
};
@ -1458,3 +1667,129 @@ impl UiDragUsecase for WorkspaceDragUsecase {
}
}
}
struct PopupPointerOwner<T> {
popup: Rc<XdgPopup>,
window_management: bool,
button: u32,
usecase: T,
}
trait PopupPointerOwnerUsecase: Sized + 'static {
fn apply_changes(&self, popup: &Rc<XdgPopup>, seat: &Rc<WlSeatGlobal>);
}
impl<T> PopupPointerOwner<T>
where
T: PopupPointerOwnerUsecase,
{
fn revert_to_window_management(&self, seat: &Rc<WlSeatGlobal>) {
self.popup.remove_interactive_move(seat);
self.popup.node_seat_state().remove_pointer_grab(seat);
seat.pointer_cursor.set_known(KnownCursor::Default);
seat.pointer_owner.owner.set(Rc::new(SimplePointerOwner {
usecase: WindowManagementUsecase,
}));
seat.changes.or_assign(CHANGE_CURSOR_MOVED);
seat.apply_changes();
}
fn revert_to_previous(&self, seat: &Rc<WlSeatGlobal>) {
if self.window_management {
self.revert_to_window_management(seat);
} else {
self.revert_to_default(seat);
}
}
}
impl<T> PointerOwner for PopupPointerOwner<T>
where
T: PopupPointerOwnerUsecase,
{
fn button(&self, seat: &Rc<WlSeatGlobal>, _time_usec: u64, button: u32, state: ButtonState) {
if button != self.button || state != ButtonState::Released {
return;
}
self.revert_to_previous(seat);
}
fn apply_changes(&self, seat: &Rc<WlSeatGlobal>) {
if seat.changes.get().not_contains(CHANGE_CURSOR_MOVED) {
return;
}
self.usecase.apply_changes(&self.popup, seat);
}
fn revert_to_default(&self, seat: &Rc<WlSeatGlobal>) {
self.popup.remove_interactive_move(seat);
self.popup.node_seat_state().remove_pointer_grab(seat);
seat.pointer_owner.set_default_pointer_owner(seat);
seat.tree_changed.trigger();
}
fn grab_node_removed(&self, seat: &Rc<WlSeatGlobal>) {
self.revert_to_previous(seat);
}
fn disable_window_management(&self, seat: &Rc<WlSeatGlobal>) {
if self.window_management {
self.revert_to_default(seat);
}
}
}
struct PopupPointerOwnerMoveUsecase {
dx: i32,
dy: i32,
}
impl PopupPointerOwnerUsecase for PopupPointerOwnerMoveUsecase {
fn apply_changes(&self, popup: &Rc<XdgPopup>, seat: &Rc<WlSeatGlobal>) {
let (x, y) = seat.pointer_cursor.position();
let (x, y) = (x.round_down(), y.round_down());
let pos = popup.node_absolute_position();
let (x, y) = pos.translate(x, y);
if (x, y) != (self.dx, self.dy) {
popup.move_(x - self.dx, y - self.dy);
seat.tree_changed.trigger();
}
}
}
struct PopupPointerOwnerResizeUsecase {
edges: ResizeEdges,
dx: i32,
dy: i32,
}
impl PopupPointerOwnerUsecase for PopupPointerOwnerResizeUsecase {
fn apply_changes(&self, popup: &Rc<XdgPopup>, seat: &Rc<WlSeatGlobal>) {
let (x, y) = seat.pointer_cursor.position();
let (x, y) = (x.round_down(), y.round_down());
let pos = popup.node_absolute_position();
let (x, y) = pos.translate(x, y);
let mut dx1 = 0;
let mut dx2 = 0;
let mut dy1 = 0;
let mut dy2 = 0;
if self.edges.left {
dx1 = x - self.dx;
dx1 = dx1.min(pos.width().saturating_sub(1));
} else if self.edges.right {
dx2 = self.dx - (pos.width() - x);
dx2 = dx2.max(-pos.width().saturating_sub(1));
}
if self.edges.top {
dy1 = y - self.dy;
dy1 = dy1.min(pos.height().saturating_sub(1));
} else if self.edges.bottom {
dy2 = self.dy - (pos.height() - y);
dy2 = dy2.max(-pos.height().saturating_sub(1));
}
if dx1 != 0 || dx2 != 0 || dy1 != 0 || dy2 != 0 {
popup.resize(dx1, dy1, dx2, dy2);
seat.tree_changed.trigger();
}
}
}

View file

@ -396,8 +396,11 @@ impl Node for Xwindow {
tree: &mut Vec<FoundNode>,
usecase: FindTreeUsecase,
) -> FindTreeResult {
if usecase == FindTreeUsecase::SelectToplevel {
return FindTreeResult::AcceptsInput;
match usecase {
FindTreeUsecase::None => {}
FindTreeUsecase::SelectToplevel => return FindTreeResult::AcceptsInput,
FindTreeUsecase::SelectToplevelOrPopup => return FindTreeResult::AcceptsInput,
FindTreeUsecase::SelectWorkspace => return FindTreeResult::Other,
}
let rect = self.x.surface.buffer_abs_pos.get();
if x < rect.width() && y < rect.height() {

View file

@ -1,13 +1,17 @@
pub mod jay_popup_ext_v1;
use {
crate::{
client::{Client, ClientError},
cursor::KnownCursor,
fixed::Fixed,
ifs::{
wl_seat::{NodeSeatState, WlSeatGlobal, tablet::TabletTool},
wl_seat::{NodeSeatState, SeatId, WlSeatGlobal, tablet::TabletTool},
wl_surface::{
tray::TrayItemId,
xdg_surface::{XdgSurface, XdgSurfaceExt},
xdg_surface::{
XdgSurface, XdgSurfaceExt, xdg_popup::jay_popup_ext_v1::JayPopupExtV1,
},
},
xdg_positioner::{
CA_FLIP_X, CA_FLIP_Y, CA_RESIZE_X, CA_RESIZE_Y, CA_SLIDE_X, CA_SLIDE_Y,
@ -22,7 +26,7 @@ use {
Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink,
NodeLocation, NodeVisitor, OutputNode, StackedNode,
},
utils::clonecell::CloneCell,
utils::{clonecell::CloneCell, smallmap::SmallMap},
wire::{XdgPopupId, xdg_popup::*},
},
std::{
@ -65,6 +69,8 @@ pub struct XdgPopup {
pub tracker: Tracker<Self>,
seat_state: NodeSeatState,
set_visible_prepared: Cell<bool>,
jay_popup_ext: CloneCell<Option<Rc<JayPopupExtV1>>>,
interactive_moves: SmallMap<SeatId, Rc<WlSeatGlobal>, 1>,
}
impl Debug for XdgPopup {
@ -93,6 +99,8 @@ impl XdgPopup {
tracker: Default::default(),
seat_state: Default::default(),
set_visible_prepared: Cell::new(false),
jay_popup_ext: Default::default(),
interactive_moves: Default::default(),
})
}
@ -222,12 +230,52 @@ impl XdgPopup {
.set_absolute_desired_extents(&rel.move_(parent.x1(), parent.y1()));
}
}
fn set_relative_position(&self, rel: Rect) {
self.relative_position.set(rel);
self.update_absolute_position();
self.send_configure(rel.x1(), rel.y1(), rel.width(), rel.height());
self.xdg.schedule_configure();
}
pub fn move_(&self, dx: i32, dy: i32) {
let rel = self.relative_position.get().move_(dx, dy);
self.set_relative_position(rel);
}
pub fn resize(&self, dx1: i32, dy1: i32, dx2: i32, dy2: i32) {
let rel = self.relative_position.get();
let rel = Rect::new(
rel.x1() + dx1,
rel.y1() + dy1,
rel.x2() + dx2,
rel.y2() + dy2,
);
let Some(rel) = rel else {
return;
};
if rel.is_empty() {
return;
}
self.set_relative_position(rel);
}
pub fn add_interactive_move(&self, seat: &Rc<WlSeatGlobal>) {
self.interactive_moves.insert(seat.id(), seat.clone());
}
pub fn remove_interactive_move(&self, seat: &Rc<WlSeatGlobal>) {
self.interactive_moves.remove(&seat.id());
}
}
impl XdgPopupRequestHandler for XdgPopup {
type Error = XdgPopupError;
fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> {
if self.jay_popup_ext.is_some() {
return Err(XdgPopupError::HasJayPopupExt);
}
self.destroy_node();
self.xdg.unset_ext();
self.xdg.surface.client.remove_obj(self)?;
@ -240,6 +288,9 @@ impl XdgPopupRequestHandler for XdgPopup {
fn reposition(&self, req: Reposition, _slf: &Rc<Self>) -> Result<(), Self::Error> {
*self.pos.borrow_mut() = self.xdg.surface.client.lookup(req.positioner)?.value();
while let Some((_, seat)) = self.interactive_moves.pop() {
seat.cancel_popup_move();
}
if let Some(parent) = self.parent.get() {
self.update_position(&*parent);
let rel = self.relative_position.get();
@ -286,6 +337,7 @@ object_base! {
impl Object for XdgPopup {
fn break_loops(&self) {
self.jay_popup_ext.take();
self.destroy_node();
}
}
@ -340,8 +392,16 @@ impl Node for XdgPopup {
tree: &mut Vec<FoundNode>,
usecase: FindTreeUsecase,
) -> FindTreeResult {
if usecase == FindTreeUsecase::SelectToplevel {
return FindTreeResult::Other;
match usecase {
FindTreeUsecase::None => {}
FindTreeUsecase::SelectToplevel => return FindTreeResult::Other,
FindTreeUsecase::SelectToplevelOrPopup => {
let len = tree.len();
let res = self.xdg.find_tree_at(x, y, tree);
tree.truncate(len);
return res;
}
FindTreeUsecase::SelectWorkspace => return FindTreeResult::Other,
}
self.xdg.find_tree_at(x, y, tree)
}
@ -378,6 +438,10 @@ impl Node for XdgPopup {
) {
tool.cursor().set_known(KnownCursor::Default)
}
fn node_into_popup(self: Rc<Self>) -> Option<Rc<XdgPopup>> {
Some(self)
}
}
impl StackedNode for XdgPopup {
@ -464,5 +528,7 @@ pub enum XdgPopupError {
Incomplete,
#[error(transparent)]
ClientError(Box<ClientError>),
#[error("The popup still has a jay_popup_ext_v1 extension object")]
HasJayPopupExt,
}
efrom!(XdgPopupError, ClientError);

View file

@ -0,0 +1,101 @@
use {
crate::{
client::{Client, ClientError},
ifs::wl_surface::xdg_surface::{xdg_popup::XdgPopup, xdg_toplevel::map_resize_edges},
leaks::Tracker,
object::{Object, Version},
wire::{JayPopupExtV1Id, jay_popup_ext_v1::*},
},
std::rc::Rc,
thiserror::Error,
};
pub struct JayPopupExtV1 {
id: JayPopupExtV1Id,
client: Rc<Client>,
pub tracker: Tracker<Self>,
version: Version,
popup: Rc<XdgPopup>,
}
impl JayPopupExtV1 {
pub fn new(
id: JayPopupExtV1Id,
client: &Rc<Client>,
version: Version,
popup: &Rc<XdgPopup>,
) -> Self {
Self {
id,
tracker: Default::default(),
version,
client: client.clone(),
popup: popup.clone(),
}
}
pub fn install(self: &Rc<Self>) -> Result<(), JayPopupExtV1Error> {
if self.popup.jay_popup_ext.is_some() {
return Err(JayPopupExtV1Error::HasExt);
}
self.popup.jay_popup_ext.set(Some(self.clone()));
Ok(())
}
}
impl JayPopupExtV1RequestHandler for JayPopupExtV1 {
type Error = JayPopupExtV1Error;
fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.popup.jay_popup_ext.take();
self.client.remove_obj(self)?;
Ok(())
}
fn move_(&self, req: Move, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let seat = self.client.lookup(req.seat)?;
let Some(serial) = self.client.map_serial(req.serial) else {
return Ok(());
};
if self.popup.seat_state.pointer_not_inside(&seat.global) {
return Ok(());
}
seat.global.start_popup_move(&self.popup, serial);
Ok(())
}
fn resize(&self, req: Resize, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let Some(edges) = map_resize_edges(req.edges) else {
return Err(JayPopupExtV1Error::UnknownResizeEdges(req.edges));
};
let seat = self.client.lookup(req.seat)?;
let Some(serial) = self.client.map_serial(req.serial) else {
return Ok(());
};
if self.popup.seat_state.pointer_not_inside(&seat.global) {
return Ok(());
}
seat.global.start_popup_resize(&self.popup, edges, serial);
Ok(())
}
}
object_base! {
self = JayPopupExtV1;
version = self.version;
}
impl Object for JayPopupExtV1 {}
simple_add_obj!(JayPopupExtV1);
#[derive(Debug, Error)]
pub enum JayPopupExtV1Error {
#[error(transparent)]
ClientError(Box<ClientError>),
#[error("The xdg_popup already has a jay_popup_ext_v1 extension")]
HasExt,
#[error("The resize edge {0} is unknown")]
UnknownResizeEdges(u32),
}
efrom!(JayPopupExtV1Error, ClientError);

View file

@ -79,6 +79,11 @@ pub const SUSPENDED_SINCE: Version = Version(6);
pub const TILED_SINCE: Version = Version(2);
pub const CONSTRAINTS_SINCE: Version = Version(7);
const RESIZE_EDGE_TOP: u32 = 1;
const RESIZE_EDGE_BOTTOM: u32 = 2;
const RESIZE_EDGE_LEFT: u32 = 4;
const RESIZE_EDGE_RIGHT: u32 = 8;
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum Decoration {
#[expect(dead_code)]
@ -598,8 +603,11 @@ impl Node for XdgToplevel {
tree: &mut Vec<FoundNode>,
usecase: FindTreeUsecase,
) -> FindTreeResult {
if usecase == FindTreeUsecase::SelectToplevel {
return FindTreeResult::AcceptsInput;
match usecase {
FindTreeUsecase::None => {}
FindTreeUsecase::SelectToplevel => return FindTreeResult::AcceptsInput,
FindTreeUsecase::SelectToplevelOrPopup => return FindTreeResult::AcceptsInput,
FindTreeUsecase::SelectWorkspace => return FindTreeResult::Other,
}
self.xdg.find_tree_at(x, y, tree)
}
@ -830,3 +838,23 @@ pub enum XdgToplevelError {
NonNegative,
}
efrom!(XdgToplevelError, ClientError);
pub fn map_resize_edges(edge: u32) -> Option<ResizeEdges> {
if !matches!(edge, 0 | 1 | 2 | 4 | 5 | 6 | 8 | 9 | 10) {
return None;
}
Some(ResizeEdges {
top: edge.contains(RESIZE_EDGE_TOP),
left: edge.contains(RESIZE_EDGE_LEFT),
right: edge.contains(RESIZE_EDGE_RIGHT),
bottom: edge.contains(RESIZE_EDGE_BOTTOM),
})
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct ResizeEdges {
pub top: bool,
pub left: bool,
pub right: bool,
pub bottom: bool,
}

View file

@ -13,7 +13,10 @@ use {
},
wl_pointer::PendingScroll,
},
wl_surface::{WlSurface, tray::TrayItemId, zwlr_layer_surface_v1::ZwlrLayerSurfaceV1},
wl_surface::{
WlSurface, tray::TrayItemId, xdg_surface::xdg_popup::XdgPopup,
zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
},
},
keyboard::KeyboardState,
rect::Rect,
@ -109,10 +112,11 @@ impl FindTreeResult {
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
#[derive(Copy, Clone)]
pub enum FindTreeUsecase {
None,
SelectToplevel,
SelectToplevelOrPopup,
SelectWorkspace,
}
@ -642,6 +646,10 @@ pub trait Node: 'static {
None
}
fn node_into_popup(self: Rc<Self>) -> Option<Rc<XdgPopup>> {
None
}
// TYPE CHECKERS
fn node_is_container(&self) -> bool {

View file

@ -1073,8 +1073,11 @@ impl OutputNode {
tree: &mut Vec<FoundNode>,
usecase: FindTreeUsecase,
) -> FindTreeResult {
if usecase == FindTreeUsecase::SelectToplevel {
return FindTreeResult::Other;
match usecase {
FindTreeUsecase::None => {}
FindTreeUsecase::SelectToplevel => return FindTreeResult::Other,
FindTreeUsecase::SelectToplevelOrPopup => return FindTreeResult::Other,
FindTreeUsecase::SelectWorkspace => return FindTreeResult::Other,
}
let len = tree.len();
for layer in layers.iter().copied() {
@ -1634,9 +1637,13 @@ impl Node for OutputNode {
usecase: FindTreeUsecase,
) -> FindTreeResult {
if self.state.lock.locked.get() {
if usecase != FindTreeUsecase::SelectToplevel
&& let Some(ls) = self.lock_surface.get()
{
let allow_surface = match usecase {
FindTreeUsecase::None => true,
FindTreeUsecase::SelectToplevel => false,
FindTreeUsecase::SelectToplevelOrPopup => false,
FindTreeUsecase::SelectWorkspace => false,
};
if allow_surface && let Some(ls) = self.lock_surface.get() {
tree.push(FoundNode {
node: ls.clone(),
x,
@ -1647,7 +1654,13 @@ impl Node for OutputNode {
return FindTreeResult::AcceptsInput;
}
let ws_rect_rel = self.workspace_rect_rel.get();
if usecase == FindTreeUsecase::SelectWorkspace && ws_rect_rel.contains(x, y) {
let select_workspace = match usecase {
FindTreeUsecase::None => false,
FindTreeUsecase::SelectToplevel => false,
FindTreeUsecase::SelectToplevelOrPopup => false,
FindTreeUsecase::SelectWorkspace => true,
};
if select_workspace && ws_rect_rel.contains(x, y) {
let (x, y) = ws_rect_rel.translate(x, y);
if let Some(ws) = self.workspace.get() {
tree.push(FoundNode {

View file

@ -0,0 +1,7 @@
request destroy (destructor) {
}
request get_ext {
id: id(jay_popup_ext_v1) (new),
popup: id(xdg_popup),
}

13
wire/jay_popup_ext_v1.txt Normal file
View file

@ -0,0 +1,13 @@
request destroy (destructor) {
}
request move {
seat: id(wl_seat),
serial: u32,
}
request resize {
seat: id(wl_seat),
serial: u32,
edges: u32,
}