diff --git a/Cargo.lock b/Cargo.lock index 79662f089..3da87c2cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4460,6 +4460,7 @@ name = "snowcap" version = "0.0.1" dependencies = [ "anyhow", + "bitflags 2.10.0", "futures", "iced", "iced_futures", diff --git a/snowcap/Cargo.toml b/snowcap/Cargo.toml index a078f83a8..309f84968 100644 --- a/snowcap/Cargo.toml +++ b/snowcap/Cargo.toml @@ -6,6 +6,7 @@ rust-version.workspace = true [dependencies] anyhow = { workspace = true } +bitflags = { workspace = true } iced = { version = "0.14.0", default-features = false, features = [ "wgpu", "tokio", diff --git a/snowcap/api/lua/snowcap/grpc/defs.lua b/snowcap/api/lua/snowcap/grpc/defs.lua index dd7545251..d9352b1fb 100644 --- a/snowcap/api/lua/snowcap/grpc/defs.lua +++ b/snowcap/api/lua/snowcap/grpc/defs.lua @@ -764,6 +764,7 @@ local snowcap_layer_v1_Layer = { ---@field input_region snowcap.widget.v1.InputRegion? ---@field mouse_area snowcap.widget.v1.MouseArea? ---@field text_input snowcap.widget.v1.TextInput? +---@field wlr_task_list snowcap.widget.v1.WlrTaskList? ---@class snowcap.widget.v1.Text ---@field text string? @@ -982,6 +983,31 @@ local snowcap_layer_v1_Layer = { ---@field submit google.protobuf.Empty? ---@field paste string? +---@class snowcap.widget.v1.WlrTaskList +---@field widget_id integer? +---@field on_enter boolean? +---@field on_update boolean? +---@field on_leave boolean? +---@field child snowcap.widget.v1.WidgetDef? + +---@class snowcap.widget.v1.WlrTaskList.Event +---@field enter snowcap.widget.v1.WlrTaskList.WlrTaskData? +---@field update snowcap.widget.v1.WlrTaskList.WlrTaskData? +---@field leave integer? + +---@class snowcap.widget.v1.WlrTaskList.WlrTaskData +---@field id integer? +---@field title string? +---@field app_id string? +---@field state snowcap.widget.v1.WlrTaskList.WlrTaskState? +---@field outputs string[]? + +---@class snowcap.widget.v1.WlrTaskList.WlrTaskState +---@field maximized boolean? +---@field minimized boolean? +---@field activated boolean? +---@field fullscreen boolean? + ---@class snowcap.widget.v1.GetWidgetEventsRequest ---@field layer_id integer? ---@field decoration_id integer? @@ -991,6 +1017,7 @@ local snowcap_layer_v1_Layer = { ---@field button snowcap.widget.v1.Button.Event? ---@field mouse_area snowcap.widget.v1.MouseArea.Event? ---@field text_input snowcap.widget.v1.TextInput.Event? +---@field wlr_task_list snowcap.widget.v1.WlrTaskList.Event? ---@class snowcap.widget.v1.GetWidgetEventsResponse ---@field widget_events snowcap.widget.v1.WidgetEvent[]? @@ -1029,9 +1056,35 @@ local snowcap_layer_v1_Layer = { ---@class snowcap.operation.v1.TextInput.SelectAll ---@field id string? +---@class snowcap.operation.v1.WlrTaskList +---@field maximize snowcap.operation.v1.WlrTaskList.MaximizeToplevel? +---@field minimize snowcap.operation.v1.WlrTaskList.MinimizeToplevel? +---@field fullscreen snowcap.operation.v1.WlrTaskList.FullscreenToplevel? +---@field activate snowcap.operation.v1.WlrTaskList.ActivateToplevel? +---@field close snowcap.operation.v1.WlrTaskList.CloseToplevel? + +---@class snowcap.operation.v1.WlrTaskList.MaximizeToplevel +---@field id integer? +---@field maximize boolean? + +---@class snowcap.operation.v1.WlrTaskList.MinimizeToplevel +---@field id integer? +---@field minimize boolean? + +---@class snowcap.operation.v1.WlrTaskList.FullscreenToplevel +---@field id integer? +---@field fullscreen boolean? + +---@class snowcap.operation.v1.WlrTaskList.ActivateToplevel +---@field id integer? + +---@class snowcap.operation.v1.WlrTaskList.CloseToplevel +---@field id integer? + ---@class snowcap.operation.v1.Operation ---@field focusable snowcap.operation.v1.Focusable? ---@field text_input snowcap.operation.v1.TextInput? +---@field wlr_task_list snowcap.operation.v1.WlrTaskList? ---@class snowcap.decoration.v1.Bounds ---@field left integer? @@ -1321,6 +1374,10 @@ snowcap.widget.v1.TextInput.Icon = {} snowcap.widget.v1.TextInput.Style = {} snowcap.widget.v1.TextInput.Style.Inner = {} snowcap.widget.v1.TextInput.Event = {} +snowcap.widget.v1.WlrTaskList = {} +snowcap.widget.v1.WlrTaskList.Event = {} +snowcap.widget.v1.WlrTaskList.WlrTaskData = {} +snowcap.widget.v1.WlrTaskList.WlrTaskState = {} snowcap.widget.v1.GetWidgetEventsRequest = {} snowcap.widget.v1.WidgetEvent = {} snowcap.widget.v1.GetWidgetEventsResponse = {} @@ -1336,6 +1393,12 @@ snowcap.operation.v1.TextInput.MoveCursor = {} snowcap.operation.v1.TextInput.MoveCursorFront = {} snowcap.operation.v1.TextInput.MoveCursorEnd = {} snowcap.operation.v1.TextInput.SelectAll = {} +snowcap.operation.v1.WlrTaskList = {} +snowcap.operation.v1.WlrTaskList.MaximizeToplevel = {} +snowcap.operation.v1.WlrTaskList.MinimizeToplevel = {} +snowcap.operation.v1.WlrTaskList.FullscreenToplevel = {} +snowcap.operation.v1.WlrTaskList.ActivateToplevel = {} +snowcap.operation.v1.WlrTaskList.CloseToplevel = {} snowcap.operation.v1.Operation = {} snowcap.decoration = {} snowcap.decoration.v1 = {} diff --git a/snowcap/api/lua/snowcap/widget.lua b/snowcap/api/lua/snowcap/widget.lua index 18105a20f..461a0812e 100644 --- a/snowcap/api/lua/snowcap/widget.lua +++ b/snowcap/api/lua/snowcap/widget.lua @@ -32,6 +32,7 @@ ---@field input_region snowcap.widget.InputRegion? ---@field mouse_area snowcap.widget.MouseArea? ---@field text_input snowcap.widget.TextInput? +---@field wlr_task_list snowcap.widget.WlrTaskList? ---@class snowcap.widget.Border ---@field color snowcap.widget.Color? @@ -436,6 +437,43 @@ local text_input_event_type = { PASTE = "press", } +---@class snowcap.widget.WlrTaskList +---@field package widget_id integer? +---@field on_enter (fun(task: snowcap.widget.wlr_task_list.WlrTaskData): any)? +---@field on_update (fun(task: snowcap.widget.wlr_task_list.WlrTaskData): any)? +---@field on_leave (fun(integer): any)? +---@field child snowcap.widget.WidgetDef? + +---@class snowcap.widget.wlr_task_list.Callbacks +---@field on_enter (fun(task: snowcap.widget.wlr_task_list.WlrTaskData): any)? +---@field on_update (fun(task: snowcap.widget.wlr_task_list.WlrTaskData): any)? +---@field on_leave (fun(integer): any)? + +---@class snowcap.widget.wlr_task_list.WlrTaskData +---@field id integer? +---@field title string? +---@field app_id string? +---@field state snowcap.widget.v1.WlrTaskList.WlrTaskState? +---@field outputs string[]? + +---@class snowcap.widget.wlr_task_list.WlrTaskState +---@field maximized boolean? +---@field minimized boolean? +---@field activated boolean? +---@field fullscreen boolean? + +---@class snowcap.widget.wlr_task_list.Event +---@field enter? snowcap.widget.wlr_task_list.WlrTaskState +---@field update? snowcap.widget.wlr_task_list.WlrTaskState +---@field leave? integer + +---@enum snowcap.widget.wlr_task_list.event.Type +local wlr_task_list_event_type = { + ENTER = "enter", + UPDATE = "update", + LEAVE = "leave", +} + ---@class snowcap.widget.Length ---@field fill {}? ---@field fill_portion integer? @@ -640,6 +678,7 @@ local font = { ---@field button fun(widget: snowcap.widget.WidgetDef)? ---@field mouse_area fun(widget: snowcap.widget.WidgetDef)? ---@field text_input fun(widget: snowcap.widget.WidgetDef)? +---@field wlr_task_list fun(widget: snowcap.widget.WidgetDef)? local widget = { length = length, @@ -832,6 +871,19 @@ local function text_input_into_api(def) } end +---@param def snowcap.widget.WlrTaskList +---@return snowcap.widget.v1.WlrTaskList +local function wlr_task_list_into_api(def) + ---@type snowcap.widget.v1.WlrTaskList + return { + widget_id = def.widget_id, + on_enter = def.on_enter ~= nil, + on_update = def.on_update ~= nil, + on_leave = def.on_leave ~= nil, + child = widget.widget_def_into_api(def.child) + } +end + ---@param def snowcap.widget.WidgetDef ---@return snowcap.widget.v1.WidgetDef function widget.widget_def_into_api(def) @@ -865,6 +917,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.wlr_task_list then + def.wlr_task_list = wlr_task_list_into_api(def.wlr_task_list) + end return def --[[@as snowcap.widget.v1.WidgetDef]] end @@ -1001,6 +1056,26 @@ function widget.text_input(text_input) } end +---@param wlr_task_list snowcap.widget.WlrTaskList +---@return snowcap.widget.WidgetDef +function widget.wlr_task_list(wlr_task_list) + local has_cb = false + + has_cb = has_cb or wlr_task_list.on_enter ~= nil + has_cb = has_cb or wlr_task_list.on_update ~= nil + has_cb = has_cb or wlr_task_list.on_leave ~= nil + + if has_cb then + wlr_task_list.widget_id = widget_id_counter + widget_id_counter = widget_id_counter + 1 + end + + ---@type snowcap.widget.WidgetDef + return { + wlr_task_list = wlr_task_list + } +end + ---@private ---@lcat nodoc ---@param wgt snowcap.widget.WidgetDef @@ -1026,6 +1101,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.wlr_task_list then + widget._traverse_widget_tree(wgt.wlr_task_list.child, callbacks, with_widget) end end @@ -1065,6 +1142,20 @@ local function collect_text_input_callbacks(text_input) } end +---@package +---@lcat nodoc +--- +---@param task_list snowcap.widget.WlrTaskList +--- +---@return snowcap.widget.wlr_task_list.Callbacks +local function collect_wlr_task_list_callbacks(task_list) + return { + on_enter = task_list.on_enter, + on_update = task_list.on_update, + on_leave = task_list.on_leave, + } +end + ---@private ---@lcat nodoc ---@param callbacks any[] @@ -1081,6 +1172,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.wlr_task_list and wgt.wlr_task_list.widget_id then + callbacks[wgt.wlr_task_list.widget_id] = collect_wlr_task_list_callbacks(wgt.wlr_task_list) + end end ---@private @@ -1192,6 +1287,46 @@ function widget._text_input_process_event(callbacks, event) return msg end +---@private +---@lcat nodoc +--- +---@param callbacks snowcap.widget.wlr_task_list.Callbacks +---@param event snowcap.widget.wlr_task_list.Event +---@return any? +function widget._wlr_task_list_process_event(callbacks, event) + callbacks = callbacks or {} + + local translate = { + [wlr_task_list_event_type.ENTER] = "on_enter", + [wlr_task_list_event_type.UPDATE] = "on_update", + [wlr_task_list_event_type.LEAVE] = "on_leave", + } + + 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[] @@ -1212,6 +1347,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.wlr_task_list then + if callbacks[widget_id] ~= nil then + ---@diagnostic disable-next-line:param-type-mismatch + msg = widget._wlr_task_list_process_event(callbacks[widget_id], event.wlr_task_list) + end end return msg diff --git a/snowcap/api/lua/snowcap/widget/operation.lua b/snowcap/api/lua/snowcap/widget/operation.lua index 71101e2ad..c88975672 100644 --- a/snowcap/api/lua/snowcap/widget/operation.lua +++ b/snowcap/api/lua/snowcap/widget/operation.lua @@ -36,10 +36,37 @@ ---@class snowcap.widget.operation.text_input.SelectAll ---@field id string? +---Operation acting on WlrTaskList. +---@class snowcap.widget.operation.WlrTaskList +---@field maximize snowcap.widget.operation.wlr_task_list.MaximizeToplevel? +---@field minimize snowcap.widget.operation.wlr_task_list.MinimizeToplevel? +---@field fullscreen snowcap.widget.operation.wlr_task_list.FullscreenToplevel? +---@field activate snowcap.widget.operation.wlr_task_list.ActivateToplevel? +---@field close snowcap.widget.operation.wlr_task_list.CloseToplevel? + +---@class snowcap.widget.operation.wlr_task_list.MaximizeToplevel +---@field id integer +---@field maximize boolean + +---@class snowcap.widget.operation.wlr_task_list.MinimizeToplevel +---@field id integer +---@field minimize boolean + +---@class snowcap.widget.operation.wlr_task_list.FullscreenToplevel +---@field id integer +---@field fullscreen boolean + +---@class snowcap.widget.operation.wlr_task_list.ActivateToplevel +---@field id integer + +---@class snowcap.widget.operation.wlr_task_list.CloseToplevel +---@field id integer + ---Update widgets' internal state. ---@class snowcap.widget.operation.Operation ---@field focusable snowcap.widget.operation.Focusable? ---@field text_input snowcap.widget.operation.TextInput? +---@field wlr_task_list snowcap.widget.operation.WlrTaskList? ---Operation acting on widgets that can be focused. ---@class snowcap.widget.operation.focusable @@ -128,6 +155,91 @@ function text_input.SelectAll(widget_id) } end +---Operates on WlrTaskList +---@class snowcap.widget.operation.wlr_task_list +local wlr_task_list = {} + +---Operation that request a toplevel to be maximized or unmaximized. +---@param toplevel_id integer Topleve Id. +---@param maximize boolean +--- +---@return snowcap.widget.operation.Operation +function wlr_task_list.MaximizeToplevel(toplevel_id, maximize) + ---@type snowcap.widget.operation.Operation + return { + wlr_task_list = { + maximize = { + id = toplevel_id, + maximize = maximize + } + } + } +end + +---Operation that request a toplevel to be minimized or unminimized. +---@param toplevel_id integer Topleve Id. +---@param minimize boolean +--- +---@return snowcap.widget.operation.Operation +function wlr_task_list.MinimizeToplevel(toplevel_id, minimize) + ---@type snowcap.widget.operation.Operation + return { + wlr_task_list = { + minimize = { + id = toplevel_id, + minimize = minimize + } + } + } +end + +---Operation that request a toplevel to be fullscreened or unfullscreened. +---@param toplevel_id integer Topleve Id. +---@param fullscreen boolean +--- +---@return snowcap.widget.operation.Operation +function wlr_task_list.FullscreenToplevel(toplevel_id, fullscreen) + ---@type snowcap.widget.operation.Operation + return { + wlr_task_list = { + fullscreen = { + id = toplevel_id, + fullscreen = fullscreen + } + } + } +end + +---Operation that request a toplevel to be activated. +---@param toplevel_id integer Toplevel Id. +--- +---@return snowcap.widget.operation.Operation +function wlr_task_list.ActivateToplevel(toplevel_id) + ---@type snowcap.widget.operation.Operation + return { + wlr_task_list = { + activate = { + id = toplevel_id, + } + } + } +end + +---Operation that request a toplevel to be closed. +---@param toplevel_id integer Toplevel Id +--- +---@return snowcap.widget.operation.Operation +function wlr_task_list.CloseToplevel(toplevel_id) + ---@type snowcap.widget.operation.Operation + return { + wlr_task_list = { + close = { + id = toplevel_id, + } + } + } +end + ---Update internal state for some widgets. --- ---`Operation` can be passed to `LayerHandle:operate` and `DecorationHandle::operate` to @@ -161,6 +273,7 @@ end local operation = { focusable = focusable, text_input = text_input, + wlr_task_list = wlr_task_list, } ---@private diff --git a/snowcap/api/protobuf/snowcap/operation/v1/operation.proto b/snowcap/api/protobuf/snowcap/operation/v1/operation.proto index 3741dd1a1..f8c62611c 100644 --- a/snowcap/api/protobuf/snowcap/operation/v1/operation.proto +++ b/snowcap/api/protobuf/snowcap/operation/v1/operation.proto @@ -41,9 +41,43 @@ message TextInput { } } +message WlrTaskList { + message MaximizeToplevel { + uint64 id = 1; + bool maximize = 2; + } + + message MinimizeToplevel { + uint64 id = 1; + bool minimize = 2; + } + + message FullscreenToplevel { + uint64 id = 1; + bool fullscreen = 2; + } + + message ActivateToplevel { + uint64 id = 1; + } + + message CloseToplevel { + uint64 id = 1; + } + + oneof op { + MaximizeToplevel maximize = 1; + MinimizeToplevel minimize = 2; + FullscreenToplevel fullscreen = 3; + ActivateToplevel activate = 4; + CloseToplevel close = 5; + } +} + message Operation { oneof target { Focusable focusable = 1; TextInput text_input = 2; + WlrTaskList wlr_task_list = 3; } } diff --git a/snowcap/api/protobuf/snowcap/widget/v1/widget.proto b/snowcap/api/protobuf/snowcap/widget/v1/widget.proto index bf892c590..7393fa050 100644 --- a/snowcap/api/protobuf/snowcap/widget/v1/widget.proto +++ b/snowcap/api/protobuf/snowcap/widget/v1/widget.proto @@ -160,6 +160,7 @@ message WidgetDef { InputRegion input_region = 9; MouseArea mouse_area = 10; TextInput text_input = 11; + WlrTaskList wlr_task_list = 12; } } @@ -453,6 +454,37 @@ message TextInput { } } +message WlrTaskList { + optional uint32 widget_id = 1; + bool on_enter = 2; + bool on_update = 3; + bool on_leave = 4; + WidgetDef child = 5; + + message Event { + oneof data { + WlrTaskData enter = 1; + WlrTaskData update = 2; + uint64 leave = 3; + } + } + + message WlrTaskData { + uint64 id = 1; + optional string title = 2; + optional string app_id = 3; + optional WlrTaskState state = 4; + repeated string outputs = 5; + } + + message WlrTaskState { + bool maximized = 1; + bool minimized = 2; + bool activated = 3; + bool fullscreen = 4; + } +} + message GetWidgetEventsRequest { oneof id { uint32 layer_id = 1; @@ -467,6 +499,7 @@ message WidgetEvent { Button.Event button = 2; MouseArea.Event mouse_area = 3; TextInput.Event text_input = 4; + WlrTaskList.Event wlr_task_list = 5; } } diff --git a/snowcap/api/rust/src/widget.rs b/snowcap/api/rust/src/widget.rs index 5dbd61b72..c4282f8b3 100644 --- a/snowcap/api/rust/src/widget.rs +++ b/snowcap/api/rust/src/widget.rs @@ -142,6 +142,7 @@ where WidgetMessage::TextInput(callbacks) => callbacks.process_event(event.into()), _ => unreachable!(), }), + Event::WlrTaskList(_event) => todo!(), } } diff --git a/snowcap/src/api/decoration/v1.rs b/snowcap/src/api/decoration/v1.rs index 43fda05ae..227174a0b 100644 --- a/snowcap/src/api/decoration/v1.rs +++ b/snowcap/src/api/decoration/v1.rs @@ -31,7 +31,7 @@ impl decoration_service_server::DecorationService for super::DecorationService { let z_index = request.z_index; run_unary(&self.sender, move |state| { - let Some(f) = crate::api::widget::v1::widget_def_to_fn(widget_def) else { + let Some(f) = crate::api::widget::v1::widget_def_to_fn(widget_def, state) else { return Err(Status::invalid_argument("widget def was null")); }; @@ -139,6 +139,8 @@ impl decoration_service_server::DecorationService for super::DecorationService { let z_index = request.z_index; run_unary(&self.sender, move |state| { + let widget_def = widget_def.and_then(|def| widget_def_to_fn(def, state)); + let Some(deco) = state .decorations .iter_mut() @@ -148,7 +150,7 @@ impl decoration_service_server::DecorationService for super::DecorationService { }; deco.update_properties( - widget_def.and_then(widget_def_to_fn), + widget_def, bounds.map(|bounds| crate::decoration::Bounds { left: bounds.left, right: bounds.right, diff --git a/snowcap/src/api/layer/v1.rs b/snowcap/src/api/layer/v1.rs index 642bdea9d..870efeebb 100644 --- a/snowcap/src/api/layer/v1.rs +++ b/snowcap/src/api/layer/v1.rs @@ -71,7 +71,7 @@ impl layer_service_server::LayerService for super::LayerService { }; run_unary(&self.sender, move |state| { - let Some(f) = crate::api::widget::v1::widget_def_to_fn(widget_def) else { + let Some(f) = crate::api::widget::v1::widget_def_to_fn(widget_def, state) else { return Err(Status::invalid_argument("widget def was null")); }; @@ -199,6 +199,8 @@ impl layer_service_server::LayerService for super::LayerService { let widget_def = request.widget_def; run_unary(&self.sender, move |state| { + let widget_def = widget_def.and_then(|def| widget_def_to_fn(def, state)); + let Some(layer) = state.layers.iter_mut().find(|layer| layer.layer_id == id) else { return Ok(UpdateLayerResponse {}); }; @@ -208,7 +210,7 @@ impl layer_service_server::LayerService for super::LayerService { anchor, exclusive_zone, keyboard_interactivity, - widget_def.and_then(widget_def_to_fn), + widget_def, ); Ok(UpdateLayerResponse {}) diff --git a/snowcap/src/api/operation/v1.rs b/snowcap/src/api/operation/v1.rs index 4d412bce4..46c0f20f5 100644 --- a/snowcap/src/api/operation/v1.rs +++ b/snowcap/src/api/operation/v1.rs @@ -31,6 +31,7 @@ impl TryFromApi for Box TryFromApi::try_from_api(focusable), Target::TextInput(text_input) => TryFromApi::try_from_api(text_input), + Target::WlrTaskList(tasklist) => TryFromApi::try_from_api(tasklist), } } } @@ -98,3 +99,42 @@ impl FromApi for Box for Box { + type Error = anyhow::Error; + + fn try_from_api(api_type: operation::v1::WlrTaskList) -> Result { + const MESSAGE: &str = "snowcap.operation.v1.WlrTaskList"; + + let Some(op) = api_type.op else { + anyhow::bail!("While converting {MESSAGE}: missing field 'op'"); + }; + + Ok(FromApi::from_api(op)) + } +} + +impl FromApi for Box { + fn from_api(api_type: operation::v1::wlr_task_list::Op) -> Self { + use crate::widget::wlr_tasklist; + use operation::v1::wlr_task_list::{self as api, Op}; + + match api_type { + Op::Maximize(api::MaximizeToplevel { id, maximize }) => Box::new( + wlr_tasklist::operation::toplevel_set_maximized(id, maximize), + ), + Op::Minimize(api::MinimizeToplevel { id, minimize }) => Box::new( + wlr_tasklist::operation::toplevel_set_minimized(id, minimize), + ), + Op::Fullscreen(api::FullscreenToplevel { id, fullscreen }) => Box::new( + wlr_tasklist::operation::toplevel_set_fullscreen(id, fullscreen), + ), + Op::Activate(api::ActivateToplevel { id }) => { + Box::new(wlr_tasklist::operation::toplevel_activate(id)) + } + Op::Close(api::CloseToplevel { id }) => { + Box::new(wlr_tasklist::operation::toplevel_close(id)) + } + } + } +} diff --git a/snowcap/src/api/widget/v1.rs b/snowcap/src/api/widget/v1.rs index a94c7f4e1..5b25de87f 100644 --- a/snowcap/src/api/widget/v1.rs +++ b/snowcap/src/api/widget/v1.rs @@ -2,6 +2,7 @@ use anyhow::Context; use iced::widget::{ Column, Container, Row, Scrollable, button, image::FilterMethod, scrollable::Scrollbar, }; +use smithay_client_toolkit::reexports::client::{Proxy, protocol::wl_seat::WlSeat}; use snowcap_api_defs::snowcap::widget::{ self, v1::{ @@ -14,9 +15,11 @@ use tonic::{Request, Response, Status}; use crate::{ api::{ResponseStream, run_server_streaming_mapped}, decoration::DecorationId, + handlers::foreign_toplevel_management::ToplevelState, layer::LayerId, + state::State, util::convert::{FromApi, TryFromApi}, - widget::{MouseAreaEvent, TextInputEvent, ViewFn, WidgetEvent, WidgetId}, + widget::{MouseAreaEvent, TextInputEvent, ViewFn, WidgetEvent, WidgetId, wlr_tasklist}, }; #[tonic::async_trait] @@ -61,6 +64,9 @@ impl widget_service_server::WidgetService for super::WidgetService { WidgetEvent::TextInput(evt) => { widget_event::Event::TextInput(evt.into()) } + WidgetEvent::WlrTaskList(evt) => { + widget_event::Event::WlrTaskList(evt.into()) + } }), }) .collect(), @@ -70,7 +76,7 @@ impl widget_service_server::WidgetService for super::WidgetService { } } -pub fn widget_def_to_fn(def: WidgetDef) -> Option { +pub fn widget_def_to_fn(def: WidgetDef, state: &State) -> Option { let def = def.widget?; match def { widget_def::Widget::Text(text_def) => { @@ -149,7 +155,7 @@ pub fn widget_def_to_fn(def: WidgetDef) -> Option { }) => { let children_widget_fns = children .into_iter() - .flat_map(widget_def_to_fn) + .flat_map(|child| widget_def_to_fn(child, state)) .collect::>(); let f: ViewFn = Box::new(move || { @@ -206,7 +212,7 @@ pub fn widget_def_to_fn(def: WidgetDef) -> Option { }) => { let children_widget_fns = children .into_iter() - .flat_map(widget_def_to_fn) + .flat_map(|child| widget_def_to_fn(child, state)) .collect::>(); let f: ViewFn = Box::new(move || { @@ -269,7 +275,7 @@ pub fn widget_def_to_fn(def: WidgetDef) -> Option { style, } = *scrollable_def; - let child_widget_fn = child.and_then(|def| widget_def_to_fn(*def)); + let child_widget_fn = child.and_then(|def| widget_def_to_fn(*def, state)); let f: ViewFn = Box::new(move || { let mut scrollable = Scrollable::new( @@ -395,7 +401,7 @@ pub fn widget_def_to_fn(def: WidgetDef) -> Option { style, } = *container_def; - let child_widget_fn = child.and_then(|def| widget_def_to_fn(*def)); + let child_widget_fn = child.and_then(|def| widget_def_to_fn(*def, state)); let f: ViewFn = Box::new(move || { let mut container = Container::new( @@ -499,7 +505,7 @@ pub fn widget_def_to_fn(def: WidgetDef) -> Option { widget_id, } = *button; - let child_widget_fn = child.and_then(|def| widget_def_to_fn(*def)); + let child_widget_fn = child.and_then(|def| widget_def_to_fn(*def, state)); let f: ViewFn = Box::new(move || { let mut button = iced::widget::Button::new( @@ -662,7 +668,7 @@ pub fn widget_def_to_fn(def: WidgetDef) -> Option { child, } = *input_region; - let child_widget_fn = child.and_then(|def| widget_def_to_fn(*def)); + let child_widget_fn = child.and_then(|def| widget_def_to_fn(*def, state)); let f: ViewFn = Box::new(move || { let mut input_region = crate::widget::input_region::InputRegion::new( @@ -703,7 +709,7 @@ pub fn widget_def_to_fn(def: WidgetDef) -> Option { widget_id, } = *mouse_area; - let child_widget_fn = child.and_then(|def| widget_def_to_fn(*def)); + let child_widget_fn = child.and_then(|def| widget_def_to_fn(*def, state)); let f: ViewFn = Box::new(move || { let mut mouse_area = iced::widget::MouseArea::new( @@ -967,6 +973,69 @@ pub fn widget_def_to_fn(def: WidgetDef) -> Option { text_input.into() }); + Some(f) + } + widget_def::Widget::WlrTaskList(wlr_task_list) => { + let widget::v1::WlrTaskList { + widget_id, + on_enter, + on_update, + on_leave, + child, + } = *wlr_task_list; + + let child_widget_fn = child.and_then(|def| widget_def_to_fn(*def, state)); + let wlr_state = state.zwlr_foreign_toplevel_mgmt_state.clone(); + let seat = state.seat.as_ref().map(WlSeat::downgrade); + + let f: ViewFn = Box::new(move || { + let mut wlr_task_list = wlr_tasklist::WlrTaskList::new( + child_widget_fn + .as_ref() + .map(|child| child()) + .unwrap_or_else(|| iced::widget::Row::new().into()), + wlr_state.clone(), + seat.clone(), + ); + + if let Some(widget_id) = widget_id { + if on_enter { + wlr_task_list = wlr_task_list.on_enter(move |task| { + crate::widget::SnowcapMessage::WidgetEvent( + WidgetId(widget_id), + WidgetEvent::WlrTaskList( + wlr_tasklist::WlrTaskListEvent::ToplevelEnter(task), + ), + ) + }) + } + + if on_update { + wlr_task_list = wlr_task_list.on_update(move |task| { + crate::widget::SnowcapMessage::WidgetEvent( + WidgetId(widget_id), + WidgetEvent::WlrTaskList( + wlr_tasklist::WlrTaskListEvent::ToplevelUpdate(task), + ), + ) + }) + } + + if on_leave { + wlr_task_list = wlr_task_list.on_leave(move |id| { + crate::widget::SnowcapMessage::WidgetEvent( + WidgetId(widget_id), + WidgetEvent::WlrTaskList( + wlr_tasklist::WlrTaskListEvent::ToplevelLeave(id), + ), + ) + }) + } + } + + wlr_task_list.into() + }); + Some(f) } } @@ -1442,3 +1511,53 @@ impl FromApi for crate::widget::text_input::Style } } } + +impl From + for snowcap_api_defs::snowcap::widget::v1::wlr_task_list::Event +{ + fn from(value: wlr_tasklist::WlrTaskListEvent) -> Self { + use snowcap_api_defs::snowcap::widget::v1::wlr_task_list::event::Data; + use wlr_tasklist::WlrTaskListEvent; + + let data = match value { + WlrTaskListEvent::ToplevelEnter(task) => Data::Enter(task.into()), + WlrTaskListEvent::ToplevelUpdate(task) => Data::Update(task.into()), + WlrTaskListEvent::ToplevelLeave(id) => Data::Leave(id), + }; + + Self { data: Some(data) } + } +} + +impl From + for snowcap_api_defs::snowcap::widget::v1::wlr_task_list::WlrTaskData +{ + fn from(value: wlr_tasklist::WlrTaskState) -> Self { + let wlr_tasklist::WlrTaskState { + id, + title, + app_id, + state, + outputs, + } = value; + + Self { + id, + title: Some(title), + app_id: Some(app_id), + state: Some(state.into()), + outputs, + } + } +} + +impl From for snowcap_api_defs::snowcap::widget::v1::wlr_task_list::WlrTaskState { + fn from(value: ToplevelState) -> Self { + Self { + maximized: value.contains(ToplevelState::Maximized), + minimized: value.contains(ToplevelState::Minimized), + activated: value.contains(ToplevelState::Activated), + fullscreen: value.contains(ToplevelState::Fullscreen), + } + } +} diff --git a/snowcap/src/handlers.rs b/snowcap/src/handlers.rs index fb133ed4e..6c853d7b5 100644 --- a/snowcap/src/handlers.rs +++ b/snowcap/src/handlers.rs @@ -51,8 +51,13 @@ impl SeatHandler for State { &mut self.seat_state } - fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat) { - // TODO: + fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle, seat: WlSeat) { + // TODO: For now we only support one seat. This is good enough as most compositor only + // support one seat as well, but could be improved either by picking the best seat (the one + // with the most desirable capabilities), or having the user pick a seat by name. + if self.seat.is_none() { + self.seat = Some(seat); + } } fn new_capability( @@ -229,6 +234,8 @@ impl CompositorHandler for State { surface: &WlSurface, output: &wl_output::WlOutput, ) { + use crate::widget::output; + let Some(layer) = self .layers .iter_mut() @@ -239,6 +246,9 @@ impl CompositorHandler for State { layer.wl_output = Some(output.clone()); + let mut oper = output::operation::enter_output(output.clone()); + layer.operate(&mut oper); + let Some(output_info) = self.output_state.info(output) else { return; }; @@ -268,9 +278,19 @@ impl CompositorHandler for State { &mut self, _conn: &Connection, _qh: &QueueHandle, - _surface: &WlSurface, - _output: &wl_output::WlOutput, + surface: &WlSurface, + output: &wl_output::WlOutput, ) { + use crate::widget::output; + + if let Some(layer) = self + .layers + .iter_mut() + .find(|layer| layer.layer.wl_surface() == surface) + { + let mut oper = output::operation::leave_output(output.clone()); + layer.operate(&mut oper); + } } } delegate_compositor!(State); diff --git a/snowcap/src/handlers/foreign_toplevel_management.rs b/snowcap/src/handlers/foreign_toplevel_management.rs index 948b7c13c..6bafc5d05 100644 --- a/snowcap/src/handlers/foreign_toplevel_management.rs +++ b/snowcap/src/handlers/foreign_toplevel_management.rs @@ -1,13 +1,115 @@ -use smithay_client_toolkit::reexports::{ - client::{Dispatch, event_created_child}, - protocols_wlr::foreign_toplevel::v1::client::{ - zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1, - zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1}, +use std::sync::{Arc, Mutex, Weak}; + +use bitflags::bitflags; +use smithay_client_toolkit::{ + reexports::{ + client::{Dispatch, Proxy, event_created_child, protocol::wl_output::WlOutput}, + protocols_wlr::foreign_toplevel::v1::client::{ + zwlr_foreign_toplevel_handle_v1::{self, ZwlrForeignToplevelHandleV1}, + zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1}, + }, }, + registry::GlobalProxy, }; use crate::state::State; +pub struct Inner { + _manager: GlobalProxy, + toplevels: Vec, +} + +#[derive(Clone)] +pub struct ZwlrForeignToplevelManagementState(Arc>); + +#[derive(Clone)] +pub struct WeakZwlrForeignToplevelManagementState(Weak>); + +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct ForeignToplevelInfo { + pub app_id: String, + pub title: String, + pub outputs: Vec, // TODO: Replace by the output id or name ? + pub state: ToplevelState, + // TODO: What about parents ? +} + +#[derive(Debug, Default)] +pub struct ForeignToplevelInner { + current_info: Option, + pending_info: ForeignToplevelInfo, +} + +#[derive(Debug, Default, Clone)] +pub struct ForeignToplevelData(Arc>); + +#[derive(Debug, Clone)] +pub enum ZwlrForeignToplevelEvent { + Added(ZwlrForeignToplevelHandleV1), + Closed(ZwlrForeignToplevelHandleV1), + Changed(ZwlrForeignToplevelHandleV1), +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ToplevelState: u32 { + const None = 0; + const Maximized = 1; + const Minimized = 2; + const Activated = 4; + const Fullscreen = 8; + } +} + +impl ZwlrForeignToplevelManagementState { + pub fn new( + globals: &smithay_client_toolkit::reexports::client::globals::GlobalList, + qh: &smithay_client_toolkit::reexports::client::QueueHandle, + ) -> Self { + let _manager = GlobalProxy::from(globals.bind(qh, 1..=3, ())); + + Self(Arc::new(Mutex::new(Inner { + _manager, + toplevels: Vec::new(), + }))) + } + + pub fn with_toplevels(&self, processor: F) -> Ret + where + F: FnOnce(&[ZwlrForeignToplevelHandleV1]) -> Ret, + { + processor(&self.0.lock().unwrap().toplevels) + } + + pub fn with_toplevels_mut(&self, processor: F) -> Ret + where + F: Fn(&mut Vec) -> Ret, + { + processor(&mut self.0.lock().unwrap().toplevels) + } + + pub fn info(&self, toplevel: &ZwlrForeignToplevelHandleV1) -> Option { + toplevel + .data::()? + .0 + .lock() + .unwrap() + .current_info + .clone() + } + + pub fn downgrade(&self) -> WeakZwlrForeignToplevelManagementState { + WeakZwlrForeignToplevelManagementState(Arc::downgrade(&self.0)) + } +} + +impl WeakZwlrForeignToplevelManagementState { + pub fn upgrade(&self) -> Option { + self.0.upgrade().map(ZwlrForeignToplevelManagementState) + } +} + impl Dispatch for State { fn event( _state: &mut Self, @@ -17,23 +119,151 @@ impl Dispatch for State { _conn: &smithay_client_toolkit::reexports::client::Connection, _qhandle: &smithay_client_toolkit::reexports::client::QueueHandle, ) { - // TODO: } event_created_child!(State, ZwlrForeignToplevelManagerV1, [ - zwlr_foreign_toplevel_manager_v1::EVT_TOPLEVEL_OPCODE => (ZwlrForeignToplevelHandleV1, ()) + zwlr_foreign_toplevel_manager_v1::EVT_TOPLEVEL_OPCODE => (ZwlrForeignToplevelHandleV1, Default::default()) ]); } -impl Dispatch for State { +impl Dispatch for State { fn event( - _state: &mut Self, - _proxy: &ZwlrForeignToplevelHandleV1, - _event: ::Event, - _data: &(), + state: &mut Self, + proxy: &ZwlrForeignToplevelHandleV1, + event: ::Event, + data: &ForeignToplevelData, _conn: &smithay_client_toolkit::reexports::client::Connection, _qhandle: &smithay_client_toolkit::reexports::client::QueueHandle, ) { - // TODO: + match event { + zwlr_foreign_toplevel_handle_v1::Event::Closed => { + state.zwlr_toplevel_closed(proxy.clone()); + state + .zwlr_foreign_toplevel_mgmt_state + .with_toplevels_mut(|toplevels| { + toplevels.retain(|t| t != proxy); + }); + proxy.destroy(); + } + zwlr_foreign_toplevel_handle_v1::Event::Title { title } => { + data.0.lock().unwrap().pending_info.title = title; + } + zwlr_foreign_toplevel_handle_v1::Event::AppId { app_id } => { + data.0.lock().unwrap().pending_info.app_id = app_id; + } + zwlr_foreign_toplevel_handle_v1::Event::OutputEnter { output } => { + data.0.lock().unwrap().pending_info.outputs.push(output); + } + zwlr_foreign_toplevel_handle_v1::Event::OutputLeave { output } => { + data.0 + .lock() + .unwrap() + .pending_info + .outputs + .retain(|o| o != &output); + } + zwlr_foreign_toplevel_handle_v1::Event::State { state: flags } => { + data.0.lock().unwrap().pending_info.state = flags.into(); + } + zwlr_foreign_toplevel_handle_v1::Event::Parent { parent: _ } => (), + zwlr_foreign_toplevel_handle_v1::Event::Done => { + let mut inner = data.0.lock().unwrap(); + let new_toplevel = inner.current_info.is_none(); + inner.current_info = Some(inner.pending_info.clone()); + std::mem::drop(inner); + + if new_toplevel { + state + .zwlr_foreign_toplevel_mgmt_state + .with_toplevels_mut(|toplevels| { + toplevels.push(proxy.clone()); + }); + state.new_zwlr_toplevel(proxy.clone()); + } else { + state.zwlr_toplevel_updated(proxy.clone()); + } + } + _ => (), + } + } +} + +impl State { + pub fn new_zwlr_toplevel(&mut self, handle: ZwlrForeignToplevelHandleV1) { + use crate::widget::wlr_tasklist::operation; + + for layer in &mut self.layers { + let mut operation = operation::new_toplevel(handle.clone()); + layer.operate(&mut operation); + } + + for deco in &mut self.decorations { + let mut operation = operation::new_toplevel(handle.clone()); + deco.operate(&mut operation); + } + + self.shell.request_redraw(); + } + + pub fn zwlr_toplevel_updated(&mut self, handle: ZwlrForeignToplevelHandleV1) { + use crate::widget::wlr_tasklist::operation; + for layer in &mut self.layers { + let mut operation = operation::update_toplevel(handle.clone()); + layer.operate(&mut operation); + } + + for deco in &mut self.decorations { + let mut operation = operation::update_toplevel(handle.clone()); + deco.operate(&mut operation); + } + + self.shell.request_redraw() + } + + pub fn zwlr_toplevel_closed(&mut self, handle: ZwlrForeignToplevelHandleV1) { + use crate::widget::wlr_tasklist::operation; + + for layer in &mut self.layers { + let mut operation = operation::remove_toplevel(handle.clone()); + layer.operate(&mut operation); + } + + for deco in &mut self.decorations { + let mut operation = operation::remove_toplevel(handle.clone()); + deco.operate(&mut operation); + } + + self.shell.request_redraw(); + } +} + +impl ForeignToplevelData { + pub fn with_info(&self, processor: F) -> Option + where + F: FnOnce(&ForeignToplevelInfo) -> Ret, + { + self.0.lock().ok()?.current_info.as_ref().map(processor) + } +} + +impl From> for ToplevelState { + fn from(value: Vec) -> Self { + value.iter().fold(Self::None, |acc, val| { + let flag = match &val { + 0 => Self::Maximized, + 1 => Self::Minimized, + 2 => Self::Activated, + 3 => Self::Fullscreen, + _ => Self::None, + }; + + acc | flag + }) + } +} + +impl Default for ToplevelState { + fn default() -> Self { + Self::None } } diff --git a/snowcap/src/state.rs b/snowcap/src/state.rs index 35897976b..1b1a2845d 100644 --- a/snowcap/src/state.rs +++ b/snowcap/src/state.rs @@ -10,7 +10,7 @@ use smithay_client_toolkit::{ client::{ Connection, QueueHandle, globals::registry_queue_init, - protocol::{wl_keyboard::WlKeyboard, wl_pointer::WlPointer}, + protocol::{wl_keyboard::WlKeyboard, wl_pointer::WlPointer, wl_seat::WlSeat}, }, protocols::{ ext::foreign_toplevel_list::v1::client::{ @@ -32,7 +32,10 @@ use xkbcommon::xkb::Keysym; use crate::{ decoration::{DecorationIdCounter, SnowcapDecoration}, - handlers::{foreign_toplevel_list::ForeignToplevelListHandleData, keyboard::KeyboardFocus}, + handlers::{ + foreign_toplevel_list::ForeignToplevelListHandleData, + foreign_toplevel_management::ZwlrForeignToplevelManagementState, keyboard::KeyboardFocus, + }, layer::{LayerIdCounter, SnowcapLayer}, runtime::{CalloopSenderSink, CurrentTokioExecutor}, server::GrpcServerState, @@ -68,6 +71,7 @@ pub struct State { pub layers: Vec, pub decorations: Vec, + pub seat: Option, // TODO: per wl_keyboard pub keyboard_focus: Option, pub keyboard_modifiers: Modifiers, @@ -80,6 +84,8 @@ pub struct State { pub foreign_toplevel_list_handles: Vec<(ExtForeignToplevelHandleV1, ForeignToplevelListHandleData)>, + + pub zwlr_foreign_toplevel_mgmt_state: ZwlrForeignToplevelManagementState, } impl State { @@ -106,6 +112,8 @@ impl State { globals.bind(&queue_handle, 1..=1, ()).unwrap(); let foreign_toplevel_list: ExtForeignToplevelListV1 = globals.bind(&queue_handle, 1..=1, ()).unwrap(); + let zwlr_foreign_toplevel_mgmt_state = + ZwlrForeignToplevelManagementState::new(&globals, &queue_handle); let wayland_source = WaylandSource::new(conn.clone(), event_queue); @@ -254,6 +262,7 @@ impl State { tiny_skia: None, layers: Vec::new(), decorations: Vec::new(), + seat: None, keyboard_focus: None, keyboard_modifiers: smithay_client_toolkit::seat::keyboard::Modifiers::default(), keyboard: None, @@ -261,6 +270,8 @@ impl State { layer_id_counter: LayerIdCounter::default(), decoration_id_counter: DecorationIdCounter::default(), foreign_toplevel_list_handles: Vec::new(), + + zwlr_foreign_toplevel_mgmt_state, }; Ok(state) diff --git a/snowcap/src/widget.rs b/snowcap/src/widget.rs index 3a2be79c6..533252c26 100644 --- a/snowcap/src/widget.rs +++ b/snowcap/src/widget.rs @@ -1,11 +1,17 @@ pub mod input_region; +pub mod output; +pub mod wlr_tasklist; use iced::{Color, Theme, event::Status}; use iced_graphics::Viewport; use iced_wgpu::core::{Clipboard, layout::Limits, widget}; use smithay_client_toolkit::reexports::client::{QueueHandle, protocol::wl_surface::WlSurface}; -use crate::{handlers::keyboard::KeyboardKey, state::State, widget::input_region::Collect}; +use crate::{ + handlers::keyboard::KeyboardKey, + state::State, + widget::{input_region::Collect, wlr_tasklist::WlrTaskListEvent}, +}; pub type Element = iced::Element<'static, SnowcapMessage, iced::Theme, crate::compositor::Renderer>; pub type UserInterface = @@ -218,6 +224,7 @@ pub enum WidgetEvent { Button, MouseArea(MouseAreaEvent), TextInput(TextInputEvent), + WlrTaskList(WlrTaskListEvent), } #[derive(Debug, Clone)] diff --git a/snowcap/src/widget/output.rs b/snowcap/src/widget/output.rs new file mode 100644 index 000000000..19ecb1876 --- /dev/null +++ b/snowcap/src/widget/output.rs @@ -0,0 +1,77 @@ +use smithay_client_toolkit::reexports::client::protocol::wl_output::WlOutput; + +#[derive(Default, Debug, Clone)] +pub struct OutputState { + pub output: Option, +} + +impl OutputState { + pub fn enter(&mut self, output: WlOutput) { + self.output = Some(output); + } + + pub fn leave(&mut self, output: WlOutput) { + if self.output == Some(output) { + self.output = None; + } + } +} + +pub mod operation { + use iced_wgpu::core::widget::Operation; + use smithay_client_toolkit::reexports::client::protocol::wl_output::WlOutput; + + pub fn enter_output(handle: WlOutput) -> impl Operation { + struct EnterOutput { + handle: WlOutput, + } + + impl Operation for EnterOutput { + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); + } + + fn custom( + &mut self, + _id: Option<&iced::widget::Id>, + _bounds: iced::Rectangle, + state: &mut dyn std::any::Any, + ) { + let Some(state) = state.downcast_mut::() else { + return; + }; + + state.enter(self.handle.clone()); + } + } + + EnterOutput { handle } + } + + pub fn leave_output(handle: WlOutput) -> impl Operation { + struct LeaveOutput { + handle: WlOutput, + } + + impl Operation for LeaveOutput { + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); + } + + fn custom( + &mut self, + _id: Option<&iced::widget::Id>, + _bounds: iced::Rectangle, + state: &mut dyn std::any::Any, + ) { + let Some(state) = state.downcast_mut::() else { + return; + }; + + state.leave(self.handle.clone()); + } + } + + LeaveOutput { handle } + } +} diff --git a/snowcap/src/widget/wlr_tasklist.rs b/snowcap/src/widget/wlr_tasklist.rs new file mode 100644 index 000000000..6a33e8830 --- /dev/null +++ b/snowcap/src/widget/wlr_tasklist.rs @@ -0,0 +1,668 @@ +//! A widget to represent a list of open window. + +use std::{ + collections::HashMap, + hash::{DefaultHasher, Hash, Hasher}, +}; + +use anyhow::Context; +use iced::{Length, Size}; +use iced_wgpu::core::{ + Element, Widget, layout, renderer, + widget::{Tree, tree}, +}; +use smithay_client_toolkit::{ + output::OutputData, + reexports::{ + client::{Proxy, Weak, protocol::wl_seat::WlSeat}, + protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1, + }, +}; + +use crate::{ + handlers::foreign_toplevel_management::{ + ForeignToplevelData, ForeignToplevelInfo, ToplevelState, + WeakZwlrForeignToplevelManagementState, ZwlrForeignToplevelManagementState, + }, + widget::output::OutputState, +}; + +pub mod operation { + use iced_wgpu::core::widget::Operation; + use smithay_client_toolkit::reexports::protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1; + + pub fn new_toplevel(handle: ZwlrForeignToplevelHandleV1) -> impl Operation { + struct AddToplevel { + handle: ZwlrForeignToplevelHandleV1, + } + + impl Operation for AddToplevel { + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<()>)) { + operate(self); + } + + fn custom( + &mut self, + _id: Option<&iced::widget::Id>, + _bounds: iced::Rectangle, + state: &mut dyn std::any::Any, + ) { + let Some(state) = state.downcast_mut::() else { + return; + }; + + state.add_toplevel(self.handle.clone()); + } + } + + AddToplevel { handle } + } + + pub fn update_toplevel(handle: ZwlrForeignToplevelHandleV1) -> impl Operation { + struct UpdateToplevel { + handle: ZwlrForeignToplevelHandleV1, + } + + impl Operation for UpdateToplevel { + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<()>)) { + operate(self); + } + + fn custom( + &mut self, + _id: Option<&iced::widget::Id>, + _bounds: iced::Rectangle, + state: &mut dyn std::any::Any, + ) { + let Some(state) = state.downcast_mut::() else { + return; + }; + + state.update_toplevel(self.handle.clone()); + } + } + + UpdateToplevel { handle } + } + + pub fn remove_toplevel(handle: ZwlrForeignToplevelHandleV1) -> impl Operation { + struct RemoveToplevel { + handle: ZwlrForeignToplevelHandleV1, + } + + impl Operation for RemoveToplevel { + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<()>)) { + operate(self); + } + + fn custom( + &mut self, + _id: Option<&iced::widget::Id>, + _bounds: iced::Rectangle, + state: &mut dyn std::any::Any, + ) { + let Some(state) = state.downcast_mut::() else { + return; + }; + + state.remove_toplevel(self.handle.clone()); + } + } + + RemoveToplevel { handle } + } + + pub fn toplevel_set_maximized(id: u64, maximized: bool) -> impl Operation { + struct ToplevelSetMaximized { + id: u64, + maximized: bool, + } + + impl Operation for ToplevelSetMaximized { + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<()>)) { + operate(self); + } + + fn custom( + &mut self, + _id: Option<&iced::widget::Id>, + _bounds: iced::Rectangle, + state: &mut dyn std::any::Any, + ) { + let Some(state) = state.downcast_mut::() else { + return; + }; + + state.toplevel_set_maximized(self.id, self.maximized); + } + } + + ToplevelSetMaximized { id, maximized } + } + + pub fn toplevel_set_minimized(id: u64, minimized: bool) -> impl Operation { + struct ToplevelSetMinimized { + id: u64, + minimized: bool, + } + + impl Operation for ToplevelSetMinimized { + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<()>)) { + operate(self); + } + + fn custom( + &mut self, + _id: Option<&iced::widget::Id>, + _bounds: iced::Rectangle, + state: &mut dyn std::any::Any, + ) { + let Some(state) = state.downcast_mut::() else { + return; + }; + + state.toplevel_set_minimized(self.id, self.minimized); + } + } + + ToplevelSetMinimized { id, minimized } + } + + pub fn toplevel_set_fullscreen(id: u64, fullscreen: bool) -> impl Operation { + struct ToplevelSetFullscreen { + id: u64, + fullscreen: bool, + } + + impl Operation for ToplevelSetFullscreen { + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<()>)) { + operate(self); + } + + fn custom( + &mut self, + _id: Option<&iced::widget::Id>, + _bounds: iced::Rectangle, + state: &mut dyn std::any::Any, + ) { + let Some(state) = state.downcast_mut::() else { + return; + }; + + state.toplevel_set_fullscreen(self.id, self.fullscreen); + } + } + + ToplevelSetFullscreen { id, fullscreen } + } + + pub fn toplevel_activate(id: u64) -> impl Operation { + struct ToplevelActivate { + id: u64, + } + + impl Operation for ToplevelActivate { + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<()>)) { + operate(self); + } + + fn custom( + &mut self, + _id: Option<&iced::widget::Id>, + _bounds: iced::Rectangle, + state: &mut dyn std::any::Any, + ) { + let Some(state) = state.downcast_mut::() else { + return; + }; + + state.toplevel_activate(self.id); + } + } + + ToplevelActivate { id } + } + + pub fn toplevel_close(id: u64) -> impl Operation { + struct ToplevelClose { + id: u64, + } + + impl Operation for ToplevelClose { + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<()>)) { + operate(self); + } + + fn custom( + &mut self, + _id: Option<&iced::widget::Id>, + _bounds: iced::Rectangle, + state: &mut dyn std::any::Any, + ) { + let Some(state) = state.downcast_mut::() else { + return; + }; + + state.toplevel_close(self.id); + } + } + + ToplevelClose { id } + } +} + +#[derive(Debug, Clone)] +pub struct WlrTaskState { + pub id: u64, + pub title: String, + pub app_id: String, + pub state: ToplevelState, + pub outputs: Vec, +} + +#[derive(Debug, Clone)] +pub enum WlrTaskListEvent { + ToplevelEnter(WlrTaskState), + ToplevelUpdate(WlrTaskState), + ToplevelLeave(u64), +} + +/// Emits events on window changes. +pub struct WlrTaskList<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> { + content: Element<'a, Message, Theme, Renderer>, + + // FIXME: Ok, this feels like bad design 101. Ideally, we'd want a service that would maintain + // both the protocol state and seat, but I'm currently undecided on how to do it, or how to + // handle multi-seat if that becomes a thing. + wlr_state: WeakZwlrForeignToplevelManagementState, + seat: Option>, + + on_enter: Option Message + 'a>>, + on_update: Option Message + 'a>>, + on_leave: Option Message + 'a>>, + _all_output: bool, +} + +/// Local state of the [`WlrTaskList`]. +#[derive(Default)] +pub struct State { + output_state: OutputState, + + toplevel_list: HashMap>, + seat: Option>, + + pending_enter: Vec<(WlrTaskState, Weak)>, + pending_update: Vec<(WlrTaskState, Weak)>, + pending_leave: Vec<(u64, Weak)>, + + initial_state_sent: bool, +} + +impl State { + fn add_toplevel(&mut self, handle: ZwlrForeignToplevelHandleV1) { + if !self.initial_state_sent { + return; + } + + let Ok(task_state) = handle.clone().try_into() else { + return; + }; + + self.pending_enter.push((task_state, handle.downgrade())); + } + + fn update_toplevel(&mut self, handle: ZwlrForeignToplevelHandleV1) { + if !self.initial_state_sent { + return; + } + + let Ok(task_state): Result = handle.clone().try_into() else { + return; + }; + + let weak = handle.downgrade(); + if self.toplevel_list.contains_key(&task_state.id) { + self.pending_update.push((task_state, weak)); + } else { + self.pending_enter.push((task_state, weak)); + } + } + + fn remove_toplevel(&mut self, handle: ZwlrForeignToplevelHandleV1) { + if !self.initial_state_sent { + return; + } + + let id = make_id_from_handle(&handle); + + self.pending_leave.push((id, handle.downgrade())); + } + + fn toplevel_set_maximized(&mut self, id: u64, maximized: bool) { + if let Some(handle) = self.toplevel_list.get(&id).and_then(|v| v.upgrade().ok()) { + if maximized { + handle.set_maximized(); + } else { + handle.unset_maximized(); + } + } + } + + fn toplevel_set_minimized(&mut self, id: u64, minimized: bool) { + if let Some(handle) = self.toplevel_list.get(&id).and_then(|v| v.upgrade().ok()) { + if minimized { + handle.set_minimized(); + } else { + handle.unset_minimized(); + } + } + } + + fn toplevel_set_fullscreen(&mut self, id: u64, fullscreen: bool) { + if let Some(handle) = self.toplevel_list.get(&id).and_then(|v| v.upgrade().ok()) { + if fullscreen { + handle.set_fullscreen(None); + } else { + handle.unset_fullscreen(); + } + } + } + + fn toplevel_activate(&mut self, id: u64) { + let Some(seat) = self.seat.as_ref().and_then(|s| s.upgrade().ok()) else { + tracing::warn!("Activate was called, but the widget doesn't have an associated seat."); + return; + }; + + if let Some(handle) = self.toplevel_list.get(&id).and_then(|v| v.upgrade().ok()) { + handle.activate(&seat); + } + } + + fn toplevel_close(&mut self, id: u64) { + if let Some(handle) = self.toplevel_list.get(&id).and_then(|v| v.upgrade().ok()) { + handle.close(); + } + } +} + +impl<'a, Message, Theme, Renderer> WlrTaskList<'a, Message, Theme, Renderer> { + #[must_use] + pub fn on_enter(mut self, on_enter: impl Fn(WlrTaskState) -> Message + 'a) -> Self { + self.on_enter = Some(Box::new(on_enter)); + self + } + + #[must_use] + pub fn on_update(mut self, on_update: impl Fn(WlrTaskState) -> Message + 'a) -> Self { + self.on_update = Some(Box::new(on_update)); + self + } + + #[must_use] + pub fn on_leave(mut self, on_leave: impl Fn(u64) -> Message + 'a) -> Self { + self.on_leave = Some(Box::new(on_leave)); + self + } +} + +impl<'a, Message, Theme, Renderer> WlrTaskList<'a, Message, Theme, Renderer> { + /// Creates a [`WlrTaskList`] with the given content. + pub fn new( + content: impl Into>, + wlr_state: ZwlrForeignToplevelManagementState, + seat: Option>, + ) -> Self { + let wlr_state = wlr_state.downgrade(); + + WlrTaskList { + content: content.into(), + wlr_state, + seat, + on_enter: None, + on_update: None, + on_leave: None, + _all_output: false, + } + } +} + +impl Widget + for WlrTaskList<'_, 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::Layout<'_>, + renderer: &Renderer, + operation: &mut dyn iced_wgpu::core::widget::Operation, + ) { + let state = tree.state.downcast_mut::(); + + state.seat = self.seat.clone(); + + operation.custom(None, layout.bounds(), &mut state.output_state); + operation.custom(None, layout.bounds(), state); + + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ); + }); + } + + fn update( + &mut self, + tree: &mut Tree, + event: &iced::Event, + layout: layout::Layout<'_>, + cursor: iced_wgpu::core::mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn iced_wgpu::core::Clipboard, + shell: &mut iced_wgpu::core::Shell<'_, Message>, + viewport: &iced::Rectangle, + ) { + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + + let state = tree.state.downcast_mut::(); + if let Some(wlr_state) = self.wlr_state.upgrade() + && !state.initial_state_sent + { + wlr_state.with_toplevels(|toplevels| { + for toplevel in toplevels { + if let Ok(task_state) = toplevel.clone().try_into() { + state.pending_enter.push((task_state, toplevel.downgrade())); + } + } + }); + + state.initial_state_sent = true; + } + + if let Some(on_enter) = self.on_enter.as_ref() { + for (pending, weak) in state.pending_enter.drain(..) { + if weak.upgrade().is_ok() { + let id = pending.id; + + shell.publish((on_enter)(pending)); + state.toplevel_list.insert(id, weak); + } + } + } + + if let Some(on_update) = self.on_update.as_ref() { + for (pending, weak) in state.pending_update.drain(..) { + if weak.upgrade().is_ok() { + shell.publish((on_update)(pending)); + } + } + } else { + state.pending_update.clear(); + } + + if let Some(on_leave) = self.on_leave.as_ref() { + for (pending, _) in state.pending_leave.drain(..) { + shell.publish((on_leave)(pending)); + + state.toplevel_list.remove(&pending); + } + } + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: layout::Layout<'_>, + cursor: iced_wgpu::core::mouse::Cursor, + viewport: &iced::Rectangle, + renderer: &Renderer, + ) -> iced_wgpu::core::mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + renderer_style: &iced_wgpu::core::renderer::Style, + layout: layout::Layout<'_>, + cursor: iced_wgpu::core::mouse::Cursor, + viewport: &iced::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::Layout<'b>, + renderer: &Renderer, + viewport: &iced::Rectangle, + translation: iced::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(value: WlrTaskList<'a, Message, Theme, Renderer>) -> Self { + Element::new(value) + } +} + +fn make_id_from_handle(handle: &ZwlrForeignToplevelHandleV1) -> u64 { + let mut hasher = DefaultHasher::default(); + handle.id().hash(&mut hasher); + hasher.finish() +} + +impl TryFrom for WlrTaskState { + type Error = anyhow::Error; + + fn try_from(value: ZwlrForeignToplevelHandleV1) -> anyhow::Result { + let id = make_id_from_handle(&value); + + let data = value + .data::() + .context("Proxy has no associated data")?; + + data.with_info(|info| { + let ForeignToplevelInfo { + app_id, + title, + outputs, + state, + } = info.clone(); + + let outputs = outputs + .iter() + .flat_map(|o| { + o.data::() + .and_then(|d| d.with_output_info(|i| i.name.clone())) + }) + .collect(); + + Self { + id, + app_id, + title, + state, + outputs, + } + }) + .context("Could not get TaskState from proxy.") + } +}