const std = @import("std"); const c = @import("c.zig").c; const Seat = @import("seat.zig").Seat; const Server = @import("server.zig").Server; const View = @import("view.zig").View; const CursorMode = enum { Passthrough, Move, Resize, }; pub const Cursor = struct { seat: *Seat, wlr_cursor: *c.wlr_cursor, wlr_xcursor_manager: *c.wlr_xcursor_manager, listen_motion: c.wl_listener, listen_motion_absolute: c.wl_listener, listen_button: c.wl_listener, listen_axis: c.wl_listener, listen_frame: c.wl_listener, listen_request_set_cursor: c.wl_listener, mode: CursorMode, grabbed_view: ?*View, grab_x: f64, grab_y: f64, grab_width: c_int, grab_height: c_int, resize_edges: u32, pub fn create(seat: *Seat) !@This() { var cursor = @This(){ .seat = seat, // Creates a wlroots utility for tracking the cursor image shown on screen. // // TODO: free this, it allocates! .wlr_cursor = c.wlr_cursor_create() orelse return error.CantCreateWlrCursor, // Creates an xcursor manager, another wlroots utility which loads up // Xcursor themes to source cursor images from and makes sure that cursor // images are available at all scale factors on the screen (necessary for // HiDPI support). We add a cursor theme at scale factor 1 to begin with. // // TODO: free this, it allocates! .wlr_xcursor_manager = c.wlr_xcursor_manager_create(null, 24) orelse return error.CantCreateWlrXCursorManager, .listen_motion = c.wl_listener{ .link = undefined, .notify = @This().handle_motion, }, .listen_motion_absolute = c.wl_listener{ .link = undefined, .notify = @This().handle_motion_absolute, }, .listen_button = c.wl_listener{ .link = undefined, .notify = @This().handle_button, }, .listen_axis = c.wl_listener{ .link = undefined, .notify = @This().handle_axis, }, .listen_frame = c.wl_listener{ .link = undefined, .notify = @This().handle_frame, }, .listen_request_set_cursor = c.wl_listener{ .link = undefined, .notify = @This().handle_request_set_cursor, }, .mode = CursorMode.Passthrough, .grabbed_view = null, .grab_x = 0.0, .grab_y = 0.0, .grab_width = 0, .grab_height = 0, .resize_edges = 0, }; c.wlr_cursor_attach_output_layout(cursor.wlr_cursor, seat.server.wlr_output_layout); _ = c.wlr_xcursor_manager_load(cursor.wlr_xcursor_manager, 1); return cursor; } pub fn init(self: *@This()) void { // wlr_cursor *only* displays an image on screen. It does not move around // when the pointer moves. However, we can attach input devices to it, and // it will generate aggregate events for all of them. In these events, we // can choose how we want to process them, forwarding them to clients and // moving the cursor around. See following post for more detail: // https://drewdevault.com/2018/07/17/Input-handling-in-wlroots.html c.wl_signal_add(&self.wlr_cursor.*.events.motion, &self.listen_motion); c.wl_signal_add(&self.wlr_cursor.*.events.motion_absolute, &self.listen_motion_absolute); c.wl_signal_add(&self.wlr_cursor.*.events.button, &self.listen_button); c.wl_signal_add(&self.wlr_cursor.*.events.axis, &self.listen_axis); c.wl_signal_add(&self.wlr_cursor.*.events.frame, &self.listen_frame); // This listens for clients requesting a specific cursor image c.wl_signal_add(&self.seat.wlr_seat.events.request_set_cursor, &self.listen_request_set_cursor); } fn process_move(self: *@This(), time: u32) void { // Move the grabbed view to the new position. self.grabbed_view.?.*.x = @floatToInt(c_int, self.wlr_cursor.x - self.grab_x); self.grabbed_view.?.*.y = @floatToInt(c_int, self.wlr_cursor.y - self.grab_y); } fn process_resize(self: *@This(), time: u32) void { // Resizing the grabbed view can be a little bit complicated, because we // could be resizing from any corner or edge. This not only resizes the view // on one or two axes, but can also move the view if you resize from the top // or left edges (or top-left corner). // // Note that I took some shortcuts here. In a more fleshed-out compositor, // you'd wait for the client to prepare a buffer at the new size, then // commit any movement that was prepared. // TODO: Handle null view var view = self.grabbed_view; var dx: f64 = self.wlr_cursor.x - self.grab_x; var dy: f64 = self.wlr_cursor.y - self.grab_y; var x: f64 = @intToFloat(f64, view.?.x); var y: f64 = @intToFloat(f64, view.?.y); var width = @intToFloat(f64, self.grab_width); var height = @intToFloat(f64, self.grab_height); if (self.resize_edges & @intCast(u32, c.WLR_EDGE_TOP) != 0) { y = self.grab_y + dy; height -= dy; if (height < 1) { y += height; } } else if (self.resize_edges & @intCast(u32, c.WLR_EDGE_BOTTOM) != 0) { height += dy; } if (self.resize_edges & @intCast(u32, c.WLR_EDGE_LEFT) != 0) { x = self.grab_x + dx; width -= dx; if (width < 1) { x += width; } } else if (self.resize_edges & @intCast(u32, c.WLR_EDGE_RIGHT) != 0) { width += dx; } view.?.x = @floatToInt(c_int, x); view.?.y = @floatToInt(c_int, y); _ = c.wlr_xdg_toplevel_set_size( view.?.wlr_xdg_surface, @floatToInt(u32, width), @floatToInt(u32, height), ); } fn process_motion(self: *@This(), time: u32) void { // If the mode is non-passthrough, delegate to those functions. if (self.mode == CursorMode.Move) { self.process_move(time); return; } else if (self.mode == CursorMode.Resize) { self.process_resize(time); return; } // Otherwise, find the view under the pointer and send the event along. var sx: f64 = undefined; var sy: f64 = undefined; var opt_surface: ?*c.wlr_surface = null; var view = self.seat.server.desktop_view_at( self.wlr_cursor.x, self.wlr_cursor.y, &opt_surface, &sx, &sy, ); if (view == null) { // If there's no view under the cursor, set the cursor image to a // default. This is what makes the cursor image appear when you move it // around the screen, not over any views. c.wlr_xcursor_manager_set_cursor_image( self.wlr_xcursor_manager, "left_ptr", self.wlr_cursor, ); } var wlr_seat = self.seat.wlr_seat; if (opt_surface) |surface| { const focus_changed = wlr_seat.pointer_state.focused_surface != surface; // "Enter" the surface if necessary. This lets the client know that the // cursor has entered one of its surfaces. // // Note that this gives the surface "pointer focus", which is distinct // from keyboard focus. You get pointer focus by moving the pointer over // a window. c.wlr_seat_pointer_notify_enter(wlr_seat, surface, sx, sy); if (!focus_changed) { // The enter event contains coordinates, so we only need to notify // on motion if the focus did not change. c.wlr_seat_pointer_notify_motion(wlr_seat, time, sx, sy); } } else { // Clear pointer focus so future button events and such are not sent to // the last client to have the cursor over it. c.wlr_seat_pointer_clear_focus(wlr_seat); } } fn handle_motion(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { // This event is forwarded by the cursor when a pointer emits a _relative_ // pointer motion event (i.e. a delta) var cursor = @fieldParentPtr(Cursor, "listen_motion", listener); var event = @ptrCast( *c.wlr_event_pointer_motion, @alignCast(@alignOf(*c.wlr_event_pointer_motion), data), ); // The cursor doesn't move unless we tell it to. The cursor automatically // handles constraining the motion to the output layout, as well as any // special configuration applied for the specific input device which // generated the event. You can pass NULL for the device if you want to move // the cursor around without any input. c.wlr_cursor_move(cursor.wlr_cursor, event.device, event.delta_x, event.delta_y); cursor.process_motion(event.time_msec); } fn handle_motion_absolute(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { // This event is forwarded by the cursor when a pointer emits an _absolute_ // motion event, from 0..1 on each axis. This happens, for example, when // wlroots is running under a Wayland window rather than KMS+DRM, and you // move the mouse over the window. You could enter the window from any edge, // so we have to warp the mouse there. There is also some hardware which // emits these events. var cursor = @fieldParentPtr(Cursor, "listen_motion_absolute", listener); var event = @ptrCast( *c.wlr_event_pointer_motion_absolute, @alignCast(@alignOf(*c.wlr_event_pointer_motion_absolute), data), ); c.wlr_cursor_warp_absolute(cursor.wlr_cursor, event.device, event.x, event.y); cursor.process_motion(event.time_msec); } fn handle_button(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { // This event is forwarded by the cursor when a pointer emits a button // event. var cursor = @fieldParentPtr(Cursor, "listen_button", listener); var event = @ptrCast( *c.wlr_event_pointer_button, @alignCast(@alignOf(*c.wlr_event_pointer_button), data), ); // Notify the client with pointer focus that a button press has occurred _ = c.wlr_seat_pointer_notify_button( cursor.seat.wlr_seat, event.time_msec, event.button, event.state, ); var sx: f64 = undefined; var sy: f64 = undefined; var surface: ?*c.wlr_surface = null; var view = cursor.seat.server.desktop_view_at( cursor.wlr_cursor.x, cursor.wlr_cursor.y, &surface, &sx, &sy, ); if (event.*.state == c.enum_wlr_button_state.WLR_BUTTON_RELEASED) { // If you released any buttons, we exit interactive move/resize mode. cursor.mode = CursorMode.Passthrough; } else { // Focus that client if the button was _pressed_ if (view) |v| { v.focus(surface.?); } } } fn handle_axis(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { // This event is forwarded by the cursor when a pointer emits an axis event, // for example when you move the scroll wheel. var cursor = @fieldParentPtr(Cursor, "listen_axis", listener); var event = @ptrCast( *c.wlr_event_pointer_axis, @alignCast(@alignOf(*c.wlr_event_pointer_axis), data), ); // Notify the client with pointer focus of the axis event. c.wlr_seat_pointer_notify_axis( cursor.seat.wlr_seat, event.time_msec, event.orientation, event.delta, event.delta_discrete, event.source, ); } fn handle_frame(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { // This event is forwarded by the cursor when a pointer emits an frame // event. Frame events are sent after regular pointer events to group // multiple events together. For instance, two axis events may happen at the // same time, in which case a frame event won't be sent in between. var cursor = @fieldParentPtr(Cursor, "listen_frame", listener); // Notify the client with pointer focus of the frame event. c.wlr_seat_pointer_notify_frame(cursor.seat.wlr_seat); } fn handle_request_set_cursor(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { // This event is rasied by the seat when a client provides a cursor image var cursor = @fieldParentPtr(Cursor, "listen_request_set_cursor", listener); var event = @ptrCast( *c.wlr_seat_pointer_request_set_cursor_event, @alignCast(@alignOf(*c.wlr_seat_pointer_request_set_cursor_event), data), ); const focused_client = cursor.seat.wlr_seat.pointer_state.focused_client; // This can be sent by any client, so we check to make sure this one is // actually has pointer focus first. if (focused_client == event.*.seat_client) { // Once we've vetted the client, we can tell the cursor to use the // provided surface as the cursor image. It will set the hardware cursor // on the output that it's currently on and continue to do so as the // cursor moves between outputs. c.wlr_cursor_set_surface( cursor.wlr_cursor, event.surface, event.hotspot_x, event.hotspot_y, ); } } };