diff --git a/api/lua/pinnacle/grpc/defs.lua b/api/lua/pinnacle/grpc/defs.lua index 774b8da80..7c64afc92 100644 --- a/api/lua/pinnacle/grpc/defs.lua +++ b/api/lua/pinnacle/grpc/defs.lua @@ -1194,9 +1194,19 @@ local pinnacle_v1_Backend = { ---@class pinnacle.window.v1.MoveGrabRequest ---@field button integer? +---@class pinnacle.window.v1.TouchMoveGrabRequest +---@field finger_id integer? + +---@class pinnacle.window.v1.TouchMoveGrabResponse + ---@class pinnacle.window.v1.ResizeGrabRequest ---@field button integer? +---@class pinnacle.window.v1.TouchResizeGrabRequest +---@field finger_id integer? + +---@class pinnacle.window.v1.TouchResizeGrabResponse + ---@class pinnacle.window.v1.SwapRequest ---@field window_id integer? ---@field target_id integer? @@ -1572,7 +1582,11 @@ pinnacle.window.v1.RaiseRequest = {} pinnacle.window.v1.LowerRequest = {} pinnacle.window.v1.LowerResponse = {} pinnacle.window.v1.MoveGrabRequest = {} +pinnacle.window.v1.TouchMoveGrabRequest = {} +pinnacle.window.v1.TouchMoveGrabResponse = {} pinnacle.window.v1.ResizeGrabRequest = {} +pinnacle.window.v1.TouchResizeGrabRequest = {} +pinnacle.window.v1.TouchResizeGrabResponse = {} pinnacle.window.v1.SwapRequest = {} pinnacle.window.v1.SwapResponse = {} pinnacle.window.v1.WindowRuleRequest = {} @@ -3031,6 +3045,40 @@ pinnacle.window.v1.WindowService.ResizeGrab.response = ".google.protobuf.Empty" function Client:pinnacle_window_v1_WindowService_ResizeGrab(data) return self:unary_request(pinnacle.window.v1.WindowService.ResizeGrab, data) end +pinnacle.window.v1.WindowService.TouchMoveGrab = {} +pinnacle.window.v1.WindowService.TouchMoveGrab.service = "pinnacle.window.v1.WindowService" +pinnacle.window.v1.WindowService.TouchMoveGrab.method = "TouchMoveGrab" +pinnacle.window.v1.WindowService.TouchMoveGrab.request = ".pinnacle.window.v1.TouchMoveGrabRequest" +pinnacle.window.v1.WindowService.TouchMoveGrab.response = ".pinnacle.window.v1.TouchMoveGrabResponse" + +---Performs a unary request. +--- +---@nodiscard +--- +---@param data pinnacle.window.v1.TouchMoveGrabRequest +--- +---@return pinnacle.window.v1.TouchMoveGrabResponse | nil response +---@return string | nil error An error string, if any +function Client:pinnacle_window_v1_WindowService_TouchMoveGrab(data) + return self:unary_request(pinnacle.window.v1.WindowService.TouchMoveGrab, data) +end +pinnacle.window.v1.WindowService.TouchResizeGrab = {} +pinnacle.window.v1.WindowService.TouchResizeGrab.service = "pinnacle.window.v1.WindowService" +pinnacle.window.v1.WindowService.TouchResizeGrab.method = "TouchResizeGrab" +pinnacle.window.v1.WindowService.TouchResizeGrab.request = ".pinnacle.window.v1.TouchResizeGrabRequest" +pinnacle.window.v1.WindowService.TouchResizeGrab.response = ".pinnacle.window.v1.TouchResizeGrabResponse" + +---Performs a unary request. +--- +---@nodiscard +--- +---@param data pinnacle.window.v1.TouchResizeGrabRequest +--- +---@return pinnacle.window.v1.TouchResizeGrabResponse | nil response +---@return string | nil error An error string, if any +function Client:pinnacle_window_v1_WindowService_TouchResizeGrab(data) + return self:unary_request(pinnacle.window.v1.WindowService.TouchResizeGrab, data) +end pinnacle.window.v1.WindowService.Swap = {} pinnacle.window.v1.WindowService.Swap.service = "pinnacle.window.v1.WindowService" pinnacle.window.v1.WindowService.Swap.method = "Swap" diff --git a/api/lua/pinnacle/window.lua b/api/lua/pinnacle/window.lua index 68e2dfc3c..626f06698 100644 --- a/api/lua/pinnacle/window.lua +++ b/api/lua/pinnacle/window.lua @@ -134,6 +134,26 @@ function window.begin_resize(button) end end +---Begins moving this window using the specified touch slot. +---@param finger_id integer +function window.begin_touch_move(finger_id) + local _, err = client:pinnacle_window_v1_WindowService_TouchMoveGrab({ finger_id = finger_id }) + + if err then + log.error(err) + end +end + +---Begins moving this window using the specified touch slot. +---@param finger_id integer +function window.begin_touch_resize(finger_id) + local _, err = client:pinnacle_window_v1_WindowService_TouchResizeGrab({ finger_id = finger_id }) + + if err then + log.error(err) + end +end + ---A window's current layout mode. ---@alias pinnacle.window.LayoutMode ---| "tiled" The window is tiled. diff --git a/api/protobuf/pinnacle/window/v1/window.proto b/api/protobuf/pinnacle/window/v1/window.proto index 67cbd4ed4..03f5a63b9 100644 --- a/api/protobuf/pinnacle/window/v1/window.proto +++ b/api/protobuf/pinnacle/window/v1/window.proto @@ -183,10 +183,20 @@ message MoveGrabRequest { uint32 button = 1; } +message TouchMoveGrabRequest { + uint32 finger_id = 1; +} +message TouchMoveGrabResponse {} + message ResizeGrabRequest { uint32 button = 1; } +message TouchResizeGrabRequest { + uint32 finger_id = 1; +} +message TouchResizeGrabResponse {} + message SwapRequest { uint32 window_id = 1; uint32 target_id = 2; @@ -244,6 +254,8 @@ service WindowService { rpc Lower(LowerRequest) returns (LowerResponse); rpc MoveGrab(MoveGrabRequest) returns (google.protobuf.Empty); rpc ResizeGrab(ResizeGrabRequest) returns (google.protobuf.Empty); + rpc TouchMoveGrab(TouchMoveGrabRequest) returns (TouchMoveGrabResponse); + rpc TouchResizeGrab(TouchResizeGrabRequest) returns (TouchResizeGrabResponse); rpc Swap(SwapRequest) returns (SwapResponse); rpc WindowRule(stream WindowRuleRequest) returns (stream WindowRuleResponse); diff --git a/api/rust/src/window.rs b/api/rust/src/window.rs index 0719feaf7..e26138acc 100644 --- a/api/rust/src/window.rs +++ b/api/rust/src/window.rs @@ -24,7 +24,7 @@ use pinnacle_api_defs::pinnacle::{ MoveToTagRequest, RaiseRequest, ResizeGrabRequest, ResizeTileRequest, SetDecorationModeRequest, SetFloatingRequest, SetFocusedRequest, SetFullscreenRequest, SetGeometryRequest, SetMaximizedRequest, SetTagRequest, SetTagsRequest, - SetVrrDemandRequest, SwapRequest, + SetVrrDemandRequest, SwapRequest, TouchMoveGrabRequest, TouchResizeGrabRequest, }, }, }; @@ -142,6 +142,30 @@ pub fn begin_resize(button: MouseButton) { .unwrap(); } +/// Begins a touch-driven interactive window move. +/// +/// This will start moving the window under the given finger until it's lifted. +/// +/// `finger_id` should correspond to the finger that triggered this function to be called. +pub fn begin_touch_move(finger_id: u32) { + Client::window() + .touch_move_grab(TouchMoveGrabRequest { finger_id }) + .block_on_tokio() + .unwrap(); +} + +/// Begins a touch-driven interactive window resize. +/// +/// This will start resizing the window under the given finger until it's lifted. +/// +/// `finger_id` should correspond to the finger that triggered this function to be called. +pub fn begin_touch_resize(finger_id: u32) { + Client::window() + .touch_resize_grab(TouchResizeGrabRequest { finger_id }) + .block_on_tokio() + .unwrap(); +} + /// Connects to a [`WindowSignal`]. /// /// # Examples diff --git a/snowcap/api/lua/snowcap/grpc/defs.lua b/snowcap/api/lua/snowcap/grpc/defs.lua index c3a1d4c89..a31b7c955 100644 --- a/snowcap/api/lua/snowcap/grpc/defs.lua +++ b/snowcap/api/lua/snowcap/grpc/defs.lua @@ -825,6 +825,7 @@ local snowcap_popup_v1_PopupEvent_Focus = { ---@field input_region snowcap.widget.v1.InputRegion? ---@field mouse_area snowcap.widget.v1.MouseArea? ---@field text_input snowcap.widget.v1.TextInput? +---@field touch_area snowcap.widget.v1.TouchArea? ---@class snowcap.widget.v1.Text ---@field text string? @@ -1045,6 +1046,39 @@ local snowcap_popup_v1_PopupEvent_Focus = { ---@field submit google.protobuf.Empty? ---@field paste string? +---@class snowcap.widget.v1.TouchArea +---@field child snowcap.widget.v1.WidgetDef? +---@field widget_id integer? +---@field on_down boolean? +---@field on_up boolean? +---@field on_enter boolean? +---@field on_move boolean? +---@field on_exit boolean? +---@field on_cancel boolean? + +---@class snowcap.widget.v1.TouchArea.Event +---@field down snowcap.widget.v1.TouchArea.DownEvent? +---@field up snowcap.widget.v1.TouchArea.Finger? +---@field enter snowcap.widget.v1.TouchArea.Finger? +---@field move snowcap.widget.v1.TouchArea.MoveEvent? +---@field exit snowcap.widget.v1.TouchArea.Finger? +---@field cancel snowcap.widget.v1.TouchArea.Finger? + +---@class snowcap.widget.v1.TouchArea.Finger +---@field id integer? + +---@class snowcap.widget.v1.TouchArea.Point +---@field x number? +---@field y number? + +---@class snowcap.widget.v1.TouchArea.DownEvent +---@field finger snowcap.widget.v1.TouchArea.Finger? +---@field point snowcap.widget.v1.TouchArea.Point? + +---@class snowcap.widget.v1.TouchArea.MoveEvent +---@field finger snowcap.widget.v1.TouchArea.Finger? +---@field point snowcap.widget.v1.TouchArea.Point? + ---@class snowcap.widget.v1.GetWidgetEventsRequest ---@field layer_id integer? ---@field decoration_id integer? @@ -1055,6 +1089,7 @@ local snowcap_popup_v1_PopupEvent_Focus = { ---@field button snowcap.widget.v1.Button.Event? ---@field mouse_area snowcap.widget.v1.MouseArea.Event? ---@field text_input snowcap.widget.v1.TextInput.Event? +---@field touch_area snowcap.widget.v1.TouchArea.Event? ---@class snowcap.widget.v1.GetWidgetEventsResponse ---@field widget_events snowcap.widget.v1.WidgetEvent[]? @@ -1469,6 +1504,12 @@ snowcap.widget.v1.TextInput.Icon = {} snowcap.widget.v1.TextInput.Style = {} snowcap.widget.v1.TextInput.Style.Inner = {} snowcap.widget.v1.TextInput.Event = {} +snowcap.widget.v1.TouchArea = {} +snowcap.widget.v1.TouchArea.Event = {} +snowcap.widget.v1.TouchArea.Finger = {} +snowcap.widget.v1.TouchArea.Point = {} +snowcap.widget.v1.TouchArea.DownEvent = {} +snowcap.widget.v1.TouchArea.MoveEvent = {} snowcap.widget.v1.GetWidgetEventsRequest = {} snowcap.widget.v1.WidgetEvent = {} snowcap.widget.v1.GetWidgetEventsResponse = {} diff --git a/snowcap/api/lua/snowcap/widget.lua b/snowcap/api/lua/snowcap/widget.lua index 3605246f0..44fbdfd76 100644 --- a/snowcap/api/lua/snowcap/widget.lua +++ b/snowcap/api/lua/snowcap/widget.lua @@ -38,6 +38,7 @@ ---@field input_region snowcap.widget.InputRegion? ---@field mouse_area snowcap.widget.MouseArea? ---@field text_input snowcap.widget.TextInput? +---@field touch_area snowcap.widget.TouchArea? ---@class snowcap.widget.Border ---@field color snowcap.widget.Color? @@ -444,6 +445,58 @@ local text_input_event_type = { PASTE = "press", } +---Emits messages on touch events. +---@class snowcap.widget.TouchArea +---@field child snowcap.widget.WidgetDef? TouchArea content +---@field on_down (fun(evt: snowcap.widget.touch_area.DownEvent): any)? Message to emit when a finger is pressed. +---@field on_up (fun(evt: snowcap.widget.touch_area.Finger): any)? Message to emit when a finger is lifted. +---@field on_enter (fun(evt: snowcap.widget.touch_area.Finger): any)? Message to emit when a finger enter the surface. +---@field on_move (fun(evt: snowcap.widget.touch_area.MoveEvent): any)? Message to emit when a finger move on the surface. +---@field on_exit (fun(evt: snowcap.widget.touch_area.Finger): any)? Message to emit when a finger leave the surface. +---@field on_cancel (fun(evt: snowcap.widget.touch_area.Finger): any)? Message to emit when a touch input is to be discarded. +---@field package widget_id integer? + +---@class snowcap.widget.touch_area.Callbacks +---@field on_down (fun(evt: snowcap.widget.touch_area.DownEvent): any)? +---@field on_up (fun(evt: snowcap.widget.touch_area.Finger): any)? +---@field on_enter (fun(evt: snowcap.widget.touch_area.Finger): any)? +---@field on_move (fun(evt: snowcap.widget.touch_area.MoveEvent): any)? +---@field on_exit (fun(evt: snowcap.widget.touch_area.Finger): any)? +---@field on_cancel (fun(evt: snowcap.widget.touch_area.Finger): any)? + +---@class snowcap.widget.touch_area.Event +---@field down snowcap.widget.touch_area.DownEvent? +---@field up snowcap.widget.touch_area.Finger? +---@field enter snowcap.widget.touch_area.Finger? +---@field move snowcap.widget.touch_area.MoveEvent? +---@field exit snowcap.widget.touch_area.Finger? +---@field cancel snowcap.widget.touch_area.Finger? + +---@class snowcap.widget.touch_area.Finger +---@field id integer? + +---@class snowcap.widget.touch_area.Point +---@field x number? +---@field y number? + +---@class snowcap.widget.touch_area.DownEvent +---@field finger snowcap.widget.touch_area.Finger? +---@field point snowcap.widget.touch_area.Point? + +---@class snowcap.widget.touch_area.MoveEvent +---@field finger snowcap.widget.touch_area.Finger? +---@field point snowcap.widget.touch_area.Point? + +---@enum snowcap.widget.touch_area.event.Type +local touch_area_event_type = { + DOWN = "down", + UP = "up", + ENTER = "enter", + MOVE = "move", + EXIT = "exit", + CANCEL = "cancel", +} + ---@class snowcap.widget.Length ---@field fill {}? ---@field fill_portion integer? @@ -851,6 +904,22 @@ local function text_input_into_api(def) } end +---@param def snowcap.widget.TouchArea +---@return snowcap.widget.v1.TouchArea +local function touch_area_into_api(def) + ---@type snowcap.widget.v1.TouchArea + return { + widget_id = def.widget_id, + child = widget.widget_def_into_api(def.child), + on_down = def.on_down ~= nil, + on_up = def.on_up ~= nil, + on_enter = def.on_enter ~= nil, + on_move = def.on_move ~= nil, + on_exit = def.on_exit ~= nil, + on_cancel = def.on_cancel ~= nil, + } +end + ---@param def snowcap.widget.WidgetDef ---@return snowcap.widget.v1.WidgetDef function widget.widget_def_into_api(def) @@ -884,6 +953,9 @@ function widget.widget_def_into_api(def) if def.text_input then def.text_input = text_input_into_api(def.text_input) end + if def.touch_area then + def.touch_area = touch_area_into_api(def.touch_area) + end return def --[[@as snowcap.widget.v1.WidgetDef]] end @@ -1020,6 +1092,31 @@ function widget.text_input(text_input) } end +---Create a new TouchArea widget. +---@param touch_area snowcap.widget.TouchArea +--- +---@return snowcap.widget.WidgetDef +function widget.touch_area(touch_area) + local has_cb = false + + has_cb = has_cb or touch_area.on_down ~= nil + has_cb = has_cb or touch_area.on_up ~= nil + has_cb = has_cb or touch_area.on_enter ~= nil + has_cb = has_cb or touch_area.on_move ~= nil + has_cb = has_cb or touch_area.on_exit ~= nil + has_cb = has_cb or touch_area.on_cancel ~= nil + + if has_cb then + touch_area.widget_id = widget_id_counter + widget_id_counter = widget_id_counter + 1 + end + + ---@type snowcap.widget.WidgetDef + return { + touch_area = touch_area, + } +end + ---@private ---@lcat nodoc ---@param wgt snowcap.widget.WidgetDef @@ -1045,6 +1142,8 @@ function widget._traverse_widget_tree(wgt, callbacks, with_widget) widget._traverse_widget_tree(wgt.input_region.child, callbacks, with_widget) elseif wgt.mouse_area then widget._traverse_widget_tree(wgt.mouse_area.child, callbacks, with_widget) + elseif wgt.touch_area then + widget._traverse_widget_tree(wgt.touch_area.child, callbacks, with_widget) end end @@ -1084,6 +1183,23 @@ local function collect_text_input_callbacks(text_input) } end +---@package +---@lcat nodoc +--- +---Collect event callbacks from a `snowcap.widget.TouchArea` +---@param touch_area snowcap.widget.TouchArea +---@return snowcap.widget.touch_area.Callbacks +local function collect_touch_area_callbacks(touch_area) + return { + on_down = touch_area.on_down, + on_up = touch_area.on_up, + on_enter = touch_area.on_enter, + on_move = touch_area.on_move, + on_exit = touch_area.on_exit, + on_cancel = touch_area.on_cancel, + } +end + ---@private ---@lcat nodoc ---@param callbacks any[] @@ -1100,6 +1216,10 @@ function widget._collect_callbacks(callbacks, wgt) if wgt.text_input and wgt.text_input.widget_id then callbacks[wgt.text_input.widget_id] = collect_text_input_callbacks(wgt.text_input) end + + if wgt.touch_area and wgt.touch_area.widget_id then + callbacks[wgt.touch_area.widget_id] = collect_touch_area_callbacks(wgt.touch_area) + end end ---@private @@ -1211,6 +1331,48 @@ function widget._text_input_process_event(callbacks, event) return msg end +---@private +---@lcat nodoc +--- +---@param callbacks snowcap.widget.touch_area.Callbacks +---@param event snowcap.widget.touch_area.Event +---@return any? +function widget._touch_area_process_event(callbacks, event) + callbacks = callbacks or {} + local translate = { + [touch_area_event_type.DOWN] = "on_down", + [touch_area_event_type.UP] = "on_up", + [touch_area_event_type.ENTER] = "on_enter", + [touch_area_event_type.MOVE] = "on_move", + [touch_area_event_type.EXIT] = "on_exit", + [touch_area_event_type.CANCEL] = "on_cancel", + } + + local event_type = nil + local cb = nil + + for k, v in pairs(translate) do + if event[k] ~= nil then + event_type = k + cb = callbacks[v] + + break + end + end + + if cb == nil then + return nil + end + + local ok, val = pcall(cb, event[event_type]) + + if not ok then + require("snowcap.log").error(val) + end + + return val +end + ---@private ---@lcat nodoc ---@param callbacks any[] @@ -1231,6 +1393,11 @@ function widget._message_from_event(callbacks, event) ---@diagnostic disable-next-line:param-type-mismatch msg = widget._text_input_process_event(callbacks[widget_id], event.text_input) end + elseif event.touch_area then + if callbacks[widget_id] ~= nil then + ---@diagnostic disable-next-line:param-type-mismatch + msg = widget._touch_area_process_event(callbacks[widget_id], event.touch_area) + end end return msg diff --git a/snowcap/api/protobuf/snowcap/widget/v1/widget.proto b/snowcap/api/protobuf/snowcap/widget/v1/widget.proto index 9cd3a2261..c08ed13d7 100644 --- a/snowcap/api/protobuf/snowcap/widget/v1/widget.proto +++ b/snowcap/api/protobuf/snowcap/widget/v1/widget.proto @@ -168,6 +168,7 @@ message WidgetDef { InputRegion input_region = 9; MouseArea mouse_area = 10; TextInput text_input = 11; + TouchArea touch_area = 12; } } @@ -463,6 +464,47 @@ message TextInput { } } +message TouchArea { + WidgetDef child = 1; + optional uint32 widget_id = 2; + bool on_down = 3; + bool on_up = 4; + bool on_enter = 5; + bool on_move = 6; + bool on_exit = 7; + bool on_cancel = 8; + + message Event { + oneof data { + DownEvent down = 1; + Finger up = 2; + Finger enter = 3; + MoveEvent move = 4; + Finger exit = 5; + Finger cancel = 6; + } + } + + message Finger { + uint32 id = 1; + } + + message Point { + float x = 1; + float y = 2; + } + + message DownEvent { + Finger finger = 1; + Point point = 2; + } + + message MoveEvent { + Finger finger = 1; + Point point = 2; + } +} + message GetWidgetEventsRequest { oneof id { uint32 layer_id = 1; @@ -478,6 +520,7 @@ message WidgetEvent { Button.Event button = 2; MouseArea.Event mouse_area = 3; TextInput.Event text_input = 4; + TouchArea.Event touch_area = 5; } } diff --git a/snowcap/api/rust/src/widget.rs b/snowcap/api/rust/src/widget.rs index f83f52932..60ef66329 100644 --- a/snowcap/api/rust/src/widget.rs +++ b/snowcap/api/rust/src/widget.rs @@ -17,6 +17,7 @@ pub mod scrollable; pub mod signal; pub mod text; pub mod text_input; +pub mod touch_area; pub mod utils; use std::{ @@ -34,6 +35,7 @@ use scrollable::Scrollable; use snowcap_api_defs::snowcap::widget; use text::Text; use text_input::TextInput; +use touch_area::TouchArea; use crate::{ signal::{HandlerPolicy, Signaler}, @@ -122,6 +124,7 @@ pub enum WidgetMessage { Button(Msg), MouseArea(mouse_area::Callbacks), TextInput(text_input::Callbacks), + TouchArea(touch_area::Callbacks), } pub fn message_from_event( @@ -149,6 +152,10 @@ where WidgetMessage::TextInput(callbacks) => callbacks.process_event(event.into()), _ => unreachable!(), }), + Event::TouchArea(event) => callbacks.get(&id).cloned().and_then(|f| match f { + WidgetMessage::TouchArea(callbacks) => callbacks.process_event(event.into()), + _ => unreachable!(), + }), } } @@ -188,6 +195,9 @@ impl WidgetDef { mouse_area.child.collect_messages(callbacks, with_widget); } Widget::TextInput(_) => (), + Widget::TouchArea(touch_area) => { + touch_area.child.collect_messages(callbacks, with_widget); + } } } } @@ -218,6 +228,14 @@ impl WidgetDef { .map(|id| (id, WidgetMessage::TextInput(text_input.callbacks.clone()))), ); } + + if let Widget::TouchArea(touch_area) = &self.widget { + callbacks.extend( + touch_area + .widget_id + .map(|id| (id, WidgetMessage::TouchArea(touch_area.callbacks.clone()))), + ) + } } } @@ -244,6 +262,7 @@ pub enum Widget { InputRegion(Box>), MouseArea(Box>), TextInput(Box>), + TouchArea(Box>), } impl>> From for WidgetDef { @@ -281,6 +300,9 @@ impl From> for widget::v1::widget_def::Widget { Widget::TextInput(text_input) => { widget::v1::widget_def::Widget::TextInput(Box::new((*text_input).into())) } + Widget::TouchArea(touch_area) => { + widget::v1::widget_def::Widget::TouchArea(Box::new((*touch_area).into())) + } } } } @@ -635,6 +657,12 @@ impl From for widget::v1::Wrapping { } } +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Point { + pub x: f32, + pub y: f32, +} + /// A complete widget program. /// /// A `Program` builds a widget for display by Snowcap and updates itself from diff --git a/snowcap/api/rust/src/widget/mouse_area.rs b/snowcap/api/rust/src/widget/mouse_area.rs index 916e14ba5..abd807416 100644 --- a/snowcap/api/rust/src/widget/mouse_area.rs +++ b/snowcap/api/rust/src/widget/mouse_area.rs @@ -6,6 +6,8 @@ use snowcap_api_defs::snowcap::widget; use super::{Widget, WidgetDef, WidgetId}; +pub use super::Point; + /// Emits messages on mouse events. #[derive(Debug, Clone, PartialEq)] pub struct MouseArea { @@ -386,12 +388,6 @@ impl From for ScrollDelta { } } -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct Point { - x: f32, - y: f32, -} - impl From for Point { fn from(value: widget::v1::mouse_area::MoveEvent) -> Self { Self { diff --git a/snowcap/api/rust/src/widget/touch_area.rs b/snowcap/api/rust/src/widget/touch_area.rs new file mode 100644 index 000000000..ff1e1de28 --- /dev/null +++ b/snowcap/api/rust/src/widget/touch_area.rs @@ -0,0 +1,295 @@ +//! Touch Event handling + +use std::sync::Arc; + +use snowcap_api_defs::snowcap::widget; + +use crate::widget::{Widget, WidgetDef, WidgetId}; + +pub use super::Point; + +/// Emits messages on touch events. +#[derive(Debug, Clone, PartialEq)] +pub struct TouchArea { + pub child: WidgetDef, + pub(crate) widget_id: Option, + pub(crate) callbacks: Callbacks, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Finger { + pub id: u32, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Event { + Down(Finger, Point), + Up(Finger), + Enter(Finger), + Move(Finger, Point), + Exit(Finger), + Cancel(Finger), +} + +#[derive(Clone)] +pub struct Callbacks { + pub(crate) on_down: Option Msg + Sync + Send>>, + pub(crate) on_up: Option Msg + Sync + Send>>, + pub(crate) on_enter: Option Msg + Sync + Send>>, + pub(crate) on_move: Option Msg + Sync + Send>>, + pub(crate) on_exit: Option Msg + Sync + Send>>, + pub(crate) on_cancel: Option Msg + Sync + Send>>, +} + +impl TouchArea { + /// Create a [`TouchArea`] with the given content. + pub fn new(child: impl Into>) -> Self { + Self { + child: child.into(), + widget_id: None, + callbacks: Callbacks { + on_down: None, + on_up: None, + on_enter: None, + on_move: None, + on_exit: None, + on_cancel: None, + }, + } + } + + /// Message to emit on a finger press. + pub fn on_down(self, on_down: F) -> Self + where + F: Fn(Finger, Point) -> Msg + Sync + Send + 'static, + { + Self { + widget_id: self.widget_id.or_else(|| Some(WidgetId::next())), + callbacks: Callbacks { + on_down: Some(Arc::new(on_down)), + ..self.callbacks + }, + ..self + } + } + + /// Message to emit when a finger is lifted. + pub fn on_up(self, on_up: F) -> Self + where + F: Fn(Finger) -> Msg + Sync + Send + 'static, + { + Self { + widget_id: self.widget_id.or_else(|| Some(WidgetId::next())), + callbacks: Callbacks { + on_up: Some(Arc::new(on_up)), + ..self.callbacks + }, + ..self + } + } + + /// Message to emit when a finger enter the area. + pub fn on_enter(self, on_enter: F) -> Self + where + F: Fn(Finger) -> Msg + Sync + Send + 'static, + { + Self { + widget_id: self.widget_id.or_else(|| Some(WidgetId::next())), + callbacks: Callbacks { + on_enter: Some(Arc::new(on_enter)), + ..self.callbacks + }, + ..self + } + } + + /// Message to emit when a finger moves on the area. + pub fn on_move(self, on_move: F) -> Self + where + F: Fn(Finger, Point) -> Msg + Sync + Send + 'static, + { + Self { + widget_id: self.widget_id.or_else(|| Some(WidgetId::next())), + callbacks: Callbacks { + on_move: Some(Arc::new(on_move)), + ..self.callbacks + }, + ..self + } + } + + /// Message to emit when a finger leaves the area. + pub fn on_exit(self, on_exit: F) -> Self + where + F: Fn(Finger) -> Msg + Sync + Send + 'static, + { + Self { + widget_id: self.widget_id.or_else(|| Some(WidgetId::next())), + callbacks: Callbacks { + on_exit: Some(Arc::new(on_exit)), + ..self.callbacks + }, + ..self + } + } + + /// Message to emit when the touch stream is cancelled. + pub fn on_cancel(self, on_cancel: F) -> Self + where + F: Fn(Finger) -> Msg + Sync + Send + 'static, + { + Self { + widget_id: self.widget_id.or_else(|| Some(WidgetId::next())), + callbacks: Callbacks { + on_cancel: Some(Arc::new(on_cancel)), + ..self.callbacks + }, + ..self + } + } +} + +impl From> for Widget { + fn from(value: TouchArea) -> Self { + Widget::TouchArea(Box::new(value)) + } +} + +impl From> for widget::v1::TouchArea { + fn from(value: TouchArea) -> Self { + Self { + child: Some(Box::new(value.child.into())), + widget_id: value.widget_id.map(WidgetId::to_inner), + on_down: value.callbacks.on_down.is_some(), + on_up: value.callbacks.on_up.is_some(), + on_enter: value.callbacks.on_enter.is_some(), + on_move: value.callbacks.on_move.is_some(), + on_exit: value.callbacks.on_exit.is_some(), + on_cancel: value.callbacks.on_cancel.is_some(), + } + } +} + +impl Callbacks { + pub fn process_event(self, evt: Event) -> Option { + match evt { + Event::Down(finger, point) => self.on_down.map(|handler| handler(finger, point)), + Event::Up(finger) => self.on_up.map(|handler| handler(finger)), + Event::Enter(finger) => self.on_enter.map(|handler| handler(finger)), + Event::Move(finger, point) => self.on_move.map(|handler| handler(finger, point)), + Event::Exit(finger) => self.on_exit.map(|handler| handler(finger)), + Event::Cancel(finger) => self.on_cancel.map(|handler| handler(finger)), + } + } +} + +impl std::fmt::Debug for Callbacks { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Callbacks") + .field( + "on_down", + &self + .on_down + .as_ref() + .map_or("None", |_| "Some(OnDownHandler)"), + ) + .field( + "on_up", + &self.on_up.as_ref().map_or("None", |_| "Some(OnUpHandler)"), + ) + .field( + "on_enter", + &self + .on_enter + .as_ref() + .map_or("None", |_| "Some(OnEnterHandler)"), + ) + .field( + "on_move", + &self + .on_move + .as_ref() + .map_or("None", |_| "Some(OnMoveHandler)"), + ) + .field( + "on_exit", + &self + .on_exit + .as_ref() + .map_or("None", |_| "Some(OnExitHandler)"), + ) + .field( + "on_cancel", + &self + .on_cancel + .as_ref() + .map_or("None", |_| "Some(OnCancelHandler)"), + ) + .finish() + } +} + +impl PartialEq for Callbacks { + fn eq(&self, other: &Self) -> bool { + fn compare(lhs: &Option>, rhs: &Option>) -> bool { + match (lhs, rhs) { + (Some(lhs), Some(rhs)) => Arc::ptr_eq(lhs, rhs), + (None, None) => true, + _ => false, + } + } + + compare(&self.on_down, &other.on_down) + && compare(&self.on_up, &other.on_up) + && compare(&self.on_enter, &other.on_enter) + && compare(&self.on_move, &other.on_move) + && compare(&self.on_exit, &other.on_exit) + && compare(&self.on_cancel, &other.on_cancel) + } +} + +impl From for Finger { + fn from(value: widget::v1::touch_area::Finger) -> Self { + Self { id: value.id } + } +} + +impl From for Point { + fn from(value: widget::v1::touch_area::Point) -> Self { + Self { + x: value.x, + y: value.y, + } + } +} + +impl From for Event { + fn from(value: widget::v1::touch_area::Event) -> Self { + use widget::v1::touch_area::{DownEvent, MoveEvent, event::Data}; + + let data = value.data.expect("Event without data"); + + match data { + Data::Down(DownEvent { finger, point }) => Self::Down( + finger + .expect("DownEvent should hold finger information") + .into(), + point + .expect("DownEvent should hold location information") + .into(), + ), + Data::Up(finger) => Self::Up(finger.into()), + Data::Enter(finger) => Self::Enter(finger.into()), + Data::Move(MoveEvent { finger, point }) => Self::Move( + finger + .expect("MoveEvent should hold finger information") + .into(), + point + .expect("MoveEvent should hold location information") + .into(), + ), + Data::Exit(finger) => Self::Exit(finger.into()), + Data::Cancel(finger) => Self::Cancel(finger.into()), + } + } +} diff --git a/snowcap/src/api/widget/v1.rs b/snowcap/src/api/widget/v1.rs index 58dd5950b..f24cc6875 100644 --- a/snowcap/src/api/widget/v1.rs +++ b/snowcap/src/api/widget/v1.rs @@ -17,7 +17,9 @@ use crate::{ layer::LayerId, popup::PopupId, util::convert::{FromApi, TryFromApi}, - widget::{MouseAreaEvent, TextInputEvent, ViewFn, WidgetEvent, WidgetId}, + widget::{ + MouseAreaEvent, TextInputEvent, TouchAreaEvent, ViewFn, WidgetEvent, WidgetId, touch_area, + }, }; #[tonic::async_trait] @@ -67,6 +69,9 @@ impl widget_service_server::WidgetService for super::WidgetService { WidgetEvent::TextInput(evt) => { widget_event::Event::TextInput(evt.into()) } + WidgetEvent::TouchArea(evt) => { + widget_event::Event::TouchArea(evt.into()) + } }), }) .collect(), @@ -994,6 +999,89 @@ pub fn widget_def_to_fn(def: WidgetDef) -> Option { text_input.into() }); + Some(f) + } + widget_def::Widget::TouchArea(touch_area) => { + let widget::v1::TouchArea { + child, + widget_id, + on_down, + on_up, + on_enter, + on_move, + on_exit, + on_cancel, + } = *touch_area; + + let child_widget_fn = child.and_then(|def| widget_def_to_fn(*def)); + + let f: ViewFn = Box::new(move || { + let mut touch_area = touch_area::TouchArea::new( + child_widget_fn + .as_ref() + .map(|child| child()) + .unwrap_or_else(|| iced::widget::Text::new("NULL").into()), + ); + + if let Some(widget_id) = widget_id { + if on_down { + touch_area = touch_area.on_down(move |id, pos| { + crate::widget::SnowcapMessage::WidgetEvent( + WidgetId(widget_id), + WidgetEvent::TouchArea(TouchAreaEvent::Down(id, pos)), + ) + }); + } + + if on_up { + touch_area = touch_area.on_up(move |id| { + crate::widget::SnowcapMessage::WidgetEvent( + WidgetId(widget_id), + WidgetEvent::TouchArea(TouchAreaEvent::Up(id)), + ) + }); + } + + if on_enter { + touch_area = touch_area.on_enter(move |id| { + crate::widget::SnowcapMessage::WidgetEvent( + WidgetId(widget_id), + WidgetEvent::TouchArea(TouchAreaEvent::Enter(id)), + ) + }); + } + + if on_move { + touch_area = touch_area.on_move(move |id, pos| { + crate::widget::SnowcapMessage::WidgetEvent( + WidgetId(widget_id), + WidgetEvent::TouchArea(TouchAreaEvent::Move(id, pos)), + ) + }); + } + + if on_exit { + touch_area = touch_area.on_exit(move |id| { + crate::widget::SnowcapMessage::WidgetEvent( + WidgetId(widget_id), + WidgetEvent::TouchArea(TouchAreaEvent::Exit(id)), + ) + }); + } + + if on_cancel { + touch_area = touch_area.on_cancel(move |id| { + crate::widget::SnowcapMessage::WidgetEvent( + WidgetId(widget_id), + WidgetEvent::TouchArea(TouchAreaEvent::Cancel(id)), + ) + }); + } + } + + touch_area.into() + }); + Some(f) } } @@ -1469,3 +1557,50 @@ impl FromApi for crate::widget::text_input::Style } } } + +impl From for snowcap_api_defs::snowcap::widget::v1::touch_area::Event { + fn from(value: TouchAreaEvent) -> Self { + use snowcap_api_defs::snowcap::widget::v1::touch_area::{self, event::Data}; + + let data = match value { + TouchAreaEvent::Down(finger, point) => { + let down_evt = touch_area::DownEvent { + finger: Some(touch_area::Finger::from_api(finger)), + point: Some(touch_area::Point::from_api(point)), + }; + + Data::Down(down_evt) + } + TouchAreaEvent::Up(finger) => Data::Up(touch_area::Finger::from_api(finger)), + TouchAreaEvent::Enter(finger) => Data::Enter(touch_area::Finger::from_api(finger)), + TouchAreaEvent::Move(finger, point) => { + let down_evt = touch_area::MoveEvent { + finger: Some(touch_area::Finger::from_api(finger)), + point: Some(touch_area::Point::from_api(point)), + }; + + Data::Move(down_evt) + } + TouchAreaEvent::Exit(finger) => Data::Exit(touch_area::Finger::from_api(finger)), + TouchAreaEvent::Cancel(finger) => Data::Cancel(touch_area::Finger::from_api(finger)), + }; + + Self { data: Some(data) } + } +} + +impl FromApi for snowcap_api_defs::snowcap::widget::v1::touch_area::Finger { + fn from_api(api_type: iced::touch::Finger) -> Self { + Self { + id: api_type.0 as u32, + } + } +} + +impl FromApi for snowcap_api_defs::snowcap::widget::v1::touch_area::Point { + fn from_api(api_type: iced::Point) -> Self { + let iced::Point { x, y } = api_type; + + Self { x, y } + } +} diff --git a/snowcap/src/decoration.rs b/snowcap/src/decoration.rs index 68d9bc5d1..6ce0daee0 100644 --- a/snowcap/src/decoration.rs +++ b/snowcap/src/decoration.rs @@ -45,7 +45,15 @@ impl State { self.popup_destroy(popup_id); } - self.decorations.retain(|d| d.decoration_id != id); + let Some(deco) = self + .decorations + .extract_if(.., |d| d.decoration_id == id) + .next() + else { + return; + }; + + self.flush_touch_for_surface(&deco.surface.wl_surface); } } diff --git a/snowcap/src/handlers.rs b/snowcap/src/handlers.rs index e3e525528..963cc2b04 100644 --- a/snowcap/src/handlers.rs +++ b/snowcap/src/handlers.rs @@ -3,6 +3,7 @@ pub mod foreign_toplevel_list; pub mod foreign_toplevel_management; pub mod keyboard; pub mod pointer; +pub mod touch; use smithay_client_toolkit::{ compositor::CompositorHandler, @@ -97,13 +98,18 @@ impl SeatHandler for State { self.pointer = Some(pointer); self.cursor_shape_device = Some(cursor_shape_device); } + + if capability == Capability::Touch { + let touch = self.seat_state.get_touch(qh, &seat).unwrap(); + self.touch_handles.push((seat.clone(), touch)); + } } fn remove_capability( &mut self, _conn: &Connection, _qh: &QueueHandle, - _seat: WlSeat, + seat: WlSeat, capability: Capability, ) { if capability == Capability::Keyboard @@ -120,10 +126,32 @@ impl SeatHandler for State { device.destroy(); } } + + if capability == Capability::Touch { + let Some((_, touch)) = self + .touch_handles + .extract_if(.., |(s, _)| s == &seat) + .next() + else { + return; + }; + + self.cancel_all_touch(&touch); + touch.release(); + } } - fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat) { - // TODO: + fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle, seat: WlSeat) { + let Some((_, touch)) = self + .touch_handles + .extract_if(.., |(s, _)| s == &seat) + .next() + else { + return; + }; + + self.cancel_all_touch(&touch); + touch.release(); } } delegate_seat!(State); diff --git a/snowcap/src/handlers/touch.rs b/snowcap/src/handlers/touch.rs new file mode 100644 index 000000000..2afca30d8 --- /dev/null +++ b/snowcap/src/handlers/touch.rs @@ -0,0 +1,193 @@ +use smithay_client_toolkit::{ + delegate_touch, + reexports::client::protocol::{wl_surface::WlSurface, wl_touch::WlTouch}, + seat::touch::TouchHandler, +}; + +use crate::state::State; + +#[derive(Clone, Debug)] +pub struct ActiveTouch { + id: i32, + touch: WlTouch, + surface: WlSurface, + last_pos: (f64, f64), +} + +impl TouchHandler for State { + fn down( + &mut self, + _conn: &smithay_client_toolkit::reexports::client::Connection, + _qh: &smithay_client_toolkit::reexports::client::QueueHandle, + touch: &smithay_client_toolkit::reexports::client::protocol::wl_touch::WlTouch, + _serial: u32, + _time: u32, + surface: smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface, + id: i32, + position: (f64, f64), + ) { + self.add_active_touch(ActiveTouch { + id, + touch: touch.clone(), + surface, + last_pos: position, + }); + } + + fn motion( + &mut self, + _conn: &smithay_client_toolkit::reexports::client::Connection, + _qh: &smithay_client_toolkit::reexports::client::QueueHandle, + _touch: &smithay_client_toolkit::reexports::client::protocol::wl_touch::WlTouch, + _time: u32, + id: i32, + position: (f64, f64), + ) { + self.active_touch_motion(id, position); + } + + fn up( + &mut self, + _conn: &smithay_client_toolkit::reexports::client::Connection, + _qh: &smithay_client_toolkit::reexports::client::QueueHandle, + _touch: &smithay_client_toolkit::reexports::client::protocol::wl_touch::WlTouch, + _serial: u32, + _time: u32, + id: i32, + ) { + self.active_touch_up(id); + } + + fn cancel( + &mut self, + _conn: &smithay_client_toolkit::reexports::client::Connection, + _qh: &smithay_client_toolkit::reexports::client::QueueHandle, + touch: &smithay_client_toolkit::reexports::client::protocol::wl_touch::WlTouch, + ) { + self.cancel_all_touch(touch); + } + + fn shape( + &mut self, + _conn: &smithay_client_toolkit::reexports::client::Connection, + _qh: &smithay_client_toolkit::reexports::client::QueueHandle, + _touch: &smithay_client_toolkit::reexports::client::protocol::wl_touch::WlTouch, + _id: i32, + _major: f64, + _minor: f64, + ) { + } + + fn orientation( + &mut self, + _conn: &smithay_client_toolkit::reexports::client::Connection, + _qh: &smithay_client_toolkit::reexports::client::QueueHandle, + _touch: &smithay_client_toolkit::reexports::client::protocol::wl_touch::WlTouch, + _id: i32, + _orientation: f64, + ) { + } +} +delegate_touch!(State); + +impl State { + fn add_active_touch(&mut self, touch: ActiveTouch) { + let Some(surface) = self.find_surface_mut(&touch.surface) else { + tracing::warn!("surface not found for touch #{}", touch.id); + return; + }; + + let id = iced::touch::Finger(touch.id as u64); + let position = iced::Point { + x: touch.last_pos.0 as f32, + y: touch.last_pos.1 as f32, + }; + + let event = iced::Event::Touch(iced::touch::Event::FingerPressed { id, position }); + + surface.pointer_location = Some(touch.last_pos); + surface.widgets.queue_event(event); + self.active_touches.push(touch); + } + + fn active_touch_motion(&mut self, id: i32, position: (f64, f64)) { + let touch: ActiveTouch = { + let Some(touch) = self.active_touches.iter_mut().find(|t| t.id == id) else { + tracing::warn!("No active touch with id: #{id}"); + return; + }; + + touch.last_pos = position; + + touch.clone() + }; + + let Some(surface) = self.find_surface_mut(&touch.surface) else { + tracing::warn!("Could not find surface for touch #{id}"); + self.active_touches.retain(|t| t.id != id); + return; + }; + + let id = iced::touch::Finger(touch.id as u64); + let position = iced::Point { + x: touch.last_pos.0 as f32, + y: touch.last_pos.1 as f32, + }; + + let event = iced::Event::Touch(iced::touch::Event::FingerMoved { id, position }); + + surface.pointer_location = Some(touch.last_pos); + surface.widgets.queue_event(event); + } + + fn active_touch_up(&mut self, id: i32) { + let Some(touch) = self.active_touches.extract_if(.., |t| t.id == id).next() else { + tracing::warn!("No active touch with id: #{id}"); + return; + }; + + let Some(surface) = self.find_surface_mut(&touch.surface) else { + tracing::warn!("Could not find surface for touch #{id}"); + return; + }; + + let id = iced::touch::Finger(touch.id as u64); + let position = iced::Point { + x: touch.last_pos.0 as f32, + y: touch.last_pos.1 as f32, + }; + + let event = iced::Event::Touch(iced::touch::Event::FingerLifted { id, position }); + + surface.pointer_location = Some(touch.last_pos); + surface.widgets.queue_event(event); + } + + pub(crate) fn cancel_all_touch(&mut self, touch: &WlTouch) { + let to_cancel = self + .active_touches + .extract_if(.., |t| &t.touch == touch) + .collect::>(); + + for touch in to_cancel { + let Some(surface) = self.find_surface_mut(&touch.surface) else { + continue; + }; + + let id = iced::touch::Finger(touch.id as u64); + let position = iced::Point { + x: touch.last_pos.0 as f32, + y: touch.last_pos.1 as f32, + }; + + let event = iced::Event::Touch(iced::touch::Event::FingerLost { id, position }); + + surface.pointer_location = Some(touch.last_pos); + surface.widgets.queue_event(event); + } + } + + pub(crate) fn flush_touch_for_surface(&mut self, wl_surface: &WlSurface) { + self.active_touches.retain(|t| &t.surface != wl_surface); + } +} diff --git a/snowcap/src/layer.rs b/snowcap/src/layer.rs index 86a7c6a30..597edd312 100644 --- a/snowcap/src/layer.rs +++ b/snowcap/src/layer.rs @@ -62,7 +62,11 @@ impl State { self.popup_destroy(popup_id); } - self.layers.retain(|p| p.layer_id != id); + let Some(layer) = self.layers.extract_if(.., |l| l.layer_id == id).next() else { + return; + }; + + self.flush_touch_for_surface(layer.layer.wl_surface()); } } diff --git a/snowcap/src/popup.rs b/snowcap/src/popup.rs index 2795862f6..cf3dc1ede 100644 --- a/snowcap/src/popup.rs +++ b/snowcap/src/popup.rs @@ -63,7 +63,15 @@ impl State { } for popup_id in to_destroy.iter().rev() { - self.popups.retain(|p| &p.popup_id != popup_id) + let Some(popup) = self + .popups + .extract_if(.., |p| &p.popup_id == popup_id) + .next() + else { + continue; + }; + + self.flush_touch_for_surface(&popup.surface.wl_surface); } } } diff --git a/snowcap/src/state.rs b/snowcap/src/state.rs index 9f4156971..ada25277e 100644 --- a/snowcap/src/state.rs +++ b/snowcap/src/state.rs @@ -12,7 +12,7 @@ use smithay_client_toolkit::{ globals::registry_queue_init, protocol::{ wl_keyboard::WlKeyboard, wl_pointer::WlPointer, wl_seat::WlSeat, - wl_surface::WlSurface, + wl_surface::WlSurface, wl_touch::WlTouch, }, }, protocols::{ @@ -36,7 +36,10 @@ use xkbcommon::xkb::Keysym; use crate::{ decoration::{DecorationIdCounter, SnowcapDecoration}, - handlers::{foreign_toplevel_list::ForeignToplevelListHandleData, keyboard::KeyboardFocus}, + handlers::{ + foreign_toplevel_list::ForeignToplevelListHandleData, keyboard::KeyboardFocus, + touch::ActiveTouch, + }, layer::{LayerIdCounter, SnowcapLayer}, popup::{PopupIdCounter, SnowcapPopup}, runtime::{CalloopSenderSink, CurrentTokioExecutor}, @@ -86,6 +89,10 @@ pub struct State { pub pointer: Option, // TODO: multiple pub pointer_focus: Option, pub last_pointer_enter_serial: Option, + + pub touch_handles: Vec<(WlSeat, WlTouch)>, + pub active_touches: Vec, + // TODO: Do we need a pointer seat as well ? pub layer_id_counter: LayerIdCounter, pub decoration_id_counter: DecorationIdCounter, @@ -295,6 +302,8 @@ impl State { pointer: None, pointer_focus: None, last_pointer_enter_serial: None, + touch_handles: Vec::default(), + active_touches: Vec::default(), layer_id_counter: LayerIdCounter::default(), decoration_id_counter: DecorationIdCounter::default(), popup_id_counter: PopupIdCounter::default(), diff --git a/snowcap/src/widget.rs b/snowcap/src/widget.rs index 3a2be79c6..1d4b8d1c2 100644 --- a/snowcap/src/widget.rs +++ b/snowcap/src/widget.rs @@ -1,4 +1,5 @@ pub mod input_region; +pub mod touch_area; use iced::{Color, Theme, event::Status}; use iced_graphics::Viewport; @@ -218,6 +219,7 @@ pub enum WidgetEvent { Button, MouseArea(MouseAreaEvent), TextInput(TextInputEvent), + TouchArea(TouchAreaEvent), } #[derive(Debug, Clone)] @@ -242,6 +244,16 @@ pub enum TextInputEvent { Paste(String), } +#[derive(Debug, Clone)] +pub enum TouchAreaEvent { + Down(iced::touch::Finger, iced::Point), + Up(iced::touch::Finger), + Enter(iced::touch::Finger), + Move(iced::touch::Finger, iced::Point), + Exit(iced::touch::Finger), + Cancel(iced::touch::Finger), +} + pub(crate) mod text_input { #[derive(Debug, Default, Clone)] pub(crate) struct Styles { diff --git a/snowcap/src/widget/touch_area.rs b/snowcap/src/widget/touch_area.rs new file mode 100644 index 000000000..a8266feb5 --- /dev/null +++ b/snowcap/src/widget/touch_area.rs @@ -0,0 +1,351 @@ +//! A container for capturing touch events. +use iced_wgpu::core::{ + self as iced_core, Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Size, + Vector, Widget, layout, mouse, overlay, renderer, + touch::{self, Finger}, + widget::{Operation, Tree, tree}, +}; + +/// Emit messages on touch events. +pub struct TouchArea<'a, Message, Theme = iced_core::Theme, Renderer = iced_renderer::Renderer> { + content: Element<'a, Message, Theme, Renderer>, + on_down: Option Message + 'a>>, + on_up: Option Message + 'a>>, + on_enter: Option Message + 'a>>, + on_move: Option Message + 'a>>, + on_exit: Option Message + 'a>>, + on_cancel: Option Message + 'a>>, +} + +impl<'a, Message, Theme, Renderer> TouchArea<'a, Message, Theme, Renderer> { + /// The message to emit when a finger is pressed. + #[must_use] + pub fn on_down(mut self, on_down: impl Fn(Finger, Point) -> Message + 'a) -> Self { + self.on_down = Some(Box::new(on_down)); + self + } + + /// The message to emit when a finger is lifted. + #[must_use] + pub fn on_up(mut self, on_up: impl Fn(Finger) -> Message + 'a) -> Self { + self.on_up = Some(Box::new(on_up)); + self + } + + /// The message to emit when a finger move in the area. + #[must_use] + pub fn on_move(mut self, on_move: impl Fn(Finger, Point) -> Message + 'a) -> Self { + self.on_move = Some(Box::new(on_move)); + self + } + + /// The message to emit when a finger enter the area. + #[must_use] + pub fn on_enter(mut self, on_enter: impl Fn(Finger) -> Message + 'a) -> Self { + self.on_enter = Some(Box::new(on_enter)); + self + } + + /// The message to emit when the finger exits the area. + #[must_use] + pub fn on_exit(mut self, on_exit: impl Fn(Finger) -> Message + 'a) -> Self { + self.on_exit = Some(Box::new(on_exit)); + self + } + + /// The message to emit when a finger input gets canceled. + #[must_use] + pub fn on_cancel(mut self, on_cancel: impl Fn(Finger) -> Message + 'a) -> Self { + self.on_cancel = Some(Box::new(on_cancel)); + self + } +} + +/// Local state of the [`TouchArea`]. +#[derive(Default)] +struct State { + tracked_finger: Vec<(Finger, Point)>, + bounds: Rectangle, +} + +impl<'a, Message, Theme, Renderer> TouchArea<'a, Message, Theme, Renderer> { + /// Creates a [`TouchArea`] with the given content. + pub fn new(content: impl Into>) -> Self { + TouchArea { + content: content.into(), + on_down: None, + on_up: None, + on_enter: None, + on_move: None, + on_exit: None, + on_cancel: None, + } + } +} + +impl Widget + for TouchArea<'_, Message, Theme, Renderer> +where + Renderer: renderer::Renderer, + Message: Clone, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)); + } + + fn size(&self) -> Size { + self.content.as_widget().size() + } + + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content + .as_widget_mut() + .layout(&mut tree.children[0], renderer, limits) + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + self.content + .as_widget_mut() + .operate(&mut tree.children[0], layout, renderer, operation); + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + + if shell.is_event_captured() { + return; + } + + update(self, tree, event, layout, cursor, shell); + } + + fn mouse_interaction( + &self, + _tree: &Tree, + _layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse::Interaction::None + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + renderer_style, + layout, + cursor, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'b>, + renderer: &Renderer, + viewport: &Rectangle, + translation: Vector, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + viewport, + translation, + ) + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a + Clone, + Theme: 'a, + Renderer: 'a + renderer::Renderer, +{ + fn from( + area: TouchArea<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(area) + } +} + +/// Processes the given [`Event`] and updates the [`State`] of a [`TouchArea`] +/// accordingly. +fn update( + widget: &mut TouchArea<'_, Message, Theme, Renderer>, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + _cursor: mouse::Cursor, + shell: &mut Shell<'_, Message>, +) { + let state: &mut State = tree.state.downcast_mut(); + + let bounds = layout.bounds(); + + if bounds != state.bounds { + let prev_finger = state + .tracked_finger + .extract_if(.., |(_, pos)| !bounds.contains(*pos)); + + if let Some(on_exit) = widget.on_exit.as_ref() { + prev_finger.for_each(|(id, _)| shell.publish(on_exit(id))); + } else { + prev_finger.for_each(drop); + } + + state.bounds = bounds; + } + + let mut capture = false; + + match event { + Event::Touch(touch::Event::FingerPressed { id, position }) => { + let id = *id; + let pos = *position; + + if bounds.contains(pos) { + if let Some(on_enter) = widget.on_enter.as_ref() { + shell.publish(on_enter(id)); + capture = true; + } + + if let Some(on_down) = widget.on_down.as_ref() { + shell.publish(on_down(id, pos - Vector::new(bounds.x, bounds.y))); + capture = true; + } + + state.tracked_finger.push((id, pos)); + } + } + Event::Touch(touch::Event::FingerLifted { id, position: _ }) => { + if let Some((id, _)) = state + .tracked_finger + .extract_if(.., |(fid, _)| fid == id) + .next() + { + if let Some(on_up) = widget.on_up.as_ref() { + shell.publish(on_up(id)); + capture = true; + } + + if let Some(on_exit) = widget.on_exit.as_ref() { + shell.publish(on_exit(id)); + capture = true; + } + } + } + Event::Touch(touch::Event::FingerMoved { id, position }) => { + let tracked = state.tracked_finger.iter().position(|(fid, _)| fid == id); + let id = *id; + let pos = *position; + let is_over = bounds.contains(pos); + + let adj_pos = pos - Vector::new(bounds.x, bounds.y); + + if tracked.is_none() && is_over { + state.tracked_finger.push((id, pos)); + + if let Some(on_enter) = widget.on_enter.as_ref() { + shell.publish(on_enter(id)); + capture = true; + } + + if let Some(on_move) = widget.on_move.as_ref() { + shell.publish(on_move(id, adj_pos)); + capture = true; + } + } else if let Some(idx) = tracked + && is_over + { + if let Some(on_move) = widget.on_move.as_ref() { + shell.publish(on_move(id, adj_pos)); + capture = true; + }; + + state.tracked_finger.get_mut(idx).unwrap().1 = pos; + } else if let Some(idx) = tracked + && !is_over + { + state.tracked_finger.swap_remove(idx); + + if let Some(on_exit) = widget.on_exit.as_ref() { + shell.publish(on_exit(id)); + capture = true; + } + } + } + Event::Touch(touch::Event::FingerLost { id, position: _ }) => { + if let Some((id, _)) = state + .tracked_finger + .extract_if(.., |(fid, _)| fid == id) + .next() + && let Some(on_cancel) = widget.on_cancel.as_ref() + { + shell.publish(on_cancel(id)); + capture = true; + } + } + _ => {} + } + + if capture { + shell.capture_event(); + } +} diff --git a/src/api/window.rs b/src/api/window.rs index a8c035d17..a84330031 100644 --- a/src/api/window.rs +++ b/src/api/window.rs @@ -1,10 +1,11 @@ mod v1; use smithay::{ + backend::input::TouchSlot, reexports::wayland_protocols::xdg::{ decoration::zv1::server::zxdg_toplevel_decoration_v1, shell::server, }, - utils::{Point, SERIAL_COUNTER, Size}, + utils::{Logical, Point, SERIAL_COUNTER, Size}, wayland::seat::WaylandFocus, }; use tracing::warn; @@ -261,32 +262,48 @@ pub fn move_grab(state: &mut State, button: u32) { } } -pub fn resize_grab(state: &mut State, button: u32) { - let Some(pointer_loc) = state +pub fn touch_move_grab(state: &mut State, finger_id: u32) { + let slot = TouchSlot::from(Some(finger_id)); + let Some(touch_loc) = state .pinnacle - .seat - .get_pointer() - .map(|ptr| ptr.current_location()) + .touch_positions + .iter() + .find_map(|(s, l)| if s == &slot { Some(l) } else { None }) else { return; }; - let Some((pointer_focus, _window_loc)) = state.pinnacle.pointer_contents.focus_under.as_ref() - else { + + let pointer_content = state.pinnacle.pointer_contents_under(*touch_loc); + let Some((pointer_focus, _)) = pointer_content.focus_under.as_ref() else { return; }; + let Some(window) = pointer_focus.window_for(&state.pinnacle) else { return; }; let Some(wl_surf) = window.wl_surface() else { return; }; - let Some(window_loc) = state.pinnacle.space.element_location(&window) else { - return; - }; + let seat = state.pinnacle.seat.clone(); - let pointer_loc: Point = pointer_loc.to_i32_round(); + state.touch_move_request_server( + &wl_surf, + &seat, + SERIAL_COUNTER.next_serial(), + slot, + *touch_loc, + ); - let window_size = window.geometry().size; + if let Some(output) = state.pinnacle.focused_output().cloned() { + state.schedule_render(&output); + } +} + +fn compute_resize_edge( + pointer_loc: Point, + window_loc: Point, + window_size: Size, +) -> server::xdg_toplevel::ResizeEdge { let window_width = Size::new(window_size.w, 0); let window_height = Size::new(0, window_size.h); @@ -364,6 +381,38 @@ pub fn resize_grab(state: &mut State, button: u32) { .unwrap_or(server::xdg_toplevel::ResizeEdge::None) }; + edges +} + +pub fn resize_grab(state: &mut State, button: u32) { + let Some(pointer_loc) = state + .pinnacle + .seat + .get_pointer() + .map(|ptr| ptr.current_location()) + else { + return; + }; + + let Some((pointer_focus, _window_loc)) = state.pinnacle.pointer_contents.focus_under.as_ref() + else { + return; + }; + let Some(window) = pointer_focus.window_for(&state.pinnacle) else { + return; + }; + let Some(wl_surf) = window.wl_surface() else { + return; + }; + let Some(window_loc) = state.pinnacle.space.element_location(&window) else { + return; + }; + + let pointer_loc: Point = pointer_loc.to_i32_round(); + let window_size = window.geometry().size; + + let edges = compute_resize_edge(pointer_loc, window_loc, window_size); + state.resize_request_server( &wl_surf, &state.pinnacle.seat.clone(), @@ -377,6 +426,50 @@ pub fn resize_grab(state: &mut State, button: u32) { } } +pub fn touch_resize_grab(state: &mut State, finger_id: u32) { + let slot = TouchSlot::from(Some(finger_id)); + + let Some(touch_loc) = state + .pinnacle + .touch_positions + .iter() + .find_map(|(s, l)| if s == &slot { Some(l) } else { None }) + else { + return; + }; + + let pointer_content = state.pinnacle.pointer_contents_under(*touch_loc); + let Some((pointer_focus, _)) = pointer_content.focus_under.as_ref() else { + return; + }; + let Some(window) = pointer_focus.window_for(&state.pinnacle) else { + return; + }; + let Some(wl_surf) = window.wl_surface() else { + return; + }; + let Some(window_loc) = state.pinnacle.space.element_location(&window) else { + return; + }; + + let window_size = window.geometry().size; + + let edges = compute_resize_edge(touch_loc.to_i32_round(), window_loc, window_size); + + state.touch_resize_request_server( + &wl_surf, + &state.pinnacle.seat.clone(), + SERIAL_COUNTER.next_serial(), + edges.into(), + slot, + *touch_loc, + ); + + if let Some(output) = state.pinnacle.focused_output().cloned() { + state.schedule_render(&output); + } +} + pub fn swap(state: &mut State, window: WindowElement, target: WindowElement) { if state.pinnacle.layout_state.pending_swap { return; diff --git a/src/api/window/v1.rs b/src/api/window/v1.rs index 3b860ba68..0414c8017 100644 --- a/src/api/window/v1.rs +++ b/src/api/window/v1.rs @@ -20,7 +20,8 @@ use pinnacle_api_defs::pinnacle::{ SetDecorationModeRequest, SetFloatingRequest, SetFocusedRequest, SetFullscreenRequest, SetGeometryRequest, SetMaximizedRequest, SetTagRequest, SetTagsRequest, SetTagsResponse, SetVrrDemandRequest, SetVrrDemandResponse, SwapRequest, SwapResponse, - WindowRuleRequest, WindowRuleResponse, + TouchMoveGrabRequest, TouchMoveGrabResponse, TouchResizeGrabRequest, + TouchResizeGrabResponse, WindowRuleRequest, WindowRuleResponse, }, }, }; @@ -811,6 +812,36 @@ impl v1::window_service_server::WindowService for super::WindowService { .await } + async fn touch_move_grab( + &self, + request: Request, + ) -> TonicResult { + let request = request.into_inner(); + let finger = request.finger_id; + + run_unary(&self.sender, move |state| { + crate::api::window::touch_move_grab(state, finger); + + Ok(TouchMoveGrabResponse {}) + }) + .await + } + + async fn touch_resize_grab( + &self, + request: Request, + ) -> TonicResult { + let request = request.into_inner(); + let finger = request.finger_id; + + run_unary(&self.sender, move |state| { + crate::api::window::touch_resize_grab(state, finger); + + Ok(TouchResizeGrabResponse {}) + }) + .await + } + async fn swap(&self, request: Request) -> TonicResult { let inner = request.into_inner(); let window_id = WindowId(inner.window_id); diff --git a/src/grab.rs b/src/grab.rs index c702ed4a6..f2e8a673e 100644 --- a/src/grab.rs +++ b/src/grab.rs @@ -4,20 +4,52 @@ pub mod move_grab; pub mod resize_grab; use smithay::{ - input::pointer::{GrabStartData, PointerHandle}, + input::{ + SeatHandler, + pointer::{self, PointerHandle}, + touch::{self, TouchHandle}, + }, reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface}, - utils::Serial, + utils::{Logical, Point, Serial}, wayland::seat::WaylandFocus, }; use crate::state::State; +pub enum InputGrabStartData { + Pointer(pointer::GrabStartData), + Touch(touch::GrabStartData), +} + +impl InputGrabStartData { + pub fn location(&self) -> Point { + match self { + Self::Pointer(g) => g.location, + Self::Touch(g) => g.location, + } + } + + pub fn as_pointer(&self) -> Option<&pointer::GrabStartData> { + match self { + Self::Pointer(g) => Some(g), + _ => None, + } + } + + pub fn as_touch(&self) -> Option<&touch::GrabStartData> { + match self { + Self::Touch(g) => Some(g), + _ => None, + } + } +} + /// Returns the [GrabStartData] from a pointer grab, if any. pub fn pointer_grab_start_data( pointer: &PointerHandle, surface: &WlSurface, serial: Serial, -) -> Option> { +) -> Option> { tracing::debug!("start of pointer_grab_start_data"); if !pointer.has_grab(serial) { tracing::debug!("pointer doesn't have grab"); @@ -35,3 +67,38 @@ pub fn pointer_grab_start_data( Some(start_data) } + +pub fn touch_grab_start_data( + touch: &TouchHandle, + surface: &WlSurface, + serial: Serial, +) -> Option> { + tracing::debug!("start of touch_grab_start_data"); + if !touch.has_grab(serial) { + tracing::debug!("touch doesn't have grab"); + return None; + } + + let start_data = touch.grab_start_data()?; + + let (focus_surface, _) = start_data.focus.as_ref()?; + + if !focus_surface.same_client_as(&surface.id()) { + tracing::debug!("surface isn't the same"); + return None; + } + + Some(start_data) +} + +impl From> for InputGrabStartData { + fn from(value: pointer::GrabStartData) -> Self { + Self::Pointer(value) + } +} + +impl From> for InputGrabStartData { + fn from(value: touch::GrabStartData) -> Self { + Self::Touch(value) + } +} diff --git a/src/grab/move_grab.rs b/src/grab/move_grab.rs index 9a1c308bf..6c6b21165 100644 --- a/src/grab/move_grab.rs +++ b/src/grab/move_grab.rs @@ -3,78 +3,58 @@ use smithay::{ // NOTE: maybe alias this to PointerGrabStartData because there's another GrabStartData in // | input::keyboard + backend::input::TouchSlot, input::{ Seat, SeatHandler, pointer::{ - AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, Focus, GestureHoldBeginEvent, - GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, - GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, - GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerGrab, PointerInnerHandle, - RelativeMotionEvent, + self, AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, Focus, + GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, + GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, + GestureSwipeEndEvent, GestureSwipeUpdateEvent, MotionEvent, PointerGrab, + PointerInnerHandle, RelativeMotionEvent, }, + touch::{DownEvent, TouchGrab, TouchInnerHandle, UpEvent}, }, + output::Output, reexports::wayland_server::protocol::wl_surface::WlSurface, utils::{IsAlive, Logical, Point, Rectangle, Serial}, }; use tracing::{debug, warn}; use crate::{ + grab::InputGrabStartData, state::{State, WithState}, window::{WindowElement, window_state::LayoutModeKind}, }; /// Data for moving a window. pub struct MoveSurfaceGrab { - pub start_data: GrabStartData, + pub start_data: InputGrabStartData, /// The window being moved pub window: WindowElement, pub initial_window_loc: Point, } -impl PointerGrab for MoveSurfaceGrab { - fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) { - handle.frame(data); - } - - fn motion( +impl MoveSurfaceGrab { + fn on_motion( &mut self, state: &mut State, - handle: &mut PointerInnerHandle<'_, State>, - _focus: Option<(::PointerFocus, Point)>, - event: &MotionEvent, + location: Point, + output_under: Option, ) { - handle.motion(state, None, event); - - if !self.window.alive() { - state - .pinnacle - .cursor_state - .set_cursor_image(CursorImageStatus::default_named()); - handle.unset_grab(self, state, event.serial, event.time, true); - return; - } - state.pinnacle.raise_window(self.window.clone()); - let mut layout_mode = self.window.with_state(|state| state.layout_mode.current()); - let output_under_pointer = state - .pinnacle - .pointer_contents - .output_under - .as_ref() - .and_then(|op| op.upgrade()); - let win_output = self.window.output(&state.pinnacle); - if matches!(layout_mode, LayoutModeKind::Spilled) && win_output != output_under_pointer { + if matches!(layout_mode, LayoutModeKind::Spilled) && win_output != output_under { layout_mode = LayoutModeKind::Tiled; } match layout_mode { LayoutModeKind::Tiled => { let tag_output = self.window.output(&state.pinnacle); - if let Some(output_under_pointer) = output_under_pointer + if let Some(output_under_pointer) = output_under && Some(&output_under_pointer) != tag_output.as_ref() { self.window.set_tags_to_output(&output_under_pointer); @@ -103,7 +83,7 @@ impl PointerGrab for MoveSurfaceGrab { if let Some(loc) = state.pinnacle.space.element_location(win) { let size = win.geometry().size; let rect = Rectangle { size, loc }; - rect.contains(event.location.to_i32_round()) + rect.contains(location.to_i32_round()) } else { false } @@ -138,7 +118,7 @@ impl PointerGrab for MoveSurfaceGrab { } } LayoutModeKind::Floating | LayoutModeKind::Spilled => { - let delta = event.location - self.start_data.location; + let delta = location - self.start_data.location(); let new_loc = self.initial_window_loc.to_f64() + delta; state @@ -151,7 +131,7 @@ impl PointerGrab for MoveSurfaceGrab { } LayoutModeKind::Maximized | LayoutModeKind::Fullscreen => { let tag_output = self.window.output(&state.pinnacle); - if let Some(output_under_pointer) = output_under_pointer + if let Some(output_under_pointer) = output_under && Some(&output_under_pointer) != tag_output.as_ref() { state @@ -164,6 +144,47 @@ impl PointerGrab for MoveSurfaceGrab { } } + fn on_unset(&mut self, state: &mut State) { + // FIXME: granular + for output in state.pinnacle.space.outputs().cloned().collect::>() { + state.schedule_render(&output); + } + } +} + +impl PointerGrab for MoveSurfaceGrab { + fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) { + handle.frame(data); + } + + fn motion( + &mut self, + state: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + _focus: Option<(::PointerFocus, Point)>, + event: &MotionEvent, + ) { + handle.motion(state, None, event); + + if !self.window.alive() { + state + .pinnacle + .cursor_state + .set_cursor_image(CursorImageStatus::default_named()); + handle.unset_grab(self, state, event.serial, event.time, true); + return; + } + + let output_under_pointer = state + .pinnacle + .pointer_contents + .output_under + .as_ref() + .and_then(|op| op.upgrade()); + + self.on_motion(state, event.location, output_under_pointer); + } + fn relative_motion( &mut self, data: &mut State, @@ -182,7 +203,10 @@ impl PointerGrab for MoveSurfaceGrab { ) { handle.button(data, event); - if !handle.current_pressed().contains(&self.start_data.button) { + if !handle + .current_pressed() + .contains(&PointerGrab::start_data(self).button) + { data.pinnacle .cursor_state .set_cursor_image(CursorImageStatus::default_named()); @@ -199,15 +223,14 @@ impl PointerGrab for MoveSurfaceGrab { handle.axis(data, details); } - fn start_data(&self) -> &GrabStartData { - &self.start_data + fn start_data(&self) -> &pointer::GrabStartData { + self.start_data + .as_pointer() + .expect("start_data is not Pointer") } fn unset(&mut self, state: &mut State) { - // FIXME: granular - for output in state.pinnacle.space.outputs().cloned().collect::>() { - state.schedule_render(&output); - } + self.on_unset(state); } fn gesture_swipe_begin( @@ -283,6 +306,106 @@ impl PointerGrab for MoveSurfaceGrab { } } +impl TouchGrab for MoveSurfaceGrab { + fn start_data(&self) -> &smithay::input::touch::GrabStartData { + self.start_data.as_touch().expect("start_data is not Touch") + } + + fn down( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + _focus: Option<(::TouchFocus, Point)>, + event: &DownEvent, + seq: Serial, + ) { + handle.down(data, None, event, seq); + } + + fn up( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &UpEvent, + seq: Serial, + ) { + handle.up(data, event, seq); + + let Some(start_data) = self.start_data.as_touch() else { + return; + }; + + if event.slot == start_data.slot { + handle.unset_grab(self, data); + } + } + + fn motion( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + focus: Option<(::TouchFocus, Point)>, + event: &smithay::input::touch::MotionEvent, + seq: Serial, + ) { + handle.motion(data, focus, event, seq); + + let Some(start_data) = self.start_data.as_touch() else { + return; + }; + + if event.slot != start_data.slot { + return; + } + + if !self.window.alive() { + handle.unset_grab(self, data); + } + + let output_under = data + .pinnacle + .space + .output_under(event.location) + .next() + .cloned(); + + self.on_motion(data, event.location, output_under); + } + + fn shape( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &smithay::input::touch::ShapeEvent, + seq: Serial, + ) { + handle.shape(data, event, seq); + } + + fn orientation( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &smithay::input::touch::OrientationEvent, + seq: Serial, + ) { + handle.orientation(data, event, seq); + } + + fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) { + handle.frame(data, seq); + } + + fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) { + handle.cancel(data, seq); + handle.unset_grab(self, data); + } + + fn unset(&mut self, data: &mut State) { + self.on_unset(data); + } +} + impl State { /// The application initiated a move grab e.g. when you drag a titlebar. pub fn move_request_client(&mut self, surface: &WlSurface, seat: &Seat, serial: Serial) { @@ -304,7 +427,7 @@ impl State { }; let grab = MoveSurfaceGrab { - start_data, + start_data: start_data.into(), window, initial_window_loc, }; @@ -342,7 +465,8 @@ impl State { focus: None, button: button_used, location: pointer.current_location(), - }; + } + .into(); let grab = MoveSurfaceGrab { start_data, @@ -356,4 +480,44 @@ impl State { .cursor_state .set_cursor_image(CursorImageStatus::Named(CursorIcon::Grabbing)); } + + pub fn touch_move_request_server( + &mut self, + surface: &WlSurface, + seat: &Seat, + serial: Serial, + slot: TouchSlot, + location: Point, + ) { + let Some(touch) = seat.get_touch() else { + tracing::warn!("seat had no touch"); + return; + }; + + let Some(window) = self.pinnacle.window_for_surface(surface).cloned() else { + warn!("Surface had no window, cancelling move request"); + return; + }; + + let initial_window_loc = self + .pinnacle + .space + .element_location(&window) + .expect("move request was called on an unmapped window") + .to_f64(); + + let start_data = smithay::input::touch::GrabStartData { + focus: None, + slot, + location, + }; + + let grab = MoveSurfaceGrab { + start_data: start_data.into(), + window, + initial_window_loc, + }; + + touch.set_grab(self, grab, serial); + } } diff --git a/src/grab/resize_grab.rs b/src/grab/resize_grab.rs index 86f465815..a5d222c86 100644 --- a/src/grab/resize_grab.rs +++ b/src/grab/resize_grab.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later use smithay::{ + backend::input::TouchSlot, desktop::WindowSurface, input::{ Seat, SeatHandler, @@ -10,18 +11,21 @@ use smithay::{ GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData, PointerGrab, PointerInnerHandle, }, + touch::{self, TouchGrab, TouchInnerHandle, UpEvent}, }, + output::Output, reexports::{ wayland_protocols::xdg::shell::server::xdg_toplevel, wayland_server::protocol::wl_surface::WlSurface, }, - utils::{IsAlive, Logical, Point, Rectangle, Size}, + utils::{IsAlive, Logical, Point, Rectangle, Serial, Size}, wayland::{compositor, shell::xdg::SurfaceCachedState}, xwayland, }; use tracing::warn; use crate::{ + grab::InputGrabStartData, layout::tree::ResizeDir, state::{State, WithState}, util::transaction::{Location, TransactionBuilder}, @@ -70,21 +74,19 @@ impl ResizeEdge { } pub struct ResizeSurfaceGrab { - start_data: GrabStartData, + start_data: InputGrabStartData, window: WindowElement, edges: ResizeEdge, initial_window_geo: Rectangle, last_window_size: Size, - button_used: u32, } impl ResizeSurfaceGrab { pub fn start( - start_data: GrabStartData, + start_data: InputGrabStartData, window: WindowElement, edges: ResizeEdge, initial_window_geo: Rectangle, - button_used: u32, ) -> Option { Some(Self { start_data, @@ -92,7 +94,6 @@ impl ResizeSurfaceGrab { edges, initial_window_geo, last_window_size: initial_window_geo.size, - button_used, }) } @@ -109,44 +110,11 @@ impl ResizeSurfaceGrab { toplevel.send_pending_configure(); } } -} - -impl PointerGrab for ResizeSurfaceGrab { - fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) { - handle.frame(data); - } - - fn motion( - &mut self, - state: &mut State, - handle: &mut PointerInnerHandle<'_, State>, - _focus: Option<(::PointerFocus, Point)>, - event: &smithay::input::pointer::MotionEvent, - ) { - handle.motion(state, None, event); - - if state.pinnacle.layout_state.pending_resize { - return; - } - - let output = self.window.output(&state.pinnacle); - - if !self.window.alive() || output.is_none() { - state - .pinnacle - .cursor_state - .set_cursor_image(CursorImageStatus::default_named()); - handle.unset_grab(self, state, event.serial, event.time, true); - return; - } - - let Some(output) = output else { - unreachable!(); - }; + fn on_motion(&mut self, state: &mut State, location: Point, output: Output) { state.pinnacle.layout_state.pending_resize = true; - let delta = (event.location - self.start_data.location).to_i32_round::(); + let delta = (location - self.start_data.location()).to_i32_round::(); let mut new_window_width = self.initial_window_geo.size.w; let mut new_window_height = self.initial_window_geo.size.h; @@ -256,6 +224,43 @@ impl PointerGrab for ResizeSurfaceGrab { transaction_builder.into_pending(Vec::new(), false, true), ); } +} + +impl PointerGrab for ResizeSurfaceGrab { + fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) { + handle.frame(data); + } + + fn motion( + &mut self, + state: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + _focus: Option<(::PointerFocus, Point)>, + event: &smithay::input::pointer::MotionEvent, + ) { + handle.motion(state, None, event); + + if state.pinnacle.layout_state.pending_resize { + return; + } + + let output = self.window.output(&state.pinnacle); + + if !self.window.alive() || output.is_none() { + state + .pinnacle + .cursor_state + .set_cursor_image(CursorImageStatus::default_named()); + handle.unset_grab(self, state, event.serial, event.time, true); + return; + } + + let Some(output) = output else { + unreachable!(); + }; + + self.on_motion(state, event.location, output); + } fn relative_motion( &mut self, @@ -275,7 +280,10 @@ impl PointerGrab for ResizeSurfaceGrab { ) { handle.button(data, event); - if !handle.current_pressed().contains(&self.button_used) { + if !handle + .current_pressed() + .contains(&PointerGrab::start_data(self).button) + { data.pinnacle .cursor_state .set_cursor_image(CursorImageStatus::default_named()); @@ -293,7 +301,9 @@ impl PointerGrab for ResizeSurfaceGrab { } fn start_data(&self) -> &GrabStartData { - &self.start_data + self.start_data + .as_pointer() + .expect("start_data isn't Pointer") } fn unset(&mut self, _data: &mut State) { @@ -373,6 +383,112 @@ impl PointerGrab for ResizeSurfaceGrab { } } +impl TouchGrab for ResizeSurfaceGrab { + fn start_data(&self) -> &smithay::input::touch::GrabStartData { + self.start_data + .as_touch() + .expect("start_data is not Touch.") + } + + fn down( + &mut self, + data: &mut State, + handle: &mut smithay::input::touch::TouchInnerHandle<'_, State>, + _focus: Option<(::TouchFocus, Point)>, + event: &smithay::input::touch::DownEvent, + seq: smithay::utils::Serial, + ) { + handle.down(data, None, event, seq); + } + + fn up( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &UpEvent, + seq: Serial, + ) { + handle.up(data, event, seq); + + let Some(start_data) = self.start_data.as_touch() else { + return; + }; + + if event.slot == start_data.slot { + handle.unset_grab(self, data); + } + } + + fn motion( + &mut self, + state: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + focus: Option<(::TouchFocus, Point)>, + event: &smithay::input::touch::MotionEvent, + seq: Serial, + ) { + handle.motion(state, focus, event, seq); + + let Some(start_data) = self.start_data.as_touch() else { + return; + }; + + if event.slot != start_data.slot { + return; + } + + if state.pinnacle.layout_state.pending_resize { + return; + } + + let output = self.window.output(&state.pinnacle); + + if !self.window.alive() || output.is_none() { + handle.unset_grab(self, state); + return; + } + + let Some(output) = output else { + unreachable!(); + }; + + self.on_motion(state, event.location, output); + } + + fn shape( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &smithay::input::touch::ShapeEvent, + seq: Serial, + ) { + handle.shape(data, event, seq); + } + + fn orientation( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &smithay::input::touch::OrientationEvent, + seq: Serial, + ) { + handle.orientation(data, event, seq); + } + + fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) { + handle.frame(data, seq); + } + + fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) { + handle.cancel(data, seq); + handle.unset_grab(self, data); + } + + fn unset(&mut self, _data: &mut State) { + self.ungrab(); + } +} + impl State { /// The application requests a resize e.g. when you drag the edges of a window. pub fn resize_request_client( @@ -381,7 +497,7 @@ impl State { seat: &Seat, serial: smithay::utils::Serial, edges: self::ResizeEdge, - button_used: u32, + _button_used: u32, ) { let pointer = seat.get_pointer().expect("seat had no pointer"); @@ -411,13 +527,8 @@ impl State { toplevel.send_pending_configure(); } - let grab = ResizeSurfaceGrab::start( - start_data, - window, - edges, - initial_window_geo, - button_used, - ); + let grab = + ResizeSurfaceGrab::start(start_data.into(), window, edges, initial_window_geo); if let Some(grab) = grab { pointer.set_grab(self, grab, serial, Focus::Clear); @@ -425,31 +536,26 @@ impl State { } } - /// The compositor requested a resize e.g. you hold the mod key and right-click drag. - pub fn resize_request_server( + fn common_resize_request_server( &mut self, surface: &WlSurface, - seat: &Seat, - serial: smithay::utils::Serial, edges: self::ResizeEdge, - button_used: u32, - ) { - let pointer = seat.get_pointer().expect("seat had no pointer"); - + start_data: InputGrabStartData, + ) -> Option { let Some(window) = self.pinnacle.window_for_surface(surface).cloned() else { tracing::error!("Surface had no window, cancelling resize request"); - return; + return None; }; if window.with_state(|state| { state.layout_mode.is_maximized() || state.layout_mode.is_fullscreen() }) { - return; + return None; } let Some(initial_window_geo) = self.pinnacle.space.element_geometry(&window) else { warn!("Resize request on unmapped surface"); - return; + return None; }; if let Some(window) = self.pinnacle.window_for_surface(surface) @@ -462,14 +568,27 @@ impl State { toplevel.send_pending_configure(); } + ResizeSurfaceGrab::start(start_data, window, edges, initial_window_geo) + } + + /// The compositor requested a resize e.g. you hold the mod key and right-click drag. + pub fn resize_request_server( + &mut self, + surface: &WlSurface, + seat: &Seat, + serial: smithay::utils::Serial, + edges: self::ResizeEdge, + button_used: u32, + ) { + let pointer = seat.get_pointer().expect("seat had no pointer"); + let start_data = smithay::input::pointer::GrabStartData { focus: None, button: button_used, location: pointer.current_location(), }; - let grab = - ResizeSurfaceGrab::start(start_data, window, edges, initial_window_geo, button_used); + let grab = self.common_resize_request_server(surface, edges, start_data.into()); if let Some(grab) = grab { pointer.set_grab(self, grab, serial, Focus::Clear); @@ -479,4 +598,31 @@ impl State { .set_cursor_image(CursorImageStatus::Named(edges.cursor_icon())); } } + + pub fn touch_resize_request_server( + &mut self, + surface: &WlSurface, + seat: &Seat, + serial: Serial, + edges: self::ResizeEdge, + slot: TouchSlot, + location: Point, + ) { + let Some(touch) = seat.get_touch() else { + tracing::warn!("seat had no touch"); + return; + }; + + let start_data = touch::GrabStartData { + focus: None, + slot, + location, + }; + + let grab = self.common_resize_request_server(surface, edges, start_data.into()); + + if let Some(grab) = grab { + touch.set_grab(self, grab, serial); + } + } } diff --git a/src/input.rs b/src/input.rs index eaf702016..df35a5cd2 100644 --- a/src/input.rs +++ b/src/input.rs @@ -20,7 +20,7 @@ use smithay::{ GestureBeginEvent, GestureEndEvent, InputBackend, InputEvent, KeyState, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent, TabletToolProximityEvent, - TabletToolTipEvent, TabletToolTipState, TouchEvent, + TabletToolTipEvent, TabletToolTipState, TouchEvent, TouchSlot, }, renderer::utils::with_renderer_surface_state, winit::WinitVirtualDevice, @@ -324,6 +324,43 @@ impl State { } } + pub fn update_surface_focus( + &mut self, + target: Option<(PointerFocusTarget, Point)>, + ) { + if let Some((focus, _)) = target { + if let Some(window) = focus.window_for(&self.pinnacle) { + self.pinnacle.raise_window(window.clone()); + for output in self.pinnacle.space.outputs_for_element(&window) { + self.schedule_render(&output); + } + if !window.is_x11_override_redirect() { + self.pinnacle.keyboard_focus_stack.set_focus(window.clone()); + } + self.pinnacle.on_demand_layer_focus = None; + } else if let Some(layer) = focus.layer_for(&self.pinnacle) { + if layer.can_receive_keyboard_focus() { + self.pinnacle.on_demand_layer_focus = Some(layer); + } else if let wlr_layer::Layer::Bottom | wlr_layer::Layer::Background = + layer.layer() + { + // Only unset focus when clicking on background stuff + self.pinnacle.keyboard_focus_stack.unset_focus(); + self.pinnacle.on_demand_layer_focus = None; + } + } else if !self.pinnacle.lock_state.is_unlocked() { + if let Some(lock_surface) = focus.lock_surface_for(&self.pinnacle) { + self.pinnacle.lock_surface_focus = Some(lock_surface); + } else { + self.pinnacle.keyboard_focus_stack.unset_focus(); + } + } + } else { + self.pinnacle.keyboard_focus_stack.unset_focus(); + self.pinnacle.on_demand_layer_focus = None; + } + } + /// Update the pointer focus if it's different from the previous one. pub fn update_pointer_focus(&mut self) { let _span = tracy_client::span!("State::update_pointer_focus"); @@ -581,37 +618,7 @@ impl State { self.pinnacle.focus_output(&output_under); } - if let Some((focus, _)) = self.pinnacle.pointer_contents.focus_under.as_ref() { - if let Some(window) = focus.window_for(&self.pinnacle) { - self.pinnacle.raise_window(window.clone()); - for output in self.pinnacle.space.outputs_for_element(&window) { - self.schedule_render(&output); - } - if !window.is_x11_override_redirect() { - self.pinnacle.keyboard_focus_stack.set_focus(window.clone()); - } - self.pinnacle.on_demand_layer_focus = None; - } else if let Some(layer) = focus.layer_for(&self.pinnacle) { - if layer.can_receive_keyboard_focus() { - self.pinnacle.on_demand_layer_focus = Some(layer); - } else if let wlr_layer::Layer::Bottom | wlr_layer::Layer::Background = - layer.layer() - { - // Only unset focus when clicking on background stuff - self.pinnacle.keyboard_focus_stack.unset_focus(); - self.pinnacle.on_demand_layer_focus = None; - } - } else if !self.pinnacle.lock_state.is_unlocked() { - if let Some(lock_surface) = focus.lock_surface_for(&self.pinnacle) { - self.pinnacle.lock_surface_focus = Some(lock_surface); - } else { - self.pinnacle.keyboard_focus_stack.unset_focus(); - } - } - } else { - self.pinnacle.keyboard_focus_stack.unset_focus(); - self.pinnacle.on_demand_layer_focus = None; - } + self.update_surface_focus(self.pinnacle.pointer_contents.focus_under.clone()); }; pointer.button( @@ -1028,6 +1035,19 @@ impl State { ); } + fn update_touch_locs(&mut self, slot: TouchSlot, loc: Point) { + if let Some(entry) = self + .pinnacle + .touch_positions + .iter_mut() + .find(|(s, _)| s == &slot) + { + entry.1 = loc; + } else { + self.pinnacle.touch_positions.push((slot, loc)); + } + } + fn on_touch_down(&mut self, event: I::TouchDownEvent) where I::Device: 'static, @@ -1040,7 +1060,16 @@ impl State { return; }; + self.update_touch_locs(event.slot(), touch_loc); + + let output_under = self.pinnacle.space.output_under(touch_loc).next().cloned(); + + if let Some(output_under) = output_under { + self.pinnacle.focus_output(&output_under); + } + let focus = self.pinnacle.pointer_contents_under(touch_loc); + self.update_surface_focus(focus.focus_under.clone()); touch.down( self, @@ -1066,6 +1095,7 @@ impl State { return; }; + self.update_touch_locs(event.slot(), touch_loc); let focus = self.pinnacle.pointer_contents_under(touch_loc); touch.motion( @@ -1084,6 +1114,10 @@ impl State { return; }; + self.pinnacle + .touch_positions + .retain(|(s, _)| s != &event.slot()); + touch.up( self, &touch::UpEvent { @@ -1102,11 +1136,16 @@ impl State { touch.frame(self); } - fn on_touch_cancel(&mut self, _event: I::TouchCancelEvent) { + fn on_touch_cancel(&mut self, event: I::TouchCancelEvent) { let Some(touch) = self.pinnacle.seat.get_touch() else { return; }; + // I assume libinput sends a cancel for every touch slot. + self.pinnacle + .touch_positions + .retain(|(s, _)| s != &event.slot()); + touch.cancel(self); } diff --git a/src/state.rs b/src/state.rs index e2342fb86..757c05f77 100644 --- a/src/state.rs +++ b/src/state.rs @@ -30,8 +30,11 @@ use crate::{ window::{Unmapped, WindowElement, ZIndexElement, rules::WindowRuleState}, }; use smithay::{ - backend::renderer::element::{ - RenderElementState, RenderElementStates, utils::select_dmabuf_feedback, + backend::{ + input::TouchSlot, + renderer::element::{ + RenderElementState, RenderElementStates, utils::select_dmabuf_feedback, + }, }, desktop::{ LayerSurface, PopupManager, Space, layer_map_for_output, @@ -56,7 +59,7 @@ use smithay::{ protocol::wl_surface::WlSurface, }, }, - utils::{Clock, HookId, Monotonic}, + utils::{Clock, HookId, Logical, Monotonic, Point}, wayland::{ compositor::{ self, CompositorClientState, CompositorHandler, CompositorState, SurfaceData, @@ -240,6 +243,8 @@ pub struct Pinnacle { pub pointer_contents: PointerContents, pub last_pointer_focus: Option<::PointerFocus>, + pub touch_positions: Vec<(TouchSlot, Point)>, + pub blocker_cleared_tx: std::sync::mpsc::Sender, pub blocker_cleared_rx: std::sync::mpsc::Receiver, @@ -547,6 +552,8 @@ impl Pinnacle { pointer_contents: Default::default(), last_pointer_focus: Default::default(), + touch_positions: Default::default(), + blocker_cleared_tx, blocker_cleared_rx,