mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2024-12-26 21:58:10 +01:00
commit
9b46a113d6
29 changed files with 2074 additions and 819 deletions
2
.github/workflows/rust.yml
vendored
2
.github/workflows/rust.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev
|
run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --verbose
|
run: cargo build --verbose
|
||||||
- name: Run tests that don't exist
|
- name: Run tests that don't exist
|
||||||
|
|
|
@ -20,6 +20,7 @@ rmp-serde = { version = "1.1.1" }
|
||||||
calloop = { version = "0.10.1", features = ["executor", "futures-io"] }
|
calloop = { version = "0.10.1", features = ["executor", "futures-io"] }
|
||||||
futures-lite = { version = "1.13.0" }
|
futures-lite = { version = "1.13.0" }
|
||||||
async-process = { version = "1.7.0" }
|
async-process = { version = "1.7.0" }
|
||||||
|
itertools = { version = "0.11.0" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["egl", "winit", "udev"]
|
default = ["egl", "winit", "udev"]
|
||||||
|
|
35
README.md
35
README.md
|
@ -1,5 +1,3 @@
|
||||||
Cool stuff happens on the dev branch sometimes, check it out!
|
|
||||||
|
|
||||||
# <div align="center">Pinnacle</div>
|
# <div align="center">Pinnacle</div>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<picture>
|
<picture>
|
||||||
|
@ -18,10 +16,15 @@ Cool stuff happens on the dev branch sometimes, check it out!
|
||||||
- [x] Udev backend
|
- [x] Udev backend
|
||||||
- This is currently just a copy of Anvil's udev backend.
|
- This is currently just a copy of Anvil's udev backend.
|
||||||
- [x] Basic tags
|
- [x] Basic tags
|
||||||
- Tags are currently very jank on the udev backend with multiple monitors. If you're checking udev out, I suggest unplugging all but one monitor or just using the winit backend until I flesh out the tag system.
|
|
||||||
- [ ] Widget system
|
|
||||||
- [ ] Layout system
|
- [ ] Layout system
|
||||||
|
- [x] Left master stack, corner, dwindle, spiral layouts
|
||||||
|
- [ ] Other three master stack directions, floating, magnifier, maximized, and fullscreen layouts
|
||||||
|
- [ ] Resizable layouts
|
||||||
|
- [ ] XWayland support
|
||||||
|
- [ ] Layer-shell support
|
||||||
- [ ] Server-side decorations
|
- [ ] Server-side decorations
|
||||||
|
- [ ] Animations and blur and all that pizazz
|
||||||
|
- [ ] Widget system
|
||||||
- [ ] The other stuff Awesome has
|
- [ ] The other stuff Awesome has
|
||||||
- [x] Is very cool :thumbsup:
|
- [x] Is very cool :thumbsup:
|
||||||
|
|
||||||
|
@ -36,15 +39,18 @@ So, this is my attempt at making an Awesome-esque Wayland compositor.
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
You'll need the following packages, as specified by [Smithay](https://github.com/Smithay/smithay):
|
You'll need the following packages, as specified by [Smithay](https://github.com/Smithay/smithay):
|
||||||
```
|
`libwayland libxkbcommon libudev libinput libgdm libseat`
|
||||||
libwayland
|
- Arch:
|
||||||
libxkbcommon
|
```
|
||||||
libudev
|
sudo pacman -S wayland libxkbcommon systemd-libs libinput libgdm seatd
|
||||||
libinput
|
```
|
||||||
libgdm
|
- Debian:
|
||||||
libseat
|
```
|
||||||
```
|
sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgdm-dev libseat-dev
|
||||||
Package names will vary across distros. TODO: list those names.
|
```
|
||||||
|
- TODO: other distros.
|
||||||
|
|
||||||
|
You'll also need Lua 5.4 for configuration.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
Build the project with:
|
Build the project with:
|
||||||
|
@ -70,7 +76,7 @@ cargo run [--release] -- --<backend>
|
||||||
- `udev`: run Pinnacle in a tty. NOTE: I tried running udev in Awesome and some things broke so uh, don't do that
|
- `udev`: run Pinnacle in a tty. NOTE: I tried running udev in Awesome and some things broke so uh, don't do that
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
Please note: this is VERY WIP and has basically no options yet.
|
Please note: this is VERY WIP and has few options.
|
||||||
|
|
||||||
Pinnacle supports configuration through Lua (and hopefully more languages if it's not too unwieldy :crab:).
|
Pinnacle supports configuration through Lua (and hopefully more languages if it's not too unwieldy :crab:).
|
||||||
|
|
||||||
|
@ -109,5 +115,6 @@ The following controls are currently hardcoded:
|
||||||
|
|
||||||
- `Ctrl + Left Mouse`: Move a window
|
- `Ctrl + Left Mouse`: Move a window
|
||||||
- `Ctrl + Right Mouse`: Resize a window
|
- `Ctrl + Right Mouse`: Resize a window
|
||||||
|
- `Ctrl + Alt + Shift + Esc`: Kill Pinnacle. This is for when the compositor inevitably locks up because I did a dumb thing :thumbsup:
|
||||||
|
|
||||||
You can find the rest of the controls in the [`example_config`](api/lua/example_config.lua).
|
You can find the rest of the controls in the [`example_config`](api/lua/example_config.lua).
|
||||||
|
|
|
@ -11,10 +11,11 @@ pcall(require, "luarocks.loader")
|
||||||
|
|
||||||
-- Neovim users be like:
|
-- Neovim users be like:
|
||||||
require("pinnacle").setup(function(pinnacle)
|
require("pinnacle").setup(function(pinnacle)
|
||||||
local input = pinnacle.input -- Key and mouse binds
|
local input = pinnacle.input -- Key and mouse binds
|
||||||
local window = pinnacle.window -- Window management
|
local window = pinnacle.window -- Window management
|
||||||
local process = pinnacle.process -- Process spawning
|
local process = pinnacle.process -- Process spawning
|
||||||
local tag = pinnacle.tag -- Tag management
|
local tag = pinnacle.tag -- Tag management
|
||||||
|
local output = pinnacle.output -- Output management
|
||||||
|
|
||||||
-- Every key supported by xkbcommon.
|
-- Every key supported by xkbcommon.
|
||||||
-- Support for just putting in a string of a key is intended.
|
-- Support for just putting in a string of a key is intended.
|
||||||
|
@ -27,6 +28,7 @@ require("pinnacle").setup(function(pinnacle)
|
||||||
local terminal = "alacritty"
|
local terminal = "alacritty"
|
||||||
|
|
||||||
-- Keybinds ----------------------------------------------------------------------
|
-- Keybinds ----------------------------------------------------------------------
|
||||||
|
|
||||||
input.keybind({ mod_key, "Alt" }, keys.q, pinnacle.quit)
|
input.keybind({ mod_key, "Alt" }, keys.q, pinnacle.quit)
|
||||||
|
|
||||||
input.keybind({ mod_key, "Alt" }, keys.c, window.close_window)
|
input.keybind({ mod_key, "Alt" }, keys.c, window.close_window)
|
||||||
|
@ -50,8 +52,65 @@ require("pinnacle").setup(function(pinnacle)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- Tags ---------------------------------------------------------------------------
|
-- Tags ---------------------------------------------------------------------------
|
||||||
tag.add("1", "2", "3", "4", "5")
|
|
||||||
tag.toggle("1")
|
output.connect_for_all(function(op)
|
||||||
|
op:add_tags("1", "2", "3", "4", "5")
|
||||||
|
-- Same as tag.add(op, "1", "2", "3", "4", "5")
|
||||||
|
tag.toggle("1", op)
|
||||||
|
end)
|
||||||
|
|
||||||
|
---@type Layout[]
|
||||||
|
local layouts = {
|
||||||
|
"MasterStack",
|
||||||
|
"Dwindle",
|
||||||
|
"Spiral",
|
||||||
|
"CornerTopLeft",
|
||||||
|
"CornerTopRight",
|
||||||
|
"CornerBottomLeft",
|
||||||
|
"CornerBottomRight",
|
||||||
|
}
|
||||||
|
local indices = {}
|
||||||
|
|
||||||
|
-- Layout cycling
|
||||||
|
-- Yes, this is very complicated and yes, I'll cook up a way to make it less complicated.
|
||||||
|
input.keybind({ mod_key }, keys.space, function()
|
||||||
|
local tags = output.get_focused():tags()
|
||||||
|
for _, tg in pairs(tags) do
|
||||||
|
if tg:active() then
|
||||||
|
local name = tg:name()
|
||||||
|
tg:set_layout(layouts[indices[name] or 1])
|
||||||
|
if indices[name] == nil then
|
||||||
|
indices[name] = 2
|
||||||
|
else
|
||||||
|
if indices[name] + 1 > #layouts then
|
||||||
|
indices[name] = 1
|
||||||
|
else
|
||||||
|
indices[name] = indices[name] + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
input.keybind({ mod_key, "Shift" }, keys.space, function()
|
||||||
|
local tags = output.get_focused():tags()
|
||||||
|
for _, tg in pairs(tags) do
|
||||||
|
if tg:active() then
|
||||||
|
local name = tg:name()
|
||||||
|
tg:set_layout(layouts[indices[name] or #layouts])
|
||||||
|
if indices[name] == nil then
|
||||||
|
indices[name] = #layouts - 1
|
||||||
|
else
|
||||||
|
if indices[name] - 1 < 1 then
|
||||||
|
indices[name] = #layouts
|
||||||
|
else
|
||||||
|
indices[name] = indices[name] - 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
input.keybind({ mod_key }, keys.KEY_1, function()
|
input.keybind({ mod_key }, keys.KEY_1, function()
|
||||||
tag.switch_to("1")
|
tag.switch_to("1")
|
||||||
|
|
|
@ -9,9 +9,19 @@ local input = {
|
||||||
}
|
}
|
||||||
|
|
||||||
---Set a keybind. If called with an already existing keybind, it gets replaced.
|
---Set a keybind. If called with an already existing keybind, it gets replaced.
|
||||||
|
---
|
||||||
|
---# Example
|
||||||
|
---
|
||||||
|
---```lua
|
||||||
|
----- The following sets Super + Return to open Alacritty
|
||||||
|
---
|
||||||
|
---input.keybind({ "Super" }, input.keys.Return, function()
|
||||||
|
--- process.spawn("Alacritty")
|
||||||
|
---end)
|
||||||
|
---```
|
||||||
---@param key Keys The key for the keybind.
|
---@param key Keys The key for the keybind.
|
||||||
---@param modifiers (Modifier)[] Which modifiers need to be pressed for the keybind to trigger.
|
---@param modifiers (Modifier)[] Which modifiers need to be pressed for the keybind to trigger.
|
||||||
---@param action fun() What to run.
|
---@param action fun() What to do.
|
||||||
function input.keybind(modifiers, key, action)
|
function input.keybind(modifiers, key, action)
|
||||||
table.insert(CallbackTable, action)
|
table.insert(CallbackTable, action)
|
||||||
SendMsg({
|
SendMsg({
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
---@alias Modifier "Alt" | "Ctrl" | "Shift" | "Super"
|
---@alias Modifier "Alt" | "Ctrl" | "Shift" | "Super"
|
||||||
|
|
||||||
---@enum Keys
|
---@enum Keys
|
||||||
local M = {
|
local keys = {
|
||||||
NoSymbol = 0x00000000,
|
NoSymbol = 0x00000000,
|
||||||
|
|
||||||
VoidSymbol = 0x00ffffff,
|
VoidSymbol = 0x00ffffff,
|
||||||
|
@ -4321,4 +4321,4 @@ local M = {
|
||||||
block = 0x100000fc,
|
block = 0x100000fc,
|
||||||
}
|
}
|
||||||
|
|
||||||
return M
|
return keys
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
---@meta _
|
---@meta _
|
||||||
|
|
||||||
---@class _Msg
|
---@class _Msg
|
||||||
---@field SetKeybind { key: Keys, modifiers: Modifiers[], callback_id: integer }
|
---@field SetKeybind { key: Keys, modifiers: Modifier[], callback_id: integer }
|
||||||
---@field SetMousebind { button: integer }
|
---@field SetMousebind { button: integer }
|
||||||
--Windows
|
--Windows
|
||||||
---@field CloseWindow { client_id: integer? }
|
---@field CloseWindow { client_id: integer? }
|
||||||
|
@ -19,27 +19,47 @@
|
||||||
---@field Spawn { command: string[], callback_id: integer? }
|
---@field Spawn { command: string[], callback_id: integer? }
|
||||||
---@field Request Request
|
---@field Request Request
|
||||||
--Tags
|
--Tags
|
||||||
---@field ToggleTag { tag_id: string }
|
---@field ToggleTag { output_name: string, tag_name: string }
|
||||||
---@field SwitchToTag { tag_id: string }
|
---@field SwitchToTag { output_name: string, tag_name: string }
|
||||||
---@field AddTags { tags: string[] }
|
---@field AddTags { output_name: string, tags: string[] }
|
||||||
---@field RemoveTags { tags: string[] }
|
---@field RemoveTags { output_name: string, tags: string[] }
|
||||||
|
---@field SetLayout { output_name: string, tag_name: string, layout: Layout }
|
||||||
|
--Outputs
|
||||||
|
---@field ConnectForAllOutputs { callback_id: integer }
|
||||||
|
|
||||||
---@alias Msg _Msg | "Quit"
|
---@alias Msg _Msg | "Quit"
|
||||||
|
|
||||||
---@class Request
|
--------------------------------------------------------------------------------------------
|
||||||
---@field GetWindowByFocus { id: integer }
|
|
||||||
---@field GetAllWindows { id: integer }
|
---@class _Request
|
||||||
|
--Windows
|
||||||
|
---@field GetWindowByAppId { app_id: string }
|
||||||
|
---@field GetWindowByTitle { title: string }
|
||||||
|
--Outputs
|
||||||
|
---@field GetOutputByName { name: string }
|
||||||
|
---@field GetOutputsByModel { model: string }
|
||||||
|
---@field GetOutputsByRes { res: integer[] }
|
||||||
|
---@field GetTagsByOutput { output: string }
|
||||||
|
---@field GetTagActive { tag_id: integer }
|
||||||
|
---@field GetTagName { tag_id: integer }
|
||||||
|
|
||||||
|
---@alias Request _Request | "GetWindowByFocus" | "GetAllWindows" | "GetOutputByFocus"
|
||||||
|
|
||||||
---@class IncomingMsg
|
---@class IncomingMsg
|
||||||
---@field CallCallback { callback_id: integer, args: Args }
|
---@field CallCallback { callback_id: integer, args: Args }
|
||||||
---@field RequestResponse { request_id: integer, response: RequestResponse }
|
---@field RequestResponse { response: RequestResponse }
|
||||||
|
|
||||||
---@class Args
|
---@class Args
|
||||||
---@field Spawn { stdout: string?, stderr: string?, exit_code: integer?, exit_msg: string? }
|
---@field Spawn { stdout: string?, stderr: string?, exit_code: integer?, exit_msg: string? }
|
||||||
|
---@field ConnectForAllOutputs { output_name: string }
|
||||||
|
|
||||||
---@class RequestResponse
|
---@class RequestResponse
|
||||||
---@field Window { window: WindowProperties }
|
---@field Window { window: WindowProperties }
|
||||||
---@field GetAllWindows { windows: WindowProperties[] }
|
---@field GetAllWindows { windows: WindowProperties[] }
|
||||||
|
---@field Outputs { names: string[] }
|
||||||
|
---@field Tags { tags: TagProperties[] }
|
||||||
|
---@field TagActive { active: boolean }
|
||||||
|
---@field TagName { name: string }
|
||||||
|
|
||||||
---@class WindowProperties
|
---@class WindowProperties
|
||||||
---@field id integer
|
---@field id integer
|
||||||
|
@ -48,3 +68,6 @@
|
||||||
---@field size integer[] A two element int array, \[1\] = w, \[2\] = h
|
---@field size integer[] A two element int array, \[1\] = w, \[2\] = h
|
||||||
---@field location integer[] A two element int array, \[1\] = x, \[2\] = y
|
---@field location integer[] A two element int array, \[1\] = x, \[2\] = y
|
||||||
---@field floating boolean
|
---@field floating boolean
|
||||||
|
|
||||||
|
---@class TagProperties
|
||||||
|
---@field id integer
|
||||||
|
|
184
api/lua/output.lua
Normal file
184
api/lua/output.lua
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
-- 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/.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
---@class Output A display.
|
||||||
|
---@field name string The name of this output (or rather, of its connector).
|
||||||
|
local op = {}
|
||||||
|
|
||||||
|
---Get all tags on this output. See `tag.get_on_output`.
|
||||||
|
---@return Tag[]
|
||||||
|
function op:tags()
|
||||||
|
return require("tag").get_on_output(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Add tags to this output. See `tag.add`.
|
||||||
|
---@param ... string The names of the tags you want to add.
|
||||||
|
function op:add_tags(...)
|
||||||
|
require("tag").add(self, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Add tags to this output as a table. See `tag.add_table`.
|
||||||
|
---@param names string[] The names of the tags you want to add, as a table.
|
||||||
|
function op:add_tags_table(names)
|
||||||
|
require("tag").add_table(self, names)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Add methods to this output.
|
||||||
|
---@param props Output
|
||||||
|
---@return Output
|
||||||
|
local function new_output(props)
|
||||||
|
-- Copy functions over
|
||||||
|
for k, v in pairs(op) do
|
||||||
|
props[k] = v
|
||||||
|
end
|
||||||
|
|
||||||
|
return props
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------------------
|
||||||
|
|
||||||
|
local output = {}
|
||||||
|
|
||||||
|
---Get an output by its name.
|
||||||
|
---
|
||||||
|
---"Name" in this sense does not mean its model or manufacturer;
|
||||||
|
---rather, "name" is the name of the connector the output is connected to.
|
||||||
|
---This should be something like "HDMI-A-0", "eDP-1", or similar.
|
||||||
|
---
|
||||||
|
---# Examples
|
||||||
|
---```lua
|
||||||
|
---local monitor = output.get_by_name("DP-1")
|
||||||
|
---print(monitor.name) -- should print `DP-1`
|
||||||
|
---```
|
||||||
|
---@param name string The name of the output.
|
||||||
|
---@return Output|nil
|
||||||
|
function output.get_by_name(name)
|
||||||
|
SendRequest({
|
||||||
|
GetOutputByName = {
|
||||||
|
name = name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
local response = ReadMsg()
|
||||||
|
|
||||||
|
local names = response.RequestResponse.response.Outputs.names
|
||||||
|
|
||||||
|
if names[1] ~= nil then
|
||||||
|
return new_output({ name = names[1] })
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Note: This may or may not be what is reported by other monitor listing utilities. Pinnacle currently fails to pick up one of my monitors' models when it is correctly picked up by tools like wlr-randr. I'll fix this in the future.
|
||||||
|
---
|
||||||
|
---Get outputs by their model.
|
||||||
|
---This is something like "DELL E2416H" or whatever gibberish monitor manufacturers call their displays.
|
||||||
|
---@param model string The model of the output(s).
|
||||||
|
---@return Output[] outputs All outputs with this model. If there are none, the returned table will be empty.
|
||||||
|
function output.get_by_model(model)
|
||||||
|
SendRequest({
|
||||||
|
GetOutputsByModel = {
|
||||||
|
model = model,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
local response = ReadMsg()
|
||||||
|
|
||||||
|
local names = response.RequestResponse.response.Outputs.names
|
||||||
|
|
||||||
|
---@type Output
|
||||||
|
local outputs = {}
|
||||||
|
for _, v in pairs(names) do
|
||||||
|
table.insert(outputs, new_output({ name = v }))
|
||||||
|
end
|
||||||
|
|
||||||
|
return outputs
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get outputs by their resolution.
|
||||||
|
---
|
||||||
|
---@param width integer The width of the outputs, in pixels.
|
||||||
|
---@param height integer The height of the outputs, in pixels.
|
||||||
|
---@return Output[] outputs All outputs with this resolution. If there are none, the returned table will be empty.
|
||||||
|
function output.get_by_res(width, height)
|
||||||
|
SendRequest({
|
||||||
|
GetOutputsByRes = {
|
||||||
|
res = { width, height },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
local response = ReadMsg()
|
||||||
|
|
||||||
|
local names = response.RequestResponse.response.Outputs.names
|
||||||
|
|
||||||
|
---@type Output
|
||||||
|
local outputs = {}
|
||||||
|
for _, v in pairs(names) do
|
||||||
|
table.insert(outputs, new_output({ name = v }))
|
||||||
|
end
|
||||||
|
|
||||||
|
return outputs
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the currently focused output. This is currently implemented as the one with the cursor on it.
|
||||||
|
---
|
||||||
|
---This function may return nil, which means you may get a warning if you try to use it without checking for nil.
|
||||||
|
---Usually this function will not be nil unless you unplug all monitors, so instead of checking,
|
||||||
|
---you can ignore the warning by either forcing the type to be non-nil with an inline comment:
|
||||||
|
---```lua
|
||||||
|
---local op = output.get_focused() --[[@as Output]]
|
||||||
|
---```
|
||||||
|
---or by disabling nil check warnings for the line:
|
||||||
|
---```lua
|
||||||
|
---local op = output.get_focused()
|
||||||
|
------@diagnostic disable-next-line:need-check-nil
|
||||||
|
---local tags_on_output = op:tags()
|
||||||
|
---```
|
||||||
|
---Type checking done by Lua LS isn't perfect.
|
||||||
|
---Note that directly using the result of this function inline will *not* raise a warning, so be careful.
|
||||||
|
---```lua
|
||||||
|
---local tags = output.get_focused():tags() -- will NOT warn for nil
|
||||||
|
---```
|
||||||
|
---@return Output|nil output The output, or nil if none are focused.
|
||||||
|
function output.get_focused()
|
||||||
|
SendMsg({
|
||||||
|
Request = "GetOutputByFocus",
|
||||||
|
})
|
||||||
|
|
||||||
|
local response = ReadMsg()
|
||||||
|
|
||||||
|
local names = response.RequestResponse.response.Outputs.names
|
||||||
|
|
||||||
|
if names[1] ~= nil then
|
||||||
|
return new_output({ name = names[1] })
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Connect a function to be run on all current and future outputs.
|
||||||
|
---
|
||||||
|
---When called, `connect_for_all` will immediately run `func` with all currently connected outputs.
|
||||||
|
---If a new output is connected, `func` will also be called with it.
|
||||||
|
---
|
||||||
|
---Please note: this function will be run *after* Pinnacle processes your entire config.
|
||||||
|
---For example, if you define tags in `func` but toggle them directly after `connect_for_all`, nothing will happen as the tags haven't been added yet.
|
||||||
|
---@param func fun(output: Output) The function that will be run.
|
||||||
|
function output.connect_for_all(func)
|
||||||
|
---@param args Args
|
||||||
|
table.insert(CallbackTable, function(args)
|
||||||
|
local args = args.ConnectForAllOutputs
|
||||||
|
func(new_output({ name = args.output_name }))
|
||||||
|
end)
|
||||||
|
SendMsg({
|
||||||
|
ConnectForAllOutputs = {
|
||||||
|
callback_id = #CallbackTable,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return output
|
|
@ -19,6 +19,7 @@ local function read_exact(socket_fd, count)
|
||||||
local len_to_read = count
|
local len_to_read = count
|
||||||
local data = ""
|
local data = ""
|
||||||
while len_to_read > 0 do
|
while len_to_read > 0 do
|
||||||
|
-- print("need to read " .. tostring(len_to_read) .. " bytes")
|
||||||
local bytes, err_msg, errnum = socket.recv(socket_fd, len_to_read)
|
local bytes, err_msg, errnum = socket.recv(socket_fd, len_to_read)
|
||||||
|
|
||||||
if bytes == nil then
|
if bytes == nil then
|
||||||
|
@ -56,6 +57,8 @@ local pinnacle = {
|
||||||
process = require("process"),
|
process = require("process"),
|
||||||
---Tag management
|
---Tag management
|
||||||
tag = require("tag"),
|
tag = require("tag"),
|
||||||
|
---Output management
|
||||||
|
output = require("output"),
|
||||||
}
|
}
|
||||||
|
|
||||||
---Quit Pinnacle.
|
---Quit Pinnacle.
|
||||||
|
@ -103,9 +106,11 @@ function pinnacle.setup(config_func)
|
||||||
|
|
||||||
---@type integer
|
---@type integer
|
||||||
local msg_len = string.unpack("=I4", msg_len_bytes)
|
local msg_len = string.unpack("=I4", msg_len_bytes)
|
||||||
|
-- print(msg_len)
|
||||||
|
|
||||||
local msg_bytes, err_msg2, err_num2 = read_exact(socket_fd, msg_len)
|
local msg_bytes, err_msg2, err_num2 = read_exact(socket_fd, msg_len)
|
||||||
assert(msg_bytes)
|
assert(msg_bytes)
|
||||||
|
-- print(msg_bytes)
|
||||||
|
|
||||||
---@type IncomingMsg
|
---@type IncomingMsg
|
||||||
local tb = msgpack.decode(msg_bytes)
|
local tb = msgpack.decode(msg_bytes)
|
||||||
|
@ -114,15 +119,6 @@ function pinnacle.setup(config_func)
|
||||||
return tb
|
return tb
|
||||||
end
|
end
|
||||||
|
|
||||||
Requests = {
|
|
||||||
id = 1,
|
|
||||||
}
|
|
||||||
function Requests:next()
|
|
||||||
local id = self.id
|
|
||||||
self.id = self.id + 1
|
|
||||||
return id
|
|
||||||
end
|
|
||||||
|
|
||||||
config_func(pinnacle)
|
config_func(pinnacle)
|
||||||
|
|
||||||
while true do
|
while true do
|
||||||
|
|
1
api/lua/stylua.toml
Normal file
1
api/lua/stylua.toml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
indent_type = "Spaces"
|
216
api/lua/tag.lua
216
api/lua/tag.lua
|
@ -6,55 +6,235 @@
|
||||||
|
|
||||||
local tag = {}
|
local tag = {}
|
||||||
|
|
||||||
|
---@alias Layout
|
||||||
|
---| "MasterStack" # One master window on the left with all other windows stacked to the right.
|
||||||
|
---| "Dwindle" # Windows split in half towards the bottom right corner.
|
||||||
|
---| "Spiral" # Windows split in half in a spiral.
|
||||||
|
---| "CornerTopLeft" # One main corner window in the top left with a column of windows on the right and a row on the bottom.
|
||||||
|
---| "CornerTopRight" # One main corner window in the top right with a column of windows on the left and a row on the bottom.
|
||||||
|
---| "CornerBottomLeft" # One main corner window in the bottom left with a column of windows on the right and a row on the top.
|
||||||
|
---| "CornerBottomRight" # One main corner window in the bottom right with a column of windows on the left and a row on the top.
|
||||||
|
|
||||||
|
---@class Tag
|
||||||
|
---@field private id integer The internal id of this tag.
|
||||||
|
local tg = {}
|
||||||
|
|
||||||
|
---@param props Tag
|
||||||
|
---@return Tag
|
||||||
|
local function new_tag(props)
|
||||||
|
-- Copy functions over
|
||||||
|
for k, v in pairs(tg) do
|
||||||
|
props[k] = v
|
||||||
|
end
|
||||||
|
|
||||||
|
return props
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get this tag's active status.
|
||||||
|
---@return boolean active True if the tag is active, otherwise false.
|
||||||
|
function tg:active()
|
||||||
|
SendRequest({
|
||||||
|
GetTagActive = {
|
||||||
|
tag_id = self.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
local response = ReadMsg()
|
||||||
|
local active = response.RequestResponse.response.TagActive.active
|
||||||
|
return active
|
||||||
|
end
|
||||||
|
|
||||||
|
function tg:name()
|
||||||
|
SendRequest({
|
||||||
|
GetTagName = {
|
||||||
|
tag_id = self.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
local response = ReadMsg()
|
||||||
|
local name = response.RequestResponse.response.TagName.name
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set this tag's layout.
|
||||||
|
---@param layout Layout
|
||||||
|
function tg:set_layout(layout) -- TODO: output param
|
||||||
|
tag.set_layout(self:name(), layout)
|
||||||
|
end
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
---Add tags.
|
---Add tags.
|
||||||
---
|
---
|
||||||
---If you need to add the strings in a table, use `tag.add_table` instead.
|
---If you need to add the names as a table, use `tag.add_table` instead.
|
||||||
---
|
---
|
||||||
---# Example
|
---# Example
|
||||||
---
|
---
|
||||||
---```lua
|
---```lua
|
||||||
---tag.add("1", "2", "3", "4", "5") -- Add tags with names 1-5
|
---local output = output.get_by_name("DP-1")
|
||||||
|
---if output ~= nil then
|
||||||
|
--- tag.add(output, "1", "2", "3", "4", "5") -- Add tags with names 1-5
|
||||||
|
---end
|
||||||
---```
|
---```
|
||||||
|
---@param output Output The output you want these tags to be added to.
|
||||||
---@param ... string The names of the new tags you want to add.
|
---@param ... string The names of the new tags you want to add.
|
||||||
function tag.add(...)
|
function tag.add(output, ...)
|
||||||
local tags = table.pack(...)
|
local tags = table.pack(...)
|
||||||
tags["n"] = nil
|
tags["n"] = nil
|
||||||
|
|
||||||
SendMsg({
|
SendMsg({
|
||||||
AddTags = {
|
AddTags = {
|
||||||
|
output_name = output.name,
|
||||||
tags = tags,
|
tags = tags,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
---Like `tag.add`, but with a table of strings instead.
|
---Like `tag.add`, but with a table of strings instead.
|
||||||
---@param tags string[] The names of the new tags you want to add, as a table.
|
---
|
||||||
function tag.add_table(tags)
|
---# Example
|
||||||
|
---
|
||||||
|
---```lua
|
||||||
|
---local tags = { "Terminal", "Browser", "Mail", "Gaming", "Potato" }
|
||||||
|
---local output = output.get_by_name("DP-1")
|
||||||
|
---if output ~= nil then
|
||||||
|
--- tag.add(output, tags) -- Add tags with the names above
|
||||||
|
---end
|
||||||
|
---```
|
||||||
|
---@param output Output The output you want these tags to be added to.
|
||||||
|
---@param names string[] The names of the new tags you want to add, as a table.
|
||||||
|
function tag.add_table(output, names)
|
||||||
SendMsg({
|
SendMsg({
|
||||||
AddTags = {
|
AddTags = {
|
||||||
tags = tags,
|
output_name = output.name,
|
||||||
|
tags = names,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
---Toggle a tag's display.
|
---Toggle a tag on the specified output. If `output` isn't specified, toggle it on the currently focused output instead.
|
||||||
|
---
|
||||||
|
---# Example
|
||||||
|
---
|
||||||
|
---```lua
|
||||||
|
----- Assuming all tags are toggled off...
|
||||||
|
---local op = output.get_by_name("DP-1")
|
||||||
|
---tag.toggle("1", op)
|
||||||
|
---tag.toggle("2", op)
|
||||||
|
----- will cause windows on both tags 1 and 2 to be displayed at the same time.
|
||||||
|
---```
|
||||||
---@param name string The name of the tag.
|
---@param name string The name of the tag.
|
||||||
function tag.toggle(name)
|
---@param output Output? The output.
|
||||||
SendMsg({
|
function tag.toggle(name, output)
|
||||||
ToggleTag = {
|
if output ~= nil then
|
||||||
tag_id = name,
|
SendMsg({
|
||||||
},
|
ToggleTag = {
|
||||||
})
|
output_name = output.name,
|
||||||
|
tag_name = name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
else
|
||||||
|
local op = require("output").get_focused()
|
||||||
|
if op ~= nil then
|
||||||
|
SendMsg({
|
||||||
|
ToggleTag = {
|
||||||
|
output_name = op.name,
|
||||||
|
tag_name = name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---Switch to a tag, deactivating any other active tags.
|
---Switch to a tag on the specified output, deactivating any other active tags on it.
|
||||||
|
---If `output` is not specified, this uses the currently focused output instead.
|
||||||
|
---
|
||||||
|
---This is used to replicate what a traditional workspace is on some other Wayland compositors.
|
||||||
|
---
|
||||||
|
---# Example
|
||||||
|
---
|
||||||
|
---```lua
|
||||||
|
---tag.switch_to("3") -- Switches to and displays *only* windows on tag 3
|
||||||
|
---```
|
||||||
---@param name string The name of the tag.
|
---@param name string The name of the tag.
|
||||||
function tag.switch_to(name)
|
---@param output Output? The output.
|
||||||
SendMsg({
|
function tag.switch_to(name, output)
|
||||||
SwitchToTag = {
|
if output ~= nil then
|
||||||
tag_id = name,
|
SendMsg({
|
||||||
|
SwitchToTag = {
|
||||||
|
output_name = output.name,
|
||||||
|
tag_name = name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
else
|
||||||
|
local op = require("output").get_focused()
|
||||||
|
if op ~= nil then
|
||||||
|
SendMsg({
|
||||||
|
SwitchToTag = {
|
||||||
|
output_name = op.name,
|
||||||
|
tag_name = name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set a layout for the tag on the specified output. If there is none, set it for the tag on the currently focused one.
|
||||||
|
---@param name string The name of the tag.
|
||||||
|
---@param layout Layout The layout.
|
||||||
|
---@param output Output? The output.
|
||||||
|
function tag.set_layout(name, layout, output)
|
||||||
|
if output ~= nil then
|
||||||
|
SendMsg({
|
||||||
|
SetLayout = {
|
||||||
|
output_name = output.name,
|
||||||
|
tag_name = name,
|
||||||
|
layout = layout,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
else
|
||||||
|
local op = require("output").get_focused()
|
||||||
|
if op ~= nil then
|
||||||
|
SendMsg({
|
||||||
|
SetLayout = {
|
||||||
|
output_name = op.name,
|
||||||
|
tag_name = name,
|
||||||
|
layout = layout,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get all tags on the specified output.
|
||||||
|
---
|
||||||
|
---You can also use `output_obj:tags()`, which delegates to this function:
|
||||||
|
---```lua
|
||||||
|
---local tags_on_output = output.get_focused():tags()
|
||||||
|
----- This is the same as
|
||||||
|
----- local tags_on_output = tag.get_on_output(output.get_focused())
|
||||||
|
---```
|
||||||
|
---@param output Output
|
||||||
|
---@return Tag[]
|
||||||
|
function tag.get_on_output(output)
|
||||||
|
SendRequest({
|
||||||
|
GetTagsByOutput = {
|
||||||
|
output = output.name,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
local response = ReadMsg()
|
||||||
|
|
||||||
|
local tag_props = response.RequestResponse.response.Tags.tags
|
||||||
|
|
||||||
|
---@type Tag[]
|
||||||
|
local tags = {}
|
||||||
|
|
||||||
|
for _, prop in pairs(tag_props) do
|
||||||
|
table.insert(tags, new_tag({ id = prop.id }))
|
||||||
|
end
|
||||||
|
|
||||||
|
return tags
|
||||||
end
|
end
|
||||||
|
|
||||||
return tag
|
return tag
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
---@field private floating boolean Whether the window is floating or not (tiled)
|
---@field private floating boolean Whether the window is floating or not (tiled)
|
||||||
local win = {}
|
local win = {}
|
||||||
|
|
||||||
---@param props { id: integer, app_id: string?, title: string?, size: { w: integer, h: integer }, location: { x: integer, y: integer }, floating: boolean }
|
---@param props Window
|
||||||
---@return Window
|
---@return Window
|
||||||
local function new_window(props)
|
local function new_window(props)
|
||||||
-- Copy functions over
|
-- Copy functions over
|
||||||
|
@ -91,15 +91,14 @@ function window.toggle_floating(client_id)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---TODO: This function is not implemented yet.
|
||||||
|
---
|
||||||
---Get a window by its app id (aka its X11 class).
|
---Get a window by its app id (aka its X11 class).
|
||||||
---@param app_id string The window's app id. For example, Alacritty's app id is "Alacritty".
|
---@param app_id string The window's app id. For example, Alacritty's app id is "Alacritty".
|
||||||
---@return Window window -- TODO: nil
|
---@return Window window -- TODO: nil
|
||||||
function window.get_by_app_id(app_id)
|
function window.get_by_app_id(app_id)
|
||||||
local req_id = Requests:next()
|
|
||||||
|
|
||||||
SendRequest({
|
SendRequest({
|
||||||
GetWindowByAppId = {
|
GetWindowByAppId = {
|
||||||
id = req_id,
|
|
||||||
app_id = app_id,
|
app_id = app_id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -127,15 +126,14 @@ function window.get_by_app_id(app_id)
|
||||||
return new_window(wind)
|
return new_window(wind)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---TODO: This function is not implemented yet.
|
||||||
|
---
|
||||||
---Get a window by its title.
|
---Get a window by its title.
|
||||||
---@param title string The window's title.
|
---@param title string The window's title.
|
||||||
---@return Window
|
---@return Window
|
||||||
function window.get_by_title(title)
|
function window.get_by_title(title)
|
||||||
local req_id = Requests:next()
|
|
||||||
|
|
||||||
SendRequest({
|
SendRequest({
|
||||||
GetWindowByTitle = {
|
GetWindowByTitle = {
|
||||||
id = req_id,
|
|
||||||
title = title,
|
title = title,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -166,13 +164,7 @@ end
|
||||||
---Get the currently focused window.
|
---Get the currently focused window.
|
||||||
---@return Window
|
---@return Window
|
||||||
function window.get_focused()
|
function window.get_focused()
|
||||||
local req_id = Requests:next()
|
SendRequest("GetWindowByFocus")
|
||||||
|
|
||||||
SendRequest({
|
|
||||||
GetWindowByFocus = {
|
|
||||||
id = req_id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
local response = ReadMsg()
|
local response = ReadMsg()
|
||||||
|
|
||||||
|
@ -199,12 +191,8 @@ end
|
||||||
|
|
||||||
---Get all windows.
|
---Get all windows.
|
||||||
---@return Window[]
|
---@return Window[]
|
||||||
function window.get_windows()
|
function window.get_all()
|
||||||
SendRequest({
|
SendRequest("GetAllWindows")
|
||||||
GetAllWindows = {
|
|
||||||
id = Requests:next(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
-- INFO: these read synchronously so this should always work IF the server works correctly
|
-- INFO: these read synchronously so this should always work IF the server works correctly
|
||||||
|
|
||||||
|
|
|
@ -112,6 +112,7 @@ pub fn send_to_client(
|
||||||
stream: &mut UnixStream,
|
stream: &mut UnixStream,
|
||||||
msg: &OutgoingMsg,
|
msg: &OutgoingMsg,
|
||||||
) -> Result<(), rmp_serde::encode::Error> {
|
) -> Result<(), rmp_serde::encode::Error> {
|
||||||
|
// tracing::debug!("Sending {msg:?}");
|
||||||
let msg = rmp_serde::to_vec_named(msg)?;
|
let msg = rmp_serde::to_vec_named(msg)?;
|
||||||
let msg_len = msg.len() as u32;
|
let msg_len = msg.len() as u32;
|
||||||
let bytes = msg_len.to_ne_bytes();
|
let bytes = msg_len.to_ne_bytes();
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
// value is a map of the enum's values
|
// value is a map of the enum's values
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
tag::TagId,
|
layout::Layout,
|
||||||
|
tag::{TagId, TagProperties},
|
||||||
window::{window_state::WindowId, WindowProperties},
|
window::{window_state::WindowId, WindowProperties},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ pub enum Msg {
|
||||||
// Input
|
// Input
|
||||||
SetKeybind {
|
SetKeybind {
|
||||||
key: u32,
|
key: u32,
|
||||||
modifiers: Vec<Modifiers>,
|
modifiers: Vec<Modifier>,
|
||||||
callback_id: CallbackId,
|
callback_id: CallbackId,
|
||||||
},
|
},
|
||||||
SetMousebind {
|
SetMousebind {
|
||||||
|
@ -42,25 +43,41 @@ pub enum Msg {
|
||||||
},
|
},
|
||||||
MoveWindowToTag {
|
MoveWindowToTag {
|
||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
tag_id: TagId,
|
tag_id: String,
|
||||||
},
|
},
|
||||||
ToggleTagOnWindow {
|
ToggleTagOnWindow {
|
||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
tag_id: TagId,
|
tag_id: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Tag management
|
// Tag management
|
||||||
ToggleTag {
|
ToggleTag {
|
||||||
tag_id: TagId,
|
output_name: String,
|
||||||
|
tag_name: String,
|
||||||
},
|
},
|
||||||
SwitchToTag {
|
SwitchToTag {
|
||||||
tag_id: TagId,
|
output_name: String,
|
||||||
|
tag_name: String,
|
||||||
},
|
},
|
||||||
AddTags {
|
AddTags {
|
||||||
tags: Vec<TagId>,
|
/// The name of the output you want these tags on.
|
||||||
|
output_name: String,
|
||||||
|
tags: Vec<String>,
|
||||||
},
|
},
|
||||||
RemoveTags {
|
RemoveTags {
|
||||||
tags: Vec<TagId>,
|
/// The name of the output you want these tags removed from.
|
||||||
|
output_name: String,
|
||||||
|
tags: Vec<String>,
|
||||||
|
},
|
||||||
|
SetLayout {
|
||||||
|
output_name: String,
|
||||||
|
tag_name: String,
|
||||||
|
layout: Layout,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Output management
|
||||||
|
ConnectForAllOutputs {
|
||||||
|
callback_id: CallbackId,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Process management
|
// Process management
|
||||||
|
@ -81,28 +98,36 @@ pub enum Msg {
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct RequestId(pub u32);
|
pub struct RequestId(pub u32);
|
||||||
|
|
||||||
|
#[allow(clippy::enum_variant_names)]
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
/// Messages that require a server response, usually to provide some data.
|
/// Messages that require a server response, usually to provide some data.
|
||||||
pub enum Request {
|
pub enum Request {
|
||||||
GetWindowByAppId { id: RequestId, app_id: String },
|
GetWindowByAppId { app_id: String },
|
||||||
GetWindowByTitle { id: RequestId, title: String },
|
GetWindowByTitle { title: String },
|
||||||
GetWindowByFocus { id: RequestId },
|
GetWindowByFocus,
|
||||||
GetAllWindows { id: RequestId },
|
GetAllWindows,
|
||||||
|
GetOutputByName { name: String },
|
||||||
|
GetOutputsByModel { model: String },
|
||||||
|
GetOutputsByRes { res: (u32, u32) },
|
||||||
|
GetOutputByFocus,
|
||||||
|
GetTagsByOutput { output: String },
|
||||||
|
GetTagActive { tag_id: TagId },
|
||||||
|
GetTagName { tag_id: TagId },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum Modifiers {
|
pub enum Modifier {
|
||||||
Shift = 0b0000_0001,
|
Shift = 0b0000_0001,
|
||||||
Ctrl = 0b0000_0010,
|
Ctrl = 0b0000_0010,
|
||||||
Alt = 0b0000_0100,
|
Alt = 0b0000_0100,
|
||||||
Super = 0b0000_1000,
|
Super = 0b0000_1000,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A bitmask of [Modifiers] for the purpose of hashing.
|
/// A bitmask of [`Modifier`]s for the purpose of hashing.
|
||||||
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
|
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
|
||||||
pub struct ModifierMask(u8);
|
pub struct ModifierMask(u8);
|
||||||
|
|
||||||
impl<T: IntoIterator<Item = Modifiers>> From<T> for ModifierMask {
|
impl<T: IntoIterator<Item = Modifier>> From<T> for ModifierMask {
|
||||||
fn from(value: T) -> Self {
|
fn from(value: T) -> Self {
|
||||||
let value = value.into_iter();
|
let value = value.into_iter();
|
||||||
let mut mask: u8 = 0b0000_0000;
|
let mut mask: u8 = 0b0000_0000;
|
||||||
|
@ -114,19 +139,19 @@ impl<T: IntoIterator<Item = Modifiers>> From<T> for ModifierMask {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModifierMask {
|
impl ModifierMask {
|
||||||
pub fn values(self) -> Vec<Modifiers> {
|
pub fn values(self) -> Vec<Modifier> {
|
||||||
let mut res = Vec::<Modifiers>::new();
|
let mut res = Vec::<Modifier>::new();
|
||||||
if self.0 & Modifiers::Shift as u8 == Modifiers::Shift as u8 {
|
if self.0 & Modifier::Shift as u8 == Modifier::Shift as u8 {
|
||||||
res.push(Modifiers::Shift);
|
res.push(Modifier::Shift);
|
||||||
}
|
}
|
||||||
if self.0 & Modifiers::Ctrl as u8 == Modifiers::Ctrl as u8 {
|
if self.0 & Modifier::Ctrl as u8 == Modifier::Ctrl as u8 {
|
||||||
res.push(Modifiers::Ctrl);
|
res.push(Modifier::Ctrl);
|
||||||
}
|
}
|
||||||
if self.0 & Modifiers::Alt as u8 == Modifiers::Alt as u8 {
|
if self.0 & Modifier::Alt as u8 == Modifier::Alt as u8 {
|
||||||
res.push(Modifiers::Alt);
|
res.push(Modifier::Alt);
|
||||||
}
|
}
|
||||||
if self.0 & Modifiers::Super as u8 == Modifiers::Super as u8 {
|
if self.0 & Modifier::Super as u8 == Modifier::Super as u8 {
|
||||||
res.push(Modifiers::Super);
|
res.push(Modifier::Super);
|
||||||
}
|
}
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
@ -141,7 +166,6 @@ pub enum OutgoingMsg {
|
||||||
args: Option<Args>,
|
args: Option<Args>,
|
||||||
},
|
},
|
||||||
RequestResponse {
|
RequestResponse {
|
||||||
request_id: RequestId,
|
|
||||||
response: RequestResponse,
|
response: RequestResponse,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -159,10 +183,17 @@ pub enum Args {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
exit_msg: Option<String>,
|
exit_msg: Option<String>,
|
||||||
},
|
},
|
||||||
|
ConnectForAllOutputs {
|
||||||
|
output_name: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum RequestResponse {
|
pub enum RequestResponse {
|
||||||
Window { window: WindowProperties },
|
Window { window: WindowProperties },
|
||||||
GetAllWindows { windows: Vec<WindowProperties> },
|
GetAllWindows { windows: Vec<WindowProperties> },
|
||||||
|
Outputs { names: Vec<String> },
|
||||||
|
Tags { tags: Vec<TagProperties> },
|
||||||
|
TagActive { active: bool },
|
||||||
|
TagName { name: String },
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,6 +97,7 @@ use smithay_drm_extras::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
api::msg::{Args, OutgoingMsg},
|
||||||
render::{pointer::PointerElement, CustomRenderElements, OutputRenderElements},
|
render::{pointer::PointerElement, CustomRenderElements, OutputRenderElements},
|
||||||
state::{take_presentation_feedback, CalloopData, State, SurfaceDmabufFeedback},
|
state::{take_presentation_feedback, CalloopData, State, SurfaceDmabufFeedback},
|
||||||
};
|
};
|
||||||
|
@ -226,11 +227,6 @@ pub fn run_udev() -> Result<(), Box<dyn Error>> {
|
||||||
pointer_element: PointerElement::default(),
|
pointer_element: PointerElement::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
|
|
||||||
let mut state = State::<UdevData>::init(
|
let mut state = State::<UdevData>::init(
|
||||||
data,
|
data,
|
||||||
&mut display,
|
&mut display,
|
||||||
|
@ -852,6 +848,32 @@ impl State<UdevData> {
|
||||||
device_id: node,
|
device_id: node,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Run any connected callbacks
|
||||||
|
{
|
||||||
|
let clone = output.clone();
|
||||||
|
self.loop_handle.insert_idle(move |data| {
|
||||||
|
let stream = data
|
||||||
|
.state
|
||||||
|
.api_state
|
||||||
|
.stream
|
||||||
|
.as_ref()
|
||||||
|
.expect("Stream doesn't exist");
|
||||||
|
let mut stream = stream.lock().expect("Couldn't lock stream");
|
||||||
|
for callback_id in data.state.output_callback_ids.iter() {
|
||||||
|
crate::api::send_to_client(
|
||||||
|
&mut stream,
|
||||||
|
&OutgoingMsg::CallCallback {
|
||||||
|
callback_id: *callback_id,
|
||||||
|
args: Some(Args::ConnectForAllOutputs {
|
||||||
|
output_name: clone.name(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("Send to client failed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let allocator = GbmAllocator::new(
|
let allocator = GbmAllocator::new(
|
||||||
device.gbm.clone(),
|
device.gbm.clone(),
|
||||||
GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
|
GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
|
||||||
|
|
|
@ -47,7 +47,6 @@ use smithay::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
layout::{Direction, Layout},
|
|
||||||
render::{pointer::PointerElement, CustomRenderElements, OutputRenderElements},
|
render::{pointer::PointerElement, CustomRenderElements, OutputRenderElements},
|
||||||
state::{CalloopData, State},
|
state::{CalloopData, State},
|
||||||
};
|
};
|
||||||
|
@ -220,11 +219,7 @@ pub fn run_winit() -> Result<(), Box<dyn Error>> {
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
Layout::master_stack(
|
state.re_layout(&output);
|
||||||
state,
|
|
||||||
state.space.elements().cloned().collect(),
|
|
||||||
Direction::Left,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
WinitEvent::Focus(_) => {}
|
WinitEvent::Focus(_) => {}
|
||||||
WinitEvent::Input(input_evt) => {
|
WinitEvent::Input(input_evt) => {
|
||||||
|
|
|
@ -19,7 +19,11 @@ use smithay::{
|
||||||
utils::{IsAlive, Logical, Point, Rectangle},
|
utils::{IsAlive, Logical, Point, Rectangle},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{backend::Backend, state::State, window::window_state::WindowState};
|
use crate::{
|
||||||
|
backend::Backend,
|
||||||
|
state::{State, WithState},
|
||||||
|
window::window_state::WindowResizeState,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct MoveSurfaceGrab<S: SeatHandler> {
|
pub struct MoveSurfaceGrab<S: SeatHandler> {
|
||||||
pub start_data: GrabStartData<S>,
|
pub start_data: GrabStartData<S>,
|
||||||
|
@ -47,7 +51,7 @@ impl<B: Backend> PointerGrab<State<B>> for MoveSurfaceGrab<State<B>> {
|
||||||
// tracing::info!("window geo is: {:?}", self.window.geometry());
|
// tracing::info!("window geo is: {:?}", self.window.geometry());
|
||||||
// tracing::info!("loc is: {:?}", data.space.element_location(&self.window));
|
// tracing::info!("loc is: {:?}", data.space.element_location(&self.window));
|
||||||
|
|
||||||
let tiled = WindowState::with_state(&self.window, |state| state.floating.is_tiled());
|
let tiled = self.window.with_state(|state| state.floating.is_tiled());
|
||||||
|
|
||||||
if tiled {
|
if tiled {
|
||||||
// INFO: this is being used instead of space.element_under(event.location) because that
|
// INFO: this is being used instead of space.element_under(event.location) because that
|
||||||
|
@ -72,10 +76,16 @@ impl<B: Backend> PointerGrab<State<B>> for MoveSurfaceGrab<State<B>> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let window_under_floating =
|
let is_floating = window_under.with_state(|state| state.floating.is_floating());
|
||||||
WindowState::with_state(&window_under, |state| state.floating.is_floating());
|
|
||||||
|
|
||||||
if window_under_floating {
|
if is_floating {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_pending_resize = window_under
|
||||||
|
.with_state(|state| !matches!(state.resize_state, WindowResizeState::Idle));
|
||||||
|
|
||||||
|
if has_pending_resize {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,14 +96,6 @@ impl<B: Backend> PointerGrab<State<B>> for MoveSurfaceGrab<State<B>> {
|
||||||
let new_loc = self.initial_window_loc.to_f64() + delta;
|
let new_loc = self.initial_window_loc.to_f64() + delta;
|
||||||
data.space
|
data.space
|
||||||
.map_element(self.window.clone(), new_loc.to_i32_round(), true);
|
.map_element(self.window.clone(), new_loc.to_i32_round(), true);
|
||||||
// let loc = data
|
|
||||||
// .space
|
|
||||||
// .element_location(&self.window)
|
|
||||||
// .unwrap_or((0, 0).into());
|
|
||||||
// tracing::info!("new loc from element_location: {}, {}", loc.x, loc.y);
|
|
||||||
// let geo = self.window.geometry();
|
|
||||||
// tracing::info!("geo loc: {}, {}", geo.loc.x, geo.loc.y);
|
|
||||||
// tracing::info!("geo size: {}, {}", geo.size.w, geo.size.h);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,10 @@ use smithay::{
|
||||||
wayland::{compositor, seat::WaylandFocus, shell::xdg::SurfaceCachedState},
|
wayland::{compositor, seat::WaylandFocus, shell::xdg::SurfaceCachedState},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{backend::Backend, state::State, window::SurfaceState};
|
use crate::{
|
||||||
|
backend::Backend,
|
||||||
|
state::{State, WithState},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct ResizeSurfaceGrab<S: SeatHandler> {
|
pub struct ResizeSurfaceGrab<S: SeatHandler> {
|
||||||
start_data: GrabStartData<S>,
|
start_data: GrabStartData<S>,
|
||||||
|
@ -40,8 +43,8 @@ impl<S: SeatHandler> ResizeSurfaceGrab<S> {
|
||||||
initial_window_rect: Rectangle<i32, Logical>,
|
initial_window_rect: Rectangle<i32, Logical>,
|
||||||
button_used: u32,
|
button_used: u32,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
ResizeSurfaceState::with_state(window.toplevel().wl_surface(), |state| {
|
window.toplevel().wl_surface().with_state(|state| {
|
||||||
*state = ResizeSurfaceState::Resizing {
|
state.resize_state = ResizeSurfaceState::Resizing {
|
||||||
edges,
|
edges,
|
||||||
initial_window_rect,
|
initial_window_rect,
|
||||||
};
|
};
|
||||||
|
@ -169,8 +172,8 @@ impl<B: Backend> PointerGrab<State<B>> for ResizeSurfaceGrab<State<B>> {
|
||||||
|
|
||||||
toplevel_surface.send_pending_configure();
|
toplevel_surface.send_pending_configure();
|
||||||
|
|
||||||
ResizeSurfaceState::with_state(toplevel_surface.wl_surface(), |state| {
|
toplevel_surface.wl_surface().with_state(|state| {
|
||||||
*state = ResizeSurfaceState::WaitingForLastCommit {
|
state.resize_state = ResizeSurfaceState::WaitingForLastCommit {
|
||||||
edges: self.edges,
|
edges: self.edges,
|
||||||
initial_window_rect: self.initial_window_rect,
|
initial_window_rect: self.initial_window_rect,
|
||||||
};
|
};
|
||||||
|
@ -193,7 +196,7 @@ impl<B: Backend> PointerGrab<State<B>> for ResizeSurfaceGrab<State<B>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
|
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
enum ResizeSurfaceState {
|
pub enum ResizeSurfaceState {
|
||||||
#[default]
|
#[default]
|
||||||
Idle,
|
Idle,
|
||||||
Resizing {
|
Resizing {
|
||||||
|
@ -225,15 +228,14 @@ impl ResizeSurfaceState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SurfaceState for ResizeSurfaceState {}
|
|
||||||
|
|
||||||
pub fn handle_commit<B: Backend>(state: &mut State<B>, surface: &WlSurface) -> Option<()> {
|
pub fn handle_commit<B: Backend>(state: &mut State<B>, surface: &WlSurface) -> Option<()> {
|
||||||
let window = state.window_for_surface(surface)?;
|
let window = state.window_for_surface(surface)?;
|
||||||
let mut window_loc = state.space.element_location(&window)?;
|
let mut window_loc = state.space.element_location(&window)?;
|
||||||
let geometry = window.geometry();
|
let geometry = window.geometry();
|
||||||
|
|
||||||
let new_loc: Point<Option<i32>, Logical> = ResizeSurfaceState::with_state(surface, |state| {
|
let new_loc: Point<Option<i32>, Logical> = surface.with_state(|state| {
|
||||||
state
|
state
|
||||||
|
.resize_state
|
||||||
.commit()
|
.commit()
|
||||||
.map(|(edges, initial_window_rect)| {
|
.map(|(edges, initial_window_rect)| {
|
||||||
let mut new_x: Option<i32> = None;
|
let mut new_x: Option<i32> = None;
|
||||||
|
|
188
src/handlers.rs
188
src/handlers.rs
|
@ -4,14 +4,16 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use smithay::{
|
use smithay::{
|
||||||
backend::renderer::utils,
|
backend::renderer::utils,
|
||||||
delegate_compositor, delegate_data_device, delegate_fractional_scale, delegate_output,
|
delegate_compositor, delegate_data_device, delegate_fractional_scale, delegate_output,
|
||||||
delegate_presentation, delegate_relative_pointer, delegate_seat, delegate_shm,
|
delegate_presentation, delegate_relative_pointer, delegate_seat, delegate_shm,
|
||||||
delegate_viewporter, delegate_xdg_shell,
|
delegate_viewporter, delegate_xdg_shell,
|
||||||
desktop::{
|
desktop::{
|
||||||
find_popup_root_surface, PopupKeyboardGrab, PopupKind, PopupPointerGrab,
|
find_popup_root_surface, utils::surface_primary_scanout_output, PopupKeyboardGrab,
|
||||||
PopupUngrabStrategy, Window,
|
PopupKind, PopupPointerGrab, PopupUngrabStrategy, Window,
|
||||||
},
|
},
|
||||||
input::{
|
input::{
|
||||||
pointer::{CursorImageStatus, Focus},
|
pointer::{CursorImageStatus, Focus},
|
||||||
|
@ -19,7 +21,7 @@ use smithay::{
|
||||||
},
|
},
|
||||||
reexports::{
|
reexports::{
|
||||||
calloop::Interest,
|
calloop::Interest,
|
||||||
wayland_protocols::xdg::shell::server::xdg_toplevel::ResizeEdge,
|
wayland_protocols::xdg::shell::server::xdg_toplevel::{self, ResizeEdge},
|
||||||
wayland_server::{
|
wayland_server::{
|
||||||
protocol::{wl_buffer::WlBuffer, wl_seat::WlSeat, wl_surface::WlSurface},
|
protocol::{wl_buffer::WlBuffer, wl_seat::WlSeat, wl_surface::WlSurface},
|
||||||
Client, Resource,
|
Client, Resource,
|
||||||
|
@ -47,10 +49,8 @@ use smithay::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
layout::Layout,
|
state::{ClientState, State, WithState},
|
||||||
output::OutputState,
|
window::{window_state::WindowResizeState, WindowBlocker, BLOCKER_COUNTER},
|
||||||
state::{ClientState, State},
|
|
||||||
window::window_state::{WindowResizeState, WindowState},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl<B: Backend> BufferHandler for State<B> {
|
impl<B: Backend> BufferHandler for State<B> {
|
||||||
|
@ -96,7 +96,7 @@ impl<B: Backend> CompositorHandler for State<B> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit(&mut self, surface: &WlSurface) {
|
fn commit(&mut self, surface: &WlSurface) {
|
||||||
tracing::debug!("commit");
|
// tracing::debug!("commit");
|
||||||
|
|
||||||
utils::on_commit_buffer_handler::<Self>(surface);
|
utils::on_commit_buffer_handler::<Self>(surface);
|
||||||
|
|
||||||
|
@ -117,13 +117,19 @@ impl<B: Backend> CompositorHandler for State<B> {
|
||||||
crate::grab::resize_grab::handle_commit(self, surface);
|
crate::grab::resize_grab::handle_commit(self, surface);
|
||||||
|
|
||||||
if let Some(window) = self.window_for_surface(surface) {
|
if let Some(window) = self.window_for_surface(surface) {
|
||||||
WindowState::with_state(&window, |state| {
|
window.with_state(|state| {
|
||||||
if let WindowResizeState::WaitingForCommit(new_pos) = state.resize_state {
|
if let WindowResizeState::Acknowledged(new_pos) = state.resize_state {
|
||||||
state.resize_state = WindowResizeState::Idle;
|
state.resize_state = WindowResizeState::Idle;
|
||||||
self.space.map_element(window.clone(), new_pos, false);
|
self.space.map_element(window.clone(), new_pos, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// let states = self
|
||||||
|
// .windows
|
||||||
|
// .iter()
|
||||||
|
// .map(|win| win.with_state(|state| state.resize_state.clone()))
|
||||||
|
// .collect::<Vec<_>>();
|
||||||
|
// tracing::debug!("states: {states:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
|
fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
|
||||||
|
@ -225,25 +231,94 @@ impl<B: Backend> XdgShellHandler for State<B> {
|
||||||
fn new_toplevel(&mut self, surface: ToplevelSurface) {
|
fn new_toplevel(&mut self, surface: ToplevelSurface) {
|
||||||
let window = Window::new(surface);
|
let window = Window::new(surface);
|
||||||
|
|
||||||
WindowState::with_state(&window, |state| {
|
window.toplevel().with_pending_state(|tl_state| {
|
||||||
state.tags = if let Some(focused_output) = &self.focus_state.focused_output {
|
tl_state.states.set(xdg_toplevel::State::TiledTop);
|
||||||
OutputState::with(focused_output, |state| {
|
tl_state.states.set(xdg_toplevel::State::TiledBottom);
|
||||||
state
|
tl_state.states.set(xdg_toplevel::State::TiledLeft);
|
||||||
.focused_tags
|
tl_state.states.set(xdg_toplevel::State::TiledRight);
|
||||||
.iter()
|
});
|
||||||
.filter_map(|(id, active)| active.then_some(id.clone()))
|
window.with_state(|state| {
|
||||||
.collect()
|
state.tags = match (
|
||||||
})
|
&self.focus_state.focused_output,
|
||||||
} else if let Some(first_tag) = self.tag_state.tags.first() {
|
self.space.outputs().next(),
|
||||||
vec![first_tag.id.clone()]
|
) {
|
||||||
} else {
|
(Some(output), _) | (None, Some(output)) => output.with_state(|state| {
|
||||||
vec![]
|
let output_tags = state.focused_tags().cloned().collect::<Vec<_>>();
|
||||||
|
if !output_tags.is_empty() {
|
||||||
|
output_tags
|
||||||
|
} else if let Some(first_tag) = state.tags.first() {
|
||||||
|
vec![first_tag.clone()]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
(None, None) => vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::debug!("new window, tags are {:?}", state.tags);
|
tracing::debug!("new window, tags are {:?}", state.tags);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let windows_on_output = self
|
||||||
|
.windows
|
||||||
|
.iter()
|
||||||
|
.filter(|win| {
|
||||||
|
win.with_state(|state| {
|
||||||
|
self.focus_state
|
||||||
|
.focused_output
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.with_state(|op_state| {
|
||||||
|
op_state
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.any(|tag| state.tags.iter().any(|tg| tg == tag))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
self.windows.push(window.clone());
|
self.windows.push(window.clone());
|
||||||
self.space.map_element(window.clone(), (0, 0), true);
|
// self.space.map_element(window.clone(), (0, 0), true);
|
||||||
|
if let Some(focused_output) = self.focus_state.focused_output.clone() {
|
||||||
|
focused_output.with_state(|state| {
|
||||||
|
let first_tag = state.focused_tags().next();
|
||||||
|
if let Some(first_tag) = first_tag {
|
||||||
|
first_tag.layout().layout(
|
||||||
|
self.windows.clone(),
|
||||||
|
state.focused_tags().cloned().collect(),
|
||||||
|
&self.space,
|
||||||
|
&focused_output,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
BLOCKER_COUNTER.store(1, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
tracing::debug!(
|
||||||
|
"blocker {}",
|
||||||
|
BLOCKER_COUNTER.load(std::sync::atomic::Ordering::SeqCst)
|
||||||
|
);
|
||||||
|
for win in windows_on_output.iter() {
|
||||||
|
compositor::add_blocker(win.toplevel().wl_surface(), WindowBlocker);
|
||||||
|
}
|
||||||
|
let clone = window.clone();
|
||||||
|
self.loop_handle.insert_idle(|data| {
|
||||||
|
crate::state::schedule_on_commit(data, vec![clone], move |data| {
|
||||||
|
BLOCKER_COUNTER.store(0, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
tracing::debug!(
|
||||||
|
"blocker {}",
|
||||||
|
BLOCKER_COUNTER.load(std::sync::atomic::Ordering::SeqCst)
|
||||||
|
);
|
||||||
|
for client in windows_on_output
|
||||||
|
.iter()
|
||||||
|
.filter_map(|win| win.toplevel().wl_surface().client())
|
||||||
|
{
|
||||||
|
data.state
|
||||||
|
.client_compositor_state(&client)
|
||||||
|
.blocker_cleared(&mut data.state, &data.display.handle())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
self.loop_handle.insert_idle(move |data| {
|
self.loop_handle.insert_idle(move |data| {
|
||||||
data.state
|
data.state
|
||||||
.seat
|
.seat
|
||||||
|
@ -255,21 +330,28 @@ impl<B: Backend> XdgShellHandler for State<B> {
|
||||||
SERIAL_COUNTER.next_serial(),
|
SERIAL_COUNTER.next_serial(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let windows: Vec<Window> = self.space.elements().cloned().collect();
|
|
||||||
|
|
||||||
self.loop_handle.insert_idle(|data| {
|
|
||||||
tracing::debug!("Layout master_stack");
|
|
||||||
Layout::master_stack(&mut data.state, windows, crate::layout::Direction::Left);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
|
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
|
||||||
tracing::debug!("toplevel destroyed");
|
tracing::debug!("toplevel destroyed");
|
||||||
self.windows.retain(|window| window.toplevel() != &surface);
|
self.windows.retain(|window| window.toplevel() != &surface);
|
||||||
let mut windows: Vec<Window> = self.space.elements().cloned().collect();
|
if let Some(focused_output) = self.focus_state.focused_output.as_ref() {
|
||||||
windows.retain(|window| window.toplevel() != &surface);
|
focused_output.with_state(|state| {
|
||||||
Layout::master_stack(self, windows, crate::layout::Direction::Left);
|
let first_tag = state.focused_tags().next();
|
||||||
|
if let Some(first_tag) = first_tag {
|
||||||
|
first_tag.layout().layout(
|
||||||
|
self.windows.clone(),
|
||||||
|
state.focused_tags().cloned().collect(),
|
||||||
|
&self.space,
|
||||||
|
focused_output,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// let mut windows: Vec<Window> = self.space.elements().cloned().collect();
|
||||||
|
// windows.retain(|window| window.toplevel() != &surface);
|
||||||
|
// Layouts::master_stack(self, windows, crate::layout::Direction::Left);
|
||||||
let focus = self
|
let focus = self
|
||||||
.focus_state
|
.focus_state
|
||||||
.current_focus()
|
.current_focus()
|
||||||
|
@ -367,40 +449,30 @@ impl<B: Backend> XdgShellHandler for State<B> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ack_configure(&mut self, surface: WlSurface, configure: Configure) {
|
fn ack_configure(&mut self, surface: WlSurface, configure: Configure) {
|
||||||
tracing::debug!("start of ack_configure");
|
|
||||||
if let Some(window) = self.window_for_surface(&surface) {
|
if let Some(window) = self.window_for_surface(&surface) {
|
||||||
tracing::debug!("found window for surface");
|
window.with_state(|state| {
|
||||||
WindowState::with_state(&window, |state| {
|
if let WindowResizeState::Requested(serial, new_loc) = state.resize_state {
|
||||||
if let WindowResizeState::WaitingForAck(serial, new_loc) = state.resize_state {
|
|
||||||
match &configure {
|
match &configure {
|
||||||
Configure::Toplevel(configure) => {
|
Configure::Toplevel(configure) => {
|
||||||
if configure.serial >= serial {
|
if configure.serial >= serial {
|
||||||
tracing::debug!("acked configure, new loc is {:?}", new_loc);
|
// tracing::debug!("acked configure, new loc is {:?}", new_loc);
|
||||||
state.resize_state = WindowResizeState::WaitingForCommit(new_loc);
|
state.resize_state = WindowResizeState::Acknowledged(new_loc);
|
||||||
|
if let Some(focused_output) =
|
||||||
|
self.focus_state.focused_output.clone()
|
||||||
|
{
|
||||||
|
window.send_frame(
|
||||||
|
&focused_output,
|
||||||
|
self.clock.now(),
|
||||||
|
Some(Duration::ZERO),
|
||||||
|
surface_primary_scanout_output,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Configure::Popup(_) => todo!(),
|
Configure::Popup(_) => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// HACK: If a window is currently going through something that generates a bunch of
|
|
||||||
// | commits, like an animation, unmapping it while it's doing that has a chance
|
|
||||||
// | to cause any send_configures to not trigger a commit. I'm not sure if this is because of
|
|
||||||
// | the way I've implemented things or if it's something else. Because of me
|
|
||||||
// | mapping the element in commit, this means that the window won't reappear on a tag
|
|
||||||
// | change. The code below is a workaround until I can figure it out.
|
|
||||||
if !self.space.elements().any(|win| win == &window) {
|
|
||||||
WindowState::with_state(&window, |state| {
|
|
||||||
if let WindowResizeState::WaitingForCommit(new_loc) = state.resize_state {
|
|
||||||
tracing::debug!("remapping window");
|
|
||||||
let win = window.clone();
|
|
||||||
self.loop_handle.insert_idle(move |data| {
|
|
||||||
data.state.space.map_element(win, new_loc, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
12
src/input.rs
12
src/input.rs
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::api::msg::{CallbackId, ModifierMask, Modifiers, OutgoingMsg};
|
use crate::api::msg::{CallbackId, Modifier, ModifierMask, OutgoingMsg};
|
||||||
use smithay::{
|
use smithay::{
|
||||||
backend::input::{
|
backend::input::{
|
||||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent,
|
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent,
|
||||||
|
@ -221,18 +221,18 @@ impl<B: Backend> State<B> {
|
||||||
time,
|
time,
|
||||||
|state, modifiers, keysym| {
|
|state, modifiers, keysym| {
|
||||||
if press_state == KeyState::Pressed {
|
if press_state == KeyState::Pressed {
|
||||||
let mut modifier_mask = Vec::<Modifiers>::new();
|
let mut modifier_mask = Vec::<Modifier>::new();
|
||||||
if modifiers.alt {
|
if modifiers.alt {
|
||||||
modifier_mask.push(Modifiers::Alt);
|
modifier_mask.push(Modifier::Alt);
|
||||||
}
|
}
|
||||||
if modifiers.shift {
|
if modifiers.shift {
|
||||||
modifier_mask.push(Modifiers::Shift);
|
modifier_mask.push(Modifier::Shift);
|
||||||
}
|
}
|
||||||
if modifiers.ctrl {
|
if modifiers.ctrl {
|
||||||
modifier_mask.push(Modifiers::Ctrl);
|
modifier_mask.push(Modifier::Ctrl);
|
||||||
}
|
}
|
||||||
if modifiers.logo {
|
if modifiers.logo {
|
||||||
modifier_mask.push(Modifiers::Super);
|
modifier_mask.push(Modifier::Super);
|
||||||
}
|
}
|
||||||
let raw_sym = if keysym.raw_syms().len() == 1 {
|
let raw_sym = if keysym.raw_syms().len() == 1 {
|
||||||
keysym.raw_syms()[0]
|
keysym.raw_syms()[0]
|
||||||
|
|
624
src/layout.rs
624
src/layout.rs
|
@ -4,14 +4,622 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
pub mod automatic;
|
use itertools::{Either, Itertools};
|
||||||
pub mod manual;
|
use smithay::{
|
||||||
|
desktop::{Space, Window},
|
||||||
|
output::Output,
|
||||||
|
utils::{Logical, Size},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct Layout;
|
use crate::{
|
||||||
|
backend::Backend,
|
||||||
|
state::{State, WithState},
|
||||||
|
tag::Tag,
|
||||||
|
window::window_state::WindowResizeState,
|
||||||
|
};
|
||||||
|
|
||||||
pub enum Direction {
|
// TODO: couple this with the layouts
|
||||||
Left,
|
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||||
Right,
|
pub enum Layout {
|
||||||
Top,
|
MasterStack,
|
||||||
Bottom,
|
Dwindle,
|
||||||
|
Spiral,
|
||||||
|
CornerTopLeft,
|
||||||
|
CornerTopRight,
|
||||||
|
CornerBottomLeft,
|
||||||
|
CornerBottomRight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layout {
|
||||||
|
pub fn layout(
|
||||||
|
&self,
|
||||||
|
windows: Vec<Window>,
|
||||||
|
tags: Vec<Tag>,
|
||||||
|
space: &Space<Window>,
|
||||||
|
output: &Output,
|
||||||
|
) {
|
||||||
|
let windows = filter_windows(&windows, tags);
|
||||||
|
|
||||||
|
let Some(output_geo) = space.output_geometry(output) else {
|
||||||
|
tracing::error!("could not get output geometry");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let output_loc = output.current_location();
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Layout::MasterStack => {
|
||||||
|
let master = windows.first();
|
||||||
|
let stack = windows.iter().skip(1);
|
||||||
|
|
||||||
|
let Some(master) = master else { return };
|
||||||
|
|
||||||
|
let stack_count = stack.clone().count();
|
||||||
|
|
||||||
|
if stack_count == 0 {
|
||||||
|
// one window
|
||||||
|
master.toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some(output_geo.size);
|
||||||
|
});
|
||||||
|
|
||||||
|
master.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
master.toplevel().send_configure(),
|
||||||
|
(output_loc.x, output_loc.y).into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let new_master_size: Size<i32, Logical> =
|
||||||
|
(output_geo.size.w / 2, output_geo.size.h).into();
|
||||||
|
master.toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some(new_master_size);
|
||||||
|
});
|
||||||
|
master.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
master.toplevel().send_configure(),
|
||||||
|
(output_loc.x, output_loc.y).into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let stack_count = stack_count;
|
||||||
|
|
||||||
|
let height = output_geo.size.h as f32 / stack_count as f32;
|
||||||
|
let mut y_s = vec![];
|
||||||
|
for i in 0..stack_count {
|
||||||
|
y_s.push((i as f32 * height).round() as i32);
|
||||||
|
}
|
||||||
|
let heights = y_s
|
||||||
|
.windows(2)
|
||||||
|
.map(|pair| pair[1] - pair[0])
|
||||||
|
.chain(vec![output_geo.size.h - y_s.last().expect("vec was empty")])
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for (i, win) in stack.enumerate() {
|
||||||
|
win.toplevel().with_pending_state(|state| {
|
||||||
|
// INFO: Some windows crash the compositor if they become too short in height,
|
||||||
|
// | so they're limited to a minimum of 40 pixels as a workaround.
|
||||||
|
state.size =
|
||||||
|
Some((output_geo.size.w / 2, i32::max(heights[i], 40)).into());
|
||||||
|
});
|
||||||
|
|
||||||
|
win.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win.toplevel().send_configure(),
|
||||||
|
(output_geo.size.w / 2 + output_loc.x, y_s[i] + output_loc.y)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Layout::Dwindle => {
|
||||||
|
let mut iter = windows.windows(2).peekable();
|
||||||
|
|
||||||
|
if iter.peek().is_none() {
|
||||||
|
if let Some(window) = windows.first() {
|
||||||
|
window.toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some(output_geo.size);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
window.toplevel().send_configure(),
|
||||||
|
(output_loc.x, output_loc.y).into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (i, wins) in iter.enumerate() {
|
||||||
|
let win1 = &wins[0];
|
||||||
|
let win2 = &wins[1];
|
||||||
|
|
||||||
|
enum Slice {
|
||||||
|
Right,
|
||||||
|
Below,
|
||||||
|
}
|
||||||
|
|
||||||
|
let slice = if i % 2 == 0 {
|
||||||
|
Slice::Right
|
||||||
|
} else {
|
||||||
|
Slice::Below
|
||||||
|
};
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
win1.toplevel()
|
||||||
|
.with_pending_state(|state| state.size = Some(output_geo.size));
|
||||||
|
win1.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win1.toplevel().send_configure(),
|
||||||
|
output_loc,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let win1_size = win1.toplevel().with_pending_state(|state| {
|
||||||
|
state.size.expect("size should have been set")
|
||||||
|
});
|
||||||
|
let win1_loc = win1.with_state(|state| {
|
||||||
|
let WindowResizeState::Requested(_, loc) = state.resize_state else { unreachable!() };
|
||||||
|
loc
|
||||||
|
});
|
||||||
|
|
||||||
|
match slice {
|
||||||
|
Slice::Right => {
|
||||||
|
let width_partition = win1_size.w / 2;
|
||||||
|
win1.toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some(
|
||||||
|
(win1_size.w - width_partition, i32::max(win1_size.h, 40))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
win1.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win1.toplevel().send_configure(),
|
||||||
|
win1_loc,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
win2.toplevel().with_pending_state(|state| {
|
||||||
|
state.size =
|
||||||
|
Some((width_partition, i32::max(win1_size.h, 40)).into());
|
||||||
|
});
|
||||||
|
win2.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win2.toplevel().send_configure(),
|
||||||
|
(win1_loc.x + (win1_size.w - width_partition), win1_loc.y)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Slice::Below => {
|
||||||
|
let height_partition = win1_size.h / 2;
|
||||||
|
win1.toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some(
|
||||||
|
(win1_size.w, i32::max(win1_size.h - height_partition, 40))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
win1.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win1.toplevel().send_configure(),
|
||||||
|
win1_loc,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
win2.toplevel().with_pending_state(|state| {
|
||||||
|
state.size =
|
||||||
|
Some((win1_size.w, i32::max(height_partition, 40)).into());
|
||||||
|
});
|
||||||
|
win2.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win2.toplevel().send_configure(),
|
||||||
|
(win1_loc.x, win1_loc.y + (win1_size.h - height_partition))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Layout::Spiral => {
|
||||||
|
let mut iter = windows.windows(2).peekable();
|
||||||
|
|
||||||
|
if iter.peek().is_none() {
|
||||||
|
if let Some(window) = windows.first() {
|
||||||
|
window.toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some(output_geo.size);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
window.toplevel().send_configure(),
|
||||||
|
(output_loc.x, output_loc.y).into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (i, wins) in iter.enumerate() {
|
||||||
|
let win1 = &wins[0];
|
||||||
|
let win2 = &wins[1];
|
||||||
|
|
||||||
|
enum Slice {
|
||||||
|
Above,
|
||||||
|
Below,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
let slice = match i % 4 {
|
||||||
|
0 => Slice::Right,
|
||||||
|
1 => Slice::Below,
|
||||||
|
2 => Slice::Left,
|
||||||
|
3 => Slice::Above,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
win1.toplevel()
|
||||||
|
.with_pending_state(|state| state.size = Some(output_geo.size));
|
||||||
|
win1.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win1.toplevel().send_configure(),
|
||||||
|
output_loc,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let win1_size = win1.toplevel().with_pending_state(|state| {
|
||||||
|
state.size.expect("size should have been set")
|
||||||
|
});
|
||||||
|
let win1_loc = win1.with_state(|state| {
|
||||||
|
let WindowResizeState::Requested(_, loc) = state.resize_state else { unreachable!() };
|
||||||
|
loc
|
||||||
|
});
|
||||||
|
|
||||||
|
match slice {
|
||||||
|
Slice::Above => {
|
||||||
|
let height_partition = win1_size.h / 2;
|
||||||
|
win1.toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some(
|
||||||
|
(win1_size.w, i32::max(win1_size.h - height_partition, 40))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
win1.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win1.toplevel().send_configure(),
|
||||||
|
(win1_loc.x, win1_loc.y + height_partition).into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
win2.toplevel().with_pending_state(|state| {
|
||||||
|
state.size =
|
||||||
|
Some((win1_size.w, i32::max(height_partition, 40)).into());
|
||||||
|
});
|
||||||
|
win2.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win2.toplevel().send_configure(),
|
||||||
|
win1_loc,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Slice::Below => {
|
||||||
|
let height_partition = win1_size.h / 2;
|
||||||
|
win1.toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some(
|
||||||
|
(win1_size.w, win1_size.h - i32::max(height_partition, 40))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
win1.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win1.toplevel().send_configure(),
|
||||||
|
win1_loc,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
win2.toplevel().with_pending_state(|state| {
|
||||||
|
state.size =
|
||||||
|
Some((win1_size.w, i32::max(height_partition, 40)).into());
|
||||||
|
});
|
||||||
|
win2.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win2.toplevel().send_configure(),
|
||||||
|
(win1_loc.x, win1_loc.y + (win1_size.h - height_partition))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Slice::Left => {
|
||||||
|
let width_partition = win1_size.w / 2;
|
||||||
|
win1.toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some(
|
||||||
|
(win1_size.w - width_partition, i32::max(win1_size.h, 40))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
win1.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win1.toplevel().send_configure(),
|
||||||
|
(win1_loc.x + width_partition, win1_loc.y).into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
win2.toplevel().with_pending_state(|state| {
|
||||||
|
state.size =
|
||||||
|
Some((width_partition, i32::max(win1_size.h, 40)).into());
|
||||||
|
});
|
||||||
|
win2.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win2.toplevel().send_configure(),
|
||||||
|
win1_loc,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Slice::Right => {
|
||||||
|
let width_partition = win1_size.w / 2;
|
||||||
|
win1.toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some(
|
||||||
|
(win1_size.w - width_partition, i32::max(win1_size.h, 40))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
win1.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win1.toplevel().send_configure(),
|
||||||
|
win1_loc,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
win2.toplevel().with_pending_state(|state| {
|
||||||
|
state.size =
|
||||||
|
Some((width_partition, i32::max(win1_size.h, 40)).into());
|
||||||
|
});
|
||||||
|
win2.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win2.toplevel().send_configure(),
|
||||||
|
(win1_loc.x + (win1_size.w - width_partition), win1_loc.y)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layout @ (Layout::CornerTopLeft
|
||||||
|
| Layout::CornerTopRight
|
||||||
|
| Layout::CornerBottomLeft
|
||||||
|
| Layout::CornerBottomRight) => match windows.len() {
|
||||||
|
0 => (),
|
||||||
|
1 => {
|
||||||
|
windows[0].toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some(output_geo.size);
|
||||||
|
});
|
||||||
|
|
||||||
|
windows[0].with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
windows[0].toplevel().send_configure(),
|
||||||
|
(output_loc.x, output_loc.y).into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
windows[0].toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some((output_geo.size.w / 2, output_geo.size.h).into());
|
||||||
|
});
|
||||||
|
windows[0].with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
windows[0].toplevel().send_configure(),
|
||||||
|
(output_loc.x, output_loc.y).into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
windows[1].toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some((output_geo.size.w / 2, output_geo.size.h).into());
|
||||||
|
});
|
||||||
|
windows[1].with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
windows[1].toplevel().send_configure(),
|
||||||
|
(output_loc.x + output_geo.size.w / 2, output_loc.y).into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let mut windows = windows.into_iter();
|
||||||
|
let Some(corner) = windows.next() else { unreachable!() };
|
||||||
|
let (horiz_stack, vert_stack): (Vec<Window>, Vec<Window>) =
|
||||||
|
windows.enumerate().partition_map(|(i, win)| {
|
||||||
|
if i % 2 == 0 {
|
||||||
|
Either::Left(win)
|
||||||
|
} else {
|
||||||
|
Either::Right(win)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let div_factor = 2;
|
||||||
|
|
||||||
|
corner.toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some(
|
||||||
|
(
|
||||||
|
output_geo.size.w / div_factor,
|
||||||
|
output_geo.size.h / div_factor,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
corner.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
corner.toplevel().send_configure(),
|
||||||
|
match layout {
|
||||||
|
Layout::CornerTopLeft => (output_loc.x, output_loc.y),
|
||||||
|
Layout::CornerTopRight => (
|
||||||
|
output_loc.x + output_geo.size.w
|
||||||
|
- output_geo.size.w / div_factor,
|
||||||
|
output_loc.y,
|
||||||
|
),
|
||||||
|
Layout::CornerBottomLeft => (
|
||||||
|
output_loc.x,
|
||||||
|
output_loc.y + output_geo.size.h
|
||||||
|
- output_geo.size.h / div_factor,
|
||||||
|
),
|
||||||
|
Layout::CornerBottomRight => (
|
||||||
|
output_loc.x + output_geo.size.w
|
||||||
|
- output_geo.size.w / div_factor,
|
||||||
|
output_loc.y + output_geo.size.h
|
||||||
|
- output_geo.size.h / div_factor,
|
||||||
|
),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let vert_stack_count = vert_stack.len();
|
||||||
|
|
||||||
|
let height = output_geo.size.h as f32 / vert_stack_count as f32;
|
||||||
|
let mut y_s = vec![];
|
||||||
|
for i in 0..vert_stack_count {
|
||||||
|
y_s.push((i as f32 * height).round() as i32);
|
||||||
|
}
|
||||||
|
let heights = y_s
|
||||||
|
.windows(2)
|
||||||
|
.map(|pair| pair[1] - pair[0])
|
||||||
|
.chain(vec![output_geo.size.h - y_s.last().expect("vec was empty")])
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for (i, win) in vert_stack.iter().enumerate() {
|
||||||
|
win.toplevel().with_pending_state(|state| {
|
||||||
|
// INFO: Some windows crash the compositor if they become too short in height,
|
||||||
|
// | so they're limited to a minimum of 40 pixels as a workaround.
|
||||||
|
state.size =
|
||||||
|
Some((output_geo.size.w / 2, i32::max(heights[i], 40)).into());
|
||||||
|
});
|
||||||
|
|
||||||
|
win.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win.toplevel().send_configure(),
|
||||||
|
(
|
||||||
|
match layout {
|
||||||
|
Layout::CornerTopLeft | Layout::CornerBottomLeft => {
|
||||||
|
output_geo.size.w / 2 + output_loc.x
|
||||||
|
}
|
||||||
|
Layout::CornerTopRight | Layout::CornerBottomRight => {
|
||||||
|
output_loc.x
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
y_s[i] + output_loc.y,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let horiz_stack_count = horiz_stack.len();
|
||||||
|
|
||||||
|
let width = output_geo.size.w as f32 / 2.0 / horiz_stack_count as f32;
|
||||||
|
let mut x_s = vec![];
|
||||||
|
for i in 0..horiz_stack_count {
|
||||||
|
x_s.push((i as f32 * width).round() as i32);
|
||||||
|
}
|
||||||
|
let widths = x_s
|
||||||
|
.windows(2)
|
||||||
|
.map(|pair| pair[1] - pair[0])
|
||||||
|
.chain(vec![
|
||||||
|
output_geo.size.w / 2 - x_s.last().expect("vec was empty"),
|
||||||
|
])
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for (i, win) in horiz_stack.iter().enumerate() {
|
||||||
|
win.toplevel().with_pending_state(|state| {
|
||||||
|
// INFO: Some windows crash the compositor if they become too short in height,
|
||||||
|
// | so they're limited to a minimum of 40 pixels as a workaround.
|
||||||
|
state.size =
|
||||||
|
Some((i32::max(widths[i], 1), output_geo.size.h / 2).into());
|
||||||
|
});
|
||||||
|
|
||||||
|
win.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(
|
||||||
|
win.toplevel().send_configure(),
|
||||||
|
match layout {
|
||||||
|
Layout::CornerTopLeft => (
|
||||||
|
x_s[i] + output_loc.x,
|
||||||
|
output_loc.y + output_geo.size.h / 2,
|
||||||
|
),
|
||||||
|
Layout::CornerTopRight => (
|
||||||
|
x_s[i] + output_loc.x + output_geo.size.w / 2,
|
||||||
|
output_loc.y + output_geo.size.h / 2,
|
||||||
|
),
|
||||||
|
Layout::CornerBottomLeft => {
|
||||||
|
(x_s[i] + output_loc.x, output_loc.y)
|
||||||
|
}
|
||||||
|
Layout::CornerBottomRight => (
|
||||||
|
x_s[i] + output_loc.x + output_geo.size.w / 2,
|
||||||
|
output_loc.y,
|
||||||
|
),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter_windows(windows: &[Window], tags: Vec<Tag>) -> Vec<Window> {
|
||||||
|
windows
|
||||||
|
.iter()
|
||||||
|
.filter(|window| {
|
||||||
|
window.with_state(|state| {
|
||||||
|
state.floating.is_tiled() && {
|
||||||
|
for tag in state.tags.iter() {
|
||||||
|
if tags.iter().any(|tg| tg == tag) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: Backend> State<B> {
|
||||||
|
pub fn swap_window_positions(&mut self, win1: &Window, win2: &Window) {
|
||||||
|
// FIXME: moving the mouse quickly will break swapping
|
||||||
|
|
||||||
|
let win1_loc = self.space.element_location(win1).unwrap(); // TODO: handle unwraps
|
||||||
|
let win2_loc = self.space.element_location(win2).unwrap();
|
||||||
|
let win1_geo = win1.geometry();
|
||||||
|
let win2_geo = win2.geometry();
|
||||||
|
|
||||||
|
win1.toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some(win2_geo.size);
|
||||||
|
});
|
||||||
|
win2.toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some(win1_geo.size);
|
||||||
|
});
|
||||||
|
|
||||||
|
let serial = win1.toplevel().send_configure();
|
||||||
|
win1.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(serial, win2_loc);
|
||||||
|
});
|
||||||
|
|
||||||
|
let serial = win2.toplevel().send_configure();
|
||||||
|
win2.with_state(|state| {
|
||||||
|
state.resize_state = WindowResizeState::Requested(serial, win1_loc);
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut elems = self
|
||||||
|
.windows
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|win| *win == win1 || *win == win2);
|
||||||
|
|
||||||
|
let (first, second) = (elems.next(), elems.next());
|
||||||
|
|
||||||
|
if let Some(first) = first {
|
||||||
|
if let Some(second) = second {
|
||||||
|
std::mem::swap(first, second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,160 +0,0 @@
|
||||||
// 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/.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
use smithay::{
|
|
||||||
desktop::Window,
|
|
||||||
wayland::{compositor, shell::xdg::XdgToplevelSurfaceData},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
backend::Backend,
|
|
||||||
state::State,
|
|
||||||
window::window_state::{WindowResizeState, WindowState},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{Direction, Layout};
|
|
||||||
|
|
||||||
impl Layout {
|
|
||||||
pub fn master_stack<B: Backend>(
|
|
||||||
state: &mut State<B>,
|
|
||||||
mut windows: Vec<Window>,
|
|
||||||
side: Direction,
|
|
||||||
) {
|
|
||||||
windows.retain(|win| WindowState::with_state(win, |state| state.floating.is_tiled()));
|
|
||||||
match side {
|
|
||||||
Direction::Left => {
|
|
||||||
let window_count = windows.len();
|
|
||||||
if window_count == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// TODO: change focused_output to be not an option
|
|
||||||
let Some(output) = state
|
|
||||||
.focus_state
|
|
||||||
.focused_output
|
|
||||||
.as_ref()
|
|
||||||
.or_else(|| state.space.outputs().next())
|
|
||||||
else {
|
|
||||||
tracing::warn!("no connected outputs");
|
|
||||||
return;
|
|
||||||
// TODO: no idea what happens if you spawn a window while no monitors are
|
|
||||||
// | connected, figure that out
|
|
||||||
};
|
|
||||||
let output_size = state.space.output_geometry(output).unwrap().size;
|
|
||||||
if window_count == 1 {
|
|
||||||
tracing::debug!("Laying out only window");
|
|
||||||
let window = windows[0].clone();
|
|
||||||
|
|
||||||
window.toplevel().with_pending_state(|tl_state| {
|
|
||||||
tl_state.size = Some(state.space.output_geometry(output).unwrap().size);
|
|
||||||
tracing::debug!("only size is {:?}", tl_state.size);
|
|
||||||
});
|
|
||||||
|
|
||||||
let initial_configure_sent =
|
|
||||||
compositor::with_states(window.toplevel().wl_surface(), |states| {
|
|
||||||
states
|
|
||||||
.data_map
|
|
||||||
.get::<XdgToplevelSurfaceData>()
|
|
||||||
.unwrap()
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.initial_configure_sent
|
|
||||||
});
|
|
||||||
tracing::debug!("initial configure sent is {initial_configure_sent}");
|
|
||||||
if initial_configure_sent {
|
|
||||||
WindowState::with_state(&window, |state| {
|
|
||||||
tracing::debug!("sending configure");
|
|
||||||
state.resize_state = WindowResizeState::WaitingForAck(
|
|
||||||
window.toplevel().send_configure(),
|
|
||||||
output.current_location(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::debug!("layed out first window");
|
|
||||||
let mut windows = windows.iter();
|
|
||||||
let first_window = windows.next().unwrap();
|
|
||||||
|
|
||||||
first_window.toplevel().with_pending_state(|tl_state| {
|
|
||||||
let mut size = state.space.output_geometry(output).unwrap().size;
|
|
||||||
size.w /= 2;
|
|
||||||
tl_state.size = Some(size);
|
|
||||||
tracing::debug!("first size is {:?}", tl_state.size);
|
|
||||||
});
|
|
||||||
|
|
||||||
let initial_configure_sent =
|
|
||||||
compositor::with_states(first_window.toplevel().wl_surface(), |states| {
|
|
||||||
states
|
|
||||||
.data_map
|
|
||||||
.get::<XdgToplevelSurfaceData>()
|
|
||||||
.unwrap()
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.initial_configure_sent
|
|
||||||
});
|
|
||||||
if initial_configure_sent {
|
|
||||||
WindowState::with_state(first_window, |state| {
|
|
||||||
tracing::debug!("sending resize state");
|
|
||||||
state.resize_state = WindowResizeState::WaitingForAck(
|
|
||||||
first_window.toplevel().send_configure(),
|
|
||||||
output.current_location(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let window_count = windows.len() as i32;
|
|
||||||
let height = output_size.h / window_count;
|
|
||||||
let x = output.current_location().x + output_size.w / 2;
|
|
||||||
|
|
||||||
for (i, win) in windows.enumerate() {
|
|
||||||
win.toplevel().with_pending_state(|state| {
|
|
||||||
let mut new_size = output_size;
|
|
||||||
new_size.w /= 2;
|
|
||||||
new_size.w = new_size.w.clamp(1, i32::MAX);
|
|
||||||
new_size.h /= window_count;
|
|
||||||
// INFO: The newest window won't have its geometry.loc set until after here and I don't know
|
|
||||||
// | why, so this is hardcoded to 40. I don't anticipate people using
|
|
||||||
// | windows that are that short, so figuring it out is low priority.
|
|
||||||
// | Kitty specifically will crash the compositor if it's resized such
|
|
||||||
// | that the bottom border goes above the bottom of the title bar if
|
|
||||||
// | this is set too low.
|
|
||||||
new_size.h = new_size.h.clamp(40, i32::MAX);
|
|
||||||
state.size = Some(new_size);
|
|
||||||
tracing::debug!("size is {:?}", state.size);
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut new_loc = output.current_location();
|
|
||||||
new_loc.x = x;
|
|
||||||
new_loc.y = (i as i32) * height;
|
|
||||||
|
|
||||||
let initial_configure_sent =
|
|
||||||
compositor::with_states(win.toplevel().wl_surface(), |states| {
|
|
||||||
states
|
|
||||||
.data_map
|
|
||||||
.get::<XdgToplevelSurfaceData>()
|
|
||||||
.unwrap()
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.initial_configure_sent
|
|
||||||
});
|
|
||||||
if initial_configure_sent {
|
|
||||||
WindowState::with_state(win, |state| {
|
|
||||||
state.resize_state = WindowResizeState::WaitingForAck(
|
|
||||||
win.toplevel().send_configure(),
|
|
||||||
new_loc,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Direction::Right => todo!(),
|
|
||||||
Direction::Top => todo!(),
|
|
||||||
Direction::Bottom => todo!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
// 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/.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
//
|
|
||||||
// love how i'm licensing this empty file
|
|
|
@ -4,31 +4,41 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use std::{cell::RefCell, collections::HashMap};
|
use std::cell::RefCell;
|
||||||
|
|
||||||
use smithay::output::Output;
|
use smithay::output::Output;
|
||||||
|
|
||||||
use crate::tag::TagId;
|
use crate::{state::WithState, tag::Tag};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct OutputName(pub String);
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct OutputState {
|
pub struct OutputState {
|
||||||
pub focused_tags: HashMap<TagId, bool>,
|
pub tags: Vec<Tag>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutputState {
|
impl WithState for Output {
|
||||||
pub fn with<F, T>(output: &Output, mut func: F) -> T
|
type State = OutputState;
|
||||||
where
|
|
||||||
F: FnMut(&mut Self) -> T,
|
|
||||||
{
|
|
||||||
output
|
|
||||||
.user_data()
|
|
||||||
.insert_if_missing(RefCell::<Self>::default);
|
|
||||||
|
|
||||||
let state = output
|
fn with_state<F, T>(&self, mut func: F) -> T
|
||||||
|
where
|
||||||
|
F: FnMut(&mut Self::State) -> T,
|
||||||
|
{
|
||||||
|
self.user_data()
|
||||||
|
.insert_if_missing(RefCell::<Self::State>::default);
|
||||||
|
|
||||||
|
let state = self
|
||||||
.user_data()
|
.user_data()
|
||||||
.get::<RefCell<Self>>()
|
.get::<RefCell<Self::State>>()
|
||||||
.expect("RefCell doesn't exist in data map (This should NEVER happen. If you see this, something oofed big-time.)");
|
.expect("RefCell not in data map");
|
||||||
|
|
||||||
func(&mut state.borrow_mut())
|
func(&mut state.borrow_mut())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OutputState {
|
||||||
|
pub fn focused_tags(&self) -> impl Iterator<Item = &Tag> {
|
||||||
|
self.tags.iter().filter(|tag| tag.active())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
771
src/state.rs
771
src/state.rs
|
@ -5,6 +5,7 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
error::Error,
|
error::Error,
|
||||||
ffi::OsString,
|
ffi::OsString,
|
||||||
os::{fd::AsRawFd, unix::net::UnixStream},
|
os::{fd::AsRawFd, unix::net::UnixStream},
|
||||||
|
@ -18,12 +19,10 @@ use crate::{
|
||||||
msg::{Args, CallbackId, Msg, OutgoingMsg, Request, RequestResponse},
|
msg::{Args, CallbackId, Msg, OutgoingMsg, Request, RequestResponse},
|
||||||
PinnacleSocketSource,
|
PinnacleSocketSource,
|
||||||
},
|
},
|
||||||
backend::{udev::UdevData, winit::WinitData},
|
|
||||||
focus::FocusState,
|
focus::FocusState,
|
||||||
layout::Layout,
|
grab::resize_grab::ResizeSurfaceState,
|
||||||
output::OutputState,
|
tag::{Tag, TagProperties},
|
||||||
tag::{Tag, TagState},
|
window::{window_state::WindowResizeState, WindowProperties},
|
||||||
window::{window_state::WindowState, WindowProperties},
|
|
||||||
};
|
};
|
||||||
use calloop::futures::Scheduler;
|
use calloop::futures::Scheduler;
|
||||||
use futures_lite::AsyncBufReadExt;
|
use futures_lite::AsyncBufReadExt;
|
||||||
|
@ -45,6 +44,7 @@ use smithay::{
|
||||||
},
|
},
|
||||||
wayland_server::{
|
wayland_server::{
|
||||||
backend::{ClientData, ClientId, DisconnectReason},
|
backend::{ClientData, ClientId, DisconnectReason},
|
||||||
|
protocol::wl_surface::WlSurface,
|
||||||
Display,
|
Display,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -89,7 +89,6 @@ pub struct State<B: Backend> {
|
||||||
pub input_state: InputState,
|
pub input_state: InputState,
|
||||||
pub api_state: ApiState,
|
pub api_state: ApiState,
|
||||||
pub focus_state: FocusState,
|
pub focus_state: FocusState,
|
||||||
pub tag_state: TagState,
|
|
||||||
|
|
||||||
pub popup_manager: PopupManager,
|
pub popup_manager: PopupManager,
|
||||||
|
|
||||||
|
@ -98,10 +97,15 @@ pub struct State<B: Backend> {
|
||||||
pub windows: Vec<Window>,
|
pub windows: Vec<Window>,
|
||||||
|
|
||||||
pub async_scheduler: Scheduler<()>,
|
pub async_scheduler: Scheduler<()>,
|
||||||
|
|
||||||
|
// TODO: move into own struct
|
||||||
|
// | basically just clean this mess up
|
||||||
|
pub output_callback_ids: Vec<CallbackId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B: Backend> State<B> {
|
impl<B: Backend> State<B> {
|
||||||
pub fn handle_msg(&mut self, msg: Msg) {
|
pub fn handle_msg(&mut self, msg: Msg) {
|
||||||
|
// tracing::debug!("Got {msg:?}");
|
||||||
match msg {
|
match msg {
|
||||||
Msg::SetKeybind {
|
Msg::SetKeybind {
|
||||||
key,
|
key,
|
||||||
|
@ -137,7 +141,7 @@ impl<B: Backend> State<B> {
|
||||||
|
|
||||||
Msg::SetWindowSize { window_id, size } => {
|
Msg::SetWindowSize { window_id, size } => {
|
||||||
let Some(window) = self.space.elements().find(|&win| {
|
let Some(window) = self.space.elements().find(|&win| {
|
||||||
WindowState::with_state(win, |state| state.id == window_id)
|
win.with_state( |state| state.id == window_id)
|
||||||
}) else { return; };
|
}) else { return; };
|
||||||
|
|
||||||
// TODO: tiled vs floating
|
// TODO: tiled vs floating
|
||||||
|
@ -150,87 +154,176 @@ impl<B: Backend> State<B> {
|
||||||
if let Some(window) = self
|
if let Some(window) = self
|
||||||
.windows
|
.windows
|
||||||
.iter()
|
.iter()
|
||||||
.find(|&win| WindowState::with_state(win, |state| state.id == window_id))
|
.find(|&win| win.with_state(|state| state.id == window_id))
|
||||||
{
|
{
|
||||||
WindowState::with_state(window, |state| {
|
window.with_state(|state| {
|
||||||
state.tags = vec![tag_id.clone()];
|
self.focus_state
|
||||||
|
.focused_output
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.with_state(|op_state| {
|
||||||
|
let tag = op_state.tags.iter().find(|tag| tag.name() == tag_id);
|
||||||
|
if let Some(tag) = tag {
|
||||||
|
state.tags = vec![tag.clone()];
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.re_layout();
|
let output = self.focus_state.focused_output.clone().unwrap();
|
||||||
|
self.re_layout(&output);
|
||||||
}
|
}
|
||||||
Msg::ToggleTagOnWindow { window_id, tag_id } => {
|
Msg::ToggleTagOnWindow { window_id, tag_id } => {
|
||||||
if let Some(window) = self
|
if let Some(window) = self
|
||||||
.windows
|
.windows
|
||||||
.iter()
|
.iter()
|
||||||
.find(|&win| WindowState::with_state(win, |state| state.id == window_id))
|
.find(|&win| win.with_state(|state| state.id == window_id))
|
||||||
{
|
{
|
||||||
WindowState::with_state(window, |state| {
|
window.with_state(|state| {
|
||||||
if state.tags.contains(&tag_id) {
|
self.focus_state
|
||||||
state.tags.retain(|id| id != &tag_id);
|
.focused_output
|
||||||
} else {
|
.as_ref()
|
||||||
state.tags.push(tag_id.clone());
|
.unwrap()
|
||||||
}
|
.with_state(|op_state| {
|
||||||
|
let tag = op_state.tags.iter().find(|tag| tag.name() == tag_id);
|
||||||
|
if let Some(tag) = tag {
|
||||||
|
if state.tags.contains(tag) {
|
||||||
|
state.tags.retain(|tg| tg != tag);
|
||||||
|
} else {
|
||||||
|
state.tags.push(tag.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
self.re_layout();
|
let output = self.focus_state.focused_output.clone().unwrap();
|
||||||
|
self.re_layout(&output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Msg::ToggleTag { tag_id } => {
|
Msg::ToggleTag { output_name, tag_name } => {
|
||||||
OutputState::with(
|
tracing::debug!("ToggleTag");
|
||||||
self.focus_state.focused_output.as_ref().unwrap(), // TODO: handle error
|
|
||||||
|state| match state.focused_tags.get_mut(&tag_id) {
|
|
||||||
Some(id) => {
|
|
||||||
*id = !*id;
|
|
||||||
tracing::debug!(
|
|
||||||
"toggled tag {tag_id:?} {}",
|
|
||||||
if *id { "on" } else { "off" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
state.focused_tags.insert(tag_id.clone(), true);
|
|
||||||
tracing::debug!("toggled tag {tag_id:?} on");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
self.re_layout();
|
let output = self.space.outputs().find(|op| op.name() == output_name).cloned();
|
||||||
}
|
if let Some(output) = output {
|
||||||
Msg::SwitchToTag { tag_id } => {
|
|
||||||
OutputState::with(self.focus_state.focused_output.as_ref().unwrap(), |state| {
|
|
||||||
for (_, active) in state.focused_tags.iter_mut() {
|
|
||||||
*active = false;
|
|
||||||
}
|
|
||||||
if let Some(active) = state.focused_tags.get_mut(&tag_id) {
|
|
||||||
*active = true;
|
|
||||||
} else {
|
|
||||||
state.focused_tags.insert(tag_id.clone(), true);
|
|
||||||
}
|
|
||||||
tracing::debug!("focused tags: {:?}", state.focused_tags);
|
|
||||||
});
|
|
||||||
|
|
||||||
self.re_layout();
|
output.with_state(|state| {
|
||||||
|
if let Some(tag) = state.tags.iter_mut().find(|tag| tag.name() == tag_name) {
|
||||||
|
tracing::debug!("Setting tag {tag:?} to {}", !tag.active());
|
||||||
|
tag.set_active(!tag.active());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.re_layout(&output);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Msg::AddTags { tags } => {
|
Msg::SwitchToTag { output_name, tag_name } => {
|
||||||
self.tag_state.tags.extend(tags.into_iter().map(|tag| Tag {
|
let output = self.space.outputs().find(|op| op.name() == output_name).cloned();
|
||||||
id: tag,
|
if let Some(output) = output {
|
||||||
windows: vec![],
|
|
||||||
}));
|
output.with_state(|state| {
|
||||||
|
if !state.tags.iter().any(|tag| tag.name() == tag_name) {
|
||||||
|
// TODO: notify error
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for tag in state.tags.iter_mut() {
|
||||||
|
tag.set_active(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(tag) = state.tags.iter_mut().find(|tag| tag.name() == tag_name) else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
tag.set_active(true);
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"focused tags: {:?}",
|
||||||
|
state
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.filter(|tag| tag.active())
|
||||||
|
.map(|tag| tag.name())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
self.re_layout(&output);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Msg::RemoveTags { tags } => {
|
// TODO: add output
|
||||||
self.tag_state.tags.retain(|tag| !tags.contains(&tag.id));
|
Msg::AddTags { output_name, tags } => {
|
||||||
|
if let Some(output) = self
|
||||||
|
.space
|
||||||
|
.outputs()
|
||||||
|
.find(|output| output.name() == output_name)
|
||||||
|
{
|
||||||
|
output.with_state(|state| {
|
||||||
|
state
|
||||||
|
.tags
|
||||||
|
.extend(tags.iter().cloned().map(Tag::new));
|
||||||
|
tracing::debug!("tags added, are now {:?}", state.tags);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Msg::RemoveTags { output_name, tags } => {
|
||||||
|
if let Some(output) = self
|
||||||
|
.space
|
||||||
|
.outputs()
|
||||||
|
.find(|output| output.name() == output_name)
|
||||||
|
{
|
||||||
|
output.with_state(|state| {
|
||||||
|
state.tags.retain(|tag| !tags.contains(&tag.name()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Msg::SetLayout { output_name, tag_name, layout } => {
|
||||||
|
let output = self.space.outputs().find(|op| op.name() == output_name).cloned();
|
||||||
|
if let Some(output) = output {
|
||||||
|
|
||||||
|
output.with_state(|state| {
|
||||||
|
if let Some(tag) = state.tags.iter_mut().find(|tag| tag.name() == tag_name) {
|
||||||
|
tag.set_layout(layout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.re_layout(&output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Msg::ConnectForAllOutputs { callback_id } => {
|
||||||
|
let stream = self
|
||||||
|
.api_state
|
||||||
|
.stream
|
||||||
|
.as_ref()
|
||||||
|
.expect("Stream doesn't exist");
|
||||||
|
let mut stream = stream.lock().expect("Couldn't lock stream");
|
||||||
|
for output in self.space.outputs() {
|
||||||
|
crate::api::send_to_client(
|
||||||
|
&mut stream,
|
||||||
|
&OutgoingMsg::CallCallback {
|
||||||
|
callback_id,
|
||||||
|
args: Some(Args::ConnectForAllOutputs {
|
||||||
|
output_name: output.name(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("Send to client failed");
|
||||||
|
}
|
||||||
|
self.output_callback_ids.push(callback_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Msg::Quit => {
|
Msg::Quit => {
|
||||||
self.loop_signal.stop();
|
self.loop_signal.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
Msg::Request(request) => match request {
|
Msg::Request(request) => {
|
||||||
Request::GetWindowByAppId { id, app_id } => todo!(),
|
let stream = self
|
||||||
Request::GetWindowByTitle { id, title } => todo!(),
|
.api_state
|
||||||
Request::GetWindowByFocus { id } => {
|
.stream
|
||||||
let Some(current_focus) = self.focus_state.current_focus() else { return; };
|
.as_ref()
|
||||||
let (app_id, title) =
|
.expect("Stream doesn't exist");
|
||||||
|
let mut stream = stream.lock().expect("Couldn't lock stream");
|
||||||
|
match request {
|
||||||
|
Request::GetWindowByAppId { app_id } => todo!(),
|
||||||
|
Request::GetWindowByTitle { title } => todo!(),
|
||||||
|
Request::GetWindowByFocus => {
|
||||||
|
let Some(current_focus) = self.focus_state.current_focus() else { return; };
|
||||||
|
let (app_id, title) =
|
||||||
compositor::with_states(current_focus.toplevel().wl_surface(), |states| {
|
compositor::with_states(current_focus.toplevel().wl_surface(), |states| {
|
||||||
let lock = states
|
let lock = states
|
||||||
.data_map
|
.data_map
|
||||||
|
@ -240,40 +333,32 @@ impl<B: Backend> State<B> {
|
||||||
.expect("Couldn't lock XdgToplevelSurfaceData");
|
.expect("Couldn't lock XdgToplevelSurfaceData");
|
||||||
(lock.app_id.clone(), lock.title.clone())
|
(lock.app_id.clone(), lock.title.clone())
|
||||||
});
|
});
|
||||||
let (window_id, floating) = WindowState::with_state(¤t_focus, |state| {
|
let (window_id, floating) =
|
||||||
(state.id, state.floating.is_floating())
|
current_focus.with_state(|state| (state.id, state.floating.is_floating()));
|
||||||
});
|
// TODO: unwrap
|
||||||
// TODO: unwrap
|
let location = self.space.element_location(¤t_focus).unwrap();
|
||||||
let location = self.space.element_location(¤t_focus).unwrap();
|
let props = WindowProperties {
|
||||||
let props = WindowProperties {
|
id: window_id,
|
||||||
id: window_id,
|
app_id,
|
||||||
app_id,
|
title,
|
||||||
title,
|
size: current_focus.geometry().size.into(),
|
||||||
size: current_focus.geometry().size.into(),
|
location: location.into(),
|
||||||
location: location.into(),
|
floating,
|
||||||
floating,
|
};
|
||||||
};
|
crate::api::send_to_client(
|
||||||
let stream = self
|
&mut stream,
|
||||||
.api_state
|
&OutgoingMsg::RequestResponse {
|
||||||
.stream
|
response: RequestResponse::Window { window: props },
|
||||||
.as_ref()
|
},
|
||||||
.expect("Stream doesn't exist");
|
)
|
||||||
let mut stream = stream.lock().expect("Couldn't lock stream");
|
.expect("Send to client failed");
|
||||||
crate::api::send_to_client(
|
}
|
||||||
&mut stream,
|
Request::GetAllWindows => {
|
||||||
&OutgoingMsg::RequestResponse {
|
let window_props = self
|
||||||
request_id: id,
|
.space
|
||||||
response: RequestResponse::Window { window: props },
|
.elements()
|
||||||
},
|
.map(|win| {
|
||||||
)
|
let (app_id, title) =
|
||||||
.expect("Send to client failed");
|
|
||||||
}
|
|
||||||
Request::GetAllWindows { id } => {
|
|
||||||
let window_props = self
|
|
||||||
.space
|
|
||||||
.elements()
|
|
||||||
.map(|win| {
|
|
||||||
let (app_id, title) =
|
|
||||||
compositor::with_states(win.toplevel().wl_surface(), |states| {
|
compositor::with_states(win.toplevel().wl_surface(), |states| {
|
||||||
let lock = states
|
let lock = states
|
||||||
.data_map
|
.data_map
|
||||||
|
@ -283,42 +368,168 @@ impl<B: Backend> State<B> {
|
||||||
.expect("Couldn't lock XdgToplevelSurfaceData");
|
.expect("Couldn't lock XdgToplevelSurfaceData");
|
||||||
(lock.app_id.clone(), lock.title.clone())
|
(lock.app_id.clone(), lock.title.clone())
|
||||||
});
|
});
|
||||||
let (window_id, floating) = WindowState::with_state(win, |state| {
|
let (window_id, floating) =
|
||||||
(state.id, state.floating.is_floating())
|
win.with_state(|state| (state.id, state.floating.is_floating()));
|
||||||
});
|
// TODO: unwrap
|
||||||
// TODO: unwrap
|
let location = self
|
||||||
let location = self
|
.space
|
||||||
.space
|
.element_location(win)
|
||||||
.element_location(win)
|
.expect("Window location doesn't exist");
|
||||||
.expect("Window location doesn't exist");
|
WindowProperties {
|
||||||
WindowProperties {
|
id: window_id,
|
||||||
id: window_id,
|
app_id,
|
||||||
app_id,
|
title,
|
||||||
title,
|
size: win.geometry().size.into(),
|
||||||
size: win.geometry().size.into(),
|
location: location.into(),
|
||||||
location: location.into(),
|
floating,
|
||||||
floating,
|
}
|
||||||
}
|
})
|
||||||
})
|
.collect::<Vec<_>>();
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// FIXME: figure out what to do if error
|
// FIXME: figure out what to do if error
|
||||||
let stream = self
|
crate::api::send_to_client(
|
||||||
.api_state
|
&mut stream,
|
||||||
.stream
|
&OutgoingMsg::RequestResponse {
|
||||||
.as_ref()
|
response: RequestResponse::GetAllWindows {
|
||||||
.expect("Stream doesn't exist");
|
windows: window_props,
|
||||||
let mut stream = stream.lock().expect("Couldn't lock stream");
|
},
|
||||||
crate::api::send_to_client(
|
|
||||||
&mut stream,
|
|
||||||
&OutgoingMsg::RequestResponse {
|
|
||||||
request_id: id,
|
|
||||||
response: RequestResponse::GetAllWindows {
|
|
||||||
windows: window_props,
|
|
||||||
},
|
},
|
||||||
},
|
)
|
||||||
)
|
.expect("Couldn't send to client");
|
||||||
.expect("Couldn't send to client");
|
}
|
||||||
|
Request::GetOutputByName { name } => {
|
||||||
|
// TODO: name better
|
||||||
|
let names = self
|
||||||
|
.space
|
||||||
|
.outputs()
|
||||||
|
.find(|output| output.name() == name)
|
||||||
|
.map(|output| output.name());
|
||||||
|
crate::api::send_to_client(
|
||||||
|
&mut stream,
|
||||||
|
&OutgoingMsg::RequestResponse {
|
||||||
|
response: RequestResponse::Outputs {
|
||||||
|
names: if let Some(name) = names {
|
||||||
|
vec![name]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
Request::GetOutputsByModel { model } => {
|
||||||
|
let names = self
|
||||||
|
.space
|
||||||
|
.outputs()
|
||||||
|
.filter(|output| output.physical_properties().model == model)
|
||||||
|
.map(|output| output.name())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
crate::api::send_to_client(
|
||||||
|
&mut stream,
|
||||||
|
&OutgoingMsg::RequestResponse {
|
||||||
|
response: RequestResponse::Outputs { names },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
Request::GetOutputsByRes { res } => {
|
||||||
|
let names = self
|
||||||
|
.space
|
||||||
|
.outputs()
|
||||||
|
.filter_map(|output| {
|
||||||
|
if let Some(mode) = output.current_mode() {
|
||||||
|
if mode.size == (res.0 as i32, res.1 as i32).into() {
|
||||||
|
Some(output.name())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
crate::api::send_to_client(
|
||||||
|
&mut stream,
|
||||||
|
&OutgoingMsg::RequestResponse {
|
||||||
|
response: RequestResponse::Outputs { names },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
Request::GetOutputByFocus => {
|
||||||
|
let names = self
|
||||||
|
.focus_state
|
||||||
|
.focused_output
|
||||||
|
.as_ref()
|
||||||
|
.map(|output| output.name())
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
crate::api::send_to_client(
|
||||||
|
&mut stream,
|
||||||
|
&OutgoingMsg::RequestResponse {
|
||||||
|
response: RequestResponse::Outputs { names },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
Request::GetTagsByOutput { output } => {
|
||||||
|
let output = self
|
||||||
|
.space
|
||||||
|
.outputs()
|
||||||
|
.find(|op| op.name() == output);
|
||||||
|
if let Some(output) = output {
|
||||||
|
let tag_props = output.with_state(|state| {
|
||||||
|
state.tags
|
||||||
|
.iter()
|
||||||
|
.map(|tag| TagProperties { id: tag.id() })
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
crate::api::send_to_client(
|
||||||
|
&mut stream,
|
||||||
|
&OutgoingMsg::RequestResponse {
|
||||||
|
response: RequestResponse::Tags { tags: tag_props }
|
||||||
|
}).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Request::GetTagActive { tag_id } => {
|
||||||
|
let tag = self
|
||||||
|
.space
|
||||||
|
.outputs()
|
||||||
|
.flat_map(|op| {
|
||||||
|
op.with_state(|state| state.tags.clone())
|
||||||
|
})
|
||||||
|
.find(|tag| tag.id() == tag_id);
|
||||||
|
if let Some(tag) = tag {
|
||||||
|
crate::api::send_to_client(
|
||||||
|
&mut stream,
|
||||||
|
&OutgoingMsg::RequestResponse {
|
||||||
|
response: RequestResponse::TagActive {
|
||||||
|
active: tag.active()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Request::GetTagName { tag_id } => {
|
||||||
|
let tag = self
|
||||||
|
.space
|
||||||
|
.outputs()
|
||||||
|
.flat_map(|op| {
|
||||||
|
op.with_state(|state| state.tags.clone())
|
||||||
|
})
|
||||||
|
.find(|tag| tag.id() == tag_id);
|
||||||
|
if let Some(tag) = tag {
|
||||||
|
crate::api::send_to_client(
|
||||||
|
&mut stream,
|
||||||
|
&OutgoingMsg::RequestResponse {
|
||||||
|
response: RequestResponse::TagName {
|
||||||
|
name: tag.name()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -472,203 +683,95 @@ impl<B: Backend> State<B> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn re_layout(&mut self) {
|
pub fn re_layout(&mut self, output: &Output) {
|
||||||
let windows =
|
let windows = self.windows.iter().filter(|win| {
|
||||||
OutputState::with(self.focus_state.focused_output.as_ref().unwrap(), |state| {
|
win.with_state(|state| state.tags.iter().any(|tag| self.output_for_tag(tag).is_some_and(|op| &op == output)))
|
||||||
for window in self.space.elements().cloned().collect::<Vec<_>>() {
|
}).cloned().collect::<Vec<_>>();
|
||||||
let should_render = WindowState::with_state(&window, |win_state| {
|
let (render, do_not_render) = output.with_state(|state| {
|
||||||
for tag_id in win_state.tags.iter() {
|
let first_tag = state.focused_tags().next();
|
||||||
if *state.focused_tags.get(tag_id).unwrap_or(&false) {
|
if let Some(first_tag) = first_tag {
|
||||||
return true;
|
first_tag.layout().layout(
|
||||||
}
|
self.windows.clone(),
|
||||||
}
|
state.focused_tags().cloned().collect(),
|
||||||
false
|
&self.space,
|
||||||
});
|
output,
|
||||||
if !should_render {
|
);
|
||||||
self.space.unmap_elem(&window);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.windows
|
|
||||||
.iter()
|
|
||||||
.filter(|&win| {
|
|
||||||
WindowState::with_state(win, |win_state| {
|
|
||||||
for tag_id in win_state.tags.iter() {
|
|
||||||
if *state.focused_tags.get(tag_id).unwrap_or(&false) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
});
|
|
||||||
|
|
||||||
tracing::debug!("Laying out {} windows", windows.len());
|
|
||||||
|
|
||||||
Layout::master_stack(self, windows, crate::layout::Direction::Left);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl State<WinitData> {
|
|
||||||
/// Create the main [`State`].
|
|
||||||
///
|
|
||||||
/// This will set the WAYLAND_DISPLAY environment variable, insert Wayland necessary sources
|
|
||||||
/// into the event loop, and run an implementation of the config API (currently Lua).
|
|
||||||
pub fn init(
|
|
||||||
backend_data: WinitData,
|
|
||||||
display: &mut Display<Self>,
|
|
||||||
loop_signal: LoopSignal,
|
|
||||||
loop_handle: LoopHandle<'static, CalloopData<WinitData>>,
|
|
||||||
) -> Result<Self, Box<dyn Error>> {
|
|
||||||
let socket = ListeningSocketSource::new_auto()?;
|
|
||||||
let socket_name = socket.socket_name().to_os_string();
|
|
||||||
|
|
||||||
std::env::set_var("WAYLAND_DISPLAY", socket_name.clone());
|
|
||||||
|
|
||||||
// Opening a new process will use up a few file descriptors, around 10 for Alacritty, for
|
|
||||||
// example. Because of this, opening up only around 100 processes would exhaust the file
|
|
||||||
// descriptor limit on my system (Arch btw) and cause a "Too many open files" crash.
|
|
||||||
//
|
|
||||||
// To fix this, I just set the limit to be higher. As Pinnacle is the whole graphical
|
|
||||||
// environment, I *think* this is ok.
|
|
||||||
if let Err(err) = smithay::reexports::nix::sys::resource::setrlimit(
|
|
||||||
smithay::reexports::nix::sys::resource::Resource::RLIMIT_NOFILE,
|
|
||||||
65536,
|
|
||||||
65536 * 2,
|
|
||||||
) {
|
|
||||||
tracing::error!("Could not raise fd limit: errno {err}");
|
|
||||||
}
|
|
||||||
|
|
||||||
loop_handle.insert_source(socket, |stream, _metadata, data| {
|
|
||||||
data.display
|
|
||||||
.handle()
|
|
||||||
.insert_client(stream, Arc::new(ClientState::default()))
|
|
||||||
.expect("Could not insert client into loop handle");
|
|
||||||
})?;
|
|
||||||
|
|
||||||
loop_handle.insert_source(
|
|
||||||
Generic::new(
|
|
||||||
display.backend().poll_fd().as_raw_fd(),
|
|
||||||
Interest::READ,
|
|
||||||
Mode::Level,
|
|
||||||
),
|
|
||||||
|_readiness, _metadata, data| {
|
|
||||||
data.display.dispatch_clients(&mut data.state)?;
|
|
||||||
Ok(PostAction::Continue)
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let (tx_channel, rx_channel) = calloop::channel::channel::<Msg>();
|
|
||||||
loop_handle.insert_source(rx_channel, |msg, _, data| match msg {
|
|
||||||
Event::Msg(msg) => data.state.handle_msg(msg),
|
|
||||||
Event::Closed => todo!(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// We want to replace the client if a new one pops up
|
|
||||||
// TODO: there should only ever be one client working at a time, and creating a new client
|
|
||||||
// | when one is already running should be impossible.
|
|
||||||
// INFO: this source try_clone()s the stream
|
|
||||||
loop_handle.insert_source(PinnacleSocketSource::new(tx_channel)?, |stream, _, data| {
|
|
||||||
if let Some(old_stream) = data
|
|
||||||
.state
|
|
||||||
.api_state
|
|
||||||
.stream
|
|
||||||
.replace(Arc::new(Mutex::new(stream)))
|
|
||||||
{
|
|
||||||
old_stream
|
|
||||||
.lock()
|
|
||||||
.expect("Couldn't lock old stream")
|
|
||||||
.shutdown(std::net::Shutdown::Both)
|
|
||||||
.expect("Couldn't shutdown old stream");
|
|
||||||
}
|
}
|
||||||
})?;
|
|
||||||
|
|
||||||
let (executor, sched) =
|
windows.iter().cloned().partition::<Vec<_>, _>(|win| {
|
||||||
calloop::futures::executor::<()>().expect("Couldn't create executor");
|
win.with_state(|win_state| {
|
||||||
loop_handle.insert_source(executor, |_, _, _| {})?;
|
if win_state.floating.is_floating() {
|
||||||
|
return true;
|
||||||
// TODO: move all this into the lua api
|
}
|
||||||
let config_path = std::env::var("PINNACLE_CONFIG").unwrap_or_else(|_| {
|
for tag in win_state.tags.iter() {
|
||||||
let mut default_path =
|
if state.focused_tags().any(|tg| tg == tag) {
|
||||||
std::env::var("XDG_CONFIG_HOME").unwrap_or("~/.config".to_string());
|
return true;
|
||||||
default_path.push_str("/pinnacle/init.lua");
|
}
|
||||||
default_path
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if Path::new(&config_path).exists() {
|
let clone = render.clone();
|
||||||
let lua_path = std::env::var("LUA_PATH").expect("Lua is not installed!");
|
self.loop_handle.insert_idle(|data| {
|
||||||
let mut local_lua_path = std::env::current_dir()
|
schedule_on_commit(data, clone, |dt| {
|
||||||
.expect("Couldn't get current dir")
|
for win in do_not_render {
|
||||||
.to_string_lossy()
|
dt.state.space.unmap_elem(&win);
|
||||||
.to_string();
|
}
|
||||||
local_lua_path.push_str("/api/lua"); // TODO: get from crate root and do dynamically
|
})
|
||||||
let new_lua_path =
|
});
|
||||||
format!("{local_lua_path}/?.lua;{local_lua_path}/?/init.lua;{local_lua_path}/lib/?.lua;{local_lua_path}/lib/?/init.lua;{lua_path}");
|
|
||||||
|
|
||||||
let lua_cpath = std::env::var("LUA_CPATH").expect("Lua is not installed!");
|
// let blocker = WindowBlockerAll::new(render.clone());
|
||||||
let new_lua_cpath = format!("{local_lua_path}/lib/?.so;{lua_cpath}");
|
// blocker.insert_into_loop(self);
|
||||||
|
// for win in render.iter() {
|
||||||
|
// compositor::add_blocker(win.toplevel().wl_surface(), blocker.clone());
|
||||||
|
// }
|
||||||
|
|
||||||
std::process::Command::new("lua5.4")
|
// let (blocker, source) = WindowBlocker::block_all::<B>(render.clone());
|
||||||
.arg(config_path)
|
// for win in render.iter() {
|
||||||
.env("LUA_PATH", new_lua_path)
|
// compositor::add_blocker(win.toplevel().wl_surface(), blocker.clone());
|
||||||
.env("LUA_CPATH", new_lua_cpath)
|
// }
|
||||||
.spawn()
|
// self.loop_handle.insert_idle(move |data| source(render.clone(), data));
|
||||||
.expect("Could not start config process");
|
|
||||||
} else {
|
|
||||||
tracing::error!("Could not find {}", config_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
let display_handle = display.handle();
|
// let (blocker, source) = WindowBlocker::new::<B>(render.clone());
|
||||||
let mut seat_state = SeatState::new();
|
// for win in render.iter() {
|
||||||
let mut seat = seat_state.new_wl_seat(&display_handle, backend_data.seat_name());
|
// compositor::add_blocker(win.toplevel().wl_surface(), blocker.clone());
|
||||||
seat.add_pointer();
|
// }
|
||||||
seat.add_keyboard(XkbConfig::default(), 200, 25)?;
|
// self.loop_handle.insert_idle(move |data| source(render.clone(), render.clone(), data));
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
backend_data,
|
|
||||||
loop_signal,
|
|
||||||
loop_handle,
|
|
||||||
clock: Clock::<Monotonic>::new()?,
|
|
||||||
compositor_state: CompositorState::new::<Self>(&display_handle),
|
|
||||||
data_device_state: DataDeviceState::new::<Self>(&display_handle),
|
|
||||||
seat_state,
|
|
||||||
pointer_location: (0.0, 0.0).into(),
|
|
||||||
shm_state: ShmState::new::<Self>(&display_handle, vec![]),
|
|
||||||
space: Space::<Window>::default(),
|
|
||||||
cursor_status: CursorImageStatus::Default,
|
|
||||||
output_manager_state: OutputManagerState::new_with_xdg_output::<Self>(&display_handle),
|
|
||||||
xdg_shell_state: XdgShellState::new::<Self>(&display_handle),
|
|
||||||
viewporter_state: ViewporterState::new::<Self>(&display_handle),
|
|
||||||
fractional_scale_manager_state: FractionalScaleManagerState::new::<Self>(
|
|
||||||
&display_handle,
|
|
||||||
),
|
|
||||||
input_state: InputState::new(),
|
|
||||||
api_state: ApiState::new(),
|
|
||||||
focus_state: FocusState::new(),
|
|
||||||
tag_state: TagState::new(),
|
|
||||||
|
|
||||||
seat,
|
|
||||||
|
|
||||||
move_mode: false,
|
|
||||||
socket_name: socket_name.to_string_lossy().to_string(),
|
|
||||||
|
|
||||||
popup_manager: PopupManager::default(),
|
|
||||||
|
|
||||||
async_scheduler: sched,
|
|
||||||
|
|
||||||
windows: vec![],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// Schedule something to be done when windows have finished committing and have become
|
||||||
|
/// idle.
|
||||||
|
pub fn schedule_on_commit<F, B: Backend>(data: &mut CalloopData<B>, windows: Vec<Window>, on_commit: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut CalloopData<B>) + 'static,
|
||||||
|
{
|
||||||
|
// tracing::debug!("scheduling on_commit");
|
||||||
|
// tracing::debug!("win len is {}", windows.len());
|
||||||
|
for window in windows.iter() {
|
||||||
|
window.with_state(|state| {
|
||||||
|
// tracing::debug!("win state is {:?}", state.resize_state);
|
||||||
|
});
|
||||||
|
if window.with_state(|state| !matches!(state.resize_state, WindowResizeState::Idle))
|
||||||
|
{
|
||||||
|
// tracing::debug!("some windows not idle");
|
||||||
|
data.state.loop_handle.insert_idle(|data| {
|
||||||
|
schedule_on_commit(data, windows, on_commit);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl State<UdevData> {
|
on_commit(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: Backend> State<B> {
|
||||||
pub fn init(
|
pub fn init(
|
||||||
backend_data: UdevData,
|
backend_data: B,
|
||||||
display: &mut Display<Self>,
|
display: &mut Display<Self>,
|
||||||
loop_signal: LoopSignal,
|
loop_signal: LoopSignal,
|
||||||
loop_handle: LoopHandle<'static, CalloopData<UdevData>>,
|
loop_handle: LoopHandle<'static, CalloopData<B>>,
|
||||||
) -> Result<Self, Box<dyn Error>> {
|
) -> Result<Self, Box<dyn Error>> {
|
||||||
let socket = ListeningSocketSource::new_auto()?;
|
let socket = ListeningSocketSource::new_auto()?;
|
||||||
let socket_name = socket.socket_name().to_os_string();
|
let socket_name = socket.socket_name().to_os_string();
|
||||||
|
@ -801,7 +904,6 @@ impl State<UdevData> {
|
||||||
input_state: InputState::new(),
|
input_state: InputState::new(),
|
||||||
api_state: ApiState::new(),
|
api_state: ApiState::new(),
|
||||||
focus_state: FocusState::new(),
|
focus_state: FocusState::new(),
|
||||||
tag_state: TagState::new(),
|
|
||||||
|
|
||||||
seat,
|
seat,
|
||||||
|
|
||||||
|
@ -813,6 +915,7 @@ impl State<UdevData> {
|
||||||
async_scheduler: sched,
|
async_scheduler: sched,
|
||||||
|
|
||||||
windows: vec![],
|
windows: vec![],
|
||||||
|
output_callback_ids: vec![],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -874,6 +977,7 @@ pub fn take_presentation_feedback(
|
||||||
/// State containing the config API's stream.
|
/// State containing the config API's stream.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ApiState {
|
pub struct ApiState {
|
||||||
|
// TODO: this may not need to be in an arc mutex because of the move to async
|
||||||
pub stream: Option<Arc<Mutex<UnixStream>>>,
|
pub stream: Option<Arc<Mutex<UnixStream>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -882,3 +986,36 @@ impl ApiState {
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait WithState {
|
||||||
|
type State: Default;
|
||||||
|
fn with_state<F, T>(&self, func: F) -> T
|
||||||
|
where
|
||||||
|
F: FnMut(&mut Self::State) -> T;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct WlSurfaceState {
|
||||||
|
pub resize_state: ResizeSurfaceState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WithState for WlSurface {
|
||||||
|
type State = WlSurfaceState;
|
||||||
|
|
||||||
|
fn with_state<F, T>(&self, mut func: F) -> T
|
||||||
|
where
|
||||||
|
F: FnMut(&mut Self::State) -> T,
|
||||||
|
{
|
||||||
|
compositor::with_states(self, |states| {
|
||||||
|
states
|
||||||
|
.data_map
|
||||||
|
.insert_if_missing(RefCell::<Self::State>::default);
|
||||||
|
let state = states
|
||||||
|
.data_map
|
||||||
|
.get::<RefCell<Self::State>>()
|
||||||
|
.expect("This should never happen");
|
||||||
|
|
||||||
|
func(&mut state.borrow_mut())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
109
src/tag.rs
109
src/tag.rs
|
@ -4,25 +4,102 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use smithay::desktop::Window;
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
hash::Hash,
|
||||||
|
rc::Rc,
|
||||||
|
sync::atomic::{AtomicU32, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
|
use smithay::output::Output;
|
||||||
pub struct TagId(String);
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
use crate::{
|
||||||
pub struct Tag {
|
backend::Backend,
|
||||||
pub id: TagId,
|
layout::Layout,
|
||||||
pub windows: Vec<Window>,
|
state::{State, WithState},
|
||||||
// TODO: layout
|
};
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
static TAG_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||||
pub struct TagState {
|
|
||||||
pub tags: Vec<Tag>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TagState {
|
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||||
pub fn new() -> Self {
|
pub struct TagId(u32);
|
||||||
Default::default()
|
|
||||||
|
impl TagId {
|
||||||
|
fn next() -> Self {
|
||||||
|
Self(TAG_ID_COUNTER.fetch_add(1, Ordering::Relaxed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TagInner {
|
||||||
|
/// The internal id of this tag.
|
||||||
|
id: TagId,
|
||||||
|
/// The name of this tag.
|
||||||
|
name: String,
|
||||||
|
/// Whether this tag is active or not.
|
||||||
|
active: bool,
|
||||||
|
/// What layout this tag has.
|
||||||
|
layout: Layout,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for TagInner {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.id == other.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for TagInner {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Tag(Rc<RefCell<TagInner>>);
|
||||||
|
|
||||||
|
impl Tag {
|
||||||
|
pub fn id(&self) -> TagId {
|
||||||
|
self.0.borrow().id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> String {
|
||||||
|
self.0.borrow().name.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn active(&self) -> bool {
|
||||||
|
self.0.borrow().active
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_active(&self, active: bool) {
|
||||||
|
self.0.borrow_mut().active = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn layout(&self) -> Layout {
|
||||||
|
self.0.borrow().layout
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_layout(&self, layout: Layout) {
|
||||||
|
self.0.borrow_mut().layout = layout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tag {
|
||||||
|
pub fn new(name: String) -> Self {
|
||||||
|
Self(Rc::new(RefCell::new(TagInner {
|
||||||
|
id: TagId::next(),
|
||||||
|
name,
|
||||||
|
active: false,
|
||||||
|
layout: Layout::MasterStack, // TODO: get from config
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct TagProperties {
|
||||||
|
pub id: TagId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: Backend> State<B> {
|
||||||
|
pub fn output_for_tag(&self, tag: &Tag) -> Option<Output> {
|
||||||
|
self.space
|
||||||
|
.outputs()
|
||||||
|
.find(|output| output.with_state(|state| state.tags.iter().any(|tg| tg == tag)))
|
||||||
|
.cloned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
135
src/window.rs
135
src/window.rs
|
@ -4,45 +4,29 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::sync::atomic::AtomicU32;
|
||||||
|
|
||||||
use smithay::{
|
use smithay::{
|
||||||
desktop::Window,
|
desktop::Window,
|
||||||
reexports::wayland_server::protocol::wl_surface::WlSurface,
|
reexports::{
|
||||||
wayland::{compositor, seat::WaylandFocus},
|
wayland_protocols::xdg::shell::server::xdg_toplevel,
|
||||||
|
wayland_server::protocol::wl_surface::WlSurface,
|
||||||
|
},
|
||||||
|
wayland::{
|
||||||
|
compositor::{Blocker, BlockerState},
|
||||||
|
seat::WaylandFocus,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::Backend, layout::Layout, state::State, window::window_state::WindowResizeState,
|
backend::Backend,
|
||||||
|
state::{State, WithState},
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::window_state::{Float, WindowId, WindowState};
|
use self::window_state::{Float, WindowId};
|
||||||
|
|
||||||
pub mod window_state;
|
pub mod window_state;
|
||||||
|
|
||||||
// TODO: maybe get rid of this and move the fn into resize_surface state because it's the only user
|
|
||||||
pub trait SurfaceState: Default + 'static {
|
|
||||||
/// Access the [`SurfaceState`] associated with a [`WlSurface`].
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// This function will panic if you use it within itself due to the use of a [`RefCell`].
|
|
||||||
fn with_state<F, T>(wl_surface: &WlSurface, function: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut Self) -> T,
|
|
||||||
{
|
|
||||||
compositor::with_states(wl_surface, |states| {
|
|
||||||
states.data_map.insert_if_missing(RefCell::<Self>::default);
|
|
||||||
let state = states
|
|
||||||
.data_map
|
|
||||||
.get::<RefCell<Self>>()
|
|
||||||
.expect("This should never happen");
|
|
||||||
|
|
||||||
function(&mut state.borrow_mut())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B: Backend> State<B> {
|
impl<B: Backend> State<B> {
|
||||||
/// Returns the [Window] associated with a given [WlSurface].
|
/// Returns the [Window] associated with a given [WlSurface].
|
||||||
pub fn window_for_surface(&self, surface: &WlSurface) -> Option<Window> {
|
pub fn window_for_surface(&self, surface: &WlSurface) -> Option<Window> {
|
||||||
|
@ -57,43 +41,11 @@ impl<B: Backend> State<B> {
|
||||||
.cloned()
|
.cloned()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Swap the positions and sizes of two windows.
|
|
||||||
pub fn swap_window_positions(&mut self, win1: &Window, win2: &Window) {
|
|
||||||
// FIXME: moving the mouse quickly will break swapping
|
|
||||||
|
|
||||||
let win1_loc = self.space.element_location(win1).unwrap(); // TODO: handle unwraps
|
|
||||||
let win2_loc = self.space.element_location(win2).unwrap();
|
|
||||||
let win1_geo = win1.geometry();
|
|
||||||
let win2_geo = win2.geometry();
|
|
||||||
// tracing::info!("win1: {:?}, {:?}", win1_loc, win1_geo);
|
|
||||||
// tracing::info!("win2: {:?}, {:?}", win2_loc, win2_geo);
|
|
||||||
|
|
||||||
win1.toplevel().with_pending_state(|state| {
|
|
||||||
state.size = Some(win2_geo.size);
|
|
||||||
});
|
|
||||||
win2.toplevel().with_pending_state(|state| {
|
|
||||||
state.size = Some(win1_geo.size);
|
|
||||||
});
|
|
||||||
|
|
||||||
let serial = win1.toplevel().send_configure();
|
|
||||||
WindowState::with_state(win1, |state| {
|
|
||||||
state.resize_state = WindowResizeState::WaitingForAck(serial, win2_loc);
|
|
||||||
});
|
|
||||||
|
|
||||||
let serial = win2.toplevel().send_configure();
|
|
||||||
WindowState::with_state(win2, |state| {
|
|
||||||
state.resize_state = WindowResizeState::WaitingForAck(serial, win1_loc);
|
|
||||||
});
|
|
||||||
|
|
||||||
// self.space.map_element(win1.clone(), win2_loc, false);
|
|
||||||
// self.space.map_element(win2.clone(), win1_loc, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle a window's floating status.
|
/// Toggle a window's floating status.
|
||||||
pub fn toggle_floating<B: Backend>(state: &mut State<B>, window: &Window) {
|
pub fn toggle_floating<B: Backend>(state: &mut State<B>, window: &Window) {
|
||||||
WindowState::with_state(window, |window_state| {
|
window.with_state(|window_state| {
|
||||||
match window_state.floating {
|
match window_state.floating {
|
||||||
Float::Tiled(prev_loc_and_size) => {
|
Float::Tiled(prev_loc_and_size) => {
|
||||||
if let Some((prev_loc, prev_size)) = prev_loc_and_size {
|
if let Some((prev_loc, prev_size)) = prev_loc_and_size {
|
||||||
|
@ -103,10 +55,17 @@ pub fn toggle_floating<B: Backend>(state: &mut State<B>, window: &Window) {
|
||||||
|
|
||||||
window.toplevel().send_pending_configure();
|
window.toplevel().send_pending_configure();
|
||||||
|
|
||||||
state.space.map_element(window.clone(), prev_loc, false); // TODO: should it activate?
|
state.space.map_element(window.clone(), prev_loc, false);
|
||||||
|
// TODO: should it activate?
|
||||||
}
|
}
|
||||||
|
|
||||||
window_state.floating = Float::Floating;
|
window_state.floating = Float::Floating;
|
||||||
|
window.toplevel().with_pending_state(|tl_state| {
|
||||||
|
tl_state.states.unset(xdg_toplevel::State::TiledTop);
|
||||||
|
tl_state.states.unset(xdg_toplevel::State::TiledBottom);
|
||||||
|
tl_state.states.unset(xdg_toplevel::State::TiledLeft);
|
||||||
|
tl_state.states.unset(xdg_toplevel::State::TiledRight);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Float::Floating => {
|
Float::Floating => {
|
||||||
window_state.floating = Float::Tiled(Some((
|
window_state.floating = Float::Tiled(Some((
|
||||||
|
@ -115,13 +74,46 @@ pub fn toggle_floating<B: Backend>(state: &mut State<B>, window: &Window) {
|
||||||
state.space.element_location(window).unwrap(),
|
state.space.element_location(window).unwrap(),
|
||||||
window.geometry().size,
|
window.geometry().size,
|
||||||
)));
|
)));
|
||||||
|
window.toplevel().with_pending_state(|tl_state| {
|
||||||
|
tl_state.states.set(xdg_toplevel::State::TiledTop);
|
||||||
|
tl_state.states.set(xdg_toplevel::State::TiledBottom);
|
||||||
|
tl_state.states.set(xdg_toplevel::State::TiledLeft);
|
||||||
|
tl_state.states.set(xdg_toplevel::State::TiledRight);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let windows = state.space.elements().cloned().collect::<Vec<_>>();
|
let output = state.focus_state.focused_output.clone().unwrap();
|
||||||
Layout::master_stack(state, windows, crate::layout::Direction::Left);
|
state.re_layout(&output);
|
||||||
state.space.raise_element(window, true);
|
|
||||||
|
let render = output.with_state(|op_state| {
|
||||||
|
state
|
||||||
|
.windows
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.filter(|win| {
|
||||||
|
win.with_state(|win_state| {
|
||||||
|
if win_state.floating.is_floating() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for tag in win_state.tags.iter() {
|
||||||
|
if op_state.focused_tags().any(|tg| tg == tag) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
|
||||||
|
let clone = window.clone();
|
||||||
|
state.loop_handle.insert_idle(move |data| {
|
||||||
|
crate::state::schedule_on_commit(data, render, move |dt| {
|
||||||
|
dt.state.space.raise_element(&clone, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
@ -135,3 +127,16 @@ pub struct WindowProperties {
|
||||||
pub location: (i32, i32),
|
pub location: (i32, i32),
|
||||||
pub floating: bool,
|
pub floating: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct WindowBlocker;
|
||||||
|
pub static BLOCKER_COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||||
|
|
||||||
|
impl Blocker for WindowBlocker {
|
||||||
|
fn state(&self) -> BlockerState {
|
||||||
|
if BLOCKER_COUNTER.load(std::sync::atomic::Ordering::SeqCst) > 0 {
|
||||||
|
BlockerState::Pending
|
||||||
|
} else {
|
||||||
|
BlockerState::Released
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
|
fmt,
|
||||||
sync::atomic::{AtomicU32, Ordering},
|
sync::atomic::{AtomicU32, Ordering},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,9 +15,9 @@ use smithay::{
|
||||||
utils::{Logical, Point, Serial, Size},
|
utils::{Logical, Point, Serial, Size},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::tag::{Tag, TagId, TagState};
|
use crate::{state::WithState, tag::Tag};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct WindowId(u32);
|
pub struct WindowId(u32);
|
||||||
|
|
||||||
// TODO: this probably doesn't need to be atomic
|
// TODO: this probably doesn't need to be atomic
|
||||||
|
@ -36,16 +37,7 @@ pub struct WindowState {
|
||||||
/// The window's resize state. See [WindowResizeState] for more.
|
/// The window's resize state. See [WindowResizeState] for more.
|
||||||
pub resize_state: WindowResizeState,
|
pub resize_state: WindowResizeState,
|
||||||
/// What tags the window is currently on.
|
/// What tags the window is currently on.
|
||||||
pub tags: Vec<TagId>,
|
pub tags: Vec<Tag>,
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a vec of references to all the tags the window is on.
|
|
||||||
pub fn tags<'a>(tag_state: &'a TagState, window: &Window) -> Vec<&'a Tag> {
|
|
||||||
tag_state
|
|
||||||
.tags
|
|
||||||
.iter()
|
|
||||||
.filter(|&tag| WindowState::with_state(window, |state| state.tags.contains(&tag.id)))
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The state of a window's resize operation.
|
/// The state of a window's resize operation.
|
||||||
|
@ -55,36 +47,46 @@ pub fn tags<'a>(tag_state: &'a TagState, window: &Window) -> Vec<&'a Tag> {
|
||||||
/// sending a configure event. However, the client will probably not acknowledge the configure
|
/// sending a configure event. However, the client will probably not acknowledge the configure
|
||||||
/// until *after* the window has moved, causing flickering.
|
/// until *after* the window has moved, causing flickering.
|
||||||
///
|
///
|
||||||
/// To solve this, we need to create two additional steps: [`WaitingForAck`] and [`WaitingForCommit`].
|
/// To solve this, we need to create two additional steps: [`Requested`] and [`Acknowledged`].
|
||||||
/// If we need to change a window's location when we change its size, instead of
|
/// If we need to change a window's location when we change its size, instead of
|
||||||
/// calling `map_element()`, we change the window's [`WindowState`] and set
|
/// calling `map_element()`, we change the window's [`WindowState`] and set
|
||||||
/// its [`resize_state`] to `WaitingForAck` with the new position we want.
|
/// its [`resize_state`] to `Requested` with the new position we want.
|
||||||
///
|
///
|
||||||
/// When the client acks the configure, we can move the state to `WaitingForCommit` in
|
/// When the client acks the configure, we can move the state to `Acknowledged` in
|
||||||
/// [`XdgShellHandler.ack_configure()`]. Finally, in [`CompositorHandler.commit()`], we set the
|
/// [`XdgShellHandler.ack_configure()`]. Finally, in [`CompositorHandler.commit()`], we set the
|
||||||
/// state back to [`Idle`] and map the window.
|
/// state back to [`Idle`] and map the window.
|
||||||
///
|
///
|
||||||
/// [`space.map_element()`]: smithay::desktop::space::Space#method.map_element
|
/// [`space.map_element()`]: smithay::desktop::space::Space#method.map_element
|
||||||
/// [`with_pending_state()`]: smithay::wayland::shell::xdg::ToplevelSurface#method.with_pending_state
|
/// [`with_pending_state()`]: smithay::wayland::shell::xdg::ToplevelSurface#method.with_pending_state
|
||||||
/// [`Idle`]: WindowResizeState::Idle
|
/// [`Idle`]: WindowResizeState::Idle
|
||||||
/// [`WaitingForAck`]: WindowResizeState::WaitingForAck
|
/// [`Requested`]: WindowResizeState::Requested
|
||||||
/// [`WaitingForCommit`]: WindowResizeState::WaitingForCommit
|
/// [`Acknowledged`]: WindowResizeState::Acknowledged
|
||||||
/// [`resize_state`]: WindowState#structfield.resize_state
|
/// [`resize_state`]: WindowState#structfield.resize_state
|
||||||
/// [`XdgShellHandler.ack_configure()`]: smithay::wayland::shell::xdg::XdgShellHandler#method.ack_configure
|
/// [`XdgShellHandler.ack_configure()`]: smithay::wayland::shell::xdg::XdgShellHandler#method.ack_configure
|
||||||
/// [`CompositorHandler.commit()`]: smithay::wayland::compositor::CompositorHandler#tymethod.commit
|
/// [`CompositorHandler.commit()`]: smithay::wayland::compositor::CompositorHandler#tymethod.commit
|
||||||
#[derive(Debug, Default)]
|
#[derive(Default, Clone)]
|
||||||
pub enum WindowResizeState {
|
pub enum WindowResizeState {
|
||||||
/// The window doesn't need to be moved.
|
/// The window doesn't need to be moved.
|
||||||
#[default]
|
#[default]
|
||||||
Idle,
|
Idle,
|
||||||
/// The window has received a configure request with a new size. The desired location and the
|
/// The window has received a configure request with a new size. The desired location and the
|
||||||
/// configure request's serial should be provided here.
|
/// configure request's serial should be provided here.
|
||||||
WaitingForAck(Serial, Point<i32, Logical>),
|
Requested(Serial, Point<i32, Logical>),
|
||||||
/// The client has received the configure request and has successfully changed its size. It's
|
/// The client has received the configure request and has successfully changed its size. It's
|
||||||
/// now safe to move the window in [`CompositorHandler.commit()`] without flickering.
|
/// now safe to move the window in [`CompositorHandler.commit()`] without flickering.
|
||||||
///
|
///
|
||||||
/// [`CompositorHandler.commit()`]: smithay::wayland::compositor::CompositorHandler#tymethod.commit
|
/// [`CompositorHandler.commit()`]: smithay::wayland::compositor::CompositorHandler#tymethod.commit
|
||||||
WaitingForCommit(Point<i32, Logical>),
|
Acknowledged(Point<i32, Logical>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for WindowResizeState {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Idle => write!(f, "Idle"),
|
||||||
|
Self::Requested(_arg0, _arg1) => write!(f, "Requested"),
|
||||||
|
Self::Acknowledged(_arg0) => write!(f, "Acknowledged"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Float {
|
pub enum Float {
|
||||||
|
@ -116,20 +118,23 @@ impl WindowState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Access a [Window]'s state, optionally returning something.
|
impl WithState for Window {
|
||||||
pub fn with_state<F, T>(window: &Window, mut func: F) -> T
|
type State = WindowState;
|
||||||
|
|
||||||
|
fn with_state<F, T>(&self, mut func: F) -> T
|
||||||
where
|
where
|
||||||
F: FnMut(&mut Self) -> T,
|
F: FnMut(&mut Self::State) -> T,
|
||||||
{
|
{
|
||||||
window
|
self.user_data()
|
||||||
.user_data()
|
.insert_if_missing(RefCell::<Self::State>::default);
|
||||||
.insert_if_missing(RefCell::<Self>::default);
|
|
||||||
|
|
||||||
let state = window
|
let state = self
|
||||||
.user_data()
|
.user_data()
|
||||||
.get::<RefCell<Self>>()
|
.get::<RefCell<Self::State>>()
|
||||||
.expect("This should never happen");
|
.expect("RefCell not in data map");
|
||||||
|
|
||||||
func(&mut state.borrow_mut())
|
func(&mut state.borrow_mut())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ use smithay::{
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
grab::{move_grab::MoveSurfaceGrab, resize_grab::ResizeSurfaceGrab},
|
grab::{move_grab::MoveSurfaceGrab, resize_grab::ResizeSurfaceGrab},
|
||||||
state::State,
|
state::{State, WithState},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn move_request<B: Backend>(
|
pub fn move_request<B: Backend>(
|
||||||
|
@ -94,6 +94,9 @@ pub fn resize_request<B: Backend>(
|
||||||
if let Some(start_data) = crate::pointer::pointer_grab_start_data(&pointer, wl_surface, serial)
|
if let Some(start_data) = crate::pointer::pointer_grab_start_data(&pointer, wl_surface, serial)
|
||||||
{
|
{
|
||||||
let window = state.window_for_surface(wl_surface).unwrap();
|
let window = state.window_for_surface(wl_surface).unwrap();
|
||||||
|
if window.with_state(|state| state.floating.is_tiled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let initial_window_loc = state.space.element_location(&window).unwrap();
|
let initial_window_loc = state.space.element_location(&window).unwrap();
|
||||||
let initial_window_size = window.geometry().size;
|
let initial_window_size = window.geometry().size;
|
||||||
|
@ -124,13 +127,16 @@ pub fn resize_request_force<B: Backend>(
|
||||||
edges: xdg_toplevel::ResizeEdge,
|
edges: xdg_toplevel::ResizeEdge,
|
||||||
button_used: u32,
|
button_used: u32,
|
||||||
) {
|
) {
|
||||||
println!("resize_request_force started with edges {:?}", edges);
|
|
||||||
let wl_surface = surface.wl_surface();
|
let wl_surface = surface.wl_surface();
|
||||||
|
|
||||||
let pointer = seat.get_pointer().unwrap();
|
let pointer = seat.get_pointer().unwrap();
|
||||||
|
|
||||||
let window = state.window_for_surface(wl_surface).unwrap();
|
let window = state.window_for_surface(wl_surface).unwrap();
|
||||||
|
|
||||||
|
if window.with_state(|state| state.floating.is_tiled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let initial_window_loc = state.space.element_location(&window).unwrap();
|
let initial_window_loc = state.space.element_location(&window).unwrap();
|
||||||
let initial_window_size = window.geometry().size;
|
let initial_window_size = window.geometry().size;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue