diff --git a/src/cursor.zig b/src/cursor.zig new file mode 100644 index 0000000..2ef9fab --- /dev/null +++ b/src/cursor.zig @@ -0,0 +1,338 @@ +const std = @import("std"); +const c = @import("c.zig").c; + +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_cursor: c.wl_listener, + + cursor_mode: CursorMode, + grabbed_view: ?*View, + grab_x: f64, + grab_y: f64, + grab_width: c_int, + grab_height: c_int, + resize_edges: u32, + + pub fn init(seat: *Seat) !@This() { + var cursor = @This(){ + .server = seat.server, + .seat = seat, + + // Creates a wlroots utility for tracking the cursor image shown on screen. + .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. + .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.*.output_layout); + _ = c.wlr_xcursor_manager_load(server.cursor_mgr, 1); + + // 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(&cursor.wlr_cursor.*.events.motion, &cursor.listen_motion); + c.wl_signal_add(&cursor.wlr_cursor.*.events.motion_absolute, &cursor.listen_motion_absolute); + c.wl_signal_add(&cursor.wlr_cursor.*.events.button, &cursor.listen_button); + c.wl_signal_add(&cursor.wlr_cursor.*.events.axis, &cursor.listen_axis); + c.wl_signal_add(&cursor.wlr_cursor.*.events.frame, &cursor.listen_frame); + + // This listens for clients requesting a specific cursor image + c.wl_signal_add(&server.seat.*.events.request_set_cursor, &cursor.listen_request_set_cursor); + + return cursor; + } + + fn process_cursor_move(server: *Server, time: u32) void { + // Move the grabbed view to the new position. + server.*.grabbed_view.?.*.x = @floatToInt(c_int, server.*.cursor.*.x - server.*.grab_x); + server.*.grabbed_view.?.*.y = @floatToInt(c_int, server.*.cursor.*.y - server.*.grab_y); + } + + fn process_cursor_resize(server: *Server, 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. + var view = server.*.grabbed_view; + + var dx: f64 = (server.*.cursor.*.x - server.*.grab_x); + var dy: f64 = (server.*.cursor.*.y - server.*.grab_y); + var x: f64 = @intToFloat(f64, view.?.*.x); + var y: f64 = @intToFloat(f64, view.?.*.y); + + var width = @intToFloat(f64, server.*.grab_width); + var height = @intToFloat(f64, server.*.grab_height); + if (server.*.resize_edges & @intCast(u32, c.WLR_EDGE_TOP) != 0) { + y = server.*.grab_y + dy; + height -= dy; + if (height < 1) { + y += height; + } + } else if (server.*.resize_edges & @intCast(u32, c.WLR_EDGE_BOTTOM) != 0) { + height += dy; + } + if (server.*.resize_edges & @intCast(u32, c.WLR_EDGE_LEFT) != 0) { + x = server.*.grab_x + dx; + width -= dx; + if (width < 1) { + x += width; + } + } else if (server.*.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.?.*.xdg_surface, + @floatToInt(u32, width), + @floatToInt(u32, height), + ); + } + + fn process_cursor_motion(server: *Server, time: u32) void { + // If the mode is non-passthrough, delegate to those functions. + if (server.*.cursor_mode == CursorMode.Move) { + process_cursor_move(server, time); + return; + } else if (server.*.cursor_mode == CursorMode.Resize) { + process_cursor_resize(server, time); + return; + } + + // Otherwise, find the view under the pointer and send the event along. + var sx: f64 = undefined; + var sy: f64 = undefined; + var seat = server.*.seat; + var opt_surface: ?*c.wlr_surface = null; + var view = desktop_view_at( + server, + server.*.cursor.*.x, + server.*.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( + server.*.cursor_mgr, + "left_ptr", + server.*.cursor, + ); + } + + if (opt_surface) |surface| { + const focus_changed = 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(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(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(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 server = @fieldParentPtr(Server, "cursor_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(server.*.cursor, event.*.device, event.*.delta_x, event.*.delta_y); + process_cursor_motion(server, 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 server = @fieldParentPtr(Server, "cursor_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(server.*.cursor, event.*.device, event.*.x, event.*.y); + process_cursor_motion(server, 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 server = @fieldParentPtr(Server, "cursor_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( + server.*.seat, + event.*.time_msec, + event.*.button, + event.*.state, + ); + + var sx: f64 = undefined; + var sy: f64 = undefined; + + var surface: ?*c.wlr_surface = null; + var view = desktop_view_at( + server, + server.*.cursor.*.x, + server.*.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. + server.*.cursor_mode = CursorMode.Passthrough; + } else { + // Focus that client if the button was _pressed_ + if (view) |v| { + focus_view(v, 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 server = @fieldParentPtr(Server, "cursor_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( + server.*.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 server = @fieldParentPtr(Server, "cursor_frame", listener); + // Notify the client with pointer focus of the frame event. + c.wlr_seat_pointer_notify_frame(server.*.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 server = @fieldParentPtr(Server, "request_cursor", listener); + var event = @ptrCast( + *c.wlr_seat_pointer_request_set_cursor_event, + @alignCast(@alignOf(*c.wlr_seat_pointer_request_set_cursor_event), data), + ); + var focused_client = server.*.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( + server.*.cursor, + event.*.surface, + event.*.hotspot_x, + event.*.hotspot_y, + ); + } + } +}; diff --git a/src/keyboard.zig b/src/keyboard.zig new file mode 100644 index 0000000..e769ac0 --- /dev/null +++ b/src/keyboard.zig @@ -0,0 +1,117 @@ +const std = @import("std"); +const c = @import("c.zig").c; + +const Keyboard = struct { + seat: *Seat, + device: *c.wlr_input_device, + + listen_modifiers: c.wl_listener, + listen_key: c.wl_listener, + + pub fn init(seat: *Seat, device: *c.wlr_input_device) @This() { + var keyboard = @This(){ + .seat = seat, + .device = device, + + .listen_modifiers = c.wl_listener{ + .link = undefined, + .notify = handle_modifiers, + }, + .listen_key = c.wl_listener{ + .link = undefined, + .notify = handle_key, + }, + }; + + // We need to prepare an XKB keymap and assign it to the keyboard. This + // assumes the defaults (e.g. layout = "us"). + const rules = c.xkb_rule_names{ + .rules = null, + .model = null, + .layout = null, + .variant = null, + .options = null, + }; + const context = c.xkb_context_new(c.enum_xkb_context_flags.XKB_CONTEXT_NO_FLAGS); + defer c.xkb_context_unref(context); + + const keymap = man_c.xkb_map_new_from_names( + context, + &rules, + c.enum_xkb_keymap_compile_flags.XKB_KEYMAP_COMPILE_NO_FLAGS, + ); + defer c.xkb_keymap_unref(keymap); + + var keyboard_device = device.*.unnamed_37.keyboard; + c.wlr_keyboard_set_keymap(keyboard_device, keymap); + c.wlr_keyboard_set_repeat_info(keyboard_device, 25, 600); + + // Setup listeners for keyboard events + c.wl_signal_add(&keyboard_device.*.events.modifiers, &keyboard.*.listen_modifiers); + c.wl_signal_add(&keyboard_device.*.events.key, &keyboard.*.listen_key); + + return keyboard; + } + + fn handle_modifiers(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { + // This event is raised when a modifier key, such as shift or alt, is + // pressed. We simply communicate this to the client. */ + var keyboard = @fieldParentPtr(Keyboard, "listen_modifiers", listener); + + // A seat can only have one keyboard, but this is a limitation of the + // Wayland protocol - not wlroots. We assign all connected keyboards to the + // same seat. You can swap out the underlying wlr_keyboard like this and + // wlr_seat handles this transparently. + c.wlr_seat_set_keyboard(keyboard.*.server.*.seat, keyboard.*.device); + + // Send modifiers to the client. + c.wlr_seat_keyboard_notify_modifiers(keyboard.*.server.*.seat, &keyboard.*.device.*.unnamed_37.keyboard.*.modifiers); + } + + fn handle_key(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { + // This event is raised when a key is pressed or released. + const keyboard = @fieldParentPtr(Keyboard, "listen_key", listener); + const event = @ptrCast( + *c.wlr_event_keyboard_key, + @alignCast(@alignOf(*c.wlr_event_keyboard_key), data), + ); + + const server = keyboard.*.server; + const seat = server.*.seat; + const keyboard_device = keyboard.*.device.*.unnamed_37.keyboard; + + // Translate libinput keycode -> xkbcommon + const keycode = event.*.keycode + 8; + // Get a list of keysyms based on the keymap for this keyboard + var syms: *c.xkb_keysym_t = undefined; + const nsyms = c.xkb_state_key_get_syms(keyboard_device.*.xkb_state, keycode, &syms); + + var handled = false; + const modifiers = c.wlr_keyboard_get_modifiers(keyboard_device); + if (modifiers & @intCast(u32, c.WLR_MODIFIER_LOGO) != 0 and + event.*.state == c.enum_wlr_key_state.WLR_KEY_PRESSED) + { + // If mod is held down and this button was _pressed_, we attempt to + // process it as a compositor keybinding. + var i: usize = 0; + while (i < nsyms) { + handled = keyboard.seat.server.handle_keybinding(syms[i]); + if (handled) { + break; + } + i += 1; + } + } + + if (!handled) { + // Otherwise, we pass it along to the client. + c.wlr_seat_set_keyboard(seat, keyboard.*.device); + c.wlr_seat_keyboard_notify_key( + seat, + event.*.time_msec, + event.*.keycode, + @intCast(u32, @enumToInt(event.*.state)), + ); + } + } +}; diff --git a/src/main.zig b/src/main.zig index e2982b3..9034289 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,718 +2,6 @@ const std = @import("std"); const c = @import("c.zig").c; const man_c = @import("c.zig").manual; -const Server = struct { - wl_display: *c.wl_display, - backend: *c.wlr_backend, - renderer: *c.wlr_renderer, - xdg_shell: *c.wlr_xdg_shell, - new_xdg_surface: c.wl_listener, - views: std.ArrayList(View), - - cursor: *c.wlr_cursor, - cursor_mgr: *c.wlr_xcursor_manager, - cursor_motion: c.wl_listener, - cursor_motion_absolute: c.wl_listener, - cursor_button: c.wl_listener, - cursor_axis: c.wl_listener, - cursor_frame: c.wl_listener, - - seat: *c.wlr_seat, - new_input: c.wl_listener, - request_cursor: c.wl_listener, - keyboards: c.wl_list, - cursor_mode: CursorMode, - grabbed_view: ?*View, - grab_x: f64, - grab_y: f64, - grab_width: c_int, - grab_height: c_int, - resize_edges: u32, - - output_layout: *c.wlr_output_layout, - outputs: std.ArrayList(Output), - new_output: c.wl_listener, -}; - -const Output = struct { - server: *Server, - wlr_output: *c.wlr_output, - frame: c.wl_listener, -}; - -const View = struct { - server: *Server, - xdg_surface: *c.wlr_xdg_surface, - map: c.wl_listener, - unmap: c.wl_listener, - destroy: c.wl_listener, - request_move: c.wl_listener, - request_resize: c.wl_listener, - mapped: bool, - x: c_int, - y: c_int, -}; - -const Keyboard = struct { - link: c.wl_list, - server: *Server, - device: *c.wlr_input_device, - - modifiers: c.wl_listener, - key: c.wl_listener, -}; - -const CursorMode = enum { - Passthrough, - Move, - Resize, -}; - -fn new_list() c.wl_list { - return c.wl_list{ - .prev = null, - .next = null, - }; -} - -fn new_listener() c.wl_listener { - return c.wl_listener{ - .link = new_list(), - .notify = null, - }; -} - -const RenderData = struct { - output: *c.wlr_output, - renderer: *c.wlr_renderer, - view: *View, - when: *c.struct_timespec, -}; - -fn render_surface(surface: [*c]c.wlr_surface, sx: c_int, sy: c_int, data: ?*c_void) callconv(.C) void { - // This function is called for every surface that needs to be rendered. - var rdata = @ptrCast(*RenderData, @alignCast(@alignOf(RenderData), data)); - var view = rdata.*.view; - var output = rdata.*.output; - - // We first obtain a wlr_texture, which is a GPU resource. wlroots - // automatically handles negotiating these with the client. The underlying - // resource could be an opaque handle passed from the client, or the client - // could have sent a pixel buffer which we copied to the GPU, or a few other - // means. You don't have to worry about this, wlroots takes care of it. - var texture = c.wlr_surface_get_texture(surface); - if (texture == null) { - return; - } - - // The view has a position in layout coordinates. If you have two displays, - // one next to the other, both 1080p, a view on the rightmost display might - // have layout coordinates of 2000,100. We need to translate that to - // output-local coordinates, or (2000 - 1920). - var ox: f64 = 0.0; - var oy: f64 = 0.0; - c.wlr_output_layout_output_coords(view.*.server.*.output_layout, output, &ox, &oy); - ox += @intToFloat(f64, view.*.x + sx); - oy += @intToFloat(f64, view.*.y + sy); - - // We also have to apply the scale factor for HiDPI outputs. This is only - // part of the puzzle, TinyWL does not fully support HiDPI. - var box = c.wlr_box{ - .x = @floatToInt(c_int, ox * output.*.scale), - .y = @floatToInt(c_int, oy * output.*.scale), - .width = @floatToInt(c_int, @intToFloat(f32, surface.*.current.width) * output.*.scale), - .height = @floatToInt(c_int, @intToFloat(f32, surface.*.current.height) * output.*.scale), - }; - - // Those familiar with OpenGL are also familiar with the role of matricies - // in graphics programming. We need to prepare a matrix to render the view - // with. wlr_matrix_project_box is a helper which takes a box with a desired - // x, y coordinates, width and height, and an output geometry, then - // prepares an orthographic projection and multiplies the necessary - // transforms to produce a model-view-projection matrix. - // - // Naturally you can do this any way you like, for example to make a 3D - // compositor. - var matrix: [9]f32 = undefined; - var transform = c.wlr_output_transform_invert(surface.*.current.transform); - c.wlr_matrix_project_box(&matrix, &box, transform, 0.0, &output.*.transform_matrix); - - // This takes our matrix, the texture, and an alpha, and performs the actual - // rendering on the GPU. - _ = c.wlr_render_texture_with_matrix(rdata.*.renderer, texture, &matrix, 1.0); - - // This lets the client know that we've displayed that frame and it can - // prepare another one now if it likes. - c.wlr_surface_send_frame_done(surface, rdata.*.when); -} - -fn output_frame(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { - // This function is called every time an output is ready to display a frame, - // generally at the output's refresh rate (e.g. 60Hz). - var output = @fieldParentPtr(Output, "frame", listener); - var renderer = output.*.server.*.renderer; - - var now: c.struct_timespec = undefined; - _ = c.clock_gettime(c.CLOCK_MONOTONIC, &now); - - // wlr_output_attach_render makes the OpenGL context current. - if (!c.wlr_output_attach_render(output.*.wlr_output, null)) { - return; - } - // The "effective" resolution can change if you rotate your outputs. - var width: c_int = undefined; - var height: c_int = undefined; - c.wlr_output_effective_resolution(output.*.wlr_output, &width, &height); - // Begin the renderer (calls glViewport and some other GL sanity checks) - c.wlr_renderer_begin(renderer, width, height); - - const color = [_]f32{ 0.3, 0.3, 0.3, 1.0 }; - c.wlr_renderer_clear(renderer, &color); - - // Each subsequent window we render is rendered on top of the last. Because - // our view list is ordered front-to-back, we iterate over it backwards. - for (output.*.server.views.span()) |*view| { - if (!view.*.mapped) { - // An unmapped view should not be rendered. - continue; - } - var rdata = RenderData{ - .output = output.*.wlr_output, - .view = view, - .renderer = renderer, - .when = &now, - }; - // This calls our render_surface function for each surface among the - // xdg_surface's toplevel and popups. - c.wlr_xdg_surface_for_each_surface(view.*.xdg_surface, render_surface, &rdata); - } - - // Hardware cursors are rendered by the GPU on a separate plane, and can be - // moved around without re-rendering what's beneath them - which is more - // efficient. However, not all hardware supports hardware cursors. For this - // reason, wlroots provides a software fallback, which we ask it to render - // here. wlr_cursor handles configuring hardware vs software cursors for you, - // and this function is a no-op when hardware cursors are in use. - c.wlr_output_render_software_cursors(output.*.wlr_output, null); - - // Conclude rendering and swap the buffers, showing the final frame - // on-screen. - c.wlr_renderer_end(renderer); - // TODO: handle failure - _ = c.wlr_output_commit(output.*.wlr_output); -} - -fn server_new_output(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { - var server = @fieldParentPtr(Server, "new_output", listener); - var wlr_output = @ptrCast(*c.wlr_output, @alignCast(@alignOf(*c.wlr_output), data)); - - // Some backends don't have modes. DRM+KMS does, and we need to set a mode - // before we can use the output. The mode is a tuple of (width, height, - // refresh rate), and each monitor supports only a specific set of modes. We - // just pick the monitor's preferred mode, a more sophisticated compositor - // would let the user configure it. - - // if not empty - if (c.wl_list_empty(&wlr_output.*.modes) == 0) { - var mode = c.wlr_output_preferred_mode(wlr_output); - c.wlr_output_set_mode(wlr_output, mode); - c.wlr_output_enable(wlr_output, true); - if (!c.wlr_output_commit(wlr_output)) { - return; - } - } - - // Allocates and configures our state for this output - server.*.outputs.append(Output{ - .server = undefined, - .wlr_output = undefined, - .frame = undefined, - }) catch unreachable; - var output = &server.*.outputs.span()[server.*.outputs.span().len - 1]; - output.*.wlr_output = wlr_output; - output.*.server = server; - - // Sets up a listener for the frame notify event. - output.*.frame.notify = output_frame; - c.wl_signal_add(&wlr_output.*.events.frame, &output.*.frame); - - // Adds this to the output layout. The add_auto function arranges outputs - // from left-to-right in the order they appear. A more sophisticated - // compositor would let the user configure the arrangement of outputs in the - // layout. - c.wlr_output_layout_add_auto(server.*.output_layout, wlr_output); - - // Creating the global adds a wl_output global to the display, which Wayland - // clients can see to find out information about the output (such as - // DPI, scale factor, manufacturer, etc). - c.wlr_output_create_global(wlr_output); -} - -fn focus_view(view: *View, surface: *c.wlr_surface) void { - const server = view.server; - const seat = server.*.seat; - const prev_surface = seat.*.keyboard_state.focused_surface; - - if (prev_surface == surface) { - // Don't re-focus an already focused surface. - return; - } - - if (prev_surface != null) { - // Deactivate the previously focused surface. This lets the client know - // it no longer has focus and the client will repaint accordingly, e.g. - // stop displaying a caret. - var prev_xdg_surface = c.wlr_xdg_surface_from_wlr_surface(prev_surface); - _ = c.wlr_xdg_toplevel_set_activated(prev_xdg_surface, false); - } - - // Find the index - const idx = for (server.*.views.span()) |*v, i| { - if (v == view) { - break i; - } - } else unreachable; - - // Move the view to the front - server.*.views.append(server.*.views.orderedRemove(idx)) catch unreachable; - - var moved_view = &server.*.views.span()[server.*.views.span().len - 1]; - - // Activate the new surface - _ = c.wlr_xdg_toplevel_set_activated(moved_view.*.xdg_surface, true); - - // Tell the seat to have the keyboard enter this surface. wlroots will keep - // track of this and automatically send key events to the appropriate - // clients without additional work on your part. - var keyboard = c.wlr_seat_get_keyboard(seat); - c.wlr_seat_keyboard_notify_enter(seat, moved_view.*.xdg_surface.*.surface, &keyboard.*.keycodes, keyboard.*.num_keycodes, &keyboard.*.modifiers); -} - -fn xdg_surface_map(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { - // Called when the surface is mapped, or ready to display on-screen. - var view = @fieldParentPtr(View, "map", listener); - view.*.mapped = true; - focus_view(view, view.*.xdg_surface.*.surface); -} - -fn xdg_surface_unmap(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { - var view = @fieldParentPtr(View, "unmap", listener); - view.*.mapped = false; -} - -fn xdg_surface_destroy(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { - var view = @fieldParentPtr(View, "destroy", listener); - var server = view.*.server; - const idx = for (server.*.views.span()) |*v, i| { - if (v == view) { - break i; - } - } else return; - _ = server.*.views.orderedRemove(idx); -} - -fn xdg_toplevel_request_move(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { - // ignore for now -} - -fn xdg_toplevel_request_resize(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { - // ignore for now -} - -fn server_new_xdg_surface(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { - // This event is raised when wlr_xdg_shell receives a new xdg surface from a - // client, either a toplevel (application window) or popup. - var server = @fieldParentPtr(Server, "new_xdg_surface", listener); - var xdg_surface = @ptrCast(*c.wlr_xdg_surface, @alignCast(@alignOf(*c.wlr_xdg_surface), data)); - - if (xdg_surface.*.role != c.enum_wlr_xdg_surface_role.WLR_XDG_SURFACE_ROLE_TOPLEVEL) { - return; - } - - // Allocate a View for this surface - server.*.views.append(undefined) catch unreachable; - var view = &server.*.views.span()[server.*.views.span().len - 1]; - - view.*.server = server; - view.*.xdg_surface = xdg_surface; - - // Listen to the various events it can emit - view.*.map.notify = xdg_surface_map; - c.wl_signal_add(&xdg_surface.*.events.map, &view.*.map); - - view.*.unmap.notify = xdg_surface_unmap; - c.wl_signal_add(&xdg_surface.*.events.unmap, &view.*.unmap); - - view.*.destroy.notify = xdg_surface_destroy; - c.wl_signal_add(&xdg_surface.*.events.destroy, &view.*.destroy); - - var toplevel = xdg_surface.*.unnamed_160.toplevel; - view.*.request_move.notify = xdg_toplevel_request_move; - c.wl_signal_add(&toplevel.*.events.request_move, &view.*.request_move); - - view.*.request_resize.notify = xdg_toplevel_request_resize; - c.wl_signal_add(&toplevel.*.events.request_resize, &view.*.request_resize); -} - -fn keyboard_handle_modifiers(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { - // This event is raised when a modifier key, such as shift or alt, is - // pressed. We simply communicate this to the client. */ - var keyboard = @fieldParentPtr(Keyboard, "modifiers", listener); - - // A seat can only have one keyboard, but this is a limitation of the - // Wayland protocol - not wlroots. We assign all connected keyboards to the - // same seat. You can swap out the underlying wlr_keyboard like this and - // wlr_seat handles this transparently. - c.wlr_seat_set_keyboard(keyboard.*.server.*.seat, keyboard.*.device); - - // Send modifiers to the client. - c.wlr_seat_keyboard_notify_modifiers(keyboard.*.server.*.seat, &keyboard.*.device.*.unnamed_37.keyboard.*.modifiers); -} - -fn handle_keybinding(server: *Server, sym: c.xkb_keysym_t) bool { - // Here we handle compositor keybindings. This is when the compositor is - // processing keys, rather than passing them on to the client for its own - // processing. - // - // This function assumes the proper modifier is held down. - switch (sym) { - c.XKB_KEY_Escape => c.wl_display_terminate(server.*.wl_display), - c.XKB_KEY_F1 => { - // Cycle to the next view - //if (c.wl_list_length(&server.*.views) > 1) { - // const current_view = @fieldParentPtr(View, "link", server.*.views.next); - // const next_view = @fieldParentPtr(View, "link", current_view.*.link.next); - // focus_view(next_view, next_view.*.xdg_surface.*.surface); - // // Move the previous view to the end of the list - // c.wl_list_remove(¤t_view.*.link); - // c.wl_list_insert(server.*.views.prev, ¤t_view.*.link); - //} - }, - else => return false, - } - return true; -} - -fn keyboard_handle_key(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { - // This event is raised when a key is pressed or released. - const keyboard = @fieldParentPtr(Keyboard, "key", listener); - const event = @ptrCast(*c.wlr_event_keyboard_key, @alignCast(@alignOf(*c.wlr_event_keyboard_key), data)); - - const server = keyboard.*.server; - const seat = server.*.seat; - const keyboard_device = keyboard.*.device.*.unnamed_37.keyboard; - - // Translate libinput keycode -> xkbcommon - const keycode = event.*.keycode + 8; - // Get a list of keysyms based on the keymap for this keyboard - var syms: [*c]c.xkb_keysym_t = undefined; - const nsyms = c.xkb_state_key_get_syms(keyboard_device.*.xkb_state, keycode, &syms); - - var handled = false; - const modifiers = c.wlr_keyboard_get_modifiers(keyboard_device); - if (modifiers & @intCast(u32, c.WLR_MODIFIER_LOGO) != 0 and event.*.state == c.enum_wlr_key_state.WLR_KEY_PRESSED) { - // If mod is held down and this button was _pressed_, we attempt to - // process it as a compositor keybinding. - var i: usize = 0; - while (i < nsyms) { - handled = handle_keybinding(server, syms[i]); - if (handled) { - break; - } - i += 1; - } - } - - if (!handled) { - // Otherwise, we pass it along to the client. - c.wlr_seat_set_keyboard(seat, keyboard.*.device); - c.wlr_seat_keyboard_notify_key(seat, event.*.time_msec, event.*.keycode, @intCast(u32, @enumToInt(event.*.state))); - } -} - -fn server_new_keyboard(server: *Server, device: *c.wlr_input_device) void { - var keyboard = std.heap.c_allocator.create(Keyboard) catch unreachable; - keyboard.*.server = server; - keyboard.*.device = device; - - // We need to prepare an XKB keymap and assign it to the keyboard. This - // assumes the defaults (e.g. layout = "us"). - const rules = c.xkb_rule_names{ - .rules = null, - .model = null, - .layout = null, - .variant = null, - .options = null, - }; - const context = c.xkb_context_new(c.enum_xkb_context_flags.XKB_CONTEXT_NO_FLAGS); - defer c.xkb_context_unref(context); - - const keymap = man_c.xkb_map_new_from_names(context, &rules, c.enum_xkb_keymap_compile_flags.XKB_KEYMAP_COMPILE_NO_FLAGS); - defer c.xkb_keymap_unref(keymap); - - var keyboard_device = device.*.unnamed_37.keyboard; - c.wlr_keyboard_set_keymap(keyboard_device, keymap); - c.wlr_keyboard_set_repeat_info(keyboard_device, 25, 600); - - // Setup listeners for keyboard events - keyboard.*.modifiers.notify = keyboard_handle_modifiers; - c.wl_signal_add(&keyboard_device.*.events.modifiers, &keyboard.*.modifiers); - keyboard.*.key.notify = keyboard_handle_key; - c.wl_signal_add(&keyboard_device.*.events.key, &keyboard.*.key); - - c.wlr_seat_set_keyboard(server.*.seat, device); - - // And add the keyboard to our list of keyboards - c.wl_list_insert(&server.*.keyboards, &keyboard.*.link); -} - -fn server_new_pointer(server: *Server, device: *c.struct_wlr_input_device) void { - // We don't do anything special with pointers. All of our pointer handling - // is proxied through wlr_cursor. On another compositor, you might take this - // opportunity to do libinput configuration on the device to set - // acceleration, etc. - c.wlr_cursor_attach_input_device(server.*.cursor, device); -} - -fn server_new_input(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { - // This event is raised by the backend when a new input device becomes available. - var server = @fieldParentPtr(Server, "new_input", listener); - var device = @ptrCast(*c.wlr_input_device, @alignCast(@alignOf(*c.wlr_input_device), data)); - - switch (device.*.type) { - .WLR_INPUT_DEVICE_KEYBOARD => server_new_keyboard(server, device), - .WLR_INPUT_DEVICE_POINTER => server_new_pointer(server, device), - else => {}, - } - - // We need to let the wlr_seat know what our capabilities are, which is - // communiciated to the client. In TinyWL we always have a cursor, even if - // there are no pointer devices, so we always include that capability. - var caps: u32 = @intCast(u32, c.WL_SEAT_CAPABILITY_POINTER); - // if list not empty - if (c.wl_list_empty(&server.*.keyboards) == 0) { - caps |= @intCast(u32, c.WL_SEAT_CAPABILITY_KEYBOARD); - } - c.wlr_seat_set_capabilities(server.*.seat, caps); -} - -fn seat_request_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 server = @fieldParentPtr(Server, "request_cursor", listener); - var event = @ptrCast(*c.wlr_seat_pointer_request_set_cursor_event, @alignCast(@alignOf(*c.wlr_seat_pointer_request_set_cursor_event), data)); - var focused_client = server.*.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(server.*.cursor, event.*.surface, event.*.hotspot_x, event.*.hotspot_y); - } -} - -fn view_at(view: *View, lx: f64, ly: f64, surface: *?*c.wlr_surface, sx: *f64, sy: *f64) bool { - // XDG toplevels may have nested surfaces, such as popup windows for context - // menus or tooltips. This function tests if any of those are underneath the - // coordinates lx and ly (in output Layout Coordinates). If so, it sets the - // surface pointer to that wlr_surface and the sx and sy coordinates to the - // coordinates relative to that surface's top-left corner. - var view_sx = lx - @intToFloat(f64, view.*.x); - var view_sy = ly - @intToFloat(f64, view.*.y); - - // This variable seems to have been unsued in TinyWL - // struct wlr_surface_state *state = &view->xdg_surface->surface->current; - - var _sx: f64 = undefined; - var _sy: f64 = undefined; - var _surface = c.wlr_xdg_surface_surface_at(view.*.xdg_surface, view_sx, view_sy, &_sx, &_sy); - - if (_surface) |surface_at| { - sx.* = _sx; - sy.* = _sy; - surface.* = surface_at; - return true; - } - - return false; -} - -fn desktop_view_at(server: *Server, lx: f64, ly: f64, surface: *?*c.wlr_surface, sx: *f64, sy: *f64) ?*View { - // This iterates over all of our surfaces and attempts to find one under the - // cursor. This relies on server.*.views being ordered from top-to-bottom. - for (server.*.views.span()) |*view| { - if (view_at(view, lx, ly, surface, sx, sy)) { - return view; - } - } - return null; -} - -fn process_cursor_move(server: *Server, time: u32) void { - // Move the grabbed view to the new position. - server.*.grabbed_view.?.*.x = @floatToInt(c_int, server.*.cursor.*.x - server.*.grab_x); - server.*.grabbed_view.?.*.y = @floatToInt(c_int, server.*.cursor.*.y - server.*.grab_y); -} - -fn process_cursor_resize(server: *Server, 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. - var view = server.*.grabbed_view; - - var dx: f64 = (server.*.cursor.*.x - server.*.grab_x); - var dy: f64 = (server.*.cursor.*.y - server.*.grab_y); - var x: f64 = @intToFloat(f64, view.?.*.x); - var y: f64 = @intToFloat(f64, view.?.*.y); - - var width = @intToFloat(f64, server.*.grab_width); - var height = @intToFloat(f64, server.*.grab_height); - if (server.*.resize_edges & @intCast(u32, c.WLR_EDGE_TOP) != 0) { - y = server.*.grab_y + dy; - height -= dy; - if (height < 1) { - y += height; - } - } else if (server.*.resize_edges & @intCast(u32, c.WLR_EDGE_BOTTOM) != 0) { - height += dy; - } - if (server.*.resize_edges & @intCast(u32, c.WLR_EDGE_LEFT) != 0) { - x = server.*.grab_x + dx; - width -= dx; - if (width < 1) { - x += width; - } - } else if (server.*.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.?.*.xdg_surface, @floatToInt(u32, width), @floatToInt(u32, height)); -} - -fn process_cursor_motion(server: *Server, time: u32) void { - // If the mode is non-passthrough, delegate to those functions. - if (server.*.cursor_mode == CursorMode.Move) { - process_cursor_move(server, time); - return; - } else if (server.*.cursor_mode == CursorMode.Resize) { - process_cursor_resize(server, time); - return; - } - - // Otherwise, find the view under the pointer and send the event along. - var sx: f64 = undefined; - var sy: f64 = undefined; - var seat = server.*.seat; - var opt_surface: ?*c.wlr_surface = null; - var view = desktop_view_at(server, server.*.cursor.*.x, server.*.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(server.*.cursor_mgr, "left_ptr", server.*.cursor); - } - - if (opt_surface) |surface| { - const focus_changed = 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(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(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(seat); - } -} - -fn server_cursor_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 server = @fieldParentPtr(Server, "cursor_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(server.*.cursor, event.*.device, event.*.delta_x, event.*.delta_y); - process_cursor_motion(server, event.*.time_msec); -} - -fn server_cursor_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 server = @fieldParentPtr(Server, "cursor_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(server.*.cursor, event.*.device, event.*.x, event.*.y); - process_cursor_motion(server, event.*.time_msec); -} - -fn server_cursor_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 server = @fieldParentPtr(Server, "cursor_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(server.*.seat, event.*.time_msec, event.*.button, event.*.state); - - var sx: f64 = undefined; - var sy: f64 = undefined; - - var surface: ?*c.wlr_surface = null; - var view = desktop_view_at(server, server.*.cursor.*.x, server.*.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. - server.*.cursor_mode = CursorMode.Passthrough; - } else { - // Focus that client if the button was _pressed_ - if (view) |v| { - focus_view(v, surface.?); - } - } -} - -fn server_cursor_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 server = @fieldParentPtr(Server, "cursor_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(server.*.seat, event.*.time_msec, event.*.orientation, event.*.delta, event.*.delta_discrete, event.*.source); -} - -fn server_cursor_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 server = @fieldParentPtr(Server, "cursor_frame", listener); - // Notify the client with pointer focus of the frame event. - c.wlr_seat_pointer_notify_frame(server.*.seat); -} - const ZagError = error{ InitError, CantAddSocket, @@ -727,32 +15,6 @@ pub fn main() !void { c.wlr_log_init(c.enum_wlr_log_importance.WLR_DEBUG, null); var server: Server = undefined; - // The Wayland display is managed by libwayland. It handles accepting - // clients from the Unix socket, manging Wayland globals, and so on. - server.wl_display = c.wl_display_create() orelse return ZagError.InitError; - - // The backend is a wlroots feature which abstracts the underlying input and - // output hardware. The autocreate option will choose the most suitable - // backend based on the current environment, such as opening an X11 window - // if an X11 server is running. The NULL argument here optionally allows you - // to pass in a custom renderer if wlr_renderer doesn't meet your needs. The - // backend uses the renderer, for example, to fall back to software cursors - // if the backend does not support hardware cursors (some older GPUs - // don't). - server.backend = c.zag_wlr_backend_autocreate(server.wl_display) orelse return ZagError.InitError; - - // If we don't provide a renderer, autocreate makes a GLES2 renderer for us. - // The renderer is responsible for defining the various pixel formats it - // supports for shared memory, this configures that for clients. - server.renderer = c.zag_wlr_backend_get_renderer(server.backend); - c.wlr_renderer_init_wl_display(server.renderer, server.wl_display); - - // This creates some hands-off wlroots interfaces. The compositor is - // necessary for clients to allocate surfaces and the data device manager - // handles the clipboard. Each of these wlroots interfaces has room for you - // to dig your fingers in and play with their behavior if you want. - _ = c.wlr_compositor_create(server.wl_display, server.renderer); - _ = c.wlr_data_device_manager_create(server.wl_display); // Creates an output layout, which a wlroots utility for working with an // arrangement of screens in a physical layout. @@ -773,54 +35,6 @@ pub fn main() !void { server.new_xdg_surface.notify = server_new_xdg_surface; c.wl_signal_add(&server.xdg_shell.*.events.new_surface, &server.new_xdg_surface); - // Creates a cursor, which is a wlroots utility for tracking the cursor - // image shown on screen. - server.cursor = c.wlr_cursor_create(); - c.wlr_cursor_attach_output_layout(server.cursor, server.output_layout); - - // 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. - server.cursor_mgr = c.wlr_xcursor_manager_create(null, 24); - _ = c.wlr_xcursor_manager_load(server.cursor_mgr, 1); - - // 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. More detail on this process is described in my - // input handling blog post: - // - // https://drewdevault.com/2018/07/17/Input-handling-in-wlroots.html - // - // And more comments are sprinkled throughout the notify functions above. - server.cursor_motion.notify = server_cursor_motion; - c.wl_signal_add(&server.cursor.*.events.motion, &server.cursor_motion); - - server.cursor_motion_absolute.notify = server_cursor_motion_absolute; - c.wl_signal_add(&server.cursor.*.events.motion_absolute, &server.cursor_motion_absolute); - - server.cursor_button.notify = server_cursor_button; - c.wl_signal_add(&server.cursor.*.events.button, &server.cursor_button); - - server.cursor_axis.notify = server_cursor_axis; - c.wl_signal_add(&server.cursor.*.events.axis, &server.cursor_axis); - - server.cursor_frame.notify = server_cursor_frame; - c.wl_signal_add(&server.cursor.*.events.frame, &server.cursor_frame); - - // Configures a seat, which is a single "seat" at which a user sits and - // operates the computer. This conceptually includes up to one keyboard, - // pointer, touch, and drawing tablet device. We also rig up a listener to - // let us know when new input devices are available on the backend. - c.wl_list_init(&server.keyboards); - server.new_input.notify = server_new_input; - c.wl_signal_add(&server.backend.*.events.new_input, &server.new_input); - server.seat = c.wlr_seat_create(server.wl_display, "seat0"); - server.request_cursor.notify = seat_request_cursor; - c.wl_signal_add(&server.seat.*.events.request_set_cursor, &server.request_cursor); - // Add a Unix socket to the Wayland display. const socket = c.wl_display_add_socket_auto(server.wl_display); if (socket == null) { diff --git a/src/output.zig b/src/output.zig new file mode 100644 index 0000000..1e0d421 --- /dev/null +++ b/src/output.zig @@ -0,0 +1,110 @@ +const std = @import("std"); +const c = @import("c.zig").c; + +const Output = struct { + server: *Server, + wlr_output: *c.wlr_output, + frame: c.wl_listener, +}; + +fn output_frame(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { + // This function is called every time an output is ready to display a frame, + // generally at the output's refresh rate (e.g. 60Hz). + var output = @fieldParentPtr(Output, "frame", listener); + var renderer = output.*.server.*.renderer; + + var now: c.struct_timespec = undefined; + _ = c.clock_gettime(c.CLOCK_MONOTONIC, &now); + + // wlr_output_attach_render makes the OpenGL context current. + if (!c.wlr_output_attach_render(output.*.wlr_output, null)) { + return; + } + // The "effective" resolution can change if you rotate your outputs. + var width: c_int = undefined; + var height: c_int = undefined; + c.wlr_output_effective_resolution(output.*.wlr_output, &width, &height); + // Begin the renderer (calls glViewport and some other GL sanity checks) + c.wlr_renderer_begin(renderer, width, height); + + const color = [_]f32{ 0.3, 0.3, 0.3, 1.0 }; + c.wlr_renderer_clear(renderer, &color); + + // Each subsequent window we render is rendered on top of the last. Because + // our view list is ordered front-to-back, we iterate over it backwards. + for (output.*.server.views.span()) |*view| { + if (!view.*.mapped) { + // An unmapped view should not be rendered. + continue; + } + var rdata = RenderData{ + .output = output.*.wlr_output, + .view = view, + .renderer = renderer, + .when = &now, + }; + // This calls our render_surface function for each surface among the + // xdg_surface's toplevel and popups. + c.wlr_xdg_surface_for_each_surface(view.*.xdg_surface, render_surface, &rdata); + } + + // Hardware cursors are rendered by the GPU on a separate plane, and can be + // moved around without re-rendering what's beneath them - which is more + // efficient. However, not all hardware supports hardware cursors. For this + // reason, wlroots provides a software fallback, which we ask it to render + // here. wlr_cursor handles configuring hardware vs software cursors for you, + // and this function is a no-op when hardware cursors are in use. + c.wlr_output_render_software_cursors(output.*.wlr_output, null); + + // Conclude rendering and swap the buffers, showing the final frame + // on-screen. + c.wlr_renderer_end(renderer); + // TODO: handle failure + _ = c.wlr_output_commit(output.*.wlr_output); +} + +fn server_new_output(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { + var server = @fieldParentPtr(Server, "new_output", listener); + var wlr_output = @ptrCast(*c.wlr_output, @alignCast(@alignOf(*c.wlr_output), data)); + + // Some backends don't have modes. DRM+KMS does, and we need to set a mode + // before we can use the output. The mode is a tuple of (width, height, + // refresh rate), and each monitor supports only a specific set of modes. We + // just pick the monitor's preferred mode, a more sophisticated compositor + // would let the user configure it. + + // if not empty + if (c.wl_list_empty(&wlr_output.*.modes) == 0) { + var mode = c.wlr_output_preferred_mode(wlr_output); + c.wlr_output_set_mode(wlr_output, mode); + c.wlr_output_enable(wlr_output, true); + if (!c.wlr_output_commit(wlr_output)) { + return; + } + } + + // Allocates and configures our state for this output + server.*.outputs.append(Output{ + .server = undefined, + .wlr_output = undefined, + .frame = undefined, + }) catch unreachable; + var output = &server.*.outputs.span()[server.*.outputs.span().len - 1]; + output.*.wlr_output = wlr_output; + output.*.server = server; + + // Sets up a listener for the frame notify event. + output.*.frame.notify = output_frame; + c.wl_signal_add(&wlr_output.*.events.frame, &output.*.frame); + + // Adds this to the output layout. The add_auto function arranges outputs + // from left-to-right in the order they appear. A more sophisticated + // compositor would let the user configure the arrangement of outputs in the + // layout. + c.wlr_output_layout_add_auto(server.*.output_layout, wlr_output); + + // Creating the global adds a wl_output global to the display, which Wayland + // clients can see to find out information about the output (such as + // DPI, scale factor, manufacturer, etc). + c.wlr_output_create_global(wlr_output); +} diff --git a/src/render.zig b/src/render.zig new file mode 100644 index 0000000..dc03de1 --- /dev/null +++ b/src/render.zig @@ -0,0 +1,63 @@ +const RenderData = struct { + output: *c.wlr_output, + renderer: *c.wlr_renderer, + view: *View, + when: *c.struct_timespec, +}; + +fn render_surface(surface: [*c]c.wlr_surface, sx: c_int, sy: c_int, data: ?*c_void) callconv(.C) void { + // This function is called for every surface that needs to be rendered. + var rdata = @ptrCast(*RenderData, @alignCast(@alignOf(RenderData), data)); + var view = rdata.*.view; + var output = rdata.*.output; + + // We first obtain a wlr_texture, which is a GPU resource. wlroots + // automatically handles negotiating these with the client. The underlying + // resource could be an opaque handle passed from the client, or the client + // could have sent a pixel buffer which we copied to the GPU, or a few other + // means. You don't have to worry about this, wlroots takes care of it. + var texture = c.wlr_surface_get_texture(surface); + if (texture == null) { + return; + } + + // The view has a position in layout coordinates. If you have two displays, + // one next to the other, both 1080p, a view on the rightmost display might + // have layout coordinates of 2000,100. We need to translate that to + // output-local coordinates, or (2000 - 1920). + var ox: f64 = 0.0; + var oy: f64 = 0.0; + c.wlr_output_layout_output_coords(view.*.server.*.output_layout, output, &ox, &oy); + ox += @intToFloat(f64, view.*.x + sx); + oy += @intToFloat(f64, view.*.y + sy); + + // We also have to apply the scale factor for HiDPI outputs. This is only + // part of the puzzle, TinyWL does not fully support HiDPI. + var box = c.wlr_box{ + .x = @floatToInt(c_int, ox * output.*.scale), + .y = @floatToInt(c_int, oy * output.*.scale), + .width = @floatToInt(c_int, @intToFloat(f32, surface.*.current.width) * output.*.scale), + .height = @floatToInt(c_int, @intToFloat(f32, surface.*.current.height) * output.*.scale), + }; + + // Those familiar with OpenGL are also familiar with the role of matricies + // in graphics programming. We need to prepare a matrix to render the view + // with. wlr_matrix_project_box is a helper which takes a box with a desired + // x, y coordinates, width and height, and an output geometry, then + // prepares an orthographic projection and multiplies the necessary + // transforms to produce a model-view-projection matrix. + // + // Naturally you can do this any way you like, for example to make a 3D + // compositor. + var matrix: [9]f32 = undefined; + var transform = c.wlr_output_transform_invert(surface.*.current.transform); + c.wlr_matrix_project_box(&matrix, &box, transform, 0.0, &output.*.transform_matrix); + + // This takes our matrix, the texture, and an alpha, and performs the actual + // rendering on the GPU. + _ = c.wlr_render_texture_with_matrix(rdata.*.renderer, texture, &matrix, 1.0); + + // This lets the client know that we've displayed that frame and it can + // prepare another one now if it likes. + c.wlr_surface_send_frame_done(surface, rdata.*.when); +} diff --git a/src/seat.zig b/src/seat.zig new file mode 100644 index 0000000..7ec9b07 --- /dev/null +++ b/src/seat.zig @@ -0,0 +1,71 @@ +const std = @import("std"); +const c = @import("c.zig").c; + +// TODO: InputManager and multi-seat support +pub const Seat = struct { + server: *Server, + + wlr_seat: *c.wlr_seat, + listen_new_input: c.wl_listener, + + // Multiple mice are handled by the same Cursor + cursor: Cursor, + // Mulitple keyboards are handled separately + keyboards: std.ArrayList(Keyboard), + + pub fn init(server: *Server, allocator: *std.mem.Allocator) @This() { + var seat = @This(){ + .server = server, + // This seems to be the default seat name used by compositors + .wlr_seat = c.wlr_seat_create(server.*.wl_display, "seat0"), + .cursor = undefined, + .keyboards = std.ArrayList(Keyboard).init(allocator), + + .listen_new_input = c.wl_listener{ + .link = undefined, + .notify = handle_new_input, + }, + }; + + seat.cursor = cursor.Cursor.init(server); + + // Set up handler for all new input devices made available. This + // includes keyboards, pointers, touch, etc. + c.wl_signal_add(&server.*.backend.*.events.new_input, &seat.new_input); + } + + fn add_keyboard(self: *@This(), device: *c.wlr_input_device) void { + self.keyboards.append(Keyboard.init(self, device)); + c.wlr_seat_set_keyboard(self, device); + } + + fn add_pointer(self: *@This(), device: *c.struct_wlr_input_device) void { + // We don't do anything special with pointers. All of our pointer handling + // is proxied through wlr_cursor. On another compositor, you might take this + // opportunity to do libinput configuration on the device to set + // acceleration, etc. + c.wlr_cursor_attach_input_device(self.cursor.wlr_cursor, device); + } + + fn handle_new_input(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { + // This event is raised by the backend when a new input device becomes available. + var seat = @fieldParentPtr(Seat, "listen_new_input", listener); + var device = @ptrCast(*c.wlr_input_device, @alignCast(@alignOf(*c.wlr_input_device), data)); + + switch (device.*.type) { + .WLR_INPUT_DEVICE_KEYBOARD => seat.add_keyboard(device), + .WLR_INPUT_DEVICE_POINTER => seat.add_pointer(device), + else => {}, + } + + // We need to let the wlr_seat know what our capabilities are, which is + // communiciated to the client. In TinyWL we always have a cursor, even if + // there are no pointer devices, so we always include that capability. + var caps: u32 = @intCast(u32, c.WL_SEAT_CAPABILITY_POINTER); + // if list not empty + if (c.wl_list_empty(&server.*.keyboards) == 0) { + caps |= @intCast(u32, c.WL_SEAT_CAPABILITY_KEYBOARD); + } + c.wlr_seat_set_capabilities(server.*.seat, caps); + } +}; diff --git a/src/server.zig b/src/server.zig new file mode 100644 index 0000000..fc1fef0 --- /dev/null +++ b/src/server.zig @@ -0,0 +1,71 @@ +const std = @import("std"); +const c = @import("c.zig").c; + +pub const Server = struct { + wl_display: *c.wl_display, + backend: *c.wlr_backend, + renderer: *c.wlr_renderer, + xdg_shell: *c.wlr_xdg_shell, + new_xdg_surface: c.wl_listener, + views: std.ArrayList(View), + + output_layout: *c.wlr_output_layout, + outputs: std.ArrayList(Output), + new_output: c.wl_listener, + + pub fn init(allocator: *std.mem.Allocator) !@This() { + var server: @This() = undefined; + + // The Wayland display is managed by libwayland. It handles accepting + // clients from the Unix socket, manging Wayland globals, and so on. + server.wl_display = c.wl_display_create() orelse return error.CantCreateWlDisplay; + + // The backend is a wlroots feature which abstracts the underlying input and + // output hardware. The autocreate option will choose the most suitable + // backend based on the current environment, such as opening an X11 window + // if an X11 server is running. The NULL argument here optionally allows you + // to pass in a custom renderer if wlr_renderer doesn't meet your needs. The + // backend uses the renderer, for example, to fall back to software cursors + // if the backend does not support hardware cursors (some older GPUs + // don't). + server.backend = c.zag_wlr_backend_autocreate(server.wl_display) orelse return error.CantCreateWlrBackend; + + // If we don't provide a renderer, autocreate makes a GLES2 renderer for us. + // The renderer is responsible for defining the various pixel formats it + // supports for shared memory, this configures that for clients. + server.renderer = c.zag_wlr_backend_get_renderer(server.backend) orelse return error.CantGetWlrRenderer; + c.wlr_renderer_init_wl_display(server.renderer, server.wl_display) orelse return error.CantInitWlDisplay; + + // This creates some hands-off wlroots interfaces. The compositor is + // necessary for clients to allocate surfaces and the data device manager + // handles the clipboard. Each of these wlroots interfaces has room for you + // to dig your fingers in and play with their behavior if you want. + _ = c.wlr_compositor_create(server.wl_display, server.renderer) orelse return error.CantCreateWlrCompositor; + _ = c.wlr_data_device_manager_create(server.wl_display) orelse return error.CantCreateWlrDataDeviceManager; + } + + pub fn handle_keybinding(self: *@This(), sym: c.xkb_keysym_t) bool { + // Here we handle compositor keybindings. This is when the compositor is + // processing keys, rather than passing them on to the client for its own + // processing. + // + // This function assumes the proper modifier is held down. + switch (sym) { + c.XKB_KEY_Escape => c.wl_display_terminate(server.*.wl_display), + c.XKB_KEY_F1 => { + // Cycle to the next view + //if (c.wl_list_length(&server.*.views) > 1) { + // const current_view = @fieldParentPtr(View, "link", server.*.views.next); + // const next_view = @fieldParentPtr(View, "link", current_view.*.link.next); + // focus_view(next_view, next_view.*.xdg_surface.*.surface); + // // Move the previous view to the end of the list + // c.wl_list_remove(¤t_view.*.link); + // c.wl_list_insert(server.*.views.prev, ¤t_view.*.link); + //} + }, + else => return false, + } + return true; + } + +}; diff --git a/src/view.zig b/src/view.zig new file mode 100644 index 0000000..0156734 --- /dev/null +++ b/src/view.zig @@ -0,0 +1,92 @@ +const std = @import("std"); +const c = @import("c.zig").c; + +pub const View = struct { + server: *Server, + xdg_surface: *c.wlr_xdg_surface, + map: c.wl_listener, + unmap: c.wl_listener, + destroy: c.wl_listener, + request_move: c.wl_listener, + request_resize: c.wl_listener, + mapped: bool, + x: c_int, + y: c_int, +}; + +fn focus_view(view: *View, surface: *c.wlr_surface) void { + const server = view.server; + const seat = server.*.seat; + const prev_surface = seat.*.keyboard_state.focused_surface; + + if (prev_surface == surface) { + // Don't re-focus an already focused surface. + return; + } + + if (prev_surface != null) { + // Deactivate the previously focused surface. This lets the client know + // it no longer has focus and the client will repaint accordingly, e.g. + // stop displaying a caret. + var prev_xdg_surface = c.wlr_xdg_surface_from_wlr_surface(prev_surface); + _ = c.wlr_xdg_toplevel_set_activated(prev_xdg_surface, false); + } + + // Find the index + const idx = for (server.*.views.span()) |*v, i| { + if (v == view) { + break i; + } + } else unreachable; + + // Move the view to the front + server.*.views.append(server.*.views.orderedRemove(idx)) catch unreachable; + + var moved_view = &server.*.views.span()[server.*.views.span().len - 1]; + + // Activate the new surface + _ = c.wlr_xdg_toplevel_set_activated(moved_view.*.xdg_surface, true); + + // Tell the seat to have the keyboard enter this surface. wlroots will keep + // track of this and automatically send key events to the appropriate + // clients without additional work on your part. + var keyboard = c.wlr_seat_get_keyboard(seat); + c.wlr_seat_keyboard_notify_enter(seat, moved_view.*.xdg_surface.*.surface, &keyboard.*.keycodes, keyboard.*.num_keycodes, &keyboard.*.modifiers); +} + +fn view_at(view: *View, lx: f64, ly: f64, surface: *?*c.wlr_surface, sx: *f64, sy: *f64) bool { + // XDG toplevels may have nested surfaces, such as popup windows for context + // menus or tooltips. This function tests if any of those are underneath the + // coordinates lx and ly (in output Layout Coordinates). If so, it sets the + // surface pointer to that wlr_surface and the sx and sy coordinates to the + // coordinates relative to that surface's top-left corner. + var view_sx = lx - @intToFloat(f64, view.*.x); + var view_sy = ly - @intToFloat(f64, view.*.y); + + // This variable seems to have been unsued in TinyWL + // struct wlr_surface_state *state = &view->xdg_surface->surface->current; + + var _sx: f64 = undefined; + var _sy: f64 = undefined; + var _surface = c.wlr_xdg_surface_surface_at(view.*.xdg_surface, view_sx, view_sy, &_sx, &_sy); + + if (_surface) |surface_at| { + sx.* = _sx; + sy.* = _sy; + surface.* = surface_at; + return true; + } + + return false; +} + +fn desktop_view_at(server: *Server, lx: f64, ly: f64, surface: *?*c.wlr_surface, sx: *f64, sy: *f64) ?*View { + // This iterates over all of our surfaces and attempts to find one under the + // cursor. This relies on server.*.views being ordered from top-to-bottom. + for (server.*.views.span()) |*view| { + if (view_at(view, lx, ly, surface, sx, sy)) { + return view; + } + } + return null; +} diff --git a/src/xdg_shell.zig b/src/xdg_shell.zig new file mode 100644 index 0000000..3bcc736 --- /dev/null +++ b/src/xdg_shell.zig @@ -0,0 +1,68 @@ +const std = @import("std"); +const c = @import("c.zig").c; + +fn xdg_surface_map(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { + // Called when the surface is mapped, or ready to display on-screen. + var view = @fieldParentPtr(View, "map", listener); + view.*.mapped = true; + focus_view(view, view.*.xdg_surface.*.surface); +} + +fn xdg_surface_unmap(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { + var view = @fieldParentPtr(View, "unmap", listener); + view.*.mapped = false; +} + +fn xdg_surface_destroy(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { + var view = @fieldParentPtr(View, "destroy", listener); + var server = view.*.server; + const idx = for (server.*.views.span()) |*v, i| { + if (v == view) { + break i; + } + } else return; + _ = server.*.views.orderedRemove(idx); +} + +fn xdg_toplevel_request_move(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { + // ignore for now +} + +fn xdg_toplevel_request_resize(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { + // ignore for now +} + +fn server_new_xdg_surface(listener: [*c]c.wl_listener, data: ?*c_void) callconv(.C) void { + // This event is raised when wlr_xdg_shell receives a new xdg surface from a + // client, either a toplevel (application window) or popup. + var server = @fieldParentPtr(Server, "new_xdg_surface", listener); + var xdg_surface = @ptrCast(*c.wlr_xdg_surface, @alignCast(@alignOf(*c.wlr_xdg_surface), data)); + + if (xdg_surface.*.role != c.enum_wlr_xdg_surface_role.WLR_XDG_SURFACE_ROLE_TOPLEVEL) { + return; + } + + // Allocate a View for this surface + server.*.views.append(undefined) catch unreachable; + var view = &server.*.views.span()[server.*.views.span().len - 1]; + + view.*.server = server; + view.*.xdg_surface = xdg_surface; + + // Listen to the various events it can emit + view.*.map.notify = xdg_surface_map; + c.wl_signal_add(&xdg_surface.*.events.map, &view.*.map); + + view.*.unmap.notify = xdg_surface_unmap; + c.wl_signal_add(&xdg_surface.*.events.unmap, &view.*.unmap); + + view.*.destroy.notify = xdg_surface_destroy; + c.wl_signal_add(&xdg_surface.*.events.destroy, &view.*.destroy); + + var toplevel = xdg_surface.*.unnamed_160.toplevel; + view.*.request_move.notify = xdg_toplevel_request_move; + c.wl_signal_add(&toplevel.*.events.request_move, &view.*.request_move); + + view.*.request_resize.notify = xdg_toplevel_request_resize; + c.wl_signal_add(&toplevel.*.events.request_resize, &view.*.request_resize); +}