diff --git a/api/lua/pinnacle/grpc/defs.lua b/api/lua/pinnacle/grpc/defs.lua index c8e3c505b..74f0640ca 100644 --- a/api/lua/pinnacle/grpc/defs.lua +++ b/api/lua/pinnacle/grpc/defs.lua @@ -627,6 +627,12 @@ local pinnacle_window_v1_DecorationMode = { DECORATION_MODE_SERVER_SIDE = 2, } +---@enum pinnacle.window.v1.TrySetFocusedResponse.TrySetFocusedStatus +local pinnacle_window_v1_TrySetFocusedResponse_TrySetFocusedStatus = { + TRY_SET_FOCUSED_STATUS_SUCCESS = 0, + TRY_SET_FOCUSED_STATUS_WINDOW_MINIMIZED = 1, +} + ---@enum pinnacle.signal.v1.StreamControl local pinnacle_signal_v1_StreamControl = { STREAM_CONTROL_UNSPECIFIED = 0, @@ -1087,6 +1093,12 @@ local pinnacle_v1_Backend = { ---@class pinnacle.window.v1.GetLayoutModeResponse ---@field layout_mode pinnacle.window.v1.LayoutMode? +---@class pinnacle.window.v1.GetMinimizedRequest +---@field window_id integer? + +---@class pinnacle.window.v1.GetMinimizedResponse +---@field minimized boolean? + ---@class pinnacle.window.v1.GetTagIdsRequest ---@field window_id integer? @@ -1131,6 +1143,12 @@ local pinnacle_v1_Backend = { ---@field window_id integer? ---@field set_or_toggle pinnacle.util.v1.SetOrToggle? +---@class pinnacle.window.v1.SetMinimizedRequest +---@field window_id integer? +---@field set_or_toggle pinnacle.util.v1.SetOrToggle? + +---@class pinnacle.window.v1.SetMinimizedResponse + ---@class pinnacle.window.v1.SetFloatingRequest ---@field window_id integer? ---@field set_or_toggle pinnacle.util.v1.SetOrToggle? @@ -1139,6 +1157,13 @@ local pinnacle_v1_Backend = { ---@field window_id integer? ---@field set_or_toggle pinnacle.util.v1.SetOrToggle? +---@class pinnacle.window.v1.TrySetFocusedRequest +---@field window_id integer? +---@field set_or_toggle pinnacle.util.v1.SetOrToggle? + +---@class pinnacle.window.v1.TrySetFocusedResponse +---@field status pinnacle.window.v1.TrySetFocusedResponse.TrySetFocusedStatus? + ---@class pinnacle.window.v1.SetDecorationModeRequest ---@field window_id integer? ---@field decoration_mode pinnacle.window.v1.DecorationMode? @@ -1535,6 +1560,8 @@ pinnacle.window.v1.GetFocusedRequest = {} pinnacle.window.v1.GetFocusedResponse = {} pinnacle.window.v1.GetLayoutModeRequest = {} pinnacle.window.v1.GetLayoutModeResponse = {} +pinnacle.window.v1.GetMinimizedRequest = {} +pinnacle.window.v1.GetMinimizedResponse = {} pinnacle.window.v1.GetTagIdsRequest = {} pinnacle.window.v1.GetTagIdsResponse = {} pinnacle.window.v1.GetWindowsInDirRequest = {} @@ -1546,8 +1573,12 @@ pinnacle.window.v1.SetGeometryRequest = {} pinnacle.window.v1.ResizeTileRequest = {} pinnacle.window.v1.SetFullscreenRequest = {} pinnacle.window.v1.SetMaximizedRequest = {} +pinnacle.window.v1.SetMinimizedRequest = {} +pinnacle.window.v1.SetMinimizedResponse = {} pinnacle.window.v1.SetFloatingRequest = {} pinnacle.window.v1.SetFocusedRequest = {} +pinnacle.window.v1.TrySetFocusedRequest = {} +pinnacle.window.v1.TrySetFocusedResponse = {} pinnacle.window.v1.SetDecorationModeRequest = {} pinnacle.window.v1.MoveToTagRequest = {} pinnacle.window.v1.SetTagRequest = {} @@ -1654,6 +1685,7 @@ pinnacle.output.v1.Vrr = pinnacle_output_v1_Vrr pinnacle.render.v1.Filter = pinnacle_render_v1_Filter pinnacle.window.v1.LayoutMode = pinnacle_window_v1_LayoutMode pinnacle.window.v1.DecorationMode = pinnacle_window_v1_DecorationMode +pinnacle.window.v1.TrySetFocusedResponse.TrySetFocusedStatus = pinnacle_window_v1_TrySetFocusedResponse_TrySetFocusedStatus pinnacle.signal.v1.StreamControl = pinnacle_signal_v1_StreamControl pinnacle.v1.Backend = pinnacle_v1_Backend @@ -2677,6 +2709,23 @@ pinnacle.window.v1.WindowService.GetLayoutMode.response = ".pinnacle.window.v1.G function Client:pinnacle_window_v1_WindowService_GetLayoutMode(data) return self:unary_request(pinnacle.window.v1.WindowService.GetLayoutMode, data) end +pinnacle.window.v1.WindowService.GetMinimized = {} +pinnacle.window.v1.WindowService.GetMinimized.service = "pinnacle.window.v1.WindowService" +pinnacle.window.v1.WindowService.GetMinimized.method = "GetMinimized" +pinnacle.window.v1.WindowService.GetMinimized.request = ".pinnacle.window.v1.GetMinimizedRequest" +pinnacle.window.v1.WindowService.GetMinimized.response = ".pinnacle.window.v1.GetMinimizedResponse" + +---Performs a unary request. +--- +---@nodiscard +--- +---@param data pinnacle.window.v1.GetMinimizedRequest +--- +---@return pinnacle.window.v1.GetMinimizedResponse | nil response +---@return string | nil error An error string, if any +function Client:pinnacle_window_v1_WindowService_GetMinimized(data) + return self:unary_request(pinnacle.window.v1.WindowService.GetMinimized, data) +end pinnacle.window.v1.WindowService.GetTagIds = {} pinnacle.window.v1.WindowService.GetTagIds.service = "pinnacle.window.v1.WindowService" pinnacle.window.v1.WindowService.GetTagIds.method = "GetTagIds" @@ -2813,6 +2862,23 @@ pinnacle.window.v1.WindowService.SetMaximized.response = ".google.protobuf.Empty function Client:pinnacle_window_v1_WindowService_SetMaximized(data) return self:unary_request(pinnacle.window.v1.WindowService.SetMaximized, data) end +pinnacle.window.v1.WindowService.SetMinimized = {} +pinnacle.window.v1.WindowService.SetMinimized.service = "pinnacle.window.v1.WindowService" +pinnacle.window.v1.WindowService.SetMinimized.method = "SetMinimized" +pinnacle.window.v1.WindowService.SetMinimized.request = ".pinnacle.window.v1.SetMinimizedRequest" +pinnacle.window.v1.WindowService.SetMinimized.response = ".pinnacle.window.v1.SetMinimizedResponse" + +---Performs a unary request. +--- +---@nodiscard +--- +---@param data pinnacle.window.v1.SetMinimizedRequest +--- +---@return pinnacle.window.v1.SetMinimizedResponse | nil response +---@return string | nil error An error string, if any +function Client:pinnacle_window_v1_WindowService_SetMinimized(data) + return self:unary_request(pinnacle.window.v1.WindowService.SetMinimized, data) +end pinnacle.window.v1.WindowService.SetFloating = {} pinnacle.window.v1.WindowService.SetFloating.service = "pinnacle.window.v1.WindowService" pinnacle.window.v1.WindowService.SetFloating.method = "SetFloating" @@ -2847,6 +2913,23 @@ pinnacle.window.v1.WindowService.SetFocused.response = ".google.protobuf.Empty" function Client:pinnacle_window_v1_WindowService_SetFocused(data) return self:unary_request(pinnacle.window.v1.WindowService.SetFocused, data) end +pinnacle.window.v1.WindowService.TrySetFocused = {} +pinnacle.window.v1.WindowService.TrySetFocused.service = "pinnacle.window.v1.WindowService" +pinnacle.window.v1.WindowService.TrySetFocused.method = "TrySetFocused" +pinnacle.window.v1.WindowService.TrySetFocused.request = ".pinnacle.window.v1.TrySetFocusedRequest" +pinnacle.window.v1.WindowService.TrySetFocused.response = ".pinnacle.window.v1.TrySetFocusedResponse" + +---Performs a unary request. +--- +---@nodiscard +--- +---@param data pinnacle.window.v1.TrySetFocusedRequest +--- +---@return pinnacle.window.v1.TrySetFocusedResponse | nil response +---@return string | nil error An error string, if any +function Client:pinnacle_window_v1_WindowService_TrySetFocused(data) + return self:unary_request(pinnacle.window.v1.WindowService.TrySetFocused, data) +end pinnacle.window.v1.WindowService.SetDecorationMode = {} pinnacle.window.v1.WindowService.SetDecorationMode.service = "pinnacle.window.v1.WindowService" pinnacle.window.v1.WindowService.SetDecorationMode.method = "SetDecorationMode" diff --git a/api/lua/pinnacle/window.lua b/api/lua/pinnacle/window.lua index 68e2dfc3c..b2edf23bc 100644 --- a/api/lua/pinnacle/window.lua +++ b/api/lua/pinnacle/window.lua @@ -401,6 +401,32 @@ function WindowHandle:toggle_maximized() end end +--- Set this window to be minimized or not minimized. +--- @param minimized boolean +function WindowHandle:set_minimized(minimized) + local _, err = client:pinnacle_window_v1_WindowService_SetMinimized({ + window_id = self.id, + set_or_toggle = set_or_toggle[minimized] + }) + + if err then + log.error(err) + end +end + + +--- Toggles this window between minimized and not. +function WindowHandle:toggle_minimized() + local _, err = client:pinnacle_window_v1_WindowService_SetMinimized({ + window_id = self.id, + set_or_toggle = set_or_toggle.TOGGLE + }) + + if err then + log.error(err) + end +end + ---Sets this window to floating or not. --- ---@param floating boolean @@ -428,11 +454,23 @@ function WindowHandle:toggle_floating() end end + +--- @alias pinnacle.window.TrySetFocusedError +--- | "WindowMinimized" +local try_set_focused_error = { + WindowMinimized = window_v1.TrySetFocusedResponse.TrySetFocusedStatus.TRY_SET_FOCUSED_STATUS_WINDOW_MINIMIZED +} + +require("pinnacle.util").make_bijective(try_set_focused_error) + ---Focuses or unfocuses this window. --- +---@deprecated silently fails if trying to focus a minimized window, use `WindowHandle:try_set_focused` ---@param focused boolean +---@see pinnacle.window.WindowHandle.try_set_focused replacement function. function WindowHandle:set_focused(focused) - local _, err = client:pinnacle_window_v1_WindowService_SetFocused({ + log.warn("Using deprecated function WindowHandle:set_focused(), which silently fails if focusing a minimized window.") + local _, err = client:pinnacle_window_v1_WindowService_TrySetFocused({ window_id = self.id, set_or_toggle = set_or_toggle[focused], }) @@ -442,10 +480,34 @@ function WindowHandle:set_focused(focused) end end + +--- @class pinnacle.window.TrySetFocusedResult +--- @field err? pinnacle.window.TrySetFocusedError +--- +--- Tries to focus or unfocus this window +--- @param focused boolean +--- @return pinnacle.window.TrySetFocusedResult +function WindowHandle:try_set_focused(focused) + local response, err = client:pinnacle_window_v1_WindowService_TrySetFocused({ + window_id = self.id, + set_or_toggle = set_or_toggle[focused] + }) + + if err then + log.error(err) + end + assert(response) + + return { err = try_set_focused_error[response.status] } +end + ---Toggles this window to and from focused. --- +--- @deprecated silently fails if trying to focus a minimized window, use `WindowHandle:try_toggle_focused`. +--- @see pinnacle.window.WindowHandle.try_toggle_focused replacement function. function WindowHandle:toggle_focused() - local _, err = client:pinnacle_window_v1_WindowService_SetFocused({ + log.warn("Using deprecated function WindowHandle:toggle_focused(), which silently fails if focusing a minimized window.") + local _, err = client:pinnacle_window_v1_WindowService_TrySetFocused({ window_id = self.id, set_or_toggle = set_or_toggle.TOGGLE, }) @@ -455,6 +517,25 @@ function WindowHandle:toggle_focused() end end +--- @class pinnacle.window.TryToggleFocusedResult +--- @field err? pinnacle.window.TrySetFocusedError +--- +--- Tries to toggle this window to and from focused, failing if the window is minimized and it is trying to be focused. +--- @return pinnacle.window.TrySetFocusedResult +function WindowHandle:try_toggle_focused() + local response, err = client:pinnacle_window_v1_WindowService_TrySetFocused({ + window_id = self.id, + set_or_toggle = set_or_toggle.TOGGLE, + }) + + if err then + log.error(err) + end + assert(response) + + return { err = try_set_focused_error[response.status] } +end + ---Sets this window's decoration mode. --- ---If not set, the client is allowed to choose its decoration mode, defaulting to client-side if it doesn't. @@ -758,6 +839,16 @@ function WindowHandle:maximized() return response and response.layout_mode == layout_mode_def.LAYOUT_MODE_MAXIMIZED or false end +--- Gets whether this window is minimized. +--- +--- @return boolean +function WindowHandle:minimized() + local response, err = + client:pinnacle_window_v1_WindowService_GetMinimized({ window_id = self.id }) + + return response and response.minimized or false +end + ---Gets all tags on this window. --- ---@return pinnacle.tag.TagHandle[] diff --git a/api/protobuf/pinnacle/window/v1/window.proto b/api/protobuf/pinnacle/window/v1/window.proto index 67cbd4ed4..72bc4ab69 100644 --- a/api/protobuf/pinnacle/window/v1/window.proto +++ b/api/protobuf/pinnacle/window/v1/window.proto @@ -62,6 +62,13 @@ message GetLayoutModeResponse { LayoutMode layout_mode = 1; } +message GetMinimizedRequest { + uint32 window_id = 1; +} +message GetMinimizedResponse { + bool minimized = 1; +} + message GetTagIdsRequest { uint32 window_id = 1; } @@ -116,16 +123,39 @@ message SetMaximizedRequest { pinnacle.util.v1.SetOrToggle set_or_toggle = 2; } +message SetMinimizedRequest { + uint32 window_id = 1; + pinnacle.util.v1.SetOrToggle set_or_toggle = 2; +} + +message SetMinimizedResponse {} + message SetFloatingRequest { uint32 window_id = 1; pinnacle.util.v1.SetOrToggle set_or_toggle = 2; } message SetFocusedRequest { + // TrySetFocusedRequest is for the new RPC. + option deprecated = true; uint32 window_id = 1; pinnacle.util.v1.SetOrToggle set_or_toggle = 2; } +message TrySetFocusedRequest { + uint32 window_id = 1; + pinnacle.util.v1.SetOrToggle set_or_toggle = 2; +} + +message TrySetFocusedResponse { + enum TrySetFocusedStatus { + TRY_SET_FOCUSED_STATUS_SUCCESS = 0; + TRY_SET_FOCUSED_STATUS_WINDOW_MINIMIZED = 1; + } + + TrySetFocusedStatus status = 1; +} + enum DecorationMode { DECORATION_MODE_UNSPECIFIED = 0; DECORATION_MODE_CLIENT_SIDE = 1; @@ -222,6 +252,7 @@ service WindowService { rpc GetSize(GetSizeRequest) returns (GetSizeResponse); rpc GetFocused(GetFocusedRequest) returns (GetFocusedResponse); rpc GetLayoutMode(GetLayoutModeRequest) returns (GetLayoutModeResponse); + rpc GetMinimized(GetMinimizedRequest) returns (GetMinimizedResponse); rpc GetTagIds(GetTagIdsRequest) returns (GetTagIdsResponse); rpc GetWindowsInDir(GetWindowsInDirRequest) returns (GetWindowsInDirResponse); rpc GetForeignToplevelListIdentifier(GetForeignToplevelListIdentifierRequest) returns (GetForeignToplevelListIdentifierResponse); @@ -231,8 +262,13 @@ service WindowService { rpc ResizeTile(ResizeTileRequest) returns (google.protobuf.Empty); rpc SetFullscreen(SetFullscreenRequest) returns (google.protobuf.Empty); rpc SetMaximized(SetMaximizedRequest) returns (google.protobuf.Empty); - rpc SetFloating(SetFloatingRequest) returns (google.protobuf.Empty); - rpc SetFocused(SetFocusedRequest) returns (google.protobuf.Empty); + rpc SetMinimized(SetMinimizedRequest) returns (SetMinimizedResponse); + rpc SetFloating(SetFloatingRequest) returns (google.protobuf.Empty); + rpc SetFocused(SetFocusedRequest) returns (google.protobuf.Empty) { + // use `TrySetFocused` instead. + option deprecated = true; + }; + rpc TrySetFocused(TrySetFocusedRequest) returns (TrySetFocusedResponse); rpc SetDecorationMode(SetDecorationModeRequest) returns (google.protobuf.Empty); rpc MoveToTag(MoveToTagRequest) returns (google.protobuf.Empty); rpc SetTag(SetTagRequest) returns (google.protobuf.Empty); diff --git a/api/rust/examples/default_config/main.rs b/api/rust/examples/default_config/main.rs index bad0efc1b..559e7dbcf 100644 --- a/api/rust/examples/default_config/main.rs +++ b/api/rust/examples/default_config/main.rs @@ -151,6 +151,33 @@ async fn config() { .group("Window") .description("Toggle maximized on the focused window"); + // `mod_key + n` makes minimized + input::keybind(mod_key, 'n') + .on_press(|| { + if let Some(window) = window::get_focused() { + window.set_minimized(true); + } + }) + .group("Window") + .description("Minimize the focused window"); + + // `mon_key + shift + n` unminimises the "most recently focused" + // minimised window that is on the active tags in this current output. + input::keybind(mod_key | Mod::SHIFT, 'n') + .on_press(|| { + let Some(output) = output::get_focused() else { return }; + let Some(most_recently_minimised_active_window) = output + .keyboard_focus_stack_visible() + .filter(|w| w.minimized()) + .last() + else { + return; + }; + most_recently_minimised_active_window.set_minimized(false); + }) + .group("Window") + .description("Unminimize the most recently focused window on the active tags"); + // Media keybinds ------------------------------------------------------ input::keybind(Mod::empty(), Keysym::XF86_AudioRaiseVolume) @@ -462,7 +489,7 @@ async fn config() { // Enable sloppy focus window::connect_signal(WindowSignal::PointerEnter(Box::new(|win| { - win.set_focused(true); + let _ = win.try_set_focused(true); }))); // Focus outputs when the pointer enters them diff --git a/api/rust/src/util.rs b/api/rust/src/util.rs index fe95701de..7c0d0375b 100644 --- a/api/rust/src/util.rs +++ b/api/rust/src/util.rs @@ -12,6 +12,8 @@ use crate::BlockOnTokio; pub use crate::batch_boxed; pub use crate::batch_boxed_async; +pub mod convert; + /// A horizontal or vertical axis. #[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] pub enum Axis { diff --git a/api/rust/src/util/convert.rs b/api/rust/src/util/convert.rs new file mode 100644 index 000000000..ef8513cd3 --- /dev/null +++ b/api/rust/src/util/convert.rs @@ -0,0 +1,103 @@ +// 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/. + +//! Utilities for conversion (primarily from and to API types). + +// CONVERSIONS: Native -> Api + +/// An API type that is constructible from a native type. +pub trait FromNative { + /// Create the API type from the native type. + fn from_native(native_value: NativeType) -> Self; +} + +/// An API type that is possibly constructible from a native type. +pub trait TryFromNative: Sized { + /// Error type for the conversion. + type Error; + + /// Attempt to convert the native type into this API type. + fn try_from_native(native_value: NativeType) -> Result; +} + +/// Native type convertible into some API type +/// +/// Prefer implementing [`FromNative`] on the API type. +pub trait IntoApi { + /// Convert this type into an api type. + fn into_api(self) -> ApiType; +} + +impl> IntoApi for Native { + fn into_api(self) -> Api { + Api::from_native(self) + } +} + +/// Native type possibly convertible into some API type +/// +/// Prefer implementing [`TryFromNative`] +pub trait TryIntoApi { + /// Error type for the conversion. + type Error; + /// Try to convert this type into an API type + fn try_into_api(self) -> Result; +} + +impl> TryIntoApi for Native { + type Error = Api::Error; + + fn try_into_api(self) -> Result { + Api::try_from_native(self) + } +} + +// CONVERSIONS: Api -> Native + +/// A native type that is constructible from an API type. +pub trait FromApi { + /// Create the native type from the api type. + fn from_api(api_value: ApiType) -> Self; +} + +/// An native type that is possibly constructible from an API type. +pub trait TryFromApi: Sized { + /// Error type for the conversion. + type Error; + + /// Attempt to convert the API type into this native type. + fn try_from_api(api_value: ApiType) -> Result; +} + +/// API type convertible into some native type +/// +/// Prefer implementing [`FromApi`] on the native type. +pub trait IntoNative { + /// Convert this API type into a native type. + fn into_native(self) -> NativeType; +} + +impl> IntoNative for Api { + fn into_native(self) -> Native { + Native::from_api(self) + } +} + +/// API type possibly convertible into some native type +/// +/// Prefer implementing [`TryFromApi`] +pub trait TryIntoNative { + /// Error type for the conversion. + type Error; + /// Try to convert this type into a native type + fn try_into_native(self) -> Result; +} + +impl> TryIntoNative for Api { + type Error = Native::Error; + + fn try_into_native(self) -> Result { + Native::try_from_api(self) + } +} diff --git a/api/rust/src/window.rs b/api/rust/src/window.rs index 0719feaf7..179c7ae4f 100644 --- a/api/rust/src/window.rs +++ b/api/rust/src/window.rs @@ -19,12 +19,12 @@ use pinnacle_api_defs::pinnacle::{ self, v1::{ GetAppIdRequest, GetFocusedRequest, GetForeignToplevelListIdentifierRequest, - GetLayoutModeRequest, GetLocRequest, GetSizeRequest, GetTagIdsRequest, GetTitleRequest, - GetWindowsInDirRequest, LowerRequest, MoveGrabRequest, MoveToOutputRequest, - MoveToTagRequest, RaiseRequest, ResizeGrabRequest, ResizeTileRequest, - SetDecorationModeRequest, SetFloatingRequest, SetFocusedRequest, SetFullscreenRequest, - SetGeometryRequest, SetMaximizedRequest, SetTagRequest, SetTagsRequest, - SetVrrDemandRequest, SwapRequest, + GetLayoutModeRequest, GetLocRequest, GetMinimizedRequest, GetSizeRequest, + GetTagIdsRequest, GetTitleRequest, GetWindowsInDirRequest, LowerRequest, + MoveGrabRequest, MoveToOutputRequest, MoveToTagRequest, RaiseRequest, + ResizeGrabRequest, ResizeTileRequest, SetDecorationModeRequest, SetFloatingRequest, + SetFullscreenRequest, SetGeometryRequest, SetMaximizedRequest, SetMinimizedRequest, + SetTagRequest, SetTagsRequest, SetVrrDemandRequest, SwapRequest, TrySetFocusedRequest, }, }, }; @@ -38,7 +38,10 @@ use crate::{ output::OutputHandle, signal::{SignalHandle, WindowSignal}, tag::TagHandle, - util::{Batch, Direction, Point, Size}, + util::{ + Batch, Direction, Point, Size, + convert::{TryFromApi, TryIntoNative}, + }, }; /// Gets handles to all windows. @@ -235,6 +238,27 @@ impl VrrDemand { } } +/// Error when trying to focus/unfocus a window. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TrySetFocusedError { + /// Window was minimized and could not be focused. + WindowMinimized, +} + +impl TryFromApi for () { + type Error = TrySetFocusedError; + + fn try_from_api( + api_value: pinnacle_api_defs::pinnacle::window::v1::TrySetFocusedResponse, + ) -> Result { + use pinnacle_api_defs::pinnacle::window::v1::try_set_focused_response::TrySetFocusedStatus; + match api_value.status() { + TrySetFocusedStatus::Success => Ok(()), + TrySetFocusedStatus::WindowMinimized => Err(TrySetFocusedError::WindowMinimized), + } + } +} + impl WindowHandle { /// Sends a close request to this window. /// @@ -365,6 +389,34 @@ impl WindowHandle { .unwrap(); } + /// Sets this window to minimized or not. + pub fn set_minimized(&self, set: bool) { + let window_id = self.id; + Client::window() + .set_minimized(SetMinimizedRequest { + window_id, + set_or_toggle: match set { + true => SetOrToggle::Set, + false => SetOrToggle::Unset, + } + .into(), + }) + .block_on_tokio() + .unwrap(); + } + + /// Toggles this window between minimized and not. + pub fn toggle_minimized(&self) { + let window_id = self.id; + Client::window() + .set_minimized(SetMinimizedRequest { + window_id, + set_or_toggle: SetOrToggle::Toggle.into(), + }) + .block_on_tokio() + .unwrap(); + } + /// Sets this window to floating or not. /// /// Floating windows will not be tiled and can be moved around and resized freely. @@ -398,10 +450,20 @@ impl WindowHandle { } /// Focuses or unfocuses this window. + /// + /// Silently fails if trying to focus a minimized window. + #[deprecated = "use `WindowHandle::try_set_focused` instead"] pub fn set_focused(&self, set: bool) { + let _ = self.try_set_focused(set); + } + + /// Tries to focus or unfocus this window. + /// + /// Fails if the window is minimized. + pub fn try_set_focused(&self, set: bool) -> Result<(), TrySetFocusedError> { let window_id = self.id; Client::window() - .set_focused(SetFocusedRequest { + .try_set_focused(TrySetFocusedRequest { window_id, set_or_toggle: match set { true => SetOrToggle::Set, @@ -410,19 +472,33 @@ impl WindowHandle { .into(), }) .block_on_tokio() - .unwrap(); + .expect("successful rpc communication is expected") + .into_inner() + .try_into_native() } /// Toggles this window between focused and unfocused. + /// + /// Silently fails if trying to focus a minimized window. + #[deprecated = "use `WindowHandle::try_toggle_focused` instead"] pub fn toggle_focused(&self) { + let _ = self.try_toggle_focused(); + } + + /// Tries to toggle this window between focused and unfocused. + /// + /// Fails if the window is minimized. + pub fn try_toggle_focused(&self) -> Result<(), TrySetFocusedError> { let window_id = self.id; Client::window() - .set_focused(SetFocusedRequest { + .try_set_focused(TrySetFocusedRequest { window_id, set_or_toggle: SetOrToggle::Toggle.into(), }) .block_on_tokio() - .unwrap(); + .expect("successful rpc communication is expected") + .into_inner() + .try_into_native() } /// Sets this window's decoration mode. @@ -807,6 +883,22 @@ impl WindowHandle { self.layout_mode_async().await == LayoutMode::Maximized } + /// Gets whether or not this window is minimized + pub fn minimized(&self) -> bool { + self.minimized_async().block_on_tokio() + } + + /// Async impl for [`Self::minimized`] + pub async fn minimized_async(&self) -> bool { + let window_id = self.id; + Client::window() + .get_minimized(GetMinimizedRequest { window_id }) + .await + .unwrap() + .into_inner() + .minimized + } + /// Gets handles to all tags on this window. pub fn tags(&self) -> impl Iterator + use<> { self.tags_async().block_on_tokio() diff --git a/src/api/window.rs b/src/api/window.rs index a8c035d17..cc2a88df6 100644 --- a/src/api/window.rs +++ b/src/api/window.rs @@ -65,38 +65,122 @@ pub fn set_geometry( ); } -// TODO: minimized +/// Sets or toggles if a window is minimized. +/// +/// Minimized windows are always unfocused. +pub fn set_minimized(state: &mut State, window: &WindowElement, set: impl Into>) { + if window.is_x11_override_redirect() { + return; + } + + let set = set.into(); + let is_minimized = window.with_state(|state| state.minimized); + let set = match set { + Some(absolute_set) => absolute_set, + None => !is_minimized, + }; + window.with_state_mut(|state| state.minimized = set); + + if set && state.pinnacle.keyboard_focus_stack.current_focus() == Some(window) { + state.pinnacle.keyboard_focus_stack.unset_focus(); + } + + // Note: tag moving will automatically adjust the output on the window directly even if + // minimised, so we can rely on this. + let Some(output) = window.output(&state.pinnacle) else { + warn!("adjusted minimization-state of window without an output."); + return; + }; + + // This means we can rely on the output associated with the [`WindowElementState`] even while + // minimized, and we can use it to schedule layouts. + if set != is_minimized { + state.pinnacle.request_layout(&output); + state.schedule_render(&output); + if let Some(x11surface) = window.x11_surface() { + // Error can occur here on connection failure, but window is dead anyway at that point. + let _ = x11surface.set_hidden(set); + } + state.pinnacle.update_xwayland_stacking_order(); + } +} + +/// Error when trying to focus/unfocus a window. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TrySetFocusedError { + /// Window was minimized and could not be focused. + WindowMinimized, +} + +impl core::fmt::Display for TrySetFocusedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("failed to focus window: ")?; + match self { + TrySetFocusedError::WindowMinimized => f.write_str("window was currently minimized"), + } + } +} + +/// Convert the result of [`try_set_focused`] into a protocol-friendly response. +fn try_set_focused_protocol_result( + r: Result<(), TrySetFocusedError>, +) -> pinnacle_api_defs::pinnacle::window::v1::TrySetFocusedResponse { + use pinnacle_api_defs::pinnacle::window::v1::{ + TrySetFocusedResponse, try_set_focused_response::TrySetFocusedStatus, + }; + TrySetFocusedResponse { + status: match r + .map(|_| TrySetFocusedStatus::Success) + .map_err(|e| match e { + TrySetFocusedError::WindowMinimized => TrySetFocusedStatus::WindowMinimized, + }) { + Ok(status) | Err(status) => status.into(), + }, + } +} /// Sets a window to focused or not. /// /// If the window is on another output and an attempt is made to /// focus it, the focused output will change to that output UNLESS /// the window overlaps the currently focused output. -pub fn set_focused(state: &mut State, window: &WindowElement, set: impl Into>) { +/// +/// If the window is being set to be focused, and the window is currently minimized, +/// then an error will be emitted +pub fn try_set_focused( + state: &mut State, + window: &WindowElement, + set: impl Into>, +) -> Result<(), TrySetFocusedError> { if window.is_x11_override_redirect() { - return; + return Ok(()); } let Some(output) = window.output(&state.pinnacle) else { - return; + return Ok(()); }; - let set = set.into(); + let absolute_set = set.into(); let Some(keyboard) = state.pinnacle.seat.get_keyboard() else { - return; + return Ok(()); }; let is_focused = keyboard .current_focus() .is_some_and(|focus| matches!(focus, KeyboardFocusTarget::Window(win) if win == window)); - let set = match set { + let absolute_set = match absolute_set { Some(set) => set, None => !is_focused, }; - if set { + if absolute_set { + if window.with_state(|s| s.minimized) { + warn!("Cannot focus a minimized window"); + return Err(TrySetFocusedError::WindowMinimized); + } + state .pinnacle .keyboard_focus_stack @@ -122,7 +206,7 @@ pub fn set_focused(state: &mut State, window: &WindowElement, set: impl Into, + ) -> TonicResult { + let window_id = WindowId(request.into_inner().window_id); + + run_unary(&self.sender, move |state| { + let minimized = window_id + .window(&state.pinnacle) + .or_else(|| { + window_id + .unmapped_window(&state.pinnacle) + .map(|unmapped| unmapped.window.clone()) + }) + .map(|win| win.with_state(|state| state.minimized)) + .unwrap_or(false); + Ok(GetMinimizedResponse { minimized }) + }) + .await + } + async fn get_tag_ids( &self, request: Request, @@ -479,6 +502,47 @@ impl v1::window_service_server::WindowService for super::WindowService { .await } + async fn set_minimized( + &self, + request: Request, + ) -> TonicResult { + let request = request.into_inner(); + let window_id = WindowId(request.window_id); + let absolute_minimized = match request.set_or_toggle() { + SetOrToggle::Unspecified => { + return Err(Status::invalid_argument("unspecified set or toggle")); + } + SetOrToggle::Set => Some(true), + SetOrToggle::Unset => Some(false), + SetOrToggle::Toggle => None, + }; + + run_unary(&self.sender, move |state| { + if let Some(window) = window_id.window(&state.pinnacle) { + crate::api::window::set_minimized(state, &window, absolute_minimized); + } else if let Some(unmapped) = window_id.unmapped_window_mut(&mut state.pinnacle) + && let UnmappedState::WaitingForRules { rules, .. } = &mut unmapped.state + { + let absolute_minimized = match absolute_minimized { + Some(absolute) => absolute, + // Toggling + None => !rules + .minimized + .unwrap_or(crate::window::rules::WindowRules::default_minimization_state()), + }; + rules.minimized = Some(absolute_minimized); + // If the window is scheduled to focus, we will undo it explicitly to match the + // general minimization behaviour. + if absolute_minimized { + rules.focused = Some(false); + } + }; + + Ok(SetMinimizedResponse {}) + }) + .await + } + async fn set_floating(&self, request: Request) -> TonicResult<()> { let request = request.into_inner(); @@ -534,37 +598,74 @@ impl v1::window_service_server::WindowService for super::WindowService { } async fn set_focused(&self, request: Request) -> TonicResult<()> { - let request = request.into_inner(); + tracing::warn!( + "Call of deprecated focusing mechanism `set_focused`, will silently ignore focus failures due to window minimization - use try_set_focused instead." + ); + self.try_set_focused({ + let (metadata, extensions, message) = request.into_parts(); + let message = TrySetFocusedRequest { + window_id: message.window_id, + set_or_toggle: message.set_or_toggle().into(), + }; + Request::from_parts(metadata, extensions, message) + }) + .await + .map(|response| { + let (metadata, _message, extensions) = response.into_parts(); + // Discard the error information + tonic::Response::from_parts(metadata, (), extensions) + }) + } + async fn try_set_focused( + &self, + request: Request, + ) -> TonicResult { + use super::{TrySetFocusedError, try_set_focused_protocol_result as make_response}; + let request = request.into_inner(); let window_id = WindowId(request.window_id); - - let set_or_toggle = request.set_or_toggle(); - - if set_or_toggle == SetOrToggle::Unspecified { - return Err(Status::invalid_argument("unspecified set or toggle")); - } - - let set = match set_or_toggle { - SetOrToggle::Unspecified => unreachable!(), + let absolute_focus_state = match request.set_or_toggle() { + SetOrToggle::Unspecified => { + return Err(Status::invalid_argument("unspecified set or toggle")); + } SetOrToggle::Set => Some(true), SetOrToggle::Unset => Some(false), SetOrToggle::Toggle => None, }; - run_unary_no_response(&self.sender, move |state| { - if let Some(window) = window_id.window(&state.pinnacle) { - crate::api::window::set_focused(state, &window, set); - } else if let Some(unmapped) = window_id.unmapped_window_mut(&mut state.pinnacle) - && let UnmappedState::WaitingForRules { rules, .. } = &mut unmapped.state - { - match set { - Some(set) => rules.focused = Some(set), - None => { - let focused = rules.focused.get_or_insert(true); - *focused = !*focused; + run_unary(&self.sender, move |state| { + Ok(make_response({ + if let Some(window) = window_id.window(&state.pinnacle) { + crate::api::window::try_set_focused(state, &window, absolute_focus_state) + } else if let Some(unmapped) = window_id.unmapped_window_mut(&mut state.pinnacle) + && let UnmappedState::WaitingForRules { rules, .. } = &mut unmapped.state + { + let minimized = rules + .minimized + .unwrap_or(WindowRules::default_minimization_state()); + match absolute_focus_state { + Some(absolute_focus_state) => { + if minimized && absolute_focus_state { + Err(TrySetFocusedError::WindowMinimized) + } else { + rules.focused = Some(absolute_focus_state); + Ok(()) + } + } + None => { + let focused = rules.focused.get_or_insert(true); + if minimized && !*focused { + Err(TrySetFocusedError::WindowMinimized) + } else { + *focused = !*focused; + Ok(()) + } + } } + } else { + Ok(()) } - } + })) }) .await } diff --git a/src/focus.rs b/src/focus.rs index b880ee1d2..422132496 100644 --- a/src/focus.rs +++ b/src/focus.rs @@ -365,7 +365,7 @@ impl WindowKeyboardFocusStack { /// Gets the currently focused window on this stack. /// /// This is the topmost window that is on an active tag and not - /// an OR window. + /// an OR window, and is not minimized. pub fn current_focus(&self) -> Option<&WindowElement> { if !self.focused { return None; @@ -375,6 +375,7 @@ impl WindowKeyboardFocusStack { .iter() .rev() .filter(|win| win.is_on_active_tag()) + .filter(|win| win.with_state(|state| !state.minimized)) .find(|win| !win.is_x11_override_redirect()) } } diff --git a/src/handlers/foreign_toplevel.rs b/src/handlers/foreign_toplevel.rs index 6ef4331ea..5f23467fd 100644 --- a/src/handlers/foreign_toplevel.rs +++ b/src/handlers/foreign_toplevel.rs @@ -21,6 +21,9 @@ impl ForeignToplevelHandler for State { return; }; + // TODO make a nice `self.pinnacle` function somewhere for this?? IDK though >.< + let was_minimized = window.with_state(|state| state.minimized); + window.with_state_mut(|state| state.minimized = false); self.pinnacle.keyboard_focus_stack.set_focus(window.clone()); self.pinnacle.raise_window(window.clone()); @@ -37,6 +40,11 @@ impl ForeignToplevelHandler for State { crate::api::tag::switch_to(self, &tag); } } else { + // Need to re-layout things if the window was un-minimized. + if was_minimized { + self.pinnacle.update_xwayland_stacking_order(); + self.pinnacle.request_layout(&output); + } self.schedule_render(&output); } } @@ -103,7 +111,6 @@ impl ForeignToplevelHandler for State { }); } - // TODO: fn set_minimized(&mut self, wl_surface: WlSurface) { let _span = tracy_client::span!("ForeignToplevelHandler::set_minimized"); @@ -121,7 +128,6 @@ impl ForeignToplevelHandler for State { self.schedule_render(&output); } - // TODO: fn unset_minimized(&mut self, wl_surface: WlSurface) { let _span = tracy_client::span!("ForeignToplevelHandler::unset_minimized"); diff --git a/src/handlers/xwayland.rs b/src/handlers/xwayland.rs index 3a3c380ba..56f67e3c1 100644 --- a/src/handlers/xwayland.rs +++ b/src/handlers/xwayland.rs @@ -601,7 +601,9 @@ impl Pinnacle { .iter() .filter_map(|z| z.window()) .filter(|win| !win.is_x11_override_redirect()) - .partition::, _>(|win| win.is_on_active_tag()); + .partition::, _>(|win| { + win.is_on_active_tag() && win.with_state(|state| !state.minimized) + }); let active_windows = active_windows.into_iter().flat_map(|win| win.x11_surface()); let non_active_windows = non_active_windows diff --git a/src/layout.rs b/src/layout.rs index 2a8468002..384baca6c 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -57,7 +57,9 @@ impl Pinnacle { .filter(|win| win.output(self).as_ref() == Some(output)) .cloned() .partition::, _>(|win| { - win.with_state(|state| state.tags.intersection(&focused_tags).next().is_some()) + win.with_state(|state| { + state.tags.intersection(&focused_tags).next().is_some() && !state.minimized + }) }) }); @@ -448,7 +450,12 @@ impl State { for win in self.pinnacle.windows.iter() { let is_tiled = win.with_state(|state| state.layout_mode.is_tiled()); let is_on_active_tag = win.is_on_active_tag(); - if !is_tiled && is_on_active_tag && !self.pinnacle.space.elements().any(|w| w == win) { + let is_not_minimized = win.with_state(|state| !state.minimized); + if !is_tiled + && is_not_minimized + && is_on_active_tag + && !self.pinnacle.space.elements().any(|w| w == win) + { wins_to_update.push(win.clone()); } } @@ -479,6 +486,7 @@ impl Pinnacle { .filter(|win| { win.with_state(|state| state.tags.intersection(&focused_tags).next().is_some()) }) + .filter(|win| win.with_state(|state| !state.minimized)) .cloned() .collect::>() }); diff --git a/src/window/rules.rs b/src/window/rules.rs index 7bc8e381c..120cb85c9 100644 --- a/src/window/rules.rs +++ b/src/window/rules.rs @@ -35,6 +35,7 @@ pub struct WindowRuleState { pub struct WindowRules { pub layout_mode: Option, pub focused: Option, + pub minimized: Option, pub floating_x: Option, pub floating_y: Option, pub floating_size: Option>, @@ -42,6 +43,14 @@ pub struct WindowRules { pub tags: Option>, } +impl WindowRules { + /// Get the the "default" minimization state to apply to a window if the rules do not specify + /// any. + pub const fn default_minimization_state() -> bool { + false + } +} + #[derive(Debug, Clone, Default)] pub struct ClientRequests { pub layout_mode: Option, @@ -165,6 +174,7 @@ impl Pinnacle { floating_size, decoration_mode, tags, + minimized, } = rules; let ClientRequests { @@ -183,10 +193,13 @@ impl Pinnacle { }) .unwrap_or(LayoutMode::new_tiled()); + let minimized = minimized.unwrap_or(WindowRules::default_minimization_state()); + unmapped.window.with_state_mut(|state| { state.layout_mode = layout_mode; state.floating_x = *floating_x; state.floating_y = *floating_y; + state.minimized = minimized; state.floating_size = floating_size.unwrap_or(state.floating_size); state.decoration_mode = (*decoration_mode).or(*client_decoration_mode); if let Some(tags) = tags { @@ -213,6 +226,7 @@ impl Pinnacle { } WindowSurface::X11(surface) => { let _ = surface.set_mapped(true); + let _ = surface.set_hidden(minimized); } } diff --git a/src/window/window_state.rs b/src/window/window_state.rs index d9f041c54..6bfa6f52a 100644 --- a/src/window/window_state.rs +++ b/src/window/window_state.rs @@ -382,6 +382,7 @@ pub struct WindowElementState { pub tags: IndexSet, pub layout_mode: LayoutMode, pub old_layout_mode: Option, + /// Whether the window is minimised. pub minimized: bool, pub decoration_mode: Option, pub floating_x: Option, diff --git a/tests/integration/api/window.rs b/tests/integration/api/window.rs index 51ef8adac..d4729612c 100644 --- a/tests/integration/api/window.rs +++ b/tests/integration/api/window.rs @@ -541,11 +541,12 @@ fn window_handle_set_focused() { Lang::Rust => fixture.spawn_blocking(|| { pinnacle_api::window::get_focused() .unwrap() - .set_focused(false); + .try_set_focused(false) + .unwrap() }), Lang::Lua => spawn_lua_blocking! { fixture, - Window.get_focused():set_focused(false) + Window.get_focused():try_set_focused(false) }, } @@ -559,11 +560,12 @@ fn window_handle_set_focused() { pinnacle_api::window::get_all() .next() .unwrap() - .set_focused(true); + .try_set_focused(true) + .unwrap() }), Lang::Lua => spawn_lua_blocking! { fixture, - Window.get_all()[1]:set_focused(true) + Window.get_all()[1]:try_set_focused(true) }, } @@ -581,11 +583,12 @@ fn window_handle_set_focused() { pinnacle_api::window::get_all() .nth(1) .unwrap() - .set_focused(true); + .try_set_focused(true) + .unwrap() }), Lang::Lua => spawn_lua_blocking! { fixture, - Window.get_all()[2]:set_focused(true) + Window.get_all()[2]:try_set_focused(true) }, } @@ -615,7 +618,8 @@ fn window_handle_toggle_focused() { Lang::Rust => fixture.spawn_blocking(|| { pinnacle_api::window::get_focused() .unwrap() - .toggle_focused(); + .try_toggle_focused() + .unwrap() }), Lang::Lua => spawn_lua_blocking! { fixture, @@ -633,11 +637,12 @@ fn window_handle_toggle_focused() { pinnacle_api::window::get_all() .next() .unwrap() - .toggle_focused(); + .try_toggle_focused() + .unwrap() }), Lang::Lua => spawn_lua_blocking! { fixture, - Window.get_all()[1]:toggle_focused() + Window.get_all()[1]:try_toggle_focused() }, } @@ -655,11 +660,12 @@ fn window_handle_toggle_focused() { pinnacle_api::window::get_all() .nth(1) .unwrap() - .toggle_focused(); + .try_toggle_focused() + .unwrap() }), Lang::Lua => spawn_lua_blocking! { fixture, - Window.get_all()[2]:toggle_focused() + Window.get_all()[2]:try_toggle_focused() }, } diff --git a/tests/integration/focus.rs b/tests/integration/focus.rs index 504583f21..16a109933 100644 --- a/tests/integration/focus.rs +++ b/tests/integration/focus.rs @@ -76,7 +76,8 @@ fn keyboard_focus() { pinnacle_api::window::get_all() .next() .unwrap() - .set_focused(true) + .try_set_focused(true) + .unwrap() }); let current_focus = fixture