From 7db9ade574f63eda787221703191a7925b79a3b8 Mon Sep 17 00:00:00 2001 From: leviathan <1041281842@qq.com> Date: Mon, 1 Apr 2024 11:37:30 +0800 Subject: [PATCH] input-method-v2: Implement popups (cherry picked from commit 74baf7225ad95f8288f5dc1fe50345425cd62f71) --- river/InputMethodPopup.zig | 232 +++++++++++++++++++++++++++++++++++++ river/InputRelay.zig | 27 ++++- river/XwaylandView.zig | 1 + 3 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 river/InputMethodPopup.zig diff --git a/river/InputMethodPopup.zig b/river/InputMethodPopup.zig new file mode 100644 index 0000000..8719b1c --- /dev/null +++ b/river/InputMethodPopup.zig @@ -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 . + +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; +} diff --git a/river/InputRelay.zig b/river/InputRelay.zig index d70d447..6bfcf27 100644 --- a/river/InputRelay.zig +++ b/river/InputRelay.zig @@ -26,6 +26,7 @@ const wl = @import("wayland").server.wl; const util = @import("util.zig"); const TextInput = @import("TextInput.zig"); +const InputMethodPopup = @import("InputMethodPopup.zig"); const Seat = @import("Seat.zig"); 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. /// If this is null, no text input enter events will be sent. input_method: ?*wlr.InputMethodV2 = null, +input_method_popups: wl.list.Head(InputMethodPopup, .link), /// The currently enabled text input for the currently focused surface. /// Always null if there is no input method. text_input: ?*TextInput = null, @@ -50,14 +52,17 @@ grab_keyboard: wl.Listener(*wlr.InputMethodV2.KeyboardGrab) = wl.Listener(*wlr.InputMethodV2.KeyboardGrab).init(handleInputMethodGrabKeyboard), input_method_destroy: wl.Listener(*wlr.InputMethodV2) = 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) = wl.Listener(*wlr.InputMethodV2.KeyboardGrab).init(handleInputMethodGrabKeyboardDestroy), pub fn init(relay: *InputRelay) void { - relay.* = .{ .text_inputs = undefined }; + relay.* = .{ .text_inputs = undefined, .input_method_popups = undefined }; relay.text_inputs.init(); + relay.input_method_popups.init(); } 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.grab_keyboard.add(&relay.grab_keyboard); 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| { relay.focus(surface); @@ -127,7 +133,7 @@ fn handleInputMethodDestroy( relay.input_method_commit.link.remove(); relay.grab_keyboard.link.remove(); relay.input_method_destroy.link.remove(); - + relay.input_method_new_popup_surface.link.remove(); relay.input_method = null; relay.focus(null); @@ -148,6 +154,18 @@ fn handleInputMethodGrabKeyboard( 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( listener: *wl.Listener(*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(); } diff --git a/river/XwaylandView.zig b/river/XwaylandView.zig index 7155868..d85e45a 100644 --- a/river/XwaylandView.zig +++ b/river/XwaylandView.zig @@ -163,6 +163,7 @@ pub fn handleMap(listener: *wl.Listener(void)) void { const view = xwayland_view.view; const xwayland_surface = xwayland_view.xwayland_surface; + xwayland_surface.data = @intFromPtr(xwayland_view); const surface = xwayland_surface.surface.?; surface.data = @intFromPtr(&view.tree.node);