diff --git a/Cargo.lock b/Cargo.lock index 79662f089..e7adf72bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4489,6 +4489,7 @@ dependencies = [ name = "snowcap-api" version = "0.1.0" dependencies = [ + "bitflags 2.10.0", "from_variants", "futures", "hyper-util", diff --git a/api/lua/pinnacle-api-dev-1.rockspec b/api/lua/pinnacle-api-dev-1.rockspec index 3ad109169..dad584921 100644 --- a/api/lua/pinnacle-api-dev-1.rockspec +++ b/api/lua/pinnacle-api-dev-1.rockspec @@ -50,6 +50,7 @@ build = { ["pinnacle.snowcap.snowcap.widget.operation"] = "pinnacle/snowcap/snowcap/widget/operation.lua", ["pinnacle.snowcap.snowcap.layer"] = "pinnacle/snowcap/snowcap/layer.lua", ["pinnacle.snowcap.snowcap.decoration"] = "pinnacle/snowcap/snowcap/decoration.lua", + ["pinnacle.snowcap.snowcap.popup"] = "pinnacle/snowcap/snowcap/popup.lua", ["pinnacle.snowcap.snowcap.util"] = "pinnacle/snowcap/snowcap/util.lua", ["pinnacle.snowcap.snowcap.log"] = "pinnacle/snowcap/snowcap/log.lua", }, diff --git a/snowcap/api/lua/snowcap-api-dev-1.rockspec b/snowcap/api/lua/snowcap-api-dev-1.rockspec index dae013f51..a619de01d 100644 --- a/snowcap/api/lua/snowcap-api-dev-1.rockspec +++ b/snowcap/api/lua/snowcap-api-dev-1.rockspec @@ -28,6 +28,7 @@ build = { ["snowcap.widget.operation"] = "snowcap/widget/operation.lua", ["snowcap.layer"] = "snowcap/layer.lua", ["snowcap.decoration"] = "snowcap/decoration.lua", + ["snowcap.popup"] = "snowcap/popup.lua", ["snowcap.util"] = "snowcap/util.lua", ["snowcap.log"] = "snowcap/log.lua", }, diff --git a/snowcap/api/lua/snowcap/grpc/defs.lua b/snowcap/api/lua/snowcap/grpc/defs.lua index 394785a07..aab623a75 100644 --- a/snowcap/api/lua/snowcap/grpc/defs.lua +++ b/snowcap/api/lua/snowcap/grpc/defs.lua @@ -681,6 +681,34 @@ local snowcap_layer_v1_Layer = { LAYER_OVERLAY = 4, } +---@enum snowcap.popup.v1.Anchor +local snowcap_popup_v1_Anchor = { + ANCHOR_UNSPECIFIED = 0, + ANCHOR_TOP = 1, + ANCHOR_BOTTOM = 2, + ANCHOR_LEFT = 3, + ANCHOR_RIGHT = 4, + ANCHOR_TOP_LEFT = 5, + ANCHOR_TOP_RIGHT = 6, + ANCHOR_BOTTOM_LEFT = 7, + ANCHOR_BOTTOM_RIGHT = 8, + ANCHOR_NONE = 9, +} + +---@enum snowcap.popup.v1.Gravity +local snowcap_popup_v1_Gravity = { + GRAVITY_UNSPECIFIED = 0, + GRAVITY_TOP = 1, + GRAVITY_BOTTOM = 2, + GRAVITY_LEFT = 3, + GRAVITY_RIGHT = 4, + GRAVITY_TOP_LEFT = 5, + GRAVITY_TOP_RIGHT = 6, + GRAVITY_BOTTOM_LEFT = 7, + GRAVITY_BOTTOM_RIGHT = 8, + GRAVITY_NONE = 9, +} + ---@alias google.protobuf.Empty nil @@ -849,6 +877,7 @@ local snowcap_layer_v1_Layer = { ---@field clip boolean? ---@field child snowcap.widget.v1.WidgetDef? ---@field style snowcap.widget.v1.Container.Style? +---@field id string? ---@class snowcap.widget.v1.Container.Style ---@field text_color snowcap.widget.v1.Color? @@ -995,6 +1024,7 @@ local snowcap_layer_v1_Layer = { ---@class snowcap.widget.v1.GetWidgetEventsRequest ---@field layer_id integer? ---@field decoration_id integer? +---@field popup_id integer? ---@class snowcap.widget.v1.WidgetEvent ---@field widget_id integer? @@ -1112,7 +1142,8 @@ local snowcap_layer_v1_Layer = { ---@field super boolean? ---@class snowcap.input.v1.KeyboardKeyRequest ----@field id integer? +---@field layer_id integer? +---@field popup_id integer? ---@class snowcap.input.v1.KeyboardKeyResponse ---@field key integer? @@ -1277,6 +1308,71 @@ local snowcap_layer_v1_Layer = { ---@class snowcap.layer.v1.ViewResponse +---@class snowcap.popup.v1.Offset +---@field x number? +---@field y number? + +---@class snowcap.popup.v1.Rectangle +---@field x number? +---@field y number? +---@field width number? +---@field height number? + +---@class snowcap.popup.v1.ConstraintsAdjust +---@field none boolean? +---@field slide_x boolean? +---@field slide_y boolean? +---@field flip_x boolean? +---@field flip_y boolean? +---@field resize_x boolean? +---@field resize_y boolean? + +---@class snowcap.popup.v1.Position +---@field at_cursor google.protobuf.Empty? +---@field absolute snowcap.popup.v1.Rectangle? +---@field at_widget string? + +---@class snowcap.popup.v1.NewPopupRequest +---@field widget_def snowcap.widget.v1.WidgetDef? +---@field layer_id integer? +---@field deco_id integer? +---@field popup_id integer? +---@field position snowcap.popup.v1.Position? +---@field anchor snowcap.popup.v1.Anchor? +---@field gravity snowcap.popup.v1.Gravity? +---@field offset snowcap.popup.v1.Offset? +---@field constraints_adjust snowcap.popup.v1.ConstraintsAdjust? +---@field no_grab boolean? +---@field no_replace boolean? + +---@class snowcap.popup.v1.NewPopupResponse +---@field popup_id integer? + +---@class snowcap.popup.v1.CloseRequest +---@field popup_id integer? + +---@class snowcap.popup.v1.OperatePopupRequest +---@field popup_id integer? +---@field operation snowcap.operation.v1.Operation? + +---@class snowcap.popup.v1.OperatePopupResponse + +---@class snowcap.popup.v1.UpdatePopupRequest +---@field popup_id integer? +---@field widget_def snowcap.widget.v1.WidgetDef? +---@field position snowcap.popup.v1.Position? +---@field anchor snowcap.popup.v1.Anchor? +---@field gravity snowcap.popup.v1.Gravity? +---@field offset snowcap.popup.v1.Offset? +---@field constraints_adjust snowcap.popup.v1.ConstraintsAdjust? + +---@class snowcap.popup.v1.UpdatePopupResponse + +---@class snowcap.popup.v1.ViewRequest +---@field popup_id integer? + +---@class snowcap.popup.v1.ViewResponse + ---@class snowcap.v0alpha1.Nothing ---@class snowcap.v1.Nothing @@ -1402,6 +1498,21 @@ snowcap.layer.v1.UpdateLayerRequest = {} snowcap.layer.v1.UpdateLayerResponse = {} snowcap.layer.v1.ViewRequest = {} snowcap.layer.v1.ViewResponse = {} +snowcap.popup = {} +snowcap.popup.v1 = {} +snowcap.popup.v1.Offset = {} +snowcap.popup.v1.Rectangle = {} +snowcap.popup.v1.ConstraintsAdjust = {} +snowcap.popup.v1.Position = {} +snowcap.popup.v1.NewPopupRequest = {} +snowcap.popup.v1.NewPopupResponse = {} +snowcap.popup.v1.CloseRequest = {} +snowcap.popup.v1.OperatePopupRequest = {} +snowcap.popup.v1.OperatePopupResponse = {} +snowcap.popup.v1.UpdatePopupRequest = {} +snowcap.popup.v1.UpdatePopupResponse = {} +snowcap.popup.v1.ViewRequest = {} +snowcap.popup.v1.ViewResponse = {} snowcap.v0alpha1 = {} snowcap.v0alpha1.Nothing = {} snowcap.v1 = {} @@ -1424,6 +1535,8 @@ snowcap.layer.v0alpha1.Layer = snowcap_layer_v0alpha1_Layer snowcap.layer.v1.Anchor = snowcap_layer_v1_Anchor snowcap.layer.v1.KeyboardInteractivity = snowcap_layer_v1_KeyboardInteractivity snowcap.layer.v1.Layer = snowcap_layer_v1_Layer +snowcap.popup.v1.Anchor = snowcap_popup_v1_Anchor +snowcap.popup.v1.Gravity = snowcap_popup_v1_Gravity snowcap.widget.v1.WidgetService = {} snowcap.widget.v1.WidgetService.GetWidgetEvents = {} @@ -1730,6 +1843,92 @@ snowcap.layer.v1.LayerService.RequestView.response = ".snowcap.layer.v1.ViewResp function Client:snowcap_layer_v1_LayerService_RequestView(data) return self:unary_request(snowcap.layer.v1.LayerService.RequestView, data) end +snowcap.popup.v1.PopupService = {} +snowcap.popup.v1.PopupService.NewPopup = {} +snowcap.popup.v1.PopupService.NewPopup.service = "snowcap.popup.v1.PopupService" +snowcap.popup.v1.PopupService.NewPopup.method = "NewPopup" +snowcap.popup.v1.PopupService.NewPopup.request = ".snowcap.popup.v1.NewPopupRequest" +snowcap.popup.v1.PopupService.NewPopup.response = ".snowcap.popup.v1.NewPopupResponse" + +---Performs a unary request. +--- +---@nodiscard +--- +---@param data snowcap.popup.v1.NewPopupRequest +--- +---@return snowcap.popup.v1.NewPopupResponse | nil response +---@return string | nil error An error string, if any +function Client:snowcap_popup_v1_PopupService_NewPopup(data) + return self:unary_request(snowcap.popup.v1.PopupService.NewPopup, data) +end +snowcap.popup.v1.PopupService.Close = {} +snowcap.popup.v1.PopupService.Close.service = "snowcap.popup.v1.PopupService" +snowcap.popup.v1.PopupService.Close.method = "Close" +snowcap.popup.v1.PopupService.Close.request = ".snowcap.popup.v1.CloseRequest" +snowcap.popup.v1.PopupService.Close.response = ".google.protobuf.Empty" + +---Performs a unary request. +--- +---@nodiscard +--- +---@param data snowcap.popup.v1.CloseRequest +--- +---@return google.protobuf.Empty | nil response +---@return string | nil error An error string, if any +function Client:snowcap_popup_v1_PopupService_Close(data) + return self:unary_request(snowcap.popup.v1.PopupService.Close, data) +end +snowcap.popup.v1.PopupService.OperatePopup = {} +snowcap.popup.v1.PopupService.OperatePopup.service = "snowcap.popup.v1.PopupService" +snowcap.popup.v1.PopupService.OperatePopup.method = "OperatePopup" +snowcap.popup.v1.PopupService.OperatePopup.request = ".snowcap.popup.v1.OperatePopupRequest" +snowcap.popup.v1.PopupService.OperatePopup.response = ".snowcap.popup.v1.OperatePopupResponse" + +---Performs a unary request. +--- +---@nodiscard +--- +---@param data snowcap.popup.v1.OperatePopupRequest +--- +---@return snowcap.popup.v1.OperatePopupResponse | nil response +---@return string | nil error An error string, if any +function Client:snowcap_popup_v1_PopupService_OperatePopup(data) + return self:unary_request(snowcap.popup.v1.PopupService.OperatePopup, data) +end +snowcap.popup.v1.PopupService.UpdatePopup = {} +snowcap.popup.v1.PopupService.UpdatePopup.service = "snowcap.popup.v1.PopupService" +snowcap.popup.v1.PopupService.UpdatePopup.method = "UpdatePopup" +snowcap.popup.v1.PopupService.UpdatePopup.request = ".snowcap.popup.v1.UpdatePopupRequest" +snowcap.popup.v1.PopupService.UpdatePopup.response = ".snowcap.popup.v1.UpdatePopupResponse" + +---Performs a unary request. +--- +---@nodiscard +--- +---@param data snowcap.popup.v1.UpdatePopupRequest +--- +---@return snowcap.popup.v1.UpdatePopupResponse | nil response +---@return string | nil error An error string, if any +function Client:snowcap_popup_v1_PopupService_UpdatePopup(data) + return self:unary_request(snowcap.popup.v1.PopupService.UpdatePopup, data) +end +snowcap.popup.v1.PopupService.RequestView = {} +snowcap.popup.v1.PopupService.RequestView.service = "snowcap.popup.v1.PopupService" +snowcap.popup.v1.PopupService.RequestView.method = "RequestView" +snowcap.popup.v1.PopupService.RequestView.request = ".snowcap.popup.v1.ViewRequest" +snowcap.popup.v1.PopupService.RequestView.response = ".snowcap.popup.v1.ViewResponse" + +---Performs a unary request. +--- +---@nodiscard +--- +---@param data snowcap.popup.v1.ViewRequest +--- +---@return snowcap.popup.v1.ViewResponse | nil response +---@return string | nil error An error string, if any +function Client:snowcap_popup_v1_PopupService_RequestView(data) + return self:unary_request(snowcap.popup.v1.PopupService.RequestView, data) +end return { google = google, snowcap = snowcap, diff --git a/snowcap/api/lua/snowcap/grpc/protobuf.lua b/snowcap/api/lua/snowcap/grpc/protobuf.lua index ac7b68fc2..08f6de7f5 100644 --- a/snowcap/api/lua/snowcap/grpc/protobuf.lua +++ b/snowcap/api/lua/snowcap/grpc/protobuf.lua @@ -17,6 +17,7 @@ function protobuf.build_protos() "snowcap/widget/" .. version .. "/widget.proto", "snowcap/operation/" .. version .. "/operation.proto", "snowcap/decoration/" .. version .. "/decoration.proto", + "snowcap/popup/" .. version .. "/popup.proto", "google/protobuf/empty.proto", } diff --git a/snowcap/api/lua/snowcap/layer.lua b/snowcap/api/lua/snowcap/layer.lua index fc0a0861a..0a3f7525e 100644 --- a/snowcap/api/lua/snowcap/layer.lua +++ b/snowcap/api/lua/snowcap/layer.lua @@ -17,6 +17,12 @@ local layer_handle = {} ---@field private _update fun(msg:any) local LayerHandle = {} +---Convert a LayerHandle into a Popup's ParentHandle +---@return snowcap.popup.ParentHandle +function LayerHandle:as_parent() + return require("snowcap.popup").parent.Layer(self) +end + ---@param id integer ---@param update fun(msg: any) ---@return snowcap.layer.LayerHandle @@ -164,7 +170,7 @@ end ---@param on_event fun(handle: snowcap.layer.LayerHandle, event: snowcap.input.KeyEvent) function LayerHandle:on_key_event(on_event) local err = client:snowcap_input_v1_InputService_KeyboardKey( - { id = self.id }, + { layer_id = self.id }, function(response) ---@cast response snowcap.input.v1.KeyboardKeyResponse diff --git a/snowcap/api/lua/snowcap/popup.lua b/snowcap/api/lua/snowcap/popup.lua new file mode 100644 index 000000000..8e2799858 --- /dev/null +++ b/snowcap/api/lua/snowcap/popup.lua @@ -0,0 +1,397 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +local log = require("snowcap.log") +local client = require("snowcap.grpc.client").client + +local widget = require("snowcap.widget") + +---Support for popup surface widgets using `xdg-shell::xdg_popup` +---@class snowcap.popup +local popup = {} + +local popup_handle = {} + +---A handle to a popup's parent surface. +---@class snowcap.popup.ParentHandle +---@field layer? integer Popup's parent surface is a Layer. +---@field popup? integer Popup's parent surface is another Popup. +local ParentHandle = {} + +function ParentHandle:__tostring() + if self.layer ~= nil then + return ("ParentHandle{Layer#%d}"):format(self.layer) + elseif self.popup ~= nil then + return("ParentHandle{Popup#%d}"):format(self.popup) + else + return "ParenHandle{empty}" + end +end + +---Build a handle to a popup's parent surface. +---@enum snowcap.popup.parent +local parent = { + ---Build a ParentHandle from a LayerHandle. + --- + ---@param handle snowcap.layer.LayerHandle + ---@return snowcap.popup.ParentHandle + Layer = function(handle) + return setmetatable({ layer = handle.id }, ParentHandle) + end, + ---Build a ParentHandle from a PopupHandle. + --- + ---@param handle snowcap.popup.PopupHandle + ---@return snowcap.popup.ParentHandle + Popup = function(handle) + return setmetatable({ popup = handle.id }, ParentHandle) + end, +} +popup.parent = parent + +---A handle to a popup surface. +---@class snowcap.popup.PopupHandle +---@field id integer Popup's id. +---@field private _update fun(msg:any) +local PopupHandle = {} + +---Convert a PopupHandle into a Popup's ParentHandle +---@return snowcap.popup.ParentHandle +function PopupHandle:as_parent() + return parent.Popup(self) +end + +---Create a new popup handle. +---@lcat nodoc +---@package +---@param id integer +---@param update fun(msg: any) +---@return snowcap.popup.PopupHandle +function popup_handle.new(id, update) + ---@type snowcap.popup.PopupHandle + local self = { + id = id, + _update = update, + } + setmetatable(self, { __index = PopupHandle }) + return self +end + +---Anchoring rectangle. +---@class snowcap.popup.Rectangle +---@field x number +---@field y number +---@field width number +---@field height number + +---Position the Popup will be placed at. +--- +---This is an implementation detail. Use [`snowcap.popup.position`] instead. +---@class snowcap.popup.Position +---@field package at_cursor? {} Position the popup at the cursor. +---@field package absolute? snowcap.popup.Rectangle Position the popup on an arbitrary Rectangle boundaries. +---@field package at_widget? string Position the popup on a Widget boundaries. + +---Position the Popup will be placed at. +---@enum snowcap.popup.position +local position = { + ---Position the popup at the cursor. + ---@type snowcap.popup.Position + AtCursor = { at_cursor = {} }, + ---Position the anchor at an arbitrary point. + ---@type fun(x: number, y: number): snowcap.popup.Position + Point = function(x, y) + return { + absolute = { + x = x, + y = y, + width = 1, + height = 1, + }, + } + end, + ---Position the anchor on a Rectangle boundaries. + ---@type fun(x: number, y: number, width: number, heigh: number): snowcap.popup.Position + Rectangle = function(x, y, width, height) + return { + absolute = { + x = x, + y = y, + width = width, + height = height, + }, + } + end, + ---Position the anchor on a Widget boundaries. + ---@type fun(widget_id: string): snowcap.popup.Position + AtWidget = function(widget_id) + return { + at_widget = widget_id, + } + end, +} +popup.position = position + +---Position of the anchor point on the anchor rectangle. +---@enum snowcap.popup.Anchor +local anchor = { + TOP = 1, + BOTTOM = 2, + LEFT = 3, + RIGHT = 4, + TOP_LEFT = 5, + TOP_RIGHT = 6, + BOTTOM_LEFT = 7, + BOTTOM_RIGHT = 8, + NONE = 9, +} +popup.anchor = anchor + +---Direction of the gravity of the Popup. +---@enum snowcap.popup.Gravity +local gravity = { + TOP = 1, + BOTTOM = 2, + LEFT = 3, + RIGHT = 4, + TOP_LEFT = 5, + TOP_RIGHT = 6, + BOTTOM_LEFT = 7, + BOTTOM_RIGHT = 8, + NONE = 9, +} +popup.gravity = gravity + +---Popup position offset +---@class snowcap.popup.Offset +---@field x number +---@field y number + +---Define ways the compositor can adjust the popup if its position would make it partially +---constrained. +--- +---Except for none, every field are considered part of a bitfield. +---@class snowcap.popup.ConstraintsAdjust +---@field none? boolean Don't move the child surface when constrained. +---@field slide_x? boolean Move along the x axis until unconstrained. +---@field slide_y? boolean Move along the y axis until unconstrained. +---@field flip_x? boolean Invert the anchor and gravity on the x axis. +---@field flip_y? boolean Invert the anchor and gravity on the y axis. +---@field resize_x? boolean Horizontally resize the surface. +---@field resize_y? boolean Vertically resize the surface. + +---popup.new_widget parameters. +--- +---Only one parent handle will be taken into account. Setting more than one is undefined behavior. +---@class snowcap.popup.PopupArgs +---@field program snowcap.widget.Program Popup's content. +---@field parent snowcap.popup.ParentHandle Popup's parent surface handle. +---@field position snowcap.popup.Position Position the Popup should be placed at. +---@field anchor? snowcap.popup.Anchor Popup's anchor point on the Position boundaries. +---@field gravity? snowcap.popup.Gravity Popup's gravity. +---@field offset? snowcap.popup.Offset Popup's offset from the ancho point. +---@field contraints_adjust? snowcap.popup.ConstraintsAdjust Popup's contraints adjustment. +---@field no_grab? boolean If true, the Popup will not request an explicit keyboard grab upon creation. +---@field no_replace? boolean If true, the Popup will fail if there is already another popup with the same parent. + +---@param args snowcap.popup.PopupArgs +---@return snowcap.popup.PopupHandle|nil handle A handle to the popup surface, or nil if an error occurred +function popup.new_widget(args) + ---@type table + local callbacks = {} + + local widget_def = args.program:view() + + widget._traverse_widget_tree(widget_def, callbacks, widget._collect_callbacks) + + ---@type snowcap.popup.v1.NewPopupRequest + local request = { + widget_def = widget.widget_def_into_api(widget_def), + position = args.position --[[@as snowcap.popup.v1.Position]], + anchor = args.anchor, + gravity = args.gravity, + offset = args.offset --[[@as snowcap.popup.v1.Offset]], + constraints_adjust = args.contraints_adjust --[[@as snowcap.popup.v1.ConstraintsAdjust]], + no_grab = args.no_grab, + no_replace = args.no_replace, + } + + assert(args.parent, "No ParentHandle") + + if args.parent.layer then + request.layer_id = args.parent.layer + elseif args.parent.popup then + request.popup_id = args.parent.popup + else + log.error("Parent surface missing.") + return nil + end + + local response, err = client:snowcap_popup_v1_PopupService_NewPopup(request) + + if err then + log.error(err) + return nil + end + + assert(response) + + if not response.popup_id then + log.error("no popup_id received") + return nil + end + + local popup_id = response.popup_id --[[@as integer]] + + err = client:snowcap_widget_v1_WidgetService_GetWidgetEvents({ + popup_id = popup_id, + }, function(response) ---@diagnostic disable-line:redefined-local + for _, event in ipairs(response.widget_events) do + local msg = widget._message_from_event(callbacks, event) + + if msg then + local ok, update_err = pcall(function() + args.program:update(msg) + end) + + if not ok then + log.error(update_err) + end + end + end + + ---@diagnostic disable-next-line:redefined-local + local widget_def = args.program:view() + callbacks = {} + + widget._traverse_widget_tree(widget_def, callbacks, widget._collect_callbacks) + + ---@diagnostic disable-next-line:redefined-local + local _, err = client:snowcap_popup_v1_PopupService_UpdatePopup({ + popup_id = popup_id, + widget_def = widget.widget_def_into_api(widget_def), + }) + + if err then + log.error(err) + end + end) + + return popup_handle.new(popup_id, function(msg) + args.program:update(msg) + + ---@diagnostic disable-next-line: redefined-local + local _, err = client:snowcap_popup_v1_PopupService_RequestView({ + popup_id = popup_id, + }) + + if err then + log.error(err) + end + end) +end + +---Do something when a key event is received. +---@param on_event fun(handle: snowcap.popup.PopupHandle, event: snowcap.input.KeyEvent) +function PopupHandle:on_key_event(on_event) + local err = client:snowcap_input_v1_InputService_KeyboardKey( + { popup_id = self.id }, + function(response) + ---@cast response snowcap.input.v1.KeyboardKeyResponse + + local mods = response.modifiers or {} + mods.shift = mods.shift or false + mods.ctrl = mods.ctrl or false + mods.alt = mods.alt or false + mods.super = mods.super or false + + ---@cast mods snowcap.input.Modifiers + + ---@type snowcap.input.KeyEvent + local event = { + key = response.key or 0, + mods = mods, + pressed = response.pressed, + captured = response.captured, + text = response.text, + } + + on_event(self, event) + end + ) + + if err then + log.error(err) + end +end + +---Do something on key press. +---@param on_press fun(mods: snowcap.input.Modifiers, key: snowcap.Key) +function PopupHandle:on_key_press(on_press) + self:on_key_event(function(_, event) + if not event.pressed or event.captured then + return + end + + on_press(event.mods, event.key) + end) +end + +---Sends an `Operation` to this popup. +---@param operation snowcap.widget.operation.Operation +function PopupHandle:operate(operation) + local _, err = client:snowcap_popup_v1_PopupService_OperatePopup({ + popup_id = self.id, + operation = require("snowcap.widget.operation")._to_api(operation), ---@diagnostic disable-line: invisible + }) + + if err then + log.error(err) + end +end + +---PopupHandle:popup parameters. +--- +---Any parameters set will override a previously set value. +---@class snowcap.popup.PopupUpdateArgs +---@field position snowcap.popup.Position? Update popup's position. +---@field anchor snowcap.popup.Anchor? Update popup's anchor. +---@field gravity snowcap.popup.Gravity? Update popup's gravity. +---@field offset snowcap.popup.Offset? Update popup's offset. +---@field constraints_adjust snowcap.popup.ConstraintsAdjust? Update popup's constraints adjustment. + +---Update this popup's attributes. +---@param args snowcap.popup.PopupUpdateArgs +---@return boolean # True if the operation succeed. +function PopupHandle:update(args) + local _, err = client:snowcap_popup_v1_PopupService_UpdatePopup({ + popup_id = self.id, + position = args.position --[[@as snowcap.popup.v1.Position]], + anchor = args.anchor --[[@as snowcap.popup.v1.Anchor]], + gravity = args.gravity --[[@as snowcap.popup.v1.Gravity]], + offset = args.offset --[[@as snowcap.popup.v1.Offset]], + constraints_adjust = args.constraints_adjust --[[@as snowcap.popup.v1.ConstraintsAdjust]], + }) + + if err then + log.error(err) + end + + return err == nil +end + +---Close this popup widget. +function PopupHandle:close() + local _, err = client:snowcap_popup_v1_PopupService_Close({ popup_id = self.id }) + + if err then + log.error(err) + end +end + +---Sends a message to this Popup [`Program`]. +function PopupHandle:send_message(message) + self._update(message) +end + +return popup diff --git a/snowcap/api/lua/snowcap/widget.lua b/snowcap/api/lua/snowcap/widget.lua index 0cf965c9a..52e58fab2 100644 --- a/snowcap/api/lua/snowcap/widget.lua +++ b/snowcap/api/lua/snowcap/widget.lua @@ -107,6 +107,7 @@ ---@field scroller_border snowcap.widget.Border? ---@class snowcap.widget.Container +---@field id string? ---@field padding snowcap.widget.Padding? ---@field width snowcap.widget.Length? ---@field height snowcap.widget.Length? @@ -686,6 +687,7 @@ end local function container_into_api(def) ---@type snowcap.widget.v1.Container return { + id = def.id, padding = def.padding --[[@as snowcap.widget.v1.Padding]], width = def.width --[[@as snowcap.widget.v1.Length]], height = def.height --[[@as snowcap.widget.v1.Length]], diff --git a/snowcap/api/protobuf/snowcap/input/v1/input.proto b/snowcap/api/protobuf/snowcap/input/v1/input.proto index 06f1fd4aa..62d823ed6 100644 --- a/snowcap/api/protobuf/snowcap/input/v1/input.proto +++ b/snowcap/api/protobuf/snowcap/input/v1/input.proto @@ -10,7 +10,10 @@ message Modifiers { } message KeyboardKeyRequest { - uint32 id = 1; + oneof target { + uint32 layer_id = 1; + uint32 popup_id = 2; + } } message KeyboardKeyResponse { diff --git a/snowcap/api/protobuf/snowcap/popup/v1/popup.proto b/snowcap/api/protobuf/snowcap/popup/v1/popup.proto new file mode 100644 index 000000000..cdc563bda --- /dev/null +++ b/snowcap/api/protobuf/snowcap/popup/v1/popup.proto @@ -0,0 +1,117 @@ +syntax = "proto3"; + +package snowcap.popup.v1; + +import "google/protobuf/empty.proto"; +import "snowcap/widget/v1/widget.proto"; +import "snowcap/operation/v1/operation.proto"; + +enum Anchor { + ANCHOR_UNSPECIFIED = 0; + ANCHOR_TOP = 1; + ANCHOR_BOTTOM = 2; + ANCHOR_LEFT = 3; + ANCHOR_RIGHT = 4; + ANCHOR_TOP_LEFT = 5; + ANCHOR_TOP_RIGHT = 6; + ANCHOR_BOTTOM_LEFT = 7; + ANCHOR_BOTTOM_RIGHT = 8; + ANCHOR_NONE = 9; +} + +enum Gravity { + GRAVITY_UNSPECIFIED = 0; + GRAVITY_TOP = 1; + GRAVITY_BOTTOM = 2; + GRAVITY_LEFT = 3; + GRAVITY_RIGHT = 4; + GRAVITY_TOP_LEFT = 5; + GRAVITY_TOP_RIGHT = 6; + GRAVITY_BOTTOM_LEFT = 7; + GRAVITY_BOTTOM_RIGHT = 8; + GRAVITY_NONE = 9; +} + +message Offset { + float x = 1; + float y = 2; +} + +message Rectangle { + float x = 1; + float y = 2; + float width = 3; + float height = 4; +} + +message ConstraintsAdjust { + bool none = 1; + bool slide_x = 2; + bool slide_y = 3; + bool flip_x = 4; + bool flip_y = 5; + bool resize_x = 6; + bool resize_y = 7; +} + +message Position { + oneof strategy { + google.protobuf.Empty at_cursor = 1; + Rectangle absolute = 2; + string at_widget = 3; + } +} + +message NewPopupRequest { + snowcap.widget.v1.WidgetDef widget_def = 1; + oneof parent_id { + uint32 layer_id = 2; + uint32 deco_id = 3; + uint32 popup_id = 4; + } + Position position = 5; + Anchor anchor = 6; + Gravity gravity = 7; + Offset offset = 8; + ConstraintsAdjust constraints_adjust = 9; + bool no_grab = 10; + bool no_replace = 11; +} + +message NewPopupResponse { + uint32 popup_id = 1; +} + +message CloseRequest { + uint32 popup_id = 1; +} + +message OperatePopupRequest { + uint32 popup_id = 1; + snowcap.operation.v1.Operation operation = 2; +} +message OperatePopupResponse {} + +message UpdatePopupRequest { + uint32 popup_id = 1; + optional snowcap.widget.v1.WidgetDef widget_def = 2; + optional Position position = 5; + optional Anchor anchor = 6; + optional Gravity gravity = 7; + optional Offset offset = 8; + optional ConstraintsAdjust constraints_adjust = 9; +} +message UpdatePopupResponse {} + +message ViewRequest { + uint32 popup_id = 1; +} +message ViewResponse {} + +service PopupService { + rpc NewPopup(NewPopupRequest) returns (NewPopupResponse); + rpc Close(CloseRequest) returns (google.protobuf.Empty); + rpc OperatePopup(OperatePopupRequest) returns (OperatePopupResponse); + rpc UpdatePopup(UpdatePopupRequest) returns (UpdatePopupResponse); + rpc RequestView(ViewRequest) returns (ViewResponse); +} diff --git a/snowcap/api/protobuf/snowcap/widget/v1/widget.proto b/snowcap/api/protobuf/snowcap/widget/v1/widget.proto index 9713fde17..9cd3a2261 100644 --- a/snowcap/api/protobuf/snowcap/widget/v1/widget.proto +++ b/snowcap/api/protobuf/snowcap/widget/v1/widget.proto @@ -259,6 +259,7 @@ message Container { optional bool clip = 8; WidgetDef child = 9; optional Style style = 10; + optional string id = 11; message Style { optional Color text_color = 1; @@ -466,6 +467,7 @@ message GetWidgetEventsRequest { oneof id { uint32 layer_id = 1; uint32 decoration_id = 2; + uint32 popup_id = 3; } } diff --git a/snowcap/api/rust/Cargo.toml b/snowcap/api/rust/Cargo.toml index 2050d7a19..ce7077f83 100644 --- a/snowcap/api/rust/Cargo.toml +++ b/snowcap/api/rust/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" rust-version.workspace = true [dependencies] +bitflags = { workspace = true } from_variants = "1.0.2" futures = { workspace = true } hyper-util = { workspace = true } diff --git a/snowcap/api/rust/src/client.rs b/snowcap/api/rust/src/client.rs index 5604e709a..383966e98 100644 --- a/snowcap/api/rust/src/client.rs +++ b/snowcap/api/rust/src/client.rs @@ -6,6 +6,7 @@ use snowcap_api_defs::snowcap::{ decoration::v1::decoration_service_client::DecorationServiceClient, input::v1::input_service_client::InputServiceClient, layer::v1::layer_service_client::LayerServiceClient, + popup::v1::popup_service_client::PopupServiceClient, widget::v1::widget_service_client::WidgetServiceClient, }; use tokio::sync::{RwLock, RwLockReadGuard}; @@ -20,6 +21,7 @@ pub struct Client { input: InputServiceClient, widget: WidgetServiceClient, decoration: DecorationServiceClient, + popup: PopupServiceClient, } impl Client { @@ -39,6 +41,10 @@ impl Client { Self::get().layer.clone() } + pub fn popup() -> PopupServiceClient { + Self::get().popup.clone() + } + pub fn input() -> InputServiceClient { Self::get().input.clone() } @@ -57,6 +63,7 @@ impl Client { input: InputServiceClient::new(channel.clone()), widget: WidgetServiceClient::new(channel.clone()), decoration: DecorationServiceClient::new(channel.clone()), + popup: PopupServiceClient::new(channel.clone()), } } } diff --git a/snowcap/api/rust/src/layer.rs b/snowcap/api/rust/src/layer.rs index 459197bea..5198dd90e 100644 --- a/snowcap/api/rust/src/layer.rs +++ b/snowcap/api/rust/src/layer.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, num::NonZeroU32}; use snowcap_api_defs::snowcap::{ - input::v1::KeyboardKeyRequest, + input::v1::{KeyboardKeyRequest, keyboard_key_request::Target}, layer::{ self, v1::{CloseRequest, NewLayerRequest, OperateLayerRequest, UpdateLayerRequest, ViewRequest}, @@ -19,6 +19,7 @@ use crate::{ BlockOnTokio, client::Client, input::{KeyEvent, Modifiers}, + popup::{self, AsParent}, widget::{self, Program, WidgetDef, WidgetId, WidgetMessage}, }; @@ -349,7 +350,7 @@ where ) { let mut stream = match Client::input() .keyboard_key(KeyboardKeyRequest { - id: self.id.to_inner(), + target: Some(Target::LayerId(self.id.to_inner())), }) .block_on_tokio() { @@ -385,3 +386,9 @@ where }); } } + +impl AsParent for LayerHandle { + fn as_parent(&self) -> crate::popup::Parent { + popup::Parent(popup::ParentInner::Layer(self.id)) + } +} diff --git a/snowcap/api/rust/src/lib.rs b/snowcap/api/rust/src/lib.rs index c53802e2a..bcfe15e28 100644 --- a/snowcap/api/rust/src/lib.rs +++ b/snowcap/api/rust/src/lib.rs @@ -15,6 +15,7 @@ mod client; pub mod decoration; pub mod input; pub mod layer; +pub mod popup; pub mod widget; use client::Client; diff --git a/snowcap/api/rust/src/popup.rs b/snowcap/api/rust/src/popup.rs new file mode 100644 index 000000000..966d700e8 --- /dev/null +++ b/snowcap/api/rust/src/popup.rs @@ -0,0 +1,551 @@ +//! Support for popup surface widgets using `xdg-shell::xdg_popup` + +use std::collections::HashMap; + +use bitflags::bitflags; +use snowcap_api_defs::snowcap::{ + input::v1::{KeyboardKeyRequest, keyboard_key_request::Target}, + popup::{ + self, + v1::{CloseRequest, NewPopupRequest, OperatePopupRequest, UpdatePopupRequest, ViewRequest}, + }, + widget::v1::{GetWidgetEventsRequest, get_widget_events_request}, +}; +use tokio::sync::mpsc::UnboundedSender; +use tokio_stream::StreamExt; +use xkbcommon::xkb::Keysym; + +use crate::{ + BlockOnTokio, + client::Client, + input::{KeyEvent, Modifiers}, + widget::{self, Program, WidgetDef, WidgetId, WidgetMessage}, +}; + +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum ParentInner { + Layer(WidgetId), + Popup(WidgetId), +} + +/// Popup Parent surface. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct Parent(pub(crate) ParentInner); + +/// Position the Popup will be placed at. +#[allow(missing_docs)] +#[derive(Debug, Clone, PartialEq)] +pub enum Position { + /// Position the anchor point at the cursor. + AtCursor, + /// Position the anchor at an arbitrary point. + Point { x: f32, y: f32 }, + /// Position the anchor on a Rectangle boundaries. + Rectangle { + x: f32, + y: f32, + width: f32, + height: f32, + }, + /// Position the anchor on a Widget boundaries. + AtWidget(String), +} + +impl Position { + /// Create a new Position to place a Popup at the cursor location. + pub fn at_cursor() -> Self { + Position::AtCursor + } + + /// Create a new Position to place a Popup at an arbitrary point. + pub fn point(x: f32, y: f32) -> Self { + Position::Point { x, y } + } + + /// Create a new Position to place a Popup on an arbitrary rectangle. + pub fn rectangle(x: f32, y: f32, width: f32, height: f32) -> Self { + Position::Rectangle { + x, + y, + width, + height, + } + } + + /// Create a new Position to place a Popup relative to a Widget. + pub fn at_widget(id: impl Into) -> Self { + Position::AtWidget(id.into()) + } +} + +/// Position of the anchor point on the anchor rectangle. +#[allow(missing_docs)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum Anchor { + None, + Top, + Bottom, + Left, + Right, + TopLeft, + TopRight, + BottomLeft, + BottomRight, +} + +/// Direction of the gravity of the Popup. +#[allow(missing_docs)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum Gravity { + None, + Top, + Bottom, + Left, + Right, + TopLeft, + TopRight, + BottomLeft, + BottomRight, +} + +/// Popup position offset +#[allow(missing_docs)] +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Offset { + pub x: f32, + pub y: f32, +} + +bitflags! { + /// Define ways the compositor can adjust the popup if its position would make it partially + /// constrained. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ConstraintsAdjust: u32 { + /// Don't move the child surface when constrained. + const None = 0; + /// Move along the x axis until unconstrained. + const SlideX = 1; + /// Move along the y axis until unconstrained. + const SlideY = 2; + /// Invert the anchor and gravity on the x axis. + const FlipX = 4; + /// Invert the anchor and gravity on the y axis. + const FlipY = 8; + /// Horizontally resize the surface. + const ResizeX = 16; + /// Vertically resize the surface. + const ResizeY = 32; + } +} + +/// The error type for [`Popup::new_widget`]. +/// +/// [`Popup::new_widget`]: self::new_widget +#[derive(thiserror::Error, Debug)] +pub enum NewPopupError { + /// Snowcap returned a gRPC error status. + #[error("gRPC error: `{0}`")] + GrpcStatus(#[from] tonic::Status), +} + +/// The error type for [`PopupHandle::update`]. +#[derive(thiserror::Error, Debug)] +pub enum UpdatePopupError { + /// Snowcap returned a gRPC error status. + #[error("gRPC error: `{0}`")] + GrpcStatus(#[from] tonic::Status), +} + +/// Create a new popup. +pub fn new_widget( + mut program: P, + parent: &impl AsParent, + position: Position, + anchor: Option, + gravity: Option, + offset: Option, + constraints_adjust: Option, + no_grab: bool, + no_replace: bool, +) -> Result, NewPopupError> +where + Msg: Clone + Send + 'static, + P: Program + Send + 'static, +{ + let mut callbacks = HashMap::>::new(); + + let widget_def = program.view(); + + widget_def.collect_messages(&mut callbacks, WidgetDef::message_collector); + + let response = Client::popup() + .new_popup(NewPopupRequest { + widget_def: Some(widget_def.clone().into()), + parent_id: Some(parent.as_parent().into()), + position: Some(position.into()), + anchor: anchor + .map(From::from) + .unwrap_or(popup::v1::Anchor::Unspecified) as i32, + gravity: gravity + .map(From::from) + .unwrap_or(popup::v1::Gravity::Unspecified) as i32, + offset: offset.map(From::from), + constraints_adjust: constraints_adjust.map(From::from), + no_grab, + no_replace, + }) + .block_on_tokio()?; + + let popup_id = response.into_inner().popup_id; + + let mut event_stream = Client::widget() + .get_widget_events(GetWidgetEventsRequest { + id: Some(get_widget_events_request::Id::PopupId(popup_id)), + }) + .block_on_tokio()? + .into_inner(); + + let (msg_send, mut msg_recv) = tokio::sync::mpsc::unbounded_channel::(); + + tokio::spawn(async move { + loop { + tokio::select! { + Some(Ok(response)) = event_stream.next() => { + for widget_event in response.widget_events { + let Some(msg) = widget::message_from_event(&callbacks, widget_event) else { + continue; + }; + + program.update(msg); + } + } + Some(msg) = msg_recv.recv() => { + program.update(msg); + + if let Err(status) = Client::popup() + .request_view(ViewRequest { popup_id }) + .block_on_tokio() + { + tracing::error!("Failed to request view for {popup_id}: {status}"); + } + + continue; + } + else => break, + }; + + let widget_def = program.view(); + + callbacks.clear(); + + widget_def.collect_messages(&mut callbacks, WidgetDef::message_collector); + + Client::popup() + .update_popup(UpdatePopupRequest { + popup_id, + widget_def: Some(widget_def.into()), + ..Default::default() + }) + .await + .unwrap(); + } + }); + + Ok(PopupHandle { + id: popup_id.into(), + msg_sender: msg_send, + }) +} + +/// A handle to a popup surface. +#[derive(Clone)] +pub struct PopupHandle { + id: WidgetId, + msg_sender: UnboundedSender, +} + +impl PopupHandle { + /// Close this popup widget. + pub fn close(&self) { + if let Err(status) = Client::popup() + .close(CloseRequest { + popup_id: self.id.to_inner(), + }) + .block_on_tokio() + { + tracing::error!("Failed to close {self:?}: {status}"); + } + } +} + +impl PopupHandle +where + Msg: Clone + Send + 'static, +{ + /// Update this popup's attributes. + pub fn update( + &self, + position: Option, + anchor: Option, + gravity: Option, + offset: Option, + constraints_adjust: Option, + ) -> Result<(), UpdatePopupError> { + Client::popup() + .update_popup(UpdatePopupRequest { + popup_id: self.id.to_inner(), + widget_def: None, + position: position.map(From::from), + anchor: anchor + .map(From::from) + .or(Some(popup::v1::Anchor::Unspecified)) + .map(i32::from), + gravity: gravity + .map(From::from) + .or(Some(popup::v1::Gravity::Unspecified)) + .map(i32::from), + offset: offset.map(From::from), + constraints_adjust: constraints_adjust.map(From::from), + }) + .block_on_tokio()?; + + Ok(()) + } + + /// Update this popup's position. + pub fn set_position(&self, position: Position) -> Result<(), UpdatePopupError> { + self.update(Some(position), None, None, None, None) + } + + /// Update this popup's anchor. + pub fn set_anchor(&self, anchor: Anchor) -> Result<(), UpdatePopupError> { + self.update(None, Some(anchor), None, None, None) + } + + /// Update this popup's gravity. + pub fn set_gravity(&self, gravity: Gravity) -> Result<(), UpdatePopupError> { + self.update(None, None, Some(gravity), None, None) + } + + /// Update this popup's offset. + pub fn set_offset(&self, offset: Offset) -> Result<(), UpdatePopupError> { + self.update(None, None, None, Some(offset), None) + } + + /// Update this popup's contraints adjustment. + pub fn set_constraints_adjust( + &self, + constraints_adjust: ConstraintsAdjust, + ) -> Result<(), UpdatePopupError> { + self.update(None, None, None, None, Some(constraints_adjust)) + } + + /// Sends an [`Operation`] to this Popup. + /// + /// [`Operation`]: widget::operation::Operation + pub fn operate(&self, operation: widget::operation::Operation) { + if let Err(status) = Client::popup() + .operate_popup(OperatePopupRequest { + popup_id: self.id.to_inner(), + operation: Some(operation.into()), + }) + .block_on_tokio() + { + tracing::error!("Failed to send operation to {self:?}: {status}"); + } + } + + /// Sends a message to this Popup [`Program`]. + pub fn send_message(&self, message: Msg) { + let _ = self.msg_sender.send(message); + } + + /// Do something when a key event is received. + pub fn on_key_event( + &self, + mut on_event: impl FnMut(PopupHandle, KeyEvent) + Send + 'static, + ) { + let mut stream = match Client::input() + .keyboard_key(KeyboardKeyRequest { + target: Some(Target::PopupId(self.id.to_inner())), + }) + .block_on_tokio() + { + Ok(stream) => stream.into_inner(), + Err(status) => { + tracing::error!("Failed to set `on_key_event` handler: {status}"); + return; + } + }; + + let handle = self.clone(); + + tokio::spawn(async move { + while let Some(Ok(response)) = stream.next().await { + let event = KeyEvent::from(response); + + on_event(handle.clone(), event); + } + }); + } + + /// Do something on key press. + pub fn on_key_press( + &self, + mut on_press: impl FnMut(PopupHandle, Keysym, Modifiers) + Send + 'static, + ) { + self.on_key_event(move |handle, event| { + if !event.pressed || event.captured { + return; + } + + on_press(handle, event.key, event.mods) + }); + } +} + +impl std::fmt::Debug for PopupHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PopupHandle").field("id", &self.id).finish() + } +} + +impl std::fmt::Debug for Parent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + ParentInner::Layer(id) => f.debug_tuple("Parent::Layer").field(&id).finish(), + ParentInner::Popup(id) => f.debug_tuple("Parent::Popup").field(&id).finish(), + // ParentInner::Decoration(id) => f.debug_tuple("Parent::Decoration").field(&id).finish(), + } + } +} + +/// Used to convert a handle to a popup's [`Parent`]. +pub trait AsParent { + /// Convert a reference to a surface handle to a popup [`Parent`]. + fn as_parent(&self) -> Parent; +} + +impl AsParent for PopupHandle { + fn as_parent(&self) -> Parent { + Parent(ParentInner::Popup(self.id)) + } +} + +impl AsParent for Parent { + fn as_parent(&self) -> Parent { + *self + } +} + +impl From for popup::v1::new_popup_request::ParentId { + fn from(value: Parent) -> Self { + use popup::v1::new_popup_request::ParentId; + match value.0 { + ParentInner::Layer(id) => ParentId::LayerId(id.to_inner()), + ParentInner::Popup(id) => ParentId::PopupId(id.to_inner()), + // ParentInner::Decoration(id) => ParentId::DecoId(id.to_inner()), + } + } +} + +impl From for popup::v1::Position { + fn from(value: Position) -> Self { + Self { + strategy: Some(value.into()), + } + } +} + +impl From for popup::v1::position::Strategy { + fn from(value: Position) -> Self { + use popup::v1::{Rectangle, position::Strategy}; + + match value { + Position::AtCursor => Strategy::AtCursor(()), + Position::Point { x, y } => Strategy::Absolute(Rectangle { + x, + y, + width: 1., + height: 1., + }), + Position::Rectangle { + x, + y, + width, + height, + } => Strategy::Absolute(Rectangle { + x, + y, + width, + height, + }), + Position::AtWidget(id) => Strategy::AtWidget(id), + } + } +} + +impl From for popup::v1::Anchor { + fn from(value: Anchor) -> Self { + use popup::v1; + + match value { + Anchor::None => v1::Anchor::None, + Anchor::Top => v1::Anchor::Top, + Anchor::Bottom => v1::Anchor::Bottom, + Anchor::Left => v1::Anchor::Left, + Anchor::Right => v1::Anchor::Right, + Anchor::TopLeft => v1::Anchor::TopLeft, + Anchor::TopRight => v1::Anchor::TopRight, + Anchor::BottomLeft => v1::Anchor::BottomLeft, + Anchor::BottomRight => v1::Anchor::BottomRight, + } + } +} + +impl From for popup::v1::Gravity { + fn from(value: Gravity) -> Self { + use popup::v1; + + match value { + Gravity::None => v1::Gravity::None, + Gravity::Top => v1::Gravity::Top, + Gravity::Bottom => v1::Gravity::Bottom, + Gravity::Left => v1::Gravity::Left, + Gravity::Right => v1::Gravity::Right, + Gravity::TopLeft => v1::Gravity::TopLeft, + Gravity::TopRight => v1::Gravity::TopRight, + Gravity::BottomLeft => v1::Gravity::BottomLeft, + Gravity::BottomRight => v1::Gravity::BottomRight, + } + } +} + +impl From for popup::v1::Offset { + fn from(value: Offset) -> Self { + let Offset { x, y } = value; + + Self { x, y } + } +} + +impl From for popup::v1::ConstraintsAdjust { + fn from(value: ConstraintsAdjust) -> Self { + if value == ConstraintsAdjust::None { + return Self { + none: true, + ..Default::default() + }; + }; + + Self { + none: false, + slide_x: value.contains(ConstraintsAdjust::SlideX), + slide_y: value.contains(ConstraintsAdjust::SlideY), + flip_x: value.contains(ConstraintsAdjust::FlipX), + flip_y: value.contains(ConstraintsAdjust::FlipY), + resize_x: value.contains(ConstraintsAdjust::ResizeX), + resize_y: value.contains(ConstraintsAdjust::ResizeY), + } + } +} diff --git a/snowcap/api/rust/src/widget/container.rs b/snowcap/api/rust/src/widget/container.rs index 91bededb9..00ca46f65 100644 --- a/snowcap/api/rust/src/widget/container.rs +++ b/snowcap/api/rust/src/widget/container.rs @@ -16,6 +16,7 @@ pub struct Container { pub clip: Option, pub child: WidgetDef, pub style: Option