input-method-v2: Implement popups
This commit is contained in:
		
							
								
								
									
										232
									
								
								river/InputMethodPopup.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								river/InputMethodPopup.zig
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,232 @@ | |||||||
|  | // This file is part of river, a dynamic tiling wayland compositor. | ||||||
|  | // | ||||||
|  | // Copyright 2024 The River Developers | ||||||
|  | // | ||||||
|  | // This program is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // This program is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  | // GNU General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU General Public License | ||||||
|  | // along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | const InputMethodPopup = @This(); | ||||||
|  |  | ||||||
|  | const build_options = @import("build_options"); | ||||||
|  | const std = @import("std"); | ||||||
|  | const assert = std.debug.assert; | ||||||
|  | const mem = std.mem; | ||||||
|  | const wlr = @import("wlroots"); | ||||||
|  | const wl = @import("wayland").server.wl; | ||||||
|  | const server = &@import("main.zig").server; | ||||||
|  |  | ||||||
|  | const util = @import("util.zig"); | ||||||
|  | const InputRelay = @import("InputRelay.zig"); | ||||||
|  | const TextInput = @import("TextInput.zig"); | ||||||
|  | const Root = @import("Root.zig"); | ||||||
|  | const View = @import("View.zig"); | ||||||
|  | const LayerSurface = @import("LayerSurface.zig"); | ||||||
|  | const XdgToplevel = @import("XdgToplevel.zig"); | ||||||
|  | const XwaylandView = @import("XwaylandView.zig"); | ||||||
|  |  | ||||||
|  | const log = std.log.scoped(.input_method_popup); | ||||||
|  |  | ||||||
|  | link: wl.list.Link, | ||||||
|  | scene_tree: ?*wlr.SceneTree = null, | ||||||
|  | parent_scene_tree: ?*wlr.SceneTree = null, | ||||||
|  | scene_surface: ?*wlr.SceneTree = null, | ||||||
|  | view: ?*View = null, | ||||||
|  |  | ||||||
|  | input_relay: *InputRelay, | ||||||
|  | wlr_input_popup_surface: *wlr.InputPopupSurfaceV2, | ||||||
|  |  | ||||||
|  | popup_surface_commit: wl.Listener(*wlr.Surface) = | ||||||
|  |     wl.Listener(*wlr.Surface).init(handlePopupSurfaceCommit), | ||||||
|  |  | ||||||
|  | popup_surface_map: wl.Listener(void) = | ||||||
|  |     wl.Listener(void).init(handlePopupSurfaceMap), | ||||||
|  |  | ||||||
|  | popup_surface_unmap: wl.Listener(void) = | ||||||
|  |     wl.Listener(void).init(handlePopupSurfaceUnmap), | ||||||
|  | popup_destroy: wl.Listener(void) = | ||||||
|  |     wl.Listener(void).init(handlePopupDestroy), | ||||||
|  |  | ||||||
|  | pub fn create(wlr_input_popup_surface: *wlr.InputPopupSurfaceV2, input_relay: *InputRelay) !void { | ||||||
|  |     const input_method_popup = try util.gpa.create(InputMethodPopup); | ||||||
|  |     errdefer util.gpa.destroy(input_method_popup); | ||||||
|  |     log.debug("new input_method_pupup", .{}); | ||||||
|  |     input_method_popup.* = .{ | ||||||
|  |         .link = undefined, | ||||||
|  |         .input_relay = input_relay, | ||||||
|  |         .wlr_input_popup_surface = wlr_input_popup_surface, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     input_method_popup.wlr_input_popup_surface.events.destroy.add(&input_method_popup.popup_destroy); | ||||||
|  |     input_method_popup.wlr_input_popup_surface.surface.events.map.add(&input_method_popup.popup_surface_map); | ||||||
|  |     input_method_popup.wlr_input_popup_surface.surface.events.unmap.add(&input_method_popup.popup_surface_unmap); | ||||||
|  |     input_method_popup.wlr_input_popup_surface.surface.events.commit.add(&input_method_popup.popup_surface_commit); | ||||||
|  |     input_relay.input_method_popups.append(input_method_popup); | ||||||
|  |     input_method_popup.updatePopup(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn handlePopupDestroy(listener: *wl.Listener(void)) void { | ||||||
|  |     log.debug("destroy ime_popup", .{}); | ||||||
|  |     const input_method_popup = @fieldParentPtr(InputMethodPopup, "popup_destroy", listener); | ||||||
|  |     input_method_popup.popup_surface_map.link.remove(); | ||||||
|  |     input_method_popup.popup_surface_unmap.link.remove(); | ||||||
|  |     input_method_popup.popup_surface_commit.link.remove(); | ||||||
|  |     input_method_popup.popup_destroy.link.remove(); | ||||||
|  |     input_method_popup.link.remove(); | ||||||
|  |     util.gpa.destroy(input_method_popup); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn handlePopupSurfaceCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void { | ||||||
|  |     log.debug("popup surface commit", .{}); | ||||||
|  |     const input_method_popup = @fieldParentPtr(InputMethodPopup, "popup_surface_commit", listener); | ||||||
|  |     input_method_popup.updatePopup(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn handlePopupSurfaceMap(listener: *wl.Listener(void)) void { | ||||||
|  |     log.debug("popup surface map", .{}); | ||||||
|  |     const input_method_popup = @fieldParentPtr(InputMethodPopup, "popup_surface_map", listener); | ||||||
|  |     input_method_popup.updatePopup(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn handlePopupSurfaceUnmap(listener: *wl.Listener(void)) void { | ||||||
|  |     log.debug("popup surface unmap", .{}); | ||||||
|  |     const input_method_popup = @fieldParentPtr(InputMethodPopup, "popup_surface_unmap", listener); | ||||||
|  |     input_method_popup.scene_tree.?.node.destroy(); | ||||||
|  |     input_method_popup.scene_tree = null; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn updatePopup(input_method_popup: *InputMethodPopup) void { | ||||||
|  |     log.debug("update ime_popup", .{}); | ||||||
|  |     var text_input = input_method_popup.getTextInputFocused() orelse return; | ||||||
|  |     const focused_surface = text_input.wlr_text_input.focused_surface orelse return; | ||||||
|  |  | ||||||
|  |     if (!input_method_popup.wlr_input_popup_surface.surface.mapped) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     var output_box: wlr.Box = undefined; | ||||||
|  |     var parent: wlr.Box = undefined; | ||||||
|  |  | ||||||
|  |     input_method_popup.getParentAndOutputBox(focused_surface, &parent, &output_box); | ||||||
|  |  | ||||||
|  |     var cursor_rect = if (text_input.wlr_text_input.current.features.cursor_rectangle) | ||||||
|  |         text_input.wlr_text_input.current.cursor_rectangle | ||||||
|  |     else | ||||||
|  |         wlr.Box{ | ||||||
|  |             .x = 0, | ||||||
|  |             .y = 0, | ||||||
|  |             .width = parent.width, | ||||||
|  |             .height = parent.height, | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |     const popup_width = input_method_popup.wlr_input_popup_surface.surface.current.width; | ||||||
|  |     const popup_height = input_method_popup.wlr_input_popup_surface.surface.current.height; | ||||||
|  |  | ||||||
|  |     const cursor_rect_left = parent.x + cursor_rect.x; | ||||||
|  |     const popup_anchor_left = blk: { | ||||||
|  |         const cursor_rect_right = cursor_rect_left + cursor_rect.width; | ||||||
|  |         const available_right_of_cursor = output_box.x + output_box.width - cursor_rect_left; | ||||||
|  |         const available_left_of_cursor = cursor_rect_right - output_box.x; | ||||||
|  |         if (available_right_of_cursor < popup_width and available_left_of_cursor > popup_width) { | ||||||
|  |             break :blk cursor_rect_right - popup_width; | ||||||
|  |         } else { | ||||||
|  |             break :blk cursor_rect_left; | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const cursor_rect_up = parent.y + cursor_rect.y; | ||||||
|  |     const popup_anchor_up = blk: { | ||||||
|  |         const cursor_rect_down = cursor_rect_up + cursor_rect.height; | ||||||
|  |         const available_down_of_cursor = output_box.y + output_box.height - cursor_rect_down; | ||||||
|  |         const available_up_of_cursor = cursor_rect_up - output_box.y; | ||||||
|  |         if (available_down_of_cursor < popup_height and available_up_of_cursor > popup_height) { | ||||||
|  |             break :blk cursor_rect_up - popup_height; | ||||||
|  |         } else { | ||||||
|  |             break :blk cursor_rect_down; | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if (text_input.wlr_text_input.current.features.cursor_rectangle) { | ||||||
|  |         var box = wlr.Box{ | ||||||
|  |             .x = cursor_rect_left - popup_anchor_left, | ||||||
|  |             .y = cursor_rect_up - popup_anchor_up, | ||||||
|  |             .width = cursor_rect.width, | ||||||
|  |             .height = cursor_rect.height, | ||||||
|  |         }; | ||||||
|  |         input_method_popup.wlr_input_popup_surface.sendTextInputRectangle(&box); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (input_method_popup.scene_tree == null) { | ||||||
|  |         input_method_popup.scene_tree = input_method_popup.parent_scene_tree.?.createSceneTree() catch { | ||||||
|  |             log.err("out of memory", .{}); | ||||||
|  |             return; | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         input_method_popup.scene_surface = input_method_popup.scene_tree.? | ||||||
|  |             .createSceneSubsurfaceTree( | ||||||
|  |             input_method_popup.wlr_input_popup_surface.surface, | ||||||
|  |         ) catch { | ||||||
|  |             log.err("failed to create subsurface tree", .{}); | ||||||
|  |             input_method_popup.wlr_input_popup_surface.surface.resource.getClient().postNoMemory(); | ||||||
|  |             return; | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |     input_method_popup.scene_tree.?.node.setPosition(popup_anchor_left - parent.x, popup_anchor_up - parent.y); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn getTextInputFocused(input_method_popup: *InputMethodPopup) ?*TextInput { | ||||||
|  |     var it = input_method_popup.input_relay.text_inputs.iterator(.forward); | ||||||
|  |     while (it.next()) |text_input| { | ||||||
|  |         if (text_input.wlr_text_input.focused_surface != null) return text_input; | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn getParentAndOutputBox( | ||||||
|  |     input_method_popup: *InputMethodPopup, | ||||||
|  |     focused_surface: *wlr.Surface, | ||||||
|  |     parent: *wlr.Box, | ||||||
|  |     output_box: *wlr.Box, | ||||||
|  | ) void { | ||||||
|  |     if (wlr.LayerSurfaceV1.tryFromWlrSurface(focused_surface)) |wlr_layer_surface| { | ||||||
|  |         const layer_surface: *LayerSurface = @ptrFromInt(wlr_layer_surface.data); | ||||||
|  |         input_method_popup.parent_scene_tree = layer_surface.popup_tree; | ||||||
|  |         const output = layer_surface.output.wlr_output; | ||||||
|  |         server.root.output_layout.getBox(output, output_box); | ||||||
|  |         _ = layer_surface.popup_tree.node.coords(&parent.x, &parent.y); | ||||||
|  |     } else { | ||||||
|  |         const view = getViewFromWlrSurface(focused_surface) orelse return; | ||||||
|  |         input_method_popup.parent_scene_tree = view.tree; | ||||||
|  |         _ = view.tree.node.coords(&parent.x, &parent.y); | ||||||
|  |         const output = view.current.output orelse return; | ||||||
|  |         server.root.output_layout.getBox(output.wlr_output, output_box); | ||||||
|  |         parent.width = view.current.box.width; | ||||||
|  |         parent.height = view.current.box.height; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn getViewFromWlrSurface(wlr_surface: *wlr.Surface) ?*View { | ||||||
|  |     if (wlr.XdgSurface.tryFromWlrSurface(wlr_surface)) |xdg_surface| { | ||||||
|  |         const xdg_toplevel: *XdgToplevel = @ptrFromInt(xdg_surface.data); | ||||||
|  |         return xdg_toplevel.view; | ||||||
|  |     } | ||||||
|  |     if (build_options.xwayland) { | ||||||
|  |         if (wlr.XwaylandSurface.tryFromWlrSurface(wlr_surface)) |xwayland_surface| { | ||||||
|  |             const xwayland_view: *XwaylandView = @ptrFromInt(xwayland_surface.data); | ||||||
|  |             return xwayland_view.view; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     if (wlr.Subsurface.tryFromWlrSurface(wlr_surface)) |wlr_subsurface| { | ||||||
|  |         if (wlr_subsurface.parent) |parent| return getViewFromWlrSurface(parent); | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  | } | ||||||
| @ -26,6 +26,7 @@ const wl = @import("wayland").server.wl; | |||||||
| const util = @import("util.zig"); | const util = @import("util.zig"); | ||||||
|  |  | ||||||
| const TextInput = @import("TextInput.zig"); | const TextInput = @import("TextInput.zig"); | ||||||
|  | const InputMethodPopup = @import("InputMethodPopup.zig"); | ||||||
| const Seat = @import("Seat.zig"); | const Seat = @import("Seat.zig"); | ||||||
|  |  | ||||||
| const log = std.log.scoped(.input_relay); | const log = std.log.scoped(.input_relay); | ||||||
| @ -40,6 +41,7 @@ text_inputs: wl.list.Head(TextInput, .link), | |||||||
| /// already in use new input methods are ignored. | /// already in use new input methods are ignored. | ||||||
| /// If this is null, no text input enter events will be sent. | /// If this is null, no text input enter events will be sent. | ||||||
| input_method: ?*wlr.InputMethodV2 = null, | input_method: ?*wlr.InputMethodV2 = null, | ||||||
|  | input_method_popups: wl.list.Head(InputMethodPopup, .link), | ||||||
| /// The currently enabled text input for the currently focused surface. | /// The currently enabled text input for the currently focused surface. | ||||||
| /// Always null if there is no input method. | /// Always null if there is no input method. | ||||||
| text_input: ?*TextInput = null, | text_input: ?*TextInput = null, | ||||||
| @ -50,14 +52,17 @@ grab_keyboard: wl.Listener(*wlr.InputMethodV2.KeyboardGrab) = | |||||||
|     wl.Listener(*wlr.InputMethodV2.KeyboardGrab).init(handleInputMethodGrabKeyboard), |     wl.Listener(*wlr.InputMethodV2.KeyboardGrab).init(handleInputMethodGrabKeyboard), | ||||||
| input_method_destroy: wl.Listener(*wlr.InputMethodV2) = | input_method_destroy: wl.Listener(*wlr.InputMethodV2) = | ||||||
|     wl.Listener(*wlr.InputMethodV2).init(handleInputMethodDestroy), |     wl.Listener(*wlr.InputMethodV2).init(handleInputMethodDestroy), | ||||||
|  | input_method_new_popup_surface: wl.Listener(*wlr.InputPopupSurfaceV2) = | ||||||
|  |     wl.Listener(*wlr.InputPopupSurfaceV2).init(handleInputMethodNewPopupSurface), | ||||||
|  |  | ||||||
| grab_keyboard_destroy: wl.Listener(*wlr.InputMethodV2.KeyboardGrab) = | grab_keyboard_destroy: wl.Listener(*wlr.InputMethodV2.KeyboardGrab) = | ||||||
|     wl.Listener(*wlr.InputMethodV2.KeyboardGrab).init(handleInputMethodGrabKeyboardDestroy), |     wl.Listener(*wlr.InputMethodV2.KeyboardGrab).init(handleInputMethodGrabKeyboardDestroy), | ||||||
|  |  | ||||||
| pub fn init(relay: *InputRelay) void { | pub fn init(relay: *InputRelay) void { | ||||||
|     relay.* = .{ .text_inputs = undefined }; |     relay.* = .{ .text_inputs = undefined, .input_method_popups = undefined }; | ||||||
|  |  | ||||||
|     relay.text_inputs.init(); |     relay.text_inputs.init(); | ||||||
|  |     relay.input_method_popups.init(); | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn newInputMethod(relay: *InputRelay, input_method: *wlr.InputMethodV2) void { | pub fn newInputMethod(relay: *InputRelay, input_method: *wlr.InputMethodV2) void { | ||||||
| @ -77,6 +82,7 @@ pub fn newInputMethod(relay: *InputRelay, input_method: *wlr.InputMethodV2) void | |||||||
|     input_method.events.commit.add(&relay.input_method_commit); |     input_method.events.commit.add(&relay.input_method_commit); | ||||||
|     input_method.events.grab_keyboard.add(&relay.grab_keyboard); |     input_method.events.grab_keyboard.add(&relay.grab_keyboard); | ||||||
|     input_method.events.destroy.add(&relay.input_method_destroy); |     input_method.events.destroy.add(&relay.input_method_destroy); | ||||||
|  |     input_method.events.new_popup_surface.add(&relay.input_method_new_popup_surface); | ||||||
|  |  | ||||||
|     if (seat.focused.surface()) |surface| { |     if (seat.focused.surface()) |surface| { | ||||||
|         relay.focus(surface); |         relay.focus(surface); | ||||||
| @ -127,7 +133,7 @@ fn handleInputMethodDestroy( | |||||||
|     relay.input_method_commit.link.remove(); |     relay.input_method_commit.link.remove(); | ||||||
|     relay.grab_keyboard.link.remove(); |     relay.grab_keyboard.link.remove(); | ||||||
|     relay.input_method_destroy.link.remove(); |     relay.input_method_destroy.link.remove(); | ||||||
|  |     relay.input_method_new_popup_surface.link.remove(); | ||||||
|     relay.input_method = null; |     relay.input_method = null; | ||||||
|  |  | ||||||
|     relay.focus(null); |     relay.focus(null); | ||||||
| @ -148,6 +154,18 @@ fn handleInputMethodGrabKeyboard( | |||||||
|     keyboard_grab.events.destroy.add(&relay.grab_keyboard_destroy); |     keyboard_grab.events.destroy.add(&relay.grab_keyboard_destroy); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | fn handleInputMethodNewPopupSurface( | ||||||
|  |     listener: *wl.Listener(*wlr.InputPopupSurfaceV2), | ||||||
|  |     input_method_new_popup_surface: *wlr.InputPopupSurfaceV2, | ||||||
|  | ) void { | ||||||
|  |     log.debug("new input_method_popup_surface", .{}); | ||||||
|  |     const relay = @fieldParentPtr(InputRelay, "input_method_new_popup_surface", listener); | ||||||
|  |     InputMethodPopup.create(input_method_new_popup_surface, relay) catch { | ||||||
|  |         log.err("out of memory", .{}); | ||||||
|  |         return; | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  |  | ||||||
| fn handleInputMethodGrabKeyboardDestroy( | fn handleInputMethodGrabKeyboardDestroy( | ||||||
|     listener: *wl.Listener(*wlr.InputMethodV2.KeyboardGrab), |     listener: *wl.Listener(*wlr.InputMethodV2.KeyboardGrab), | ||||||
|     keyboard_grab: *wlr.InputMethodV2.KeyboardGrab, |     keyboard_grab: *wlr.InputMethodV2.KeyboardGrab, | ||||||
| @ -197,6 +215,11 @@ pub fn sendInputMethodState(relay: *InputRelay) void { | |||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // Update input popups | ||||||
|  |     var it = relay.input_method_popups.iterator(.forward); | ||||||
|  |     while (it.next()) |popup| { | ||||||
|  |         popup.updatePopup(); | ||||||
|  |     } | ||||||
|     input_method.sendDone(); |     input_method.sendDone(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -163,6 +163,7 @@ pub fn handleMap(listener: *wl.Listener(void)) void { | |||||||
|     const view = xwayland_view.view; |     const view = xwayland_view.view; | ||||||
|  |  | ||||||
|     const xwayland_surface = xwayland_view.xwayland_surface; |     const xwayland_surface = xwayland_view.xwayland_surface; | ||||||
|  |     xwayland_surface.data = @intFromPtr(xwayland_view); | ||||||
|     const surface = xwayland_surface.surface.?; |     const surface = xwayland_surface.surface.?; | ||||||
|     surface.data = @intFromPtr(&view.tree.node); |     surface.data = @intFromPtr(&view.tree.node); | ||||||
|  |  | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user