diff --git a/api/lua/build/grpc_client.lua b/api/lua/build/grpc_client.lua index fb64d2e94..0c6da121f 100644 --- a/api/lua/build/grpc_client.lua +++ b/api/lua/build/grpc_client.lua @@ -43,10 +43,31 @@ function extension_methods:get_headers_with_retries(timeout, retries) end end +---Call h2_stream:get_next_chunk() without timeout, retrying on spurious wakeup. +local function _each_chunk_helper(self) + local chunk, err, errno + repeat + chunk, err, errno = self:get_next_chunk() + until chunk ~= nil or errno ~= ce.ETIMEDOUT + + return chunk, err, errno +end + +---Iterate over stream's chunks. +--- +---The difference between this function and h2_stream:each_chunk is that this function +---will retry calling get_next_chunk if the function fails due to a spurious wakeup. +---@return function +---@return grpc_client.h2.Stream +function extension_methods:each_chunk2() + --return self:each_chunk() + return _each_chunk_helper, self +end + ---Extend a stream with new methods --- ----@param s http.h2_stream.stream ----@return http.h2_stream.stream +---@param s grpc_client.h2.Stream +---@return grpc_client.h2.Stream function StreamExtension.extend(s) for k, v in pairs(extension_methods) do s[k] = v @@ -354,7 +375,7 @@ function Client:server_streaming_request(request_specifier, data, callback, done end self.loop:wrap(function() - for response_body in stream:each_chunk() do + for response_body in stream:each_chunk2() do while response_body:len() > 0 do local msg_len = string.unpack(">I4", response_body:sub(2, 5)) @@ -432,7 +453,7 @@ function Client:bidirectional_streaming_request(request_specifier, callback, don end self.loop:wrap(function() - for response_body in stream:each_chunk() do + for response_body in stream:each_chunk2() do while response_body:len() > 0 do local msg_len = string.unpack(">I4", response_body:sub(2, 5)) diff --git a/api/lua/pinnacle-api-dev-1.rockspec b/api/lua/pinnacle-api-dev-1.rockspec index 4a659a9f3..968aa444f 100644 --- a/api/lua/pinnacle-api-dev-1.rockspec +++ b/api/lua/pinnacle-api-dev-1.rockspec @@ -55,6 +55,7 @@ build = { ["pinnacle.snowcap.snowcap.popup"] = "pinnacle/snowcap/snowcap/popup.lua", ["pinnacle.snowcap.snowcap.signal"] = "pinnacle/snowcap/snowcap/signal.lua", ["pinnacle.snowcap.snowcap.util"] = "pinnacle/snowcap/snowcap/util.lua", + ["pinnacle.snowcap.snowcap.util.channel"] = "pinnacle/snowcap/snowcap/util/channel.lua", ["pinnacle.snowcap.snowcap.log"] = "pinnacle/snowcap/snowcap/log.lua", }, } diff --git a/api/lua/pinnacle/grpc/defs.lua b/api/lua/pinnacle/grpc/defs.lua index 774b8da80..862187734 100644 --- a/api/lua/pinnacle/grpc/defs.lua +++ b/api/lua/pinnacle/grpc/defs.lua @@ -45,10 +45,31 @@ function extension_methods:get_headers_with_retries(timeout, retries) end end +---Call h2_stream:get_next_chunk() without timeout, retrying on spurious wakeup. +local function _each_chunk_helper(self) + local chunk, err, errno + repeat + chunk, err, errno = self:get_next_chunk() + until chunk ~= nil or errno ~= ce.ETIMEDOUT + + return chunk, err, errno +end + +---Iterate over stream's chunks. +--- +---The difference between this function and h2_stream:each_chunk is that this function +---will retry calling get_next_chunk if the function fails due to a spurious wakeup. +---@return function +---@return grpc_client.h2.Stream +function extension_methods:each_chunk2() + --return self:each_chunk() + return _each_chunk_helper, self +end + ---Extend a stream with new methods --- ----@param s http.h2_stream.stream ----@return http.h2_stream.stream +---@param s grpc_client.h2.Stream +---@return grpc_client.h2.Stream function StreamExtension.extend(s) for k, v in pairs(extension_methods) do s[k] = v @@ -356,7 +377,7 @@ function Client:server_streaming_request(request_specifier, data, callback, done end self.loop:wrap(function() - for response_body in stream:each_chunk() do + for response_body in stream:each_chunk2() do while response_body:len() > 0 do local msg_len = string.unpack(">I4", response_body:sub(2, 5)) @@ -434,7 +455,7 @@ function Client:bidirectional_streaming_request(request_specifier, callback, don end self.loop:wrap(function() - for response_body in stream:each_chunk() do + for response_body in stream:each_chunk2() do while response_body:len() > 0 do local msg_len = string.unpack(">I4", response_body:sub(2, 5)) diff --git a/snowcap/api/lua/snowcap/decoration.lua b/snowcap/api/lua/snowcap/decoration.lua index 99c1137b1..95412e3f2 100644 --- a/snowcap/api/lua/snowcap/decoration.lua +++ b/snowcap/api/lua/snowcap/decoration.lua @@ -15,17 +15,20 @@ local decoration_handle = {} ---@class snowcap.decoration.DecorationHandle ---@field id integer ----@field private update fun(msg: any) +---@field private _update fun(msg: any) +---@field private _operate fun(oper: snowcap.widget.operation.Operation) local DecorationHandle = {} ---@param id integer ---@param update fun(msg: any?) +---@param operate fun(oper: snowcap.widget.operation.Operation) ---@return snowcap.decoration.DecorationHandle -function decoration_handle.new(id, update) +function decoration_handle.new(id, update, operate) ---@type snowcap.decoration.DecorationHandle local self = { id = id, - update = update, + _update = update, + _operate = operate, } setmetatable(self, { __index = DecorationHandle }) return self @@ -80,29 +83,30 @@ function decoration.new_widget(args) local decoration_id = response.decoration_id or 0 + local sender, receiver = require("snowcap.util.channel").spsc() + ---@type fun(msg: any?) local update_on_msg = function(msg) if msg ~= nil then - args.program:update(msg) - end - - ---@diagnostic disable-next-line: redefined-local - local _, err = client:snowcap_decoration_v1_DecorationService_RequestView({ - decoration_id = decoration_id, - }) - - if err then - log.error(err) + sender:send({ + message = msg, + }) + else + sender:send({ + redraw = {} + }) end end - local handle = decoration_handle.new(decoration_id, update_on_msg) - ---@type fun(oper: snowcap.widget.operation.Operation) - local forward_operation = function(oper) - handle:operate(oper) + local operate_surface = function(oper) + sender:send({ + operation = oper, + }) end + local handle = decoration_handle.new(decoration_id, update_on_msg, operate_surface) + ---@type fun(): snowcap.signal.HandlerPolicy local close_surface = function() handle:close() @@ -112,48 +116,110 @@ function decoration.new_widget(args) args.program:connect(widget_signal.redraw_needed, update_on_msg) args.program:connect(widget_signal.send_message, update_on_msg) - args.program:connect(widget_signal.operation, forward_operation) + args.program:connect(widget_signal.operation, operate_surface) args.program:connect(widget_signal.request_close, close_surface) args.program:event({ created = widget.SurfaceHandle.from_decoration_handle(handle), }) + err = client:snowcap_decoration_v1_DecorationService_GetDecorationEvents({ + decoration_id = decoration_id, + }, function(response) ---@diagnostic disable-line:redefined-local + sender:send({ + decoration_events = response.decoration_events or {} + }) + end) + err = client:snowcap_widget_v1_WidgetService_GetWidgetEvents({ decoration_id = decoration_id, }, function(response) ---@diagnostic disable-line: redefined-local - for _, event in ipairs(response.widget_events) do - ---@diagnostic disable-next-line:invisible - 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) + sender:send({ + widget_events = response.widget_events or {} + }) + end) + + client.loop:wrap(function() + local pending_operations = {} + + local msg = receiver:recv() + while msg do + local update_view = false + + if msg.widget_events then + for _, event in ipairs(msg.widget_events) do + ---@diagnostic disable-next-line:invisible + 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 + + update_view = true + elseif msg.message then + args.program:update(msg) + elseif msg.operation then + table.insert(pending_operations, msg.operation) + elseif msg.decoration_events then + for _, decoration_event in ipairs(msg.decoration_events) do + if decoration_event.closing ~= nil then + goto main_loop_break + end end end - end - ---@diagnostic disable-next-line:redefined-local - local widget_def = args.program:view() or widget.row({ children = {} }) - callbacks = {} + if not update_view then + ---@diagnostic disable-next-line: redefined-local + local _, err = client:snowcap_decoration_v1_DecorationService_RequestView({ + decoration_id = decoration_id, + }) - widget._traverse_widget_tree(widget_def, callbacks, widget._collect_callbacks) + if err then + log.error(err) + end + else + ---@diagnostic disable-next-line:redefined-local + local widget_def = args.program:view() or widget.row({ children = {} }) + callbacks = {} - ---@diagnostic disable-next-line:redefined-local - local _, err = client:snowcap_decoration_v1_DecorationService_UpdateDecoration({ - decoration_id = decoration_id, - widget_def = widget.widget_def_into_api(widget_def), - }) + widget._traverse_widget_tree(widget_def, callbacks, widget._collect_callbacks) - if err then - log.error(err) + ---@diagnostic disable-next-line:redefined-local + local _, err = client:snowcap_decoration_v1_DecorationService_UpdateDecoration({ + decoration_id = decoration_id, + widget_def = widget.widget_def_into_api(widget_def), + }) + + if err then + log.error(err) + end + + for _, oper in ipairs(pending_operations) do + ---@diagnostic disable-next-line:redefined-local + local _, err = client:snowcap_decoration_v1_DecorationService_OperateDecoration({ + decoration_id = decoration_id, + operation = require("snowcap.widget.operation")._to_api(oper), ---@diagnostic disable-line: invisible + }) + + if err then + log.error(err) + end + end + pending_operations = {} + end + + msg = receiver:recv() end - end, function() + ::main_loop_break:: + args.program:event({ - closing = {}, + closing = {} }) end) @@ -178,20 +244,13 @@ end --- ---@param message any function DecorationHandle:send_message(message) - self.update(message) + self._update(message) end ---Sends an `Operation` to this decoration. ---@param operation snowcap.widget.operation.Operation function DecorationHandle:operate(operation) - local _, err = client:snowcap_decoration_v1_DecorationService_OperateDecoration({ - decoration_id = self.id, - operation = require("snowcap.widget.operation")._to_api(operation), ---@diagnostic disable-line: invisible - }) - - if err then - log.error(err) - end + self._operate(operation) end ---Sets the z-index at which this decoration will render. diff --git a/snowcap/api/lua/snowcap/grpc/defs.lua b/snowcap/api/lua/snowcap/grpc/defs.lua index c3a1d4c89..1c902b435 100644 --- a/snowcap/api/lua/snowcap/grpc/defs.lua +++ b/snowcap/api/lua/snowcap/grpc/defs.lua @@ -45,10 +45,31 @@ function extension_methods:get_headers_with_retries(timeout, retries) end end +---Call h2_stream:get_next_chunk() without timeout, retrying on spurious wakeup. +local function _each_chunk_helper(self) + local chunk, err, errno + repeat + chunk, err, errno = self:get_next_chunk() + until chunk ~= nil or errno ~= ce.ETIMEDOUT + + return chunk, err, errno +end + +---Iterate over stream's chunks. +--- +---The difference between this function and h2_stream:each_chunk is that this function +---will retry calling get_next_chunk if the function fails due to a spurious wakeup. +---@return function +---@return grpc_client.h2.Stream +function extension_methods:each_chunk2() + --return self:each_chunk() + return _each_chunk_helper, self +end + ---Extend a stream with new methods --- ----@param s http.h2_stream.stream ----@return http.h2_stream.stream +---@param s grpc_client.h2.Stream +---@return grpc_client.h2.Stream function StreamExtension.extend(s) for k, v in pairs(extension_methods) do s[k] = v @@ -356,7 +377,7 @@ function Client:server_streaming_request(request_specifier, data, callback, done end self.loop:wrap(function() - for response_body in stream:each_chunk() do + for response_body in stream:each_chunk2() do while response_body:len() > 0 do local msg_len = string.unpack(">I4", response_body:sub(2, 5)) @@ -434,7 +455,7 @@ function Client:bidirectional_streaming_request(request_specifier, callback, don end self.loop:wrap(function() - for response_body in stream:each_chunk() do + for response_body in stream:each_chunk2() do while response_body:len() > 0 do local msg_len = string.unpack(">I4", response_body:sub(2, 5)) @@ -1138,6 +1159,17 @@ local snowcap_popup_v1_PopupEvent_Focus = { ---@class snowcap.decoration.v1.ViewResponse +---@class snowcap.decoration.v1.GetDecorationEventsRequest +---@field decoration_id integer? + +---@class snowcap.decoration.v1.DecorationEvent +---@field closing snowcap.decoration.v1.DecorationEvent.Closing? + +---@class snowcap.decoration.v1.DecorationEvent.Closing + +---@class snowcap.decoration.v1.GetDecorationEventsResponse +---@field decoration_events snowcap.decoration.v1.DecorationEvent[]? + ---@class snowcap.input.v0alpha1.Modifiers ---@field shift boolean? ---@field ctrl boolean? @@ -1337,6 +1369,9 @@ local snowcap_popup_v1_PopupEvent_Focus = { ---@class snowcap.layer.v1.LayerEvent ---@field focus snowcap.layer.v1.LayerEvent.Focus? +---@field closing snowcap.layer.v1.LayerEvent.Closing? + +---@class snowcap.layer.v1.LayerEvent.Closing ---@class snowcap.layer.v1.GetLayerEventsResponse ---@field layer_events snowcap.layer.v1.LayerEvent[]? @@ -1411,6 +1446,9 @@ local snowcap_popup_v1_PopupEvent_Focus = { ---@class snowcap.popup.v1.PopupEvent ---@field focus snowcap.popup.v1.PopupEvent.Focus? +---@field closing snowcap.popup.v1.PopupEvent.Closing? + +---@class snowcap.popup.v1.PopupEvent.Closing ---@class snowcap.popup.v1.GetPopupEventsResponse ---@field popup_events snowcap.popup.v1.PopupEvent[]? @@ -1498,6 +1536,10 @@ snowcap.decoration.v1.UpdateDecorationRequest = {} snowcap.decoration.v1.UpdateDecorationResponse = {} snowcap.decoration.v1.ViewRequest = {} snowcap.decoration.v1.ViewResponse = {} +snowcap.decoration.v1.GetDecorationEventsRequest = {} +snowcap.decoration.v1.DecorationEvent = {} +snowcap.decoration.v1.DecorationEvent.Closing = {} +snowcap.decoration.v1.GetDecorationEventsResponse = {} snowcap.input = {} snowcap.input.v0alpha1 = {} snowcap.input.v0alpha1.Modifiers = {} @@ -1542,6 +1584,7 @@ snowcap.layer.v1.ViewRequest = {} snowcap.layer.v1.ViewResponse = {} snowcap.layer.v1.GetLayerEventsRequest = {} snowcap.layer.v1.LayerEvent = {} +snowcap.layer.v1.LayerEvent.Closing = {} snowcap.layer.v1.GetLayerEventsResponse = {} snowcap.popup = {} snowcap.popup.v1 = {} @@ -1560,6 +1603,7 @@ snowcap.popup.v1.ViewRequest = {} snowcap.popup.v1.ViewResponse = {} snowcap.popup.v1.GetPopupEventsRequest = {} snowcap.popup.v1.PopupEvent = {} +snowcap.popup.v1.PopupEvent.Closing = {} snowcap.popup.v1.GetPopupEventsResponse = {} snowcap.v0alpha1 = {} snowcap.v0alpha1.Nothing = {} @@ -1695,6 +1739,26 @@ snowcap.decoration.v1.DecorationService.RequestView.response = ".snowcap.decorat function Client:snowcap_decoration_v1_DecorationService_RequestView(data) return self:unary_request(snowcap.decoration.v1.DecorationService.RequestView, data) end +snowcap.decoration.v1.DecorationService.GetDecorationEvents = {} +snowcap.decoration.v1.DecorationService.GetDecorationEvents.service = "snowcap.decoration.v1.DecorationService" +snowcap.decoration.v1.DecorationService.GetDecorationEvents.method = "GetDecorationEvents" +snowcap.decoration.v1.DecorationService.GetDecorationEvents.request = ".snowcap.decoration.v1.GetDecorationEventsRequest" +snowcap.decoration.v1.DecorationService.GetDecorationEvents.response = ".snowcap.decoration.v1.GetDecorationEventsResponse" + +---Performs a server-streaming request. +--- +---`callback` will be called with every streamed response. +--- +---@nodiscard +--- +---@param data snowcap.decoration.v1.GetDecorationEventsRequest +---@param callback fun(response: snowcap.decoration.v1.GetDecorationEventsResponse) +---@param done? fun() +--- +---@return string | nil An error string, if any +function Client:snowcap_decoration_v1_DecorationService_GetDecorationEvents(data, callback, done) + return self:server_streaming_request(snowcap.decoration.v1.DecorationService.GetDecorationEvents, data, callback, done) +end snowcap.input.v0alpha1.InputService = {} snowcap.input.v0alpha1.InputService.KeyboardKey = {} snowcap.input.v0alpha1.InputService.KeyboardKey.service = "snowcap.input.v0alpha1.InputService" diff --git a/snowcap/api/lua/snowcap/layer.lua b/snowcap/api/lua/snowcap/layer.lua index ef1842de9..a1020b96e 100644 --- a/snowcap/api/lua/snowcap/layer.lua +++ b/snowcap/api/lua/snowcap/layer.lua @@ -16,6 +16,7 @@ local layer_handle = {} ---@class snowcap.layer.LayerHandle ---@field id integer ---@field private _update fun(msg:any) +---@field private _operate fun(oper: snowcap.widget.operation.Operation) local LayerHandle = {} ---Convert a LayerHandle into a Popup's ParentHandle @@ -26,12 +27,14 @@ end ---@param id integer ---@param update fun(msg: any?) +---@param operate fun(oper: snowcap.widget.operation.Operation) ---@return snowcap.layer.LayerHandle -function layer_handle.new(id, update) +function layer_handle.new(id, update, operate) ---@type snowcap.layer.LayerHandle local self = { id = id, _update = update, + _operate = operate, } setmetatable(self, { __index = LayerHandle }) return self @@ -132,29 +135,30 @@ function layer.new_widget(args) local layer_id = response.layer_id or 0 + local sender, receiver = require("snowcap.util.channel").spsc() + ---@type fun(msg: any?) local update_on_msg = function(msg) if msg ~= nil then - args.program:update(msg) - end - - ---@diagnostic disable-next-line: redefined-local - local _, err = client:snowcap_layer_v1_LayerService_RequestView({ - layer_id = layer_id, - }) - - if err then - log.error(err) + sender:send({ + message = msg + }) + else + sender:send({ + redraw = {} + }) end end - local handle = layer_handle.new(layer_id, update_on_msg) - ---@type fun(oper: snowcap.widget.operation.Operation) - local forward_operation = function(oper) - handle:operate(oper) + local operate_surface = function(oper) + sender:send({ + operation = oper + }) end + local handle = layer_handle.new(layer_id, update_on_msg, operate_surface) + ---@type fun(): snowcap.signal.HandlerPolicy local close_surface = function() handle:close() @@ -164,7 +168,7 @@ function layer.new_widget(args) args.program:connect(widget_signal.redraw_needed, update_on_msg) args.program:connect(widget_signal.send_message, update_on_msg) - args.program:connect(widget_signal.operation, forward_operation) + args.program:connect(widget_signal.operation, operate_surface) args.program:connect(widget_signal.request_close, close_surface) args.program:event({ @@ -174,71 +178,115 @@ function layer.new_widget(args) err = client:snowcap_layer_v1_LayerService_GetLayerEvents({ layer_id = layer_id, }, function(response) ---@diagnostic disable-line:redefined-local - response.layer_events = response.layer_events or {} - - for _, layer_event in ipairs(response.layer_events) do - local focus = layer_event.focus --[[@as snowcap.layer.FocusEvent]] - ---@type snowcap.widget.SurfaceEvent? - local event = nil - - if focus == focus_event.GAINED then - event = { - focus_gained = {}, - } - elseif focus == focus_event.LOST then - event = { - focus_lost = {}, - } - end - - if event then - args.program:event(event) - end - end - - ---@diagnostic disable-next-line: redefined-local - local _, err = client:snowcap_layer_v1_LayerService_RequestView({ - layer_id = layer_id, + sender:send({ + layer_events = response.layer_events or {} }) - - if err then - log.error(err) - end end) err = client:snowcap_widget_v1_WidgetService_GetWidgetEvents({ layer_id = layer_id, }, function(response) ---@diagnostic disable-line:redefined-local - for _, event in ipairs(response.widget_events) do - ---@diagnostic disable-next-line:invisible - 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) + sender:send({ + widget_events = response.widget_events or {} + }) + end) + + client.loop:wrap(function() + local pending_operations = {} + + local msg = receiver:recv() + while msg do + local update_view = false; + + if msg.widget_events then + for _, event in ipairs(msg.widget_events) do + ---@diagnostic disable-next-line:invisible + local message = widget._message_from_event(callbacks, event) + + if message then + local ok, update_err = pcall(function() + args.program:update(message) + end) + if not ok then + log.error(update_err) + end + end + end + update_view = true + elseif msg.message then + args.program:update(msg.message) + elseif msg.operation then + table.insert(pending_operations, msg.operation) + elseif msg.layer_events then + for _, layer_event in ipairs(msg.layer_events) do + if layer_event.closing ~= nil then + goto main_loop_break + end + + local focus = layer_event.focus --[[@as snowcap.layer.FocusEvent]] + ---@type snowcap.widget.SurfaceEvent? + local event = nil + + if focus == focus_event.GAINED then + event = { + focus_gained = {}, + } + elseif focus == focus_event.LOST then + event = { + focus_lost = {}, + } + end + + if event then + args.program:event(event) + end end end - end - ---@diagnostic disable-next-line:redefined-local - local widget_def = args.program:view() or widget.row({ children = {} }) - callbacks = {} + if not update_view then + ---@diagnostic disable-next-line: redefined-local + local _, err = client:snowcap_layer_v1_LayerService_RequestView({ + layer_id = layer_id, + }) - widget._traverse_widget_tree(widget_def, callbacks, widget._collect_callbacks) + if err then + log.error(err) + end + else + ---@diagnostic disable-next-line:redefined-local + local widget_def = args.program:view() or widget.row({ children = {} }) + callbacks = {} - ---@diagnostic disable-next-line:redefined-local - local _, err = client:snowcap_layer_v1_LayerService_UpdateLayer({ - layer_id = layer_id, - widget_def = widget.widget_def_into_api(widget_def), - }) + widget._traverse_widget_tree(widget_def, callbacks, widget._collect_callbacks) + + ---@diagnostic disable-next-line:redefined-local + local _, err = client:snowcap_layer_v1_LayerService_UpdateLayer({ + layer_id = layer_id, + widget_def = widget.widget_def_into_api(widget_def), + }) + + if err then + log.error(err) + end + + for _, oper in pairs(pending_operations) do + ---@diagnostic disable-next-line:redefined-local + local _, err = client:snowcap_layer_v1_LayerService_OperateLayer({ + layer_id = layer_id, + operation = require("snowcap.widget.operation")._to_api(oper), ---@diagnostic disable-line: invisible + }) + + if err then + log.error(err) + end + end + pending_operations = {} + end - if err then - log.error(err) + msg = receiver:recv() end - end, function() + ::main_loop_break:: + args.program:event({ closing = {}, }) @@ -334,14 +382,7 @@ end ---Sends an `Operation` to this layer. ---@param operation snowcap.widget.operation.Operation function LayerHandle:operate(operation) - local _, err = client:snowcap_layer_v1_LayerService_OperateLayer({ - layer_id = self.id, - operation = require("snowcap.widget.operation")._to_api(operation), ---@diagnostic disable-line: invisible - }) - - if err then - log.error(err) - end + self._operate(operation) end layer.anchor = anchor diff --git a/snowcap/api/lua/snowcap/popup.lua b/snowcap/api/lua/snowcap/popup.lua index 19e02415a..b0faf6384 100644 --- a/snowcap/api/lua/snowcap/popup.lua +++ b/snowcap/api/lua/snowcap/popup.lua @@ -66,6 +66,7 @@ popup.parent = parent ---@class snowcap.popup.PopupHandle ---@field id integer Popup's id. ---@field private _update fun(msg:any) +---@field private _operate fun(oper: snowcap.widget.operation.Operation) local PopupHandle = {} ---Convert a PopupHandle into a Popup's ParentHandle @@ -79,12 +80,14 @@ end ---@package ---@param id integer ---@param update fun(msg: any) +---@param operate fun(oper: snowcap.widget.operation.Operation) ---@return snowcap.popup.PopupHandle -function popup_handle.new(id, update) +function popup_handle.new(id, update, operate) ---@type snowcap.popup.PopupHandle local self = { id = id, _update = update, + _operate = operate } setmetatable(self, { __index = PopupHandle }) return self @@ -265,29 +268,30 @@ function popup.new_widget(args) local popup_id = response.popup_id --[[@as integer]] + local sender, receiver = require("snowcap.util.channel").spsc() + ---@type fun(msg: any?) local update_on_msg = function(msg) if msg ~= nil then - args.program:update(msg) - end - - ---@diagnostic disable-next-line: redefined-local - local _, err = client:snowcap_popup_v1_PopupService_RequestView({ - popup_id = popup_id, - }) - - if err then - log.error(err) + sender:send({ + message = msg + }) + else + sender:send({ + redraw = {} + }) end end - local handle = popup_handle.new(popup_id, update_on_msg) - ---@type fun(oper: snowcap.widget.operation.Operation) - local forward_operation = function(oper) - handle:operate(oper) + local operate_surface = function(oper) + sender:send({ + operation = oper, + }) end + local handle = popup_handle.new(popup_id, update_on_msg, operate_surface) + ---@type fun(): snowcap.signal.HandlerPolicy local close_surface = function() handle:close() @@ -297,7 +301,7 @@ function popup.new_widget(args) args.program:connect(widget_signal.redraw_needed, update_on_msg) args.program:connect(widget_signal.send_message, update_on_msg) - args.program:connect(widget_signal.operation, forward_operation) + args.program:connect(widget_signal.operation, operate_surface) args.program:connect(widget_signal.request_close, close_surface) args.program:event({ @@ -307,73 +311,116 @@ function popup.new_widget(args) err = client:snowcap_popup_v1_PopupService_GetPopupEvents({ popup_id = popup_id, }, function(response) ---@diagnostic disable-line:redefined-local - response.popup_events = response.popup_events or {} - - for _, popup_event in ipairs(response.popup_events) do - local focus = popup_event.focus --[[@as snowcap.popup.FocusEvent]] - ---@type snowcap.widget.SurfaceEvent? - local event = nil - - if focus == focus_event.GAINED then - event = { - focus_gained = {}, - } - elseif focus == focus_event.LOST then - event = { - focus_lost = {}, - } - end - - if event then - args.program:event(event) - end - end - - ---@diagnostic disable-next-line: redefined-local - local _, err = client:snowcap_popup_v1_PopupService_RequestView({ - popup_id = popup_id, + sender:send({ + popup_events = response.popup_events or {} }) - - if err then - log.error(err) - end end) 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) + sender:send({ + widget_events = response.widget_events or {} + }) + end) - if not ok then - log.error(update_err) + client.loop:wrap(function() + local pending_operations = {} + + local msg = receiver:recv() + while msg do + local update_view = false + + if msg.widget_events then + for _, event in ipairs(msg.widget_events) do + local message = widget._message_from_event(callbacks, event) + + if message then + local ok, update_err = pcall(function() + args.program:update(message) + end) + if not ok then + log.error(update_err) + end + end + end + update_view = true + elseif msg.message then + args.program:update(msg.message) + elseif msg.operation then + table.insert(pending_operations, msg.operation) + elseif msg.popup_events then + for _, popup_event in ipairs(msg.popup_events) do + if popup_event.closing ~= nil then + goto main_loop_break + end + + local focus = popup_event.focus --[[@as snowcap.popup.FocusEvent]] + ---@type snowcap.widget.SurfaceEvent? + local event = nil + + if focus == focus_event.GAINED then + event = { + focus_gained = {}, + } + elseif focus == focus_event.LOST then + event = { + focus_lost = {}, + } + end + + if event then + args.program:event(event) + end end end - end - ---@diagnostic disable-next-line:redefined-local - local widget_def = args.program:view() or widget.row({ children = {} }) - callbacks = {} + if not update_view then + ---@diagnostic disable-next-line: redefined-local + local _, err = client:snowcap_popup_v1_PopupService_RequestView({ + popup_id = popup_id, + }) - widget._traverse_widget_tree(widget_def, callbacks, widget._collect_callbacks) + if err then + log.error(err) + end + else + ---@diagnostic disable-next-line:redefined-local + local widget_def = args.program:view() or widget.row({ children = {} }) + 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), - }) + 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 + + for _, oper in pairs(pending_operations) do + ---@diagnostic disable-next-line:redefined-local + local _, err = client:snowcap_popup_v1_PopupService_OperatePopup({ + popup_id = popup_id, + operation = require("snowcap.widget.operation")._to_api(oper), ---@diagnostic disable-line: invisible + }) + + if err then + log.error(err) + end + end + pending_operations = {} + end - if err then - log.error(err) + msg = receiver:recv() end - end, function() + ::main_loop_break:: + args.program:event({ - closing = {}, + closing = {} }) end) @@ -429,14 +476,7 @@ 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 + self._operate(operation) end ---PopupHandle:popup parameters. diff --git a/snowcap/api/lua/snowcap/util/channel.lua b/snowcap/api/lua/snowcap/util/channel.lua new file mode 100644 index 000000000..cc8b57e42 --- /dev/null +++ b/snowcap/api/lua/snowcap/util/channel.lua @@ -0,0 +1,76 @@ +local cprom = require("cqueues.promise") + +---@class cqueues.promise +---@field get fun(self, timeout?: number): ... +---@field set fun(self, ok: boolean, ...) + +---Simple channels implementation. +---@class snowcap.util.channel +local channel = {} + +---Sending end of a channel. +--- +---@class Sender +---@field _promise cqueues.promise +local Sender = {} + +---Receiving end of a channel. +--- +---@class Receiver +---@field _promise cqueues.promise +local Receiver = {} + +---Send a message on the channel. +--- +---@generic T +---@param msg T +function Sender:send(msg) + if self._promise == nil then + return false, "Already closed" + end + + local promise = self._promise + local next = cprom.new() + + promise:set(true, msg, next) + self._promise = next + return true +end + +function Sender:close() + self._promise:set(true, nil, nil) + self._promise = nil +end + +---Wait on this channel for a new message. +--- +---@generic T +---@return T? +---@return string? +function Receiver:recv() + if self._promise then + local msg, next = self._promise:get() + + self._promise = next + return msg + else + return nil, "Sender closed." + end +end + +---Simple single producer simple receiver channel. +--- +---This channel is unbounded. +function channel.spsc() + local promise = cprom.new() + local sender = setmetatable({ + _promise = promise + }, { __index = Sender }) + local receiver = setmetatable({ + _promise = promise, + }, { __index = Receiver }) + + return sender, receiver +end + +return channel diff --git a/snowcap/api/protobuf/snowcap/decoration/v1/decoration.proto b/snowcap/api/protobuf/snowcap/decoration/v1/decoration.proto index 645322866..d520dcc98 100644 --- a/snowcap/api/protobuf/snowcap/decoration/v1/decoration.proto +++ b/snowcap/api/protobuf/snowcap/decoration/v1/decoration.proto @@ -48,10 +48,25 @@ message ViewRequest { } message ViewResponse {} +message GetDecorationEventsRequest { + uint32 decoration_id = 1; +} +message DecorationEvent { + message Closing {} + + oneof event { + Closing closing = 1; + } +} +message GetDecorationEventsResponse { + repeated DecorationEvent decoration_events = 1; +} + service DecorationService { rpc NewDecoration(NewDecorationRequest) returns (NewDecorationResponse); rpc Close(CloseRequest) returns (CloseResponse); rpc OperateDecoration(OperateDecorationRequest) returns (OperateDecorationResponse); rpc UpdateDecoration(UpdateDecorationRequest) returns (UpdateDecorationResponse); rpc RequestView(ViewRequest) returns (ViewResponse); + rpc GetDecorationEvents(GetDecorationEventsRequest) returns (stream GetDecorationEventsResponse); } diff --git a/snowcap/api/protobuf/snowcap/layer/v1/layer.proto b/snowcap/api/protobuf/snowcap/layer/v1/layer.proto index 1a47091b0..54167641f 100644 --- a/snowcap/api/protobuf/snowcap/layer/v1/layer.proto +++ b/snowcap/api/protobuf/snowcap/layer/v1/layer.proto @@ -82,8 +82,11 @@ message LayerEvent { FOCUS_LOST = 2; } + message Closing {} + oneof event { Focus focus = 1; + Closing closing = 2; } } diff --git a/snowcap/api/protobuf/snowcap/popup/v1/popup.proto b/snowcap/api/protobuf/snowcap/popup/v1/popup.proto index ea168dbc3..85d15c615 100644 --- a/snowcap/api/protobuf/snowcap/popup/v1/popup.proto +++ b/snowcap/api/protobuf/snowcap/popup/v1/popup.proto @@ -119,8 +119,11 @@ message PopupEvent { FOCUS_LOST = 2; } + message Closing {} + oneof event { Focus focus = 1; + Closing closing = 2; } } diff --git a/snowcap/api/rust/src/surface.rs b/snowcap/api/rust/src/surface.rs index b527d7b54..7304017bb 100644 --- a/snowcap/api/rust/src/surface.rs +++ b/snowcap/api/rust/src/surface.rs @@ -4,12 +4,20 @@ use crate::{ decoration::DecorationHandle, layer::LayerHandle, popup::{AsParent, Parent, PopupHandle}, + widget, }; pub mod decoration; pub mod layer; pub mod popup; +/// Internal messages used by the different surfaces. +pub(crate) enum SurfaceMessage { + Message(Msg), + Operation(widget::operation::Operation), + Redraw, +} + /// Events emitted by the surface to notify [`Program`] of state changes. #[derive(Debug)] #[non_exhaustive] diff --git a/snowcap/api/rust/src/surface/decoration.rs b/snowcap/api/rust/src/surface/decoration.rs index 07670ece2..4c4bd836c 100644 --- a/snowcap/api/rust/src/surface/decoration.rs +++ b/snowcap/api/rust/src/surface/decoration.rs @@ -6,8 +6,8 @@ use snowcap_api_defs::snowcap::{ decoration::{ self, v1::{ - CloseRequest, NewDecorationRequest, OperateDecorationRequest, UpdateDecorationRequest, - ViewRequest, + CloseRequest, GetDecorationEventsRequest, NewDecorationRequest, + OperateDecorationRequest, UpdateDecorationRequest, ViewRequest, decoration_event, }, }, widget::v1::{GetWidgetEventsRequest, get_widget_events_request}, @@ -20,7 +20,7 @@ use crate::{ BlockOnTokio, client::Client, popup::{self, AsParent}, - surface::SurfaceEvent, + surface::{SurfaceEvent, SurfaceMessage}, widget::{self, Program, WidgetDef, WidgetId, WidgetMessage, operation, signal}, }; @@ -100,14 +100,19 @@ where let decoration_id = response.into_inner().decoration_id; - let mut event_stream = Client::widget() + let mut widget_event_stream = Client::widget() .get_widget_events(GetWidgetEventsRequest { id: Some(get_widget_events_request::Id::DecorationId(decoration_id)), }) .block_on_tokio()? .into_inner(); - let (msg_send, mut msg_recv) = tokio::sync::mpsc::unbounded_channel::>(); + let mut decoration_event_stream = Client::decoration() + .get_decoration_events(GetDecorationEventsRequest { decoration_id }) + .block_on_tokio()? + .into_inner(); + + let (msg_send, mut msg_recv) = tokio::sync::mpsc::unbounded_channel::>(); let handle = DecorationHandle { id: decoration_id.into(), @@ -119,7 +124,7 @@ where let msg_send = msg_send.clone(); move |msg: signal::Message| { - if let Err(err) = msg_send.send(Some(msg.into_inner())) { + if let Err(err) = msg_send.send(SurfaceMessage::Message(msg.into_inner())) { error!("Failed to send emitted msg: {err}"); crate::signal::HandlerPolicy::Discard } else { @@ -132,7 +137,7 @@ where let msg_send = msg_send.clone(); move |_: signal::RedrawNeeded| { - if let Err(err) = msg_send.send(None) { + if let Err(err) = msg_send.send(SurfaceMessage::Redraw) { error!("Failed to send redraw signal: {err}"); crate::signal::HandlerPolicy::Discard } else { @@ -165,9 +170,11 @@ where }); tokio::spawn(async move { - loop { + let mut pending_operations = Vec::new(); + + 'main_loop: loop { tokio::select! { - Some(Ok(response)) = event_stream.next() => { + Some(Ok(response)) = widget_event_stream.next() => { for widget_event in response.widget_events { let Some(msg) = widget::message_from_event(&callbacks, widget_event) else { continue; @@ -177,8 +184,14 @@ where } } Some(msg) = msg_recv.recv() => { - if let Some(msg) = msg { - program.update(msg); + match msg { + SurfaceMessage::Message(msg) => { + program.update(msg); + }, + SurfaceMessage::Operation(operation) => { + pending_operations.push(operation); + }, + _ => {} } if let Err(status) = Client::decoration() @@ -190,6 +203,13 @@ where continue; } + Some(Ok(response)) = decoration_event_stream.next() => { + for decoration_event in response.decoration_events { + if matches!(decoration_event.event, Some(decoration_event::Event::Closing(_))) { + break 'main_loop; + } + } + } else => break, }; @@ -211,6 +231,19 @@ where }) .await .unwrap(); + + let operations = std::mem::take(&mut pending_operations); + for operation in operations.into_iter() { + if let Err(status) = Client::decoration() + .operate_decoration(OperateDecorationRequest { + decoration_id, + operation: Some(operation.into()), + }) + .await + { + error!("Failed to send operation to {decoration_id:?}: {status}"); + } + } } program.event(SurfaceEvent::Closing); @@ -222,7 +255,7 @@ where /// A handle to a decoration surface. pub struct DecorationHandle { id: WidgetId, - msg_sender: UnboundedSender>, + msg_sender: UnboundedSender>, } impl Clone for DecorationHandle { @@ -257,27 +290,19 @@ impl DecorationHandle { /// Sends a message to this decoration's [`Program`]. pub fn send_message(&self, message: Msg) { - let _ = self.msg_sender.send(Some(message)); + let _ = self.msg_sender.send(SurfaceMessage::Message(message)); } /// Forces this decoration to redraw. pub fn force_redraw(&self) { - let _ = self.msg_sender.send(None); + let _ = self.msg_sender.send(SurfaceMessage::Redraw); } /// Sends an [`Operation`] to this Decoration. /// /// [`Operation`]: widget::operation::Operation pub fn operate(&self, operation: widget::operation::Operation) { - if let Err(status) = Client::decoration() - .operate_decoration(OperateDecorationRequest { - decoration_id: self.id.to_inner(), - operation: Some(operation.into()), - }) - .block_on_tokio() - { - error!("Failed to send operation to {self:?}: {status}"); - } + let _ = self.msg_sender.send(SurfaceMessage::Operation(operation)); } /// Sets the z-index that this decoration will render at. diff --git a/snowcap/api/rust/src/surface/layer.rs b/snowcap/api/rust/src/surface/layer.rs index f25c7c66a..13b3a4212 100644 --- a/snowcap/api/rust/src/surface/layer.rs +++ b/snowcap/api/rust/src/surface/layer.rs @@ -8,7 +8,7 @@ use snowcap_api_defs::snowcap::{ self, v1::{ CloseRequest, GetLayerEventsRequest, NewLayerRequest, OperateLayerRequest, - UpdateLayerRequest, ViewRequest, + UpdateLayerRequest, ViewRequest, layer_event, }, }, widget::v1::{GetWidgetEventsRequest, get_widget_events_request}, @@ -23,7 +23,7 @@ use crate::{ client::Client, input::{KeyEvent, Modifiers}, popup::{self, AsParent}, - surface::SurfaceEvent, + surface::{SurfaceEvent, SurfaceMessage}, widget::{self, Program, WidgetDef, WidgetId, WidgetMessage, operation, signal}, }; @@ -136,7 +136,9 @@ impl TryFrom for SurfaceEvent { fn try_from(value: layer::v1::layer_event::Event) -> Result { use layer::v1::layer_event::{Event, Focus}; - let Event::Focus(f) = value; + let Event::Focus(f) = value else { + return Err(LayerEventError::Unknown); + }; match Focus::try_from(f) { Ok(Focus::Gained) => Ok(Self::FocusGained), @@ -210,7 +212,7 @@ where .block_on_tokio()? .into_inner(); - let (msg_send, mut msg_recv) = tokio::sync::mpsc::unbounded_channel::>(); + let (msg_send, mut msg_recv) = tokio::sync::mpsc::unbounded_channel::>(); let handle = LayerHandle { id: layer_id.into(), @@ -222,7 +224,7 @@ where let msg_send = msg_send.clone(); move |msg: signal::Message| { - if let Err(err) = msg_send.send(Some(msg.into_inner())) { + if let Err(err) = msg_send.send(SurfaceMessage::Message(msg.into_inner())) { error!("Failed to send emitted msg: {err}"); crate::signal::HandlerPolicy::Discard } else { @@ -235,7 +237,7 @@ where let msg_send = msg_send.clone(); move |_: signal::RedrawNeeded| { - if let Err(err) = msg_send.send(None) { + if let Err(err) = msg_send.send(SurfaceMessage::Redraw) { error!("Failed to send redraw signal: {err}"); crate::signal::HandlerPolicy::Discard } else { @@ -268,7 +270,9 @@ where }); tokio::spawn(async move { - loop { + let mut pending_operations = Vec::new(); + + 'main_loop: loop { tokio::select! { Some(Ok(response)) = widget_event_stream.next() => { for widget_event in response.widget_events { @@ -280,8 +284,14 @@ where } } Some(msg) = msg_recv.recv() => { - if let Some(msg) = msg { - program.update(msg); + match msg { + SurfaceMessage::Message(msg) => { + program.update(msg); + } + SurfaceMessage::Operation(operation) => { + pending_operations.push(operation); + } + _ => {} } if let Err(status) = Client::layer() @@ -295,6 +305,10 @@ where } Some(Ok(response)) = layer_event_stream.next() => { for layer_event in response.layer_events { + if matches!(layer_event.event, Some(layer_event::Event::Closing(_))) { + break 'main_loop; + } + let Some(event) = layer_event .event .and_then(|e| e.try_into().ok()) else { @@ -335,6 +349,19 @@ where }) .await .unwrap(); + + let operations = std::mem::take(&mut pending_operations); + for operation in operations.into_iter() { + if let Err(status) = Client::layer() + .operate_layer(OperateLayerRequest { + layer_id, + operation: Some(operation.into()), + }) + .await + { + error!("Failed to send operation to {layer_id}: {status}"); + } + } } program.event(SurfaceEvent::Closing); @@ -346,7 +373,7 @@ where /// A handle to a layer surface. pub struct LayerHandle { id: WidgetId, - msg_sender: UnboundedSender>, + msg_sender: UnboundedSender>, } impl Clone for LayerHandle { @@ -444,27 +471,19 @@ impl LayerHandle { /// Sends a message to this Layer [`Program`]. pub fn send_message(&self, message: Msg) { - let _ = self.msg_sender.send(Some(message)); + let _ = self.msg_sender.send(SurfaceMessage::Message(message)); } /// Forces this layer to redraw. pub fn force_redraw(&self) { - let _ = self.msg_sender.send(None); + let _ = self.msg_sender.send(SurfaceMessage::Redraw); } /// Sends an [`Operation`] to this Layer. /// /// [`Operation`]: widget::operation::Operation pub fn operate(&self, operation: widget::operation::Operation) { - if let Err(status) = Client::layer() - .operate_layer(OperateLayerRequest { - layer_id: self.id.to_inner(), - operation: Some(operation.into()), - }) - .block_on_tokio() - { - error!("Failed to send operation to {self:?}: {status}"); - } + let _ = self.msg_sender.send(SurfaceMessage::Operation(operation)); } } diff --git a/snowcap/api/rust/src/surface/popup.rs b/snowcap/api/rust/src/surface/popup.rs index 26868e1e2..4f433fe0c 100644 --- a/snowcap/api/rust/src/surface/popup.rs +++ b/snowcap/api/rust/src/surface/popup.rs @@ -9,7 +9,7 @@ use snowcap_api_defs::snowcap::{ self, v1::{ CloseRequest, GetPopupEventsRequest, NewPopupRequest, OperatePopupRequest, - UpdatePopupRequest, ViewRequest, + UpdatePopupRequest, ViewRequest, popup_event, }, }, widget::v1::{GetWidgetEventsRequest, get_widget_events_request}, @@ -23,7 +23,7 @@ use crate::{ BlockOnTokio, client::Client, input::{KeyEvent, Modifiers}, - surface::SurfaceEvent, + surface::{SurfaceEvent, SurfaceMessage}, widget::{ self, Program, WidgetDef, WidgetId, WidgetMessage, operation::{self, Operation}, @@ -162,7 +162,9 @@ impl TryFrom for SurfaceEvent { fn try_from(value: popup::v1::popup_event::Event) -> Result { use popup::v1::popup_event::{Event, Focus}; - let Event::Focus(f) = value; + let Event::Focus(f) = value else { + return Err(PopupEventError::Unknown); + }; match Focus::try_from(f) { Ok(Focus::Gained) => Ok(Self::FocusGained), @@ -247,7 +249,7 @@ where .block_on_tokio()? .into_inner(); - let (msg_send, mut msg_recv) = tokio::sync::mpsc::unbounded_channel::>(); + let (msg_send, mut msg_recv) = tokio::sync::mpsc::unbounded_channel::>(); let handle = PopupHandle { id: popup_id.into(), @@ -259,7 +261,7 @@ where let msg_send = msg_send.clone(); move |msg: signal::Message| { - if let Err(err) = msg_send.send(Some(msg.into_inner())) { + if let Err(err) = msg_send.send(SurfaceMessage::Message(msg.into_inner())) { error!("Failed to send emitted msg: {err}"); crate::signal::HandlerPolicy::Discard } else { @@ -272,7 +274,7 @@ where let msg_send = msg_send.clone(); move |_: signal::RedrawNeeded| { - if let Err(err) = msg_send.send(None) { + if let Err(err) = msg_send.send(SurfaceMessage::Redraw) { error!("Failed to send redraw signal: {err}"); crate::signal::HandlerPolicy::Discard } else { @@ -305,7 +307,9 @@ where }); tokio::spawn(async move { - loop { + let mut pending_operations = Vec::new(); + + 'main_loop: loop { tokio::select! { Some(Ok(response)) = widget_event_stream.next() => { for widget_event in response.widget_events { @@ -317,8 +321,14 @@ where } } Some(msg) = msg_recv.recv() => { - if let Some(msg) = msg { - program.update(msg); + match msg { + SurfaceMessage::Message(msg) => { + program.update(msg); + }, + SurfaceMessage::Operation(operation) => { + pending_operations.push(operation); + }, + _ => {} } if let Err(status) = Client::popup() @@ -332,6 +342,10 @@ where } Some(Ok(response)) = popup_event_stream.next() => { for popup_event in response.popup_events { + if matches!(popup_event.event, Some(popup_event::Event::Closing(_))) { + break 'main_loop; + }; + let Some(event) = popup_event.event .and_then(|e| e.try_into().ok()) else { continue; @@ -368,6 +382,19 @@ where }) .await .unwrap(); + + let operations = std::mem::take(&mut pending_operations); + for operation in operations.into_iter() { + if let Err(status) = Client::popup() + .operate_popup(OperatePopupRequest { + popup_id, + operation: Some(operation.into()), + }) + .await + { + tracing::error!("Failed to send operation to {popup_id:?}: {status}"); + } + } } program.event(SurfaceEvent::Closing); @@ -379,7 +406,7 @@ where /// A handle to a popup surface. pub struct PopupHandle { id: WidgetId, - msg_sender: UnboundedSender>, + msg_sender: UnboundedSender>, } impl Clone for PopupHandle { @@ -466,25 +493,17 @@ impl PopupHandle { /// /// [`Operation`]: widget::operation::Operation pub fn operate(&self, 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}"); - } + let _ = self.msg_sender.send(SurfaceMessage::Operation(operation)); } /// Sends a message to this Popup [`Program`]. pub fn send_message(&self, message: Msg) { - let _ = self.msg_sender.send(Some(message)); + let _ = self.msg_sender.send(SurfaceMessage::Message(message)); } /// Forces this popup to redraw. pub fn force_redraw(&self) { - let _ = self.msg_sender.send(None); + let _ = self.msg_sender.send(SurfaceMessage::Redraw); } } diff --git a/snowcap/src/api/decoration/v1.rs b/snowcap/src/api/decoration/v1.rs index 6bf837ae6..dddd74918 100644 --- a/snowcap/src/api/decoration/v1.rs +++ b/snowcap/src/api/decoration/v1.rs @@ -1,20 +1,23 @@ use anyhow::Context; use snowcap_api_defs::snowcap::decoration::v1::{ - CloseRequest, CloseResponse, NewDecorationRequest, NewDecorationResponse, - OperateDecorationRequest, OperateDecorationResponse, UpdateDecorationRequest, - UpdateDecorationResponse, ViewRequest, ViewResponse, decoration_service_server, + CloseRequest, CloseResponse, GetDecorationEventsRequest, GetDecorationEventsResponse, + NewDecorationRequest, NewDecorationResponse, OperateDecorationRequest, + OperateDecorationResponse, UpdateDecorationRequest, UpdateDecorationResponse, ViewRequest, + ViewResponse, decoration_service_server, }; use tonic::{Request, Response, Status}; use tracing::warn; use crate::{ - api::{run_unary, widget::v1::widget_def_to_fn}, - decoration::{DecorationId, SnowcapDecoration}, + api::{ResponseStream, run_server_streaming_mapped, run_unary, widget::v1::widget_def_to_fn}, + decoration::{DecorationEvent, DecorationId, SnowcapDecoration}, util::convert::TryFromApi, }; #[tonic::async_trait] impl decoration_service_server::DecorationService for super::DecorationService { + type GetDecorationEventsStream = ResponseStream; + async fn new_decoration( &self, request: Request, @@ -169,6 +172,29 @@ impl decoration_service_server::DecorationService for super::DecorationService { .await } + async fn get_decoration_events( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let id = request.decoration_id; + + run_server_streaming_mapped( + &self.sender, + move |state, sender| { + if let Some(decoration) = state.decoration_for_id(DecorationId(id)) { + decoration.decoration_event_sender = Some(sender); + } + }, + move |events| { + Ok(GetDecorationEventsResponse { + decoration_events: events.into_iter().map(Into::into).collect(), + }) + }, + ) + } + async fn request_view( &self, request: Request, @@ -195,3 +221,17 @@ impl decoration_service_server::DecorationService for super::DecorationService { .await } } + +impl From for snowcap_api_defs::snowcap::decoration::v1::DecorationEvent { + fn from(value: DecorationEvent) -> Self { + use snowcap_api_defs::snowcap::decoration::v1::decoration_event; + + match value { + DecorationEvent::Closing => Self { + event: Some(decoration_event::Event::Closing( + decoration_event::Closing {}, + )), + }, + } + } +} diff --git a/snowcap/src/api/layer/v1.rs b/snowcap/src/api/layer/v1.rs index a921932aa..601ede5e6 100644 --- a/snowcap/src/api/layer/v1.rs +++ b/snowcap/src/api/layer/v1.rs @@ -273,15 +273,16 @@ impl From for snowcap_api_defs::snowcap::layer::v1::LayerEvent { use crate::handlers::keyboard::KeyboardFocusEvent; use snowcap_api_defs::snowcap::layer::v1::layer_event::{self, Focus}; - let LayerEvent::Focus(f) = value; - - match f { - KeyboardFocusEvent::FocusGained => Self { + match value { + LayerEvent::Focus(KeyboardFocusEvent::FocusGained) => Self { event: Some(layer_event::Event::Focus(Focus::Gained.into())), }, - KeyboardFocusEvent::FocusLost => Self { + LayerEvent::Focus(KeyboardFocusEvent::FocusLost) => Self { event: Some(layer_event::Event::Focus(Focus::Lost.into())), }, + LayerEvent::Closing => Self { + event: Some(layer_event::Event::Closing(layer_event::Closing {})), + }, } } } diff --git a/snowcap/src/api/popup/v1.rs b/snowcap/src/api/popup/v1.rs index d299f8e53..f63eccc77 100644 --- a/snowcap/src/api/popup/v1.rs +++ b/snowcap/src/api/popup/v1.rs @@ -455,15 +455,16 @@ impl From for snowcap_api_defs::snowcap::popup::v1::PopupEvent { use crate::handlers::keyboard::KeyboardFocusEvent; use snowcap_api_defs::snowcap::popup::v1::popup_event::{self, Focus}; - let PopupEvent::Focus(f) = value; - - match f { - KeyboardFocusEvent::FocusGained => Self { + match value { + PopupEvent::Focus(KeyboardFocusEvent::FocusGained) => Self { event: Some(popup_event::Event::Focus(Focus::Gained.into())), }, - KeyboardFocusEvent::FocusLost => Self { + PopupEvent::Focus(KeyboardFocusEvent::FocusLost) => Self { event: Some(popup_event::Event::Focus(Focus::Lost.into())), }, + PopupEvent::Closing => Self { + event: Some(popup_event::Event::Closing(popup_event::Closing {})), + }, } } } diff --git a/snowcap/src/decoration.rs b/snowcap/src/decoration.rs index 68d9bc5d1..9938b30e2 100644 --- a/snowcap/src/decoration.rs +++ b/snowcap/src/decoration.rs @@ -3,6 +3,7 @@ use smithay_client_toolkit::reexports::{ protocols::ext::foreign_toplevel_list::v1::client::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, }; use snowcap_protocols::snowcap_decoration_v1::client::snowcap_decoration_surface_v1::SnowcapDecorationSurfaceV1; +use tokio::sync::mpsc::UnboundedSender; use crate::{popup::ParentId, state::State, surface::SnowcapSurface, widget::ViewFn}; @@ -21,6 +22,11 @@ impl DecorationIdCounter { } } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum DecorationEvent { + Closing, +} + impl State { pub fn decoration_for_id(&mut self, id: DecorationId) -> Option<&mut SnowcapDecoration> { self.decorations @@ -66,11 +72,16 @@ pub struct SnowcapDecoration { bounds: Bounds, pending_bounds: Option, pending_z_index: Option, + + pub decoration_event_sender: Option>>, } impl Drop for SnowcapDecoration { fn drop(&mut self) { self.decoration.destroy(); + if let Some(sender) = self.decoration_event_sender.as_ref() { + let _ = sender.send(vec![DecorationEvent::Closing]); + } } } @@ -146,6 +157,7 @@ impl SnowcapDecoration { bounds, pending_bounds: None, pending_z_index: None, + decoration_event_sender: None, }) } diff --git a/snowcap/src/layer.rs b/snowcap/src/layer.rs index 86a7c6a30..8208bc593 100644 --- a/snowcap/src/layer.rs +++ b/snowcap/src/layer.rs @@ -38,6 +38,7 @@ impl LayerIdCounter { #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum LayerEvent { Focus(KeyboardFocusEvent), + Closing, } impl State { @@ -260,6 +261,14 @@ impl SnowcapLayer { } } +impl Drop for SnowcapLayer { + fn drop(&mut self) { + if let Some(sender) = self.layer_event_sender.as_ref() { + let _ = sender.send(vec![LayerEvent::Closing]); + } + } +} + impl From for LayerEvent { fn from(value: KeyboardFocusEvent) -> Self { Self::Focus(value) diff --git a/snowcap/src/popup.rs b/snowcap/src/popup.rs index 2795862f6..c51fc0569 100644 --- a/snowcap/src/popup.rs +++ b/snowcap/src/popup.rs @@ -37,6 +37,7 @@ impl PopupIdCounter { pub enum PopupEvent { Focus(KeyboardFocusEvent), + Closing, } #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] @@ -629,6 +630,14 @@ impl SnowcapPopup { } } +impl Drop for SnowcapPopup { + fn drop(&mut self) { + if let Some(sender) = self.popup_event_sender.as_ref() { + let _ = sender.send(vec![PopupEvent::Closing]); + } + } +} + impl From for PopupEvent { fn from(value: KeyboardFocusEvent) -> Self { Self::Focus(value) diff --git a/src/api/layout/v1.rs b/src/api/layout/v1.rs index f7e56c728..5dac4ac44 100644 --- a/src/api/layout/v1.rs +++ b/src/api/layout/v1.rs @@ -113,7 +113,7 @@ impl TryFrom for crate::layout::tree::LayoutNode { top: taffy::LengthPercentageAuto::length(gaps.top), bottom: taffy::LengthPercentageAuto::length(gaps.bottom), }) - .unwrap_or(taffy::Rect::length(0.0)), + .unwrap_or(taffy::Rect::length(0.0_f32)), ..Default::default() }; diff --git a/src/layout/tree.rs b/src/layout/tree.rs index 8bb50d664..74b6f94a1 100644 --- a/src/layout/tree.rs +++ b/src/layout/tree.rs @@ -156,7 +156,7 @@ impl LayoutTree { if children.is_empty() { let mut new_node_style = tree.style(node).unwrap().clone(); let prev_margin = new_node_style.margin; - new_node_style.margin = taffy::Rect::length(0.0); + new_node_style.margin = taffy::Rect::length(0.0_f32); tree.set_style(node, new_node_style).unwrap(); let leaf_child = tree diff --git a/src/util/treediff/zs.rs b/src/util/treediff/zs.rs index 1f318afbe..780b60160 100644 --- a/src/util/treediff/zs.rs +++ b/src/util/treediff/zs.rs @@ -179,16 +179,14 @@ impl<'a, T> ZsTree<'a, T> { kr: Vec::new(), }; - let mut idx = 1; let mut tmp_data = HashMap::new(); - for n in tree.traverse_post_order() { + for (idx, n) in (1..).zip(tree.traverse_post_order()) { tmp_data.insert(n, idx); this.set_i_tree(idx, n); this.set_lld(idx, *tmp_data.get(&get_first_leaf(n)).unwrap()); if n.children().next().is_none() { this.leaf_count += 1; } - idx += 1; } this.set_keyroots();