mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-05-10 04:39:46 +02:00
Compare commits
44 Commits
feature/de
...
v0.1.41
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24c0ce0b1d | ||
|
|
d6b17bbc7c | ||
|
|
e9a541d12b | ||
|
|
588d22a9d2 | ||
|
|
53ad7a2224 | ||
|
|
26b1464381 | ||
|
|
d7580d2271 | ||
|
|
8a1447f543 | ||
|
|
53c81c4596 | ||
|
|
d3779e5a74 | ||
|
|
c84fa50fc9 | ||
|
|
41fc316a59 | ||
|
|
011bcb8bd4 | ||
|
|
dce3c91c22 | ||
|
|
a9a1e68169 | ||
|
|
cb9a7542a6 | ||
|
|
145a0ae003 | ||
|
|
96e87d8ae0 | ||
|
|
529d93595e | ||
|
|
ea35d818a1 | ||
|
|
5f629e1f1a | ||
|
|
0f1854db8b | ||
|
|
8889c3ca93 | ||
|
|
6ca49d4301 | ||
|
|
634a3e7f3b | ||
|
|
5b6fab0044 | ||
|
|
bed314b866 | ||
|
|
c165172b5a | ||
|
|
9977cca500 | ||
|
|
5d7a0ea9ad | ||
|
|
0e79c58be3 | ||
|
|
09205bfd83 | ||
|
|
98122bd9d4 | ||
|
|
9741b387a7 | ||
|
|
1dad13106a | ||
|
|
0b5141e7a4 | ||
|
|
22e8a79833 | ||
|
|
5946caaf92 | ||
|
|
01d73b7d19 | ||
|
|
9d16197825 | ||
|
|
fed09689b8 | ||
|
|
c0b298c9de | ||
|
|
5fd7017b71 | ||
|
|
dbde351e22 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.json text diff
|
||||||
12
.github/workflows/windows.yaml
vendored
12
.github/workflows/windows.yaml
vendored
@@ -13,8 +13,8 @@ on:
|
|||||||
- hotfix/*
|
- hotfix/*
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
# schedule:
|
schedule:
|
||||||
# - cron: "30 0 * * 0" # Every day at 00:30 UTC
|
- cron: "30 0 * * 0" # Every day at 00:30 UTC
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
- run: |
|
- run: |
|
||||||
cargo install cargo-wix
|
cargo install cargo-wix
|
||||||
cargo wix --no-build -p komorebi --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }}
|
cargo wix --no-build -p komorebi --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }}
|
||||||
- uses: actions/upload-artifact@v5
|
- uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
|
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
|
||||||
path: |
|
path: |
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- shell: bash
|
- shell: bash
|
||||||
run: echo "VERSION=nightly" >> $GITHUB_ENV
|
run: echo "VERSION=nightly" >> $GITHUB_ENV
|
||||||
- uses: actions/download-artifact@v6
|
- uses: actions/download-artifact@v8
|
||||||
- run: |
|
- run: |
|
||||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||||
@@ -136,7 +136,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
TAG=${{ github.event.release.tag_name }}
|
TAG=${{ github.event.release.tag_name }}
|
||||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||||
- uses: actions/download-artifact@v6
|
- uses: actions/download-artifact@v8
|
||||||
- run: |
|
- run: |
|
||||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||||
@@ -178,7 +178,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
TAG=${{ github.ref_name }}
|
TAG=${{ github.ref_name }}
|
||||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||||
- uses: actions/download-artifact@v6
|
- uses: actions/download-artifact@v8
|
||||||
- run: |
|
- run: |
|
||||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,3 +12,4 @@ komorebic/applications.json
|
|||||||
/.xwin-cache
|
/.xwin-cache
|
||||||
result
|
result
|
||||||
/.direnv
|
/.direnv
|
||||||
|
procdump.exe
|
||||||
|
|||||||
2066
Cargo.lock
generated
2066
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ members = [
|
|||||||
"komorebi",
|
"komorebi",
|
||||||
"komorebi-client",
|
"komorebi-client",
|
||||||
"komorebi-gui",
|
"komorebi-gui",
|
||||||
|
"komorebi-layouts",
|
||||||
"komorebic",
|
"komorebic",
|
||||||
"komorebic-no-console",
|
"komorebic-no-console",
|
||||||
"komorebi-bar",
|
"komorebi-bar",
|
||||||
@@ -29,13 +30,13 @@ lazy_static = "1"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = { package = "serde_json_lenient", version = "0.2" }
|
serde_json = { package = "serde_json_lenient", version = "0.2" }
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
strum = { version = "0.27", features = ["derive"] }
|
strum = { version = "0.28", features = ["derive"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-appender = "0.2"
|
tracing-appender = "0.2"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
paste = "1"
|
paste = "1"
|
||||||
sysinfo = "0.37"
|
sysinfo = "0.38"
|
||||||
uds_windows = "1"
|
uds_windows = "1"
|
||||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "8c42d8db257d30fe95bc98c2e5cd8f75da861021" }
|
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "8c42d8db257d30fe95bc98c2e5cd8f75da861021" }
|
||||||
windows-numerics = { version = "0.3" }
|
windows-numerics = { version = "0.3" }
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -57,17 +57,7 @@ If you need help doing this you can ask on Discord.
|
|||||||
|
|
||||||
## Note: komorebi for Mac
|
## Note: komorebi for Mac
|
||||||
|
|
||||||
If you made your way to this repo looking for [komorebi for
|
komorebi for Mac lives [here](https://github.com/LGUG2Z/komorebi-for-mac) :)
|
||||||
Mac](https://github.com/KomoCorp/komorebi-for-mac), the project is currently
|
|
||||||
being developed in private with [early access available to GitHub
|
|
||||||
Sponsors](https://github.com/sponsors/LGUG2Z).
|
|
||||||
|
|
||||||
If you want to see how far along development is before signing up for early
|
|
||||||
access (spoiler: it's very far along!) there is an overview video you can watch
|
|
||||||
[here](https://www.youtube.com/watch?v=u3eJcsa_MJk).
|
|
||||||
|
|
||||||
Sponsors with early access can install komorebi for Mac either by compiling
|
|
||||||
from source, by using Homebrew, or by using the project's Nix Flake.
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -89,7 +79,7 @@ Please refer to the [documentation](https://lgug2z.github.io/komorebi) for instr
|
|||||||
to [install](https://lgug2z.github.io/komorebi/installation.html) and
|
to [install](https://lgug2z.github.io/komorebi/installation.html) and
|
||||||
[configure](https://lgug2z.github.io/komorebi/example-configurations.html)
|
[configure](https://lgug2z.github.io/komorebi/example-configurations.html)
|
||||||
_komorebi_, [common workflows](https://lgug2z.github.io/komorebi/common-workflows/komorebi-config-home.html), a complete
|
_komorebi_, [common workflows](https://lgug2z.github.io/komorebi/common-workflows/komorebi-config-home.html), a complete
|
||||||
[configuration schema reference](https://komorebi.lgug2z.com/schema) and a
|
[configuration schema reference](https://komorebi-starlight.lgug2z.workers.dev/reference/komorebi-windows/) and a
|
||||||
complete [CLI reference](https://lgug2z.github.io/komorebi/cli/quickstart.html).
|
complete [CLI reference](https://lgug2z.github.io/komorebi/cli/quickstart.html).
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
@@ -434,7 +424,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
|
|||||||
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
|
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.39"}
|
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi" }
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use komorebi_client::Notification;
|
use komorebi_client::Notification;
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ crate = "komorebi-client"
|
|||||||
expression = "LicenseRef-Komorebi-2.0"
|
expression = "LicenseRef-Komorebi-2.0"
|
||||||
license-files = []
|
license-files = []
|
||||||
|
|
||||||
|
[[licenses.clarify]]
|
||||||
|
crate = "komorebi-layouts"
|
||||||
|
expression = "LicenseRef-Komorebi-2.0"
|
||||||
|
license-files = []
|
||||||
|
|
||||||
[[licenses.clarify]]
|
[[licenses.clarify]]
|
||||||
crate = "komorebic"
|
crate = "komorebic"
|
||||||
expression = "LicenseRef-Komorebi-2.0"
|
expression = "LicenseRef-Komorebi-2.0"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
BIN
docs/assets/layout-ratios_after.png
Normal file
BIN
docs/assets/layout-ratios_after.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
BIN
docs/assets/layout-ratios_before.png
Normal file
BIN
docs/assets/layout-ratios_before.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 MiB |
218
docs/common-workflows/bar-widgets/systray.md
Normal file
218
docs/common-workflows/bar-widgets/systray.md
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
# System Tray
|
||||||
|
|
||||||
|
The System Tray widget brings native Windows system tray icons into
|
||||||
|
`komorebi-bar`. It intercepts tray icon data by creating a hidden window that
|
||||||
|
mimics the Windows taskbar, receiving the same broadcast messages that
|
||||||
|
applications send via `Shell_NotifyIcon`.
|
||||||
|
|
||||||
|
## Basic configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"right_widgets": [
|
||||||
|
{
|
||||||
|
"Systray": {
|
||||||
|
"enable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hiding icons
|
||||||
|
|
||||||
|
The `hidden_icons` config field accepts a list of rules. Each rule can be either
|
||||||
|
a plain string or a structured object.
|
||||||
|
|
||||||
|
A **plain string** matches the exe name (case-insensitive). This is the original
|
||||||
|
format, so existing configs continue to work without changes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"hidden_icons": [
|
||||||
|
"SecurityHealthSystray.exe",
|
||||||
|
"PhoneExperienceHost.exe"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
A **structured object** matches one or more icon properties. All specified fields
|
||||||
|
must match (AND logic). By default matching is exact and case-insensitive.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"hidden_icons": [
|
||||||
|
{ "exe": "svchost.exe", "tooltip": "Some Specific App" },
|
||||||
|
{ "guid": "{7820AE73-23E3-4229-82C1-E41CB67D5B9C}" },
|
||||||
|
{ "tooltip": "App I want hidden" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
The two forms can be mixed freely:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"hidden_icons": [
|
||||||
|
"PhoneExperienceHost.exe",
|
||||||
|
{ "exe": "svchost.exe", "tooltip": "Specific Notification" },
|
||||||
|
{ "guid": "{7820AE73-23E3-4229-82C1-E41CB67D5B9C}" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Available fields for structured rules:
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-----------|----------------------------------------------------------|
|
||||||
|
| `exe` | Executable name (e.g. `"SecurityHealthSystray.exe"`) |
|
||||||
|
| `tooltip` | Tooltip text shown on hover |
|
||||||
|
| `guid` | Icon GUID — most stable identifier across app restarts |
|
||||||
|
|
||||||
|
### Matching strategies
|
||||||
|
|
||||||
|
Each field can be a plain string (exact case-insensitive match) or an object
|
||||||
|
with `value` and `matching_strategy` for advanced matching. This uses the same
|
||||||
|
`MatchingStrategy` as komorebi's window rules.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"hidden_icons": [
|
||||||
|
{
|
||||||
|
"exe": "explorer.exe",
|
||||||
|
"tooltip": { "value": "Network", "matching_strategy": "StartsWith" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
The above hides explorer.exe icons whose tooltip starts with "Network", while
|
||||||
|
leaving other explorer.exe icons visible.
|
||||||
|
|
||||||
|
Available strategies:
|
||||||
|
|
||||||
|
| Strategy | Description |
|
||||||
|
|---------------------|---------------------------------------------------|
|
||||||
|
| `Equals` | Exact match (default when using a plain string) |
|
||||||
|
| `StartsWith` | Value starts with the given text |
|
||||||
|
| `EndsWith` | Value ends with the given text |
|
||||||
|
| `Contains` | Value contains the given text |
|
||||||
|
| `Regex` | Value matches a regular expression |
|
||||||
|
| `DoesNotEqual` | Value does not exactly equal the given text |
|
||||||
|
| `DoesNotStartWith` | Value does not start with the given text |
|
||||||
|
| `DoesNotEndWith` | Value does not end with the given text |
|
||||||
|
| `DoesNotContain` | Value does not contain the given text |
|
||||||
|
|
||||||
|
All strategies except `Regex` are case-insensitive. For case-insensitive regex,
|
||||||
|
include `(?i)` in the pattern.
|
||||||
|
|
||||||
|
Plain strings and strategy objects can be mixed across fields:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"exe": "explorer.exe",
|
||||||
|
"tooltip": { "value": "notification", "matching_strategy": "Contains" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run komorebi-bar with `RUST_LOG=info` to see the exe, tooltip, and GUID of every
|
||||||
|
systray icon in the log output.
|
||||||
|
|
||||||
|
## Stale icon cleanup
|
||||||
|
|
||||||
|
Some applications (e.g. Docker Desktop) may exit without properly removing their
|
||||||
|
tray icon. The widget detects these stale icons by checking whether the owning
|
||||||
|
window still exists via the Win32 `IsWindow` API.
|
||||||
|
|
||||||
|
### Automatic cleanup
|
||||||
|
|
||||||
|
By default, the widget checks for stale icons every 60 seconds. The interval
|
||||||
|
can be configured with `stale_icons_check_interval` (in seconds). The value is
|
||||||
|
clamped between 30 and 600. Set to 0 to disable automatic cleanup.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"stale_icons_check_interval": 120
|
||||||
|
```
|
||||||
|
|
||||||
|
### Refresh button
|
||||||
|
|
||||||
|
A manual refresh button can be shown by setting `refresh_button`. Clicking it
|
||||||
|
immediately removes any stale icons.
|
||||||
|
|
||||||
|
- `"Visible"` — shows the button in the main icon area
|
||||||
|
- `"Overflow"` — shows the button in the hidden/overflow section (appears when
|
||||||
|
the overflow toggle is expanded)
|
||||||
|
|
||||||
|
```json
|
||||||
|
"refresh_button": "Overflow"
|
||||||
|
```
|
||||||
|
|
||||||
|
When set to `"Overflow"`, the overflow toggle arrow will appear even if there are
|
||||||
|
no hidden icons, so the refresh button remains accessible.
|
||||||
|
|
||||||
|
## Info button
|
||||||
|
|
||||||
|
An info button can be shown to open a floating panel that lists all systray icons
|
||||||
|
with their exe name, tooltip, GUID, and visibility status. This is useful for
|
||||||
|
identifying which icons to filter with `hidden_icons` rules.
|
||||||
|
|
||||||
|
- `"Visible"` — shows the button in the main icon area
|
||||||
|
- `"Overflow"` — shows the button in the hidden/overflow section
|
||||||
|
|
||||||
|
```json
|
||||||
|
"info_button": "Visible"
|
||||||
|
```
|
||||||
|
|
||||||
|
The info panel shows **all** icons, including those hidden by rules or the OS.
|
||||||
|
Each row shows the icon image, exe name, tooltip, GUID, and whether it is visible.
|
||||||
|
Copy buttons are provided on the exe, tooltip, and GUID cells for easy copying
|
||||||
|
(e.g. to paste a GUID into a filter rule).
|
||||||
|
|
||||||
|
Like the refresh button, setting `info_button` to `"Overflow"` will make the
|
||||||
|
overflow toggle arrow appear even if there are no hidden icons.
|
||||||
|
|
||||||
|
## Shortcuts button
|
||||||
|
|
||||||
|
A button that toggles komorebi-shortcuts. If the shortcuts process is running
|
||||||
|
it will be killed; otherwise it will be started.
|
||||||
|
|
||||||
|
- `"Visible"` — shows the button in the main icon area
|
||||||
|
- `"Overflow"` — shows the button in the hidden/overflow section
|
||||||
|
|
||||||
|
```json
|
||||||
|
"shortcuts_button": "Visible"
|
||||||
|
```
|
||||||
|
|
||||||
|
Like the other buttons, setting `shortcuts_button` to `"Overflow"` will make the
|
||||||
|
overflow toggle arrow appear even if there are no hidden icons.
|
||||||
|
|
||||||
|
## Mouse interactions
|
||||||
|
|
||||||
|
The widget supports left-click, right-click, middle-click, and double-click on
|
||||||
|
tray icons. Double-click sends the `LeftDoubleClick` action (via systray-util
|
||||||
|
0.2.0), which delivers `WM_LBUTTONDBLCLK` and `NIN_SELECT` messages to the icon.
|
||||||
|
|
||||||
|
## Click fallbacks
|
||||||
|
|
||||||
|
Some systray icons register a click callback but never actually respond to click
|
||||||
|
messages, effectively becoming "zombie" icons from an interaction standpoint. For
|
||||||
|
known problematic icons, the widget overrides the native click action with a
|
||||||
|
direct shell command. Fallback commands take priority — if a fallback is defined
|
||||||
|
for an icon, it always runs regardless of whether the icon reports itself as
|
||||||
|
clickable.
|
||||||
|
|
||||||
|
| Exe | Tooltip condition | Fallback command |
|
||||||
|
|--------------------------------|-------------------|---------------------------------|
|
||||||
|
| `SecurityHealthSystray.exe` | any | `start windowsdefender://` |
|
||||||
|
| `explorer.exe` | ends with `%` | `start ms-settings:apps-volume` |
|
||||||
|
| `explorer.exe` | empty | `start ms-settings:batterysaver`|
|
||||||
|
|
||||||
|
## Full example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Systray": {
|
||||||
|
"enable": true,
|
||||||
|
"hidden_icons": [
|
||||||
|
"SecurityHealthSystray.exe",
|
||||||
|
{ "exe": "explorer.exe", "tooltip": { "value": "Network", "matching_strategy": "StartsWith" } }
|
||||||
|
],
|
||||||
|
"stale_icons_check_interval": 60,
|
||||||
|
"refresh_button": "Overflow",
|
||||||
|
"info_button": "Visible",
|
||||||
|
"shortcuts_button": "Overflow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
40
docs/common-workflows/bar.md
Normal file
40
docs/common-workflows/bar.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Komorebi Bar
|
||||||
|
|
||||||
|
`komorebi-bar` is a status bar for komorebi that renders on top of the tiling
|
||||||
|
window manager. It is configured through a `komorebi.bar.json` file, either
|
||||||
|
alongside your `komorebi.json` or at the path specified in the
|
||||||
|
`bar_configurations` array.
|
||||||
|
|
||||||
|
## Widgets
|
||||||
|
|
||||||
|
Widgets are placed in the `left_widgets`, `center_widgets`, or `right_widgets`
|
||||||
|
arrays. Each widget is an object with the widget type as key and its
|
||||||
|
configuration as value.
|
||||||
|
|
||||||
|
| Widget | Description |
|
||||||
|
|--------------|--------------------------------------------------------|
|
||||||
|
| `Komorebi` | Workspaces, layout, focused window, and more |
|
||||||
|
| `Battery` | Battery level and charging status |
|
||||||
|
| `Date` | Current date in configurable format |
|
||||||
|
| `Time` | Current time in configurable format |
|
||||||
|
| `Media` | Currently playing media information |
|
||||||
|
| `Memory` | System memory usage |
|
||||||
|
| `Network` | Network activity and connection status |
|
||||||
|
| `Storage` | Disk usage information |
|
||||||
|
| `Update` | Komorebi update notification |
|
||||||
|
| `Systray` | Windows system tray icons |
|
||||||
|
|
||||||
|
Widgets with dedicated documentation pages:
|
||||||
|
|
||||||
|
- [System Tray](bar-widgets/systray.md)
|
||||||
|
|
||||||
|
> Dedicated pages for the remaining widgets will be added in the future.
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
The full configuration schema is available at
|
||||||
|
[komorebi-bar.lgug2z.com/schema](https://komorebi-bar.lgug2z.com/schema).
|
||||||
|
|
||||||
|
For running a bar on each monitor, see
|
||||||
|
[Multiple Bar Instances](multiple-bar-instances.md) and
|
||||||
|
[Multi-Monitor Setup](multi-monitor-setup.md).
|
||||||
337
docs/common-workflows/layout-ratios.md
Normal file
337
docs/common-workflows/layout-ratios.md
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
# Layout Ratios
|
||||||
|
|
||||||
|
With `komorebi` you can customize the split ratios for various layouts using
|
||||||
|
`column_ratios` and `row_ratios` in the `layout_options` configuration.
|
||||||
|
|
||||||
|
## Before and After
|
||||||
|
|
||||||
|
BSP layout example:
|
||||||
|
|
||||||
|
**Before** (default 50/50 splits):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**After** (with `column_ratios: [0.7]` and `row_ratios: [0.6]`):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"monitors": [
|
||||||
|
{
|
||||||
|
"workspaces": [
|
||||||
|
{
|
||||||
|
"name": "main",
|
||||||
|
"layout_options": {
|
||||||
|
"column_ratios": [0.3, 0.4],
|
||||||
|
"row_ratios": [0.4, 0.3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can specify up to 5 ratio values (defined by `MAX_RATIOS` constant). Each value should be between 0.1 and 0.9
|
||||||
|
(defined by `MIN_RATIO` and `MAX_RATIO` constants). Values outside this range are automatically clamped.
|
||||||
|
Columns or rows without a specified ratio will share the remaining space equally.
|
||||||
|
|
||||||
|
## Usage by Layout
|
||||||
|
|
||||||
|
| Layout | `column_ratios` | `row_ratios` |
|
||||||
|
|--------|-----------------|--------------|
|
||||||
|
| **Columns** | Width of each column | - |
|
||||||
|
| **Rows** | - | Height of each row |
|
||||||
|
| **Grid** | Width of each column (rows are equal height) | - |
|
||||||
|
| **BSP** | `[0]` as horizontal split ratio | `[0]` as vertical split ratio |
|
||||||
|
| **VerticalStack** | `[0]` as primary column width | Stack row heights |
|
||||||
|
| **RightMainVerticalStack** | `[0]` as primary column width | Stack row heights |
|
||||||
|
| **HorizontalStack** | Stack column widths | `[0]` as primary row height |
|
||||||
|
| **UltrawideVerticalStack** | `[0]` center, `[1]` left column | Tertiary stack row heights |
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Columns Layout with Custom Widths
|
||||||
|
|
||||||
|
Create 3 columns with 30%, 40%, and 30% widths:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layout_options": {
|
||||||
|
"column_ratios": [0.3, 0.4]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The third column automatically gets the remaining 30%.
|
||||||
|
|
||||||
|
### Rows Layout with Custom Heights
|
||||||
|
|
||||||
|
Create 3 rows with 20%, 50%, and 30% heights:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layout_options": {
|
||||||
|
"row_ratios": [0.2, 0.5]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The third row automatically gets the remaining 30%.
|
||||||
|
|
||||||
|
### Grid Layout with Custom Column Widths
|
||||||
|
|
||||||
|
Grid with custom column widths (rows within each column are always equal height):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layout_options": {
|
||||||
|
"column_ratios": [0.4, 0.6]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The Grid layout only supports `column_ratios`. Rows within each column are always
|
||||||
|
divided equally because the number of rows per column varies dynamically based on window count.
|
||||||
|
|
||||||
|
### VerticalStack with Custom Ratios
|
||||||
|
|
||||||
|
Primary column takes 60% width, and the stack rows are split 30%/70%:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layout_options": {
|
||||||
|
"column_ratios": [0.6],
|
||||||
|
"row_ratios": [0.3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The second row automatically gets the remaining 70%.
|
||||||
|
|
||||||
|
### HorizontalStack with Custom Ratios
|
||||||
|
|
||||||
|
Primary row takes 70% height, and the stack columns are split 40%/60%:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layout_options": {
|
||||||
|
"row_ratios": [0.7],
|
||||||
|
"column_ratios": [0.4]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The second column automatically gets the remaining 60%.
|
||||||
|
|
||||||
|
### UltrawideVerticalStack with Custom Ratios
|
||||||
|
|
||||||
|
Center column at 50%, left column at 25% (remaining 25% goes to tertiary stack),
|
||||||
|
with tertiary rows split 40%/60%:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layout_options": {
|
||||||
|
"column_ratios": [0.5, 0.25],
|
||||||
|
"row_ratios": [0.4]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The second row automatically gets the remaining 60%.
|
||||||
|
|
||||||
|
### BSP Layout with Custom Split Ratios
|
||||||
|
|
||||||
|
Use separate ratios for horizontal (left/right) and vertical (top/bottom) splits:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layout_options": {
|
||||||
|
"column_ratios": [0.6],
|
||||||
|
"row_ratios": [0.3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `column_ratios[0]`: Controls all horizontal splits (left window gets 60%, right gets 40%)
|
||||||
|
- `row_ratios[0]`: Controls all vertical splits (top window gets 30%, bottom gets 70%)
|
||||||
|
|
||||||
|
Note: BSP only uses the first value (`[0]`) from each ratio array. This single ratio is applied
|
||||||
|
consistently to all splits of that type throughout the layout. Additional values in the arrays are ignored.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Ratios are clamped between 0.1 and 0.9 (prevents zero-sized windows and ensures space for other windows)
|
||||||
|
- Default ratio is 0.5 (50%) when not specified, except for UltrawideVerticalStack secondary column which defaults to 0.25 (25%)
|
||||||
|
- Ratios are applied **progressively** - a ratio is only used when there are more windows to place after the current one
|
||||||
|
- The **last window always takes the remaining space**, regardless of defined ratios
|
||||||
|
- **Ratios that would sum to 100% or more are automatically truncated** at config load time to ensure there's always space for additional windows
|
||||||
|
- Unspecified ratios default to sharing the remaining space equally
|
||||||
|
- You only need to specify the ratios you want to customize; trailing values can be omitted
|
||||||
|
|
||||||
|
## Layout Options Rules
|
||||||
|
|
||||||
|
You can dynamically change `layout_options` based on the number of containers on a workspace
|
||||||
|
using `layout_options_rules`. This uses the same threshold-based logic as `layout_rules`:
|
||||||
|
when the container count is greater than or equal to a threshold, the highest matching
|
||||||
|
threshold's options are used.
|
||||||
|
|
||||||
|
Rules **fully replace** the base `layout_options` when they match. If no rule matches, the
|
||||||
|
base `layout_options` is used.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"monitors": [
|
||||||
|
{
|
||||||
|
"workspaces": [
|
||||||
|
{
|
||||||
|
"name": "main",
|
||||||
|
"layout": "VerticalStack",
|
||||||
|
"layout_options": {
|
||||||
|
"column_ratios": [0.6],
|
||||||
|
"row_ratios": [0.4]
|
||||||
|
},
|
||||||
|
"layout_options_rules": {
|
||||||
|
"3": { "column_ratios": [0.55] },
|
||||||
|
"5": { "column_ratios": [0.3, 0.3, 0.3], "row_ratios": [0.5] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the example above:
|
||||||
|
|
||||||
|
| Container Count | Effective `layout_options` |
|
||||||
|
|-----------------|---------------------------|
|
||||||
|
| 1-2 | Base: `column_ratios: [0.6]`, `row_ratios: [0.4]` |
|
||||||
|
| 3-4 | Rule "3": `column_ratios: [0.55]` (no row_ratios, no scrolling, no grid) |
|
||||||
|
| 5+ | Rule "5": `column_ratios: [0.3, 0.3, 0.3]`, `row_ratios: [0.5]` |
|
||||||
|
|
||||||
|
Rules can include any field that `layout_options` supports: `column_ratios`, `row_ratios`,
|
||||||
|
`scrolling`, and `grid`. When a rule matches, it completely replaces the base options. Fields
|
||||||
|
not specified in the matching rule default to their standard defaults (not the base
|
||||||
|
`layout_options` values).
|
||||||
|
|
||||||
|
### Example: Scrolling Layout with Dynamic Columns
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layout": "Scrolling",
|
||||||
|
"layout_options": {
|
||||||
|
"scrolling": { "columns": 2 }
|
||||||
|
},
|
||||||
|
"layout_options_rules": {
|
||||||
|
"4": { "scrolling": { "columns": 3 } },
|
||||||
|
"7": { "scrolling": { "columns": 4 } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This increases the visible scrolling columns as more windows are added.
|
||||||
|
|
||||||
|
## Layout Defaults
|
||||||
|
|
||||||
|
You can define global per-layout default `layout_options` and `layout_options_rules` using
|
||||||
|
the top-level `layout_defaults` setting. This avoids repeating the same configuration across
|
||||||
|
every workspace that uses the same layout.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layout_defaults": {
|
||||||
|
"VerticalStack": {
|
||||||
|
"layout_options": { "column_ratios": [0.7] },
|
||||||
|
"layout_options_rules": {
|
||||||
|
"2": { "column_ratios": [0.7] },
|
||||||
|
"3": { "column_ratios": [0.55] },
|
||||||
|
"5": { "column_ratios": [0.4] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Columns": {
|
||||||
|
"layout_options": { "column_ratios": [0.3, 0.4] },
|
||||||
|
"layout_options_rules": {
|
||||||
|
"4": { "column_ratios": [0.2, 0.3, 0.3] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HorizontalStack": {
|
||||||
|
"layout_options": { "row_ratios": [0.6] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"monitors": [
|
||||||
|
{
|
||||||
|
"workspaces": [
|
||||||
|
{
|
||||||
|
"name": "main",
|
||||||
|
"layout": "VerticalStack"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, every workspace using `VerticalStack`, `Columns`, or `HorizontalStack`
|
||||||
|
automatically gets the global `layout_options` and `layout_options_rules` without needing
|
||||||
|
to specify them per-workspace. Note that `VerticalStack` only has 2 columns (main + stack),
|
||||||
|
so only a single `column_ratios` value is meaningful, while `Columns` distributes windows
|
||||||
|
across multiple columns where additional ratios control each column's width.
|
||||||
|
|
||||||
|
### Resolution Cascade
|
||||||
|
|
||||||
|
Global defaults act as a fallback. If a workspace defines **either** `layout_options` or
|
||||||
|
`layout_options_rules`, it **completely replaces** all global `layout_defaults` for that
|
||||||
|
layout. Global defaults are only used when the workspace has **neither** setting.
|
||||||
|
|
||||||
|
Within the effective source (workspace or global):
|
||||||
|
1. Try threshold match from the rules (highest matching threshold wins)
|
||||||
|
2. If a rule matches → use it (full replacement of base options)
|
||||||
|
3. Otherwise → use the base `layout_options`
|
||||||
|
|
||||||
|
### Override Examples
|
||||||
|
|
||||||
|
| Workspace Config | Global Config | Effective Behavior |
|
||||||
|
|------------------|---------------|--------------------|
|
||||||
|
| No `layout_options`, no rules | `layout_defaults` has both | Uses global base + global rules |
|
||||||
|
| Has `layout_options` only | `layout_defaults` has both | Workspace base only (all globals ignored) |
|
||||||
|
| Has `layout_options_rules` only | `layout_defaults` has both | Workspace rules only (all globals ignored) |
|
||||||
|
| Has both | `layout_defaults` has both | All workspace (all globals ignored) |
|
||||||
|
|
||||||
|
This "complete replacement" semantic means you never get a mix of workspace and global
|
||||||
|
settings for the same layout. If you override anything at the workspace level, you take
|
||||||
|
full control of that layout's options for that workspace.
|
||||||
|
|
||||||
|
## Progressive Ratio Behavior
|
||||||
|
|
||||||
|
Ratios are applied progressively as windows are added. For example, with `row_ratios: [0.3, 0.5]` in a VerticalStack:
|
||||||
|
|
||||||
|
| Windows in Stack | Row Heights |
|
||||||
|
|------------------|-------------|
|
||||||
|
| 1 | 100% |
|
||||||
|
| 2 | 30%, 70% (remainder) |
|
||||||
|
| 3 | 30%, 50%, 20% (remainder) |
|
||||||
|
| 4 | 30%, 50%, 10%, 10% (remainder split equally) |
|
||||||
|
| 5 | 30%, 50%, 6.67%, 6.67%, 6.67% |
|
||||||
|
|
||||||
|
## Automatic Ratio Truncation
|
||||||
|
|
||||||
|
When ratios sum to 100% (or more), they are automatically truncated at config load time.
|
||||||
|
|
||||||
|
For example, if you configure `column_ratios: [0.4, 0.3, 0.3]` (sums to 100%), the last ratio (0.3) is automatically removed, resulting in effectively `[0.4, 0.3]`. This ensures there's always remaining space for the last window.
|
||||||
|
|
||||||
|
| Configured Ratios | Effective Ratios | Reason |
|
||||||
|
|-------------------|------------------|--------|
|
||||||
|
| `[0.3, 0.4]` | `[0.3, 0.4]` | Sum is 0.7, below 1.0 |
|
||||||
|
| `[0.4, 0.3, 0.3]` | `[0.4, 0.3]` | Sum would be 1.0, last ratio truncated |
|
||||||
|
| `[0.5, 0.5]` | `[0.5]` | Sum would be 1.0, last ratio truncated |
|
||||||
|
| `[0.6, 0.5]` | `[0.6]` | Sum would exceed 1.0, last ratio truncated |
|
||||||
|
|
||||||
|
This ensures the layout always fills 100% of the available space and new windows are never placed outside the visible area.
|
||||||
@@ -83,7 +83,7 @@ is a crude hack trying to compensate for the insistence of Microsoft Windows
|
|||||||
design teams to make custom borders with widths that are actually visible to
|
design teams to make custom borders with widths that are actually visible to
|
||||||
the user a thing of the past and removing this capability from the Win32 API.
|
the user a thing of the past and removing this capability from the Win32 API.
|
||||||
|
|
||||||
I know it's buggy, and I know that most of the it sucks, but this is something
|
I know it's buggy, and I know that most of the time it sucks, but this is something
|
||||||
you should be bring up with the billion dollar company and not with me, the
|
you should be bring up with the billion dollar company and not with me, the
|
||||||
solo developer.
|
solo developer.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.40/schema.bar.json",
|
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.41/schema.bar.json",
|
||||||
"font_family": "JetBrains Mono",
|
"font_family": "JetBrains Mono",
|
||||||
"theme": {
|
"theme": {
|
||||||
"palette": "Base16",
|
"palette": "Base16",
|
||||||
@@ -31,22 +31,22 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Media": {
|
"Media": {
|
||||||
"enable": true
|
"enable": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Storage": {
|
"Storage": {
|
||||||
"enable": true
|
"enable": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Memory": {
|
"Memory": {
|
||||||
"enable": true
|
"enable": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Network": {
|
"Network": {
|
||||||
"enable": true,
|
"enable": false,
|
||||||
"show_activity": true,
|
"show_activity": true,
|
||||||
"show_total_activity": true
|
"show_total_activity": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.40/schema.json",
|
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.41/schema.json",
|
||||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
||||||
"window_hiding_behaviour": "Cloak",
|
"window_hiding_behaviour": "Cloak",
|
||||||
"cross_monitor_move_behaviour": "Insert",
|
"cross_monitor_move_behaviour": "Insert",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi-bar"
|
name = "komorebi-bar"
|
||||||
version = "0.1.40"
|
version = "0.1.41"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
@@ -18,15 +18,17 @@ dirs = { workspace = true }
|
|||||||
dunce = { workspace = true }
|
dunce = { workspace = true }
|
||||||
eframe = { workspace = true }
|
eframe = { workspace = true }
|
||||||
egui-phosphor = { git = "https://github.com/amPerl/egui-phosphor", rev = "d13688738478ecd12b426e3e74c59d6577a85b59" }
|
egui-phosphor = { git = "https://github.com/amPerl/egui-phosphor", rev = "d13688738478ecd12b426e3e74c59d6577a85b59" }
|
||||||
|
egui_extras = { workspace = true }
|
||||||
font-loader = "0.11"
|
font-loader = "0.11"
|
||||||
hotwatch = { workspace = true }
|
hotwatch = { workspace = true }
|
||||||
image = "0.25"
|
image = "0.25"
|
||||||
lazy_static = { workspace = true }
|
lazy_static = { workspace = true }
|
||||||
netdev = "0.40"
|
netdev = "0.41"
|
||||||
num = "0.4"
|
num = "0.4"
|
||||||
num-derive = "0.4"
|
num-derive = "0.4"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
parking_lot = { workspace = true }
|
parking_lot = { workspace = true }
|
||||||
|
regex = "1"
|
||||||
random_word = { version = "0.5", features = ["en"] }
|
random_word = { version = "0.5", features = ["en"] }
|
||||||
reqwest = { version = "0.12", features = ["blocking"] }
|
reqwest = { version = "0.12", features = ["blocking"] }
|
||||||
schemars = { workspace = true, optional = true }
|
schemars = { workspace = true, optional = true }
|
||||||
@@ -34,6 +36,8 @@ serde = { workspace = true }
|
|||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
starship-battery = "0.10"
|
starship-battery = "0.10"
|
||||||
sysinfo = { workspace = true }
|
sysinfo = { workspace = true }
|
||||||
|
systray-util = "0.2.0"
|
||||||
|
tokio = { version = "1", features = ["rt", "sync", "time"] }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
which = { workspace = true }
|
which = { workspace = true }
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use crate::render::Color32Ext;
|
|||||||
use crate::render::Grouping;
|
use crate::render::Grouping;
|
||||||
use crate::render::RenderConfig;
|
use crate::render::RenderConfig;
|
||||||
use crate::render::RenderExt;
|
use crate::render::RenderExt;
|
||||||
|
use crate::take_widget_clicked;
|
||||||
use crate::widgets::komorebi::Komorebi;
|
use crate::widgets::komorebi::Komorebi;
|
||||||
use crate::widgets::komorebi::MonitorInfo;
|
use crate::widgets::komorebi::MonitorInfo;
|
||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
@@ -1082,6 +1083,10 @@ impl eframe::App for Komobar {
|
|||||||
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
|
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
|
||||||
|
|
||||||
CentralPanel::default().frame(frame).show(ctx, |ui| {
|
CentralPanel::default().frame(frame).show(ctx, |ui| {
|
||||||
|
// Variable to store command to execute after widgets are rendered
|
||||||
|
// This allows widgets to mark clicks as consumed before bar processes them
|
||||||
|
let mut pending_command: Option<crate::config::MouseMessage> = None;
|
||||||
|
|
||||||
if let Some(mouse_config) = &self.config.mouse {
|
if let Some(mouse_config) = &self.config.mouse {
|
||||||
let command = if ui
|
let command = if ui
|
||||||
.input(|i| i.pointer.button_double_clicked(PointerButton::Primary))
|
.input(|i| i.pointer.button_double_clicked(PointerButton::Primary))
|
||||||
@@ -1182,9 +1187,9 @@ impl eframe::App for Komobar {
|
|||||||
&None
|
&None
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(command) = command {
|
// Store the command to execute after widgets are rendered
|
||||||
command.execute(self.mouse_follows_focus);
|
// This allows widgets to mark clicks as consumed
|
||||||
}
|
pending_command = command.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply grouping logic for the bar as a whole
|
// Apply grouping logic for the bar as a whole
|
||||||
@@ -1316,6 +1321,13 @@ impl eframe::App for Komobar {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute the deferred mouse command only if no widget consumed the click
|
||||||
|
if let Some(command) = pending_command
|
||||||
|
&& !take_widget_clicked()
|
||||||
|
{
|
||||||
|
command.execute(self.mouse_follows_focus);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.40`
|
/// The `komorebi.bar.json` configuration file reference for `v0.1.41`
|
||||||
pub struct KomobarConfig {
|
pub struct KomobarConfig {
|
||||||
/// Bar height
|
/// Bar height
|
||||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 50)))]
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = 50)))]
|
||||||
@@ -621,6 +621,26 @@ extend_enum!(
|
|||||||
AllIconsAndTextOnSelected,
|
AllIconsAndTextOnSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Media widget display format
|
||||||
|
pub enum MediaDisplayFormat {
|
||||||
|
/// Show only the media info icon
|
||||||
|
Icon,
|
||||||
|
/// Show only the media info text (artist - title)
|
||||||
|
Text,
|
||||||
|
/// Show both icon and text
|
||||||
|
IconAndText,
|
||||||
|
/// Show only the control buttons (previous, play/pause, next)
|
||||||
|
ControlsOnly,
|
||||||
|
/// Show icon with control buttons
|
||||||
|
IconAndControls,
|
||||||
|
/// Show text with control buttons
|
||||||
|
TextAndControls,
|
||||||
|
/// Show icon, text, and control buttons
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
|
|||||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||||
use windows_core::BOOL;
|
use windows_core::BOOL;
|
||||||
|
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
|
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
|
||||||
pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0);
|
pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0);
|
||||||
pub static MONITOR_TOP: AtomicI32 = AtomicI32::new(0);
|
pub static MONITOR_TOP: AtomicI32 = AtomicI32::new(0);
|
||||||
@@ -46,6 +48,20 @@ pub static MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0);
|
|||||||
pub static BAR_HEIGHT: f32 = 50.0;
|
pub static BAR_HEIGHT: f32 = 50.0;
|
||||||
pub static DEFAULT_PADDING: f32 = 10.0;
|
pub static DEFAULT_PADDING: f32 = 10.0;
|
||||||
|
|
||||||
|
/// Flag to indicate that a widget has consumed a click event this frame.
|
||||||
|
/// This prevents the bar's global mouse handler from also processing the click.
|
||||||
|
pub static WIDGET_CLICKED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
/// Mark that a widget has consumed a click event this frame.
|
||||||
|
pub fn mark_widget_clicked() {
|
||||||
|
WIDGET_CLICKED.store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a widget has consumed a click event this frame and reset the flag.
|
||||||
|
pub fn take_widget_clicked() -> bool {
|
||||||
|
WIDGET_CLICKED.swap(false, Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
pub static AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
|
pub static AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||||
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
|
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use crate::MAX_LABEL_WIDTH;
|
use crate::MAX_LABEL_WIDTH;
|
||||||
|
use crate::bar::Alignment;
|
||||||
|
use crate::config::MediaDisplayFormat;
|
||||||
use crate::render::RenderConfig;
|
use crate::render::RenderConfig;
|
||||||
use crate::selected_frame::SelectableFrame;
|
use crate::selected_frame::SelectableFrame;
|
||||||
use crate::ui::CustomUi;
|
use crate::ui::CustomUi;
|
||||||
@@ -14,6 +16,7 @@ use serde::Deserialize;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
|
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
|
||||||
|
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
@@ -21,24 +24,31 @@ use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
|
|||||||
pub struct MediaConfig {
|
pub struct MediaConfig {
|
||||||
/// Enable the Media widget
|
/// Enable the Media widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
|
/// Display format of the media widget (defaults to IconAndText)
|
||||||
|
pub display: Option<MediaDisplayFormat>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<MediaConfig> for Media {
|
impl From<MediaConfig> for Media {
|
||||||
fn from(value: MediaConfig) -> Self {
|
fn from(value: MediaConfig) -> Self {
|
||||||
Self::new(value.enable)
|
Self::new(
|
||||||
|
value.enable,
|
||||||
|
value.display.unwrap_or(MediaDisplayFormat::IconAndText),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Media {
|
pub struct Media {
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
|
pub display: MediaDisplayFormat,
|
||||||
pub session_manager: GlobalSystemMediaTransportControlsSessionManager,
|
pub session_manager: GlobalSystemMediaTransportControlsSessionManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Media {
|
impl Media {
|
||||||
pub fn new(enable: bool) -> Self {
|
pub fn new(enable: bool, display: MediaDisplayFormat) -> Self {
|
||||||
Self {
|
Self {
|
||||||
enable,
|
enable,
|
||||||
|
display,
|
||||||
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
|
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.join()
|
.join()
|
||||||
@@ -54,6 +64,58 @@ impl Media {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn previous(&self) {
|
||||||
|
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||||
|
&& let Ok(op) = session.TrySkipPreviousAsync()
|
||||||
|
{
|
||||||
|
op.join().unwrap_or_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&self) {
|
||||||
|
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||||
|
&& let Ok(op) = session.TrySkipNextAsync()
|
||||||
|
{
|
||||||
|
op.join().unwrap_or_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_playing(&self) -> bool {
|
||||||
|
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||||
|
&& let Ok(info) = session.GetPlaybackInfo()
|
||||||
|
&& let Ok(status) = info.PlaybackStatus()
|
||||||
|
{
|
||||||
|
return status == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_previous_enabled(&self) -> bool {
|
||||||
|
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||||
|
&& let Ok(info) = session.GetPlaybackInfo()
|
||||||
|
&& let Ok(controls) = info.Controls()
|
||||||
|
&& let Ok(enabled) = controls.IsPreviousEnabled()
|
||||||
|
{
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_next_enabled(&self) -> bool {
|
||||||
|
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||||
|
&& let Ok(info) = session.GetPlaybackInfo()
|
||||||
|
&& let Ok(controls) = info.Controls()
|
||||||
|
&& let Ok(enabled) = controls.IsNextEnabled()
|
||||||
|
{
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_session(&self) -> bool {
|
||||||
|
self.session_manager.GetCurrentSession().is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
fn output(&mut self) -> String {
|
fn output(&mut self) -> String {
|
||||||
if let Ok(session) = self.session_manager.GetCurrentSession()
|
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||||
&& let Ok(operation) = session.TryGetMediaPropertiesAsync()
|
&& let Ok(operation) = session.TryGetMediaPropertiesAsync()
|
||||||
@@ -78,28 +140,96 @@ impl Media {
|
|||||||
impl BarWidget for Media {
|
impl BarWidget for Media {
|
||||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||||
if self.enable {
|
if self.enable {
|
||||||
|
// Don't render if there's no active media session
|
||||||
|
if !self.has_session() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let output = self.output();
|
let output = self.output();
|
||||||
if !output.is_empty() {
|
|
||||||
let mut layout_job = LayoutJob::simple(
|
let show_icon = matches!(
|
||||||
|
self.display,
|
||||||
|
MediaDisplayFormat::Icon
|
||||||
|
| MediaDisplayFormat::IconAndText
|
||||||
|
| MediaDisplayFormat::IconAndControls
|
||||||
|
| MediaDisplayFormat::Full
|
||||||
|
);
|
||||||
|
let show_text = matches!(
|
||||||
|
self.display,
|
||||||
|
MediaDisplayFormat::Text
|
||||||
|
| MediaDisplayFormat::IconAndText
|
||||||
|
| MediaDisplayFormat::TextAndControls
|
||||||
|
| MediaDisplayFormat::Full
|
||||||
|
);
|
||||||
|
let show_controls = matches!(
|
||||||
|
self.display,
|
||||||
|
MediaDisplayFormat::ControlsOnly
|
||||||
|
| MediaDisplayFormat::IconAndControls
|
||||||
|
| MediaDisplayFormat::TextAndControls
|
||||||
|
| MediaDisplayFormat::Full
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't render if there's no media info and we're not showing controls-only
|
||||||
|
if output.is_empty() && !show_controls {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let icon_font_id = config.icon_font_id.clone();
|
||||||
|
let text_font_id = config.text_font_id.clone();
|
||||||
|
let icon_color = ctx.style().visuals.selection.stroke.color;
|
||||||
|
let text_color = ctx.style().visuals.text_color();
|
||||||
|
|
||||||
|
let mut layout_job = LayoutJob::default();
|
||||||
|
|
||||||
|
if show_icon {
|
||||||
|
layout_job = LayoutJob::simple(
|
||||||
egui_phosphor::regular::HEADPHONES.to_string(),
|
egui_phosphor::regular::HEADPHONES.to_string(),
|
||||||
config.icon_font_id.clone(),
|
icon_font_id.clone(),
|
||||||
ctx.style().visuals.selection.stroke.color,
|
icon_color,
|
||||||
100.0,
|
100.0,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if show_text {
|
||||||
layout_job.append(
|
layout_job.append(
|
||||||
&output,
|
&output,
|
||||||
10.0,
|
if show_icon { 10.0 } else { 0.0 },
|
||||||
TextFormat {
|
TextFormat {
|
||||||
font_id: config.text_font_id.clone(),
|
font_id: text_font_id,
|
||||||
color: ctx.style().visuals.text_color(),
|
color: text_color,
|
||||||
valign: Align::Center,
|
valign: Align::Center,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
config.apply_on_widget(false, ui, |ui| {
|
let is_playing = self.is_playing();
|
||||||
if SelectableFrame::new(false)
|
let is_previous_enabled = self.is_previous_enabled();
|
||||||
|
let is_next_enabled = self.is_next_enabled();
|
||||||
|
let disabled_color = text_color.gamma_multiply(0.5);
|
||||||
|
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
|
||||||
|
|
||||||
|
let prev_color = if is_previous_enabled {
|
||||||
|
text_color
|
||||||
|
} else {
|
||||||
|
disabled_color
|
||||||
|
};
|
||||||
|
|
||||||
|
let next_color = if is_next_enabled {
|
||||||
|
text_color
|
||||||
|
} else {
|
||||||
|
disabled_color
|
||||||
|
};
|
||||||
|
|
||||||
|
let play_pause_icon = if is_playing {
|
||||||
|
egui_phosphor::regular::PAUSE
|
||||||
|
} else {
|
||||||
|
egui_phosphor::regular::PLAY
|
||||||
|
};
|
||||||
|
|
||||||
|
let show_label = |ui: &mut Ui| {
|
||||||
|
if (show_icon || show_text)
|
||||||
|
&& SelectableFrame::new(false)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
let available_height = ui.available_height();
|
let available_height = ui.available_height();
|
||||||
let mut custom_ui = CustomUi(ui);
|
let mut custom_ui = CustomUi(ui);
|
||||||
@@ -109,15 +239,95 @@ impl BarWidget for Media {
|
|||||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||||
available_height,
|
available_height,
|
||||||
),
|
),
|
||||||
Label::new(layout_job).selectable(false).truncate(),
|
Label::new(layout_job.clone()).selectable(false).truncate(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.on_hover_text(&output)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
self.toggle();
|
self.toggle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let show_previous = |ui: &mut Ui| {
|
||||||
|
if SelectableFrame::new(false)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add(
|
||||||
|
Label::new(LayoutJob::simple(
|
||||||
|
egui_phosphor::regular::SKIP_BACK.to_string(),
|
||||||
|
icon_font_id.clone(),
|
||||||
|
prev_color,
|
||||||
|
100.0,
|
||||||
|
))
|
||||||
|
.selectable(false),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.clicked()
|
||||||
|
&& is_previous_enabled
|
||||||
|
{
|
||||||
|
self.previous();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let show_play_pause = |ui: &mut Ui| {
|
||||||
|
if SelectableFrame::new(false)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add(
|
||||||
|
Label::new(LayoutJob::simple(
|
||||||
|
play_pause_icon.to_string(),
|
||||||
|
icon_font_id.clone(),
|
||||||
|
text_color,
|
||||||
|
100.0,
|
||||||
|
))
|
||||||
|
.selectable(false),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.on_hover_text(&output)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
self.toggle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let show_next = |ui: &mut Ui| {
|
||||||
|
if SelectableFrame::new(false)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add(
|
||||||
|
Label::new(LayoutJob::simple(
|
||||||
|
egui_phosphor::regular::SKIP_FORWARD.to_string(),
|
||||||
|
icon_font_id.clone(),
|
||||||
|
next_color,
|
||||||
|
100.0,
|
||||||
|
))
|
||||||
|
.selectable(false),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.clicked()
|
||||||
|
&& is_next_enabled
|
||||||
|
{
|
||||||
|
self.next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
config.apply_on_widget(false, ui, |ui| {
|
||||||
|
if is_reversed {
|
||||||
|
// Right panel renders right-to-left, so reverse order
|
||||||
|
if show_controls {
|
||||||
|
show_next(ui);
|
||||||
|
show_play_pause(ui);
|
||||||
|
show_previous(ui);
|
||||||
}
|
}
|
||||||
});
|
show_label(ui);
|
||||||
}
|
} else {
|
||||||
|
// Left/center panel renders left-to-right, normal order
|
||||||
|
show_label(ui);
|
||||||
|
if show_controls {
|
||||||
|
show_previous(ui);
|
||||||
|
show_play_pause(ui);
|
||||||
|
show_next(ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ pub mod media;
|
|||||||
pub mod memory;
|
pub mod memory;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub mod systray;
|
||||||
pub mod time;
|
pub mod time;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
pub mod widget;
|
pub mod widget;
|
||||||
@@ -92,10 +94,16 @@ impl IconsCache {
|
|||||||
pub fn insert_image(&self, id: ImageIconId, image: Arc<ColorImage>) {
|
pub fn insert_image(&self, id: ImageIconId, image: Arc<ColorImage>) {
|
||||||
self.images.write().unwrap().insert(id, image);
|
self.images.write().unwrap().insert(id, image);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes the cached image and texture for the given icon ID.
|
||||||
|
pub fn remove(&self, id: &ImageIconId) {
|
||||||
|
self.images.write().unwrap().remove(id);
|
||||||
|
self.textures.write().unwrap().1.remove(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn rgba_to_color_image(rgba_image: &RgbaImage) -> ColorImage {
|
pub(crate) fn rgba_to_color_image(rgba_image: &RgbaImage) -> ColorImage {
|
||||||
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
|
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
|
||||||
let pixels = rgba_image.as_flat_samples();
|
let pixels = rgba_image.as_flat_samples();
|
||||||
ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())
|
ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())
|
||||||
@@ -156,6 +164,8 @@ pub enum ImageIconId {
|
|||||||
Path(Arc<Path>),
|
Path(Arc<Path>),
|
||||||
/// Windows HWND handle.
|
/// Windows HWND handle.
|
||||||
Hwnd(isize),
|
Hwnd(isize),
|
||||||
|
/// System tray icon identifier.
|
||||||
|
SystrayIcon(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Path> for ImageIconId {
|
impl From<&Path> for ImageIconId {
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ pub struct StorageConfig {
|
|||||||
/// Show removable disks
|
/// Show removable disks
|
||||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
|
||||||
pub show_removable_disks: Option<bool>,
|
pub show_removable_disks: Option<bool>,
|
||||||
|
/// Storage display name
|
||||||
|
pub storage_display_name: Option<StorageDisplayName>,
|
||||||
/// Select when the current percentage is over this value [[1-100]]
|
/// Select when the current percentage is over this value [[1-100]]
|
||||||
pub auto_select_over: Option<u8>,
|
pub auto_select_over: Option<u8>,
|
||||||
/// Hide when the current percentage is under this value [[1-100]]
|
/// Hide when the current percentage is under this value [[1-100]]
|
||||||
@@ -48,6 +50,9 @@ impl From<StorageConfig> for Storage {
|
|||||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||||
show_read_only_disks: value.show_read_only_disks.unwrap_or(false),
|
show_read_only_disks: value.show_read_only_disks.unwrap_or(false),
|
||||||
show_removable_disks: value.show_removable_disks.unwrap_or(true),
|
show_removable_disks: value.show_removable_disks.unwrap_or(true),
|
||||||
|
storage_display_name: value
|
||||||
|
.storage_display_name
|
||||||
|
.unwrap_or(StorageDisplayName::Mount),
|
||||||
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
|
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
|
||||||
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
|
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
|
||||||
last_updated: Instant::now(),
|
last_updated: Instant::now(),
|
||||||
@@ -55,6 +60,19 @@ impl From<StorageConfig> for Storage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
pub enum StorageDisplayName {
|
||||||
|
/// Display label as mount point eg. C:\
|
||||||
|
Mount,
|
||||||
|
/// Display label as name eg. Local Disk
|
||||||
|
Name,
|
||||||
|
/// Display label as mount then name eg. C:\ Local Disk
|
||||||
|
MountAndName,
|
||||||
|
/// Display label as name then mount eg. Local Disk C:\
|
||||||
|
NameAndMount,
|
||||||
|
}
|
||||||
|
|
||||||
struct StorageDisk {
|
struct StorageDisk {
|
||||||
label: String,
|
label: String,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
@@ -67,6 +85,7 @@ pub struct Storage {
|
|||||||
label_prefix: LabelPrefix,
|
label_prefix: LabelPrefix,
|
||||||
show_read_only_disks: bool,
|
show_read_only_disks: bool,
|
||||||
show_removable_disks: bool,
|
show_removable_disks: bool,
|
||||||
|
storage_display_name: StorageDisplayName,
|
||||||
auto_select_over: Option<u8>,
|
auto_select_over: Option<u8>,
|
||||||
auto_hide_under: Option<u8>,
|
auto_hide_under: Option<u8>,
|
||||||
last_updated: Instant,
|
last_updated: Instant,
|
||||||
@@ -90,6 +109,17 @@ impl Storage {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mount = disk.mount_point();
|
let mount = disk.mount_point();
|
||||||
|
let name = disk.name();
|
||||||
|
let display_name = match self.storage_display_name {
|
||||||
|
StorageDisplayName::Mount => mount.to_string_lossy(),
|
||||||
|
StorageDisplayName::Name => name.to_string_lossy(),
|
||||||
|
StorageDisplayName::MountAndName => {
|
||||||
|
mount.to_string_lossy() + name.to_string_lossy()
|
||||||
|
}
|
||||||
|
StorageDisplayName::NameAndMount => {
|
||||||
|
name.to_string_lossy() + mount.to_string_lossy()
|
||||||
|
}
|
||||||
|
};
|
||||||
let total = disk.total_space();
|
let total = disk.total_space();
|
||||||
let available = disk.available_space();
|
let available = disk.available_space();
|
||||||
let used = total - available;
|
let used = total - available;
|
||||||
@@ -103,7 +133,7 @@ impl Storage {
|
|||||||
disks.push(StorageDisk {
|
disks.push(StorageDisk {
|
||||||
label: match self.label_prefix {
|
label: match self.label_prefix {
|
||||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||||
format!("{} {}%", mount.to_string_lossy(), percentage)
|
format!("{} {}%", display_name, percentage)
|
||||||
}
|
}
|
||||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage}%"),
|
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage}%"),
|
||||||
},
|
},
|
||||||
|
|||||||
1301
komorebi-bar/src/widgets/systray.rs
Normal file
1301
komorebi-bar/src/widgets/systray.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,10 @@ use crate::widgets::network::Network;
|
|||||||
use crate::widgets::network::NetworkConfig;
|
use crate::widgets::network::NetworkConfig;
|
||||||
use crate::widgets::storage::Storage;
|
use crate::widgets::storage::Storage;
|
||||||
use crate::widgets::storage::StorageConfig;
|
use crate::widgets::storage::StorageConfig;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use crate::widgets::systray::Systray;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use crate::widgets::systray::SystrayConfig;
|
||||||
use crate::widgets::time::Time;
|
use crate::widgets::time::Time;
|
||||||
use crate::widgets::time::TimeConfig;
|
use crate::widgets::time::TimeConfig;
|
||||||
use crate::widgets::update::Update;
|
use crate::widgets::update::Update;
|
||||||
@@ -66,6 +70,10 @@ pub enum WidgetConfig {
|
|||||||
/// Storage widget configuration
|
/// Storage widget configuration
|
||||||
#[cfg_attr(feature = "schemars", schemars(title = "Storage"))]
|
#[cfg_attr(feature = "schemars", schemars(title = "Storage"))]
|
||||||
Storage(StorageConfig),
|
Storage(StorageConfig),
|
||||||
|
/// System Tray widget configuration (Windows only)
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Systray"))]
|
||||||
|
Systray(SystrayConfig),
|
||||||
/// Time widget configuration
|
/// Time widget configuration
|
||||||
#[cfg_attr(feature = "schemars", schemars(title = "Time"))]
|
#[cfg_attr(feature = "schemars", schemars(title = "Time"))]
|
||||||
Time(TimeConfig),
|
Time(TimeConfig),
|
||||||
@@ -87,6 +95,8 @@ impl WidgetConfig {
|
|||||||
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
|
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
|
||||||
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
|
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
|
||||||
WidgetConfig::Storage(config) => Box::new(Storage::from(*config)),
|
WidgetConfig::Storage(config) => Box::new(Storage::from(*config)),
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
WidgetConfig::Systray(config) => Box::new(Systray::from(config)),
|
||||||
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
|
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
|
||||||
WidgetConfig::Update(config) => Box::new(Update::from(*config)),
|
WidgetConfig::Update(config) => Box::new(Update::from(*config)),
|
||||||
}
|
}
|
||||||
@@ -112,6 +122,8 @@ impl WidgetConfig {
|
|||||||
WidgetConfig::Memory(config) => config.enable,
|
WidgetConfig::Memory(config) => config.enable,
|
||||||
WidgetConfig::Network(config) => config.enable,
|
WidgetConfig::Network(config) => config.enable,
|
||||||
WidgetConfig::Storage(config) => config.enable,
|
WidgetConfig::Storage(config) => config.enable,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
WidgetConfig::Systray(config) => config.enable,
|
||||||
WidgetConfig::Time(config) => config.enable,
|
WidgetConfig::Time(config) => config.enable,
|
||||||
WidgetConfig::Update(config) => config.enable,
|
WidgetConfig::Update(config) => config.enable,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi-client"
|
name = "komorebi-client"
|
||||||
version = "0.1.40"
|
version = "0.1.41"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi-gui"
|
name = "komorebi-gui"
|
||||||
version = "0.1.40"
|
version = "0.1.41"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|||||||
26
komorebi-layouts/Cargo.toml
Normal file
26
komorebi-layouts/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "komorebi-layouts"
|
||||||
|
version = "0.1.41"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { workspace = true }
|
||||||
|
color-eyre = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
serde_yaml = { workspace = true }
|
||||||
|
strum = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
|
||||||
|
# Optional dependencies
|
||||||
|
schemars = { workspace = true, optional = true }
|
||||||
|
windows = { workspace = true, optional = true }
|
||||||
|
objc2-core-foundation = { version = "0.3", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"CFCGTypes",
|
||||||
|
], optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
schemars = ["dep:schemars"]
|
||||||
|
win32 = ["dep:windows"]
|
||||||
|
darwin = ["dep:objc2-core-foundation"]
|
||||||
@@ -6,13 +6,22 @@ use serde::Serialize;
|
|||||||
use strum::Display;
|
use strum::Display;
|
||||||
use strum::EnumString;
|
use strum::EnumString;
|
||||||
|
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
use super::CustomLayout;
|
use super::CustomLayout;
|
||||||
use super::DefaultLayout;
|
use super::DefaultLayout;
|
||||||
use super::Rect;
|
use super::Rect;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
use super::custom_layout::Column;
|
use super::custom_layout::Column;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
use super::custom_layout::ColumnSplit;
|
use super::custom_layout::ColumnSplit;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
use super::custom_layout::ColumnSplitWithCapacity;
|
use super::custom_layout::ColumnSplitWithCapacity;
|
||||||
|
use crate::default_layout::DEFAULT_RATIO;
|
||||||
|
use crate::default_layout::DEFAULT_SECONDARY_RATIO;
|
||||||
use crate::default_layout::LayoutOptions;
|
use crate::default_layout::LayoutOptions;
|
||||||
|
use crate::default_layout::MAX_RATIO;
|
||||||
|
use crate::default_layout::MAX_RATIOS;
|
||||||
|
use crate::default_layout::MIN_RATIO;
|
||||||
|
|
||||||
pub trait Arrangement {
|
pub trait Arrangement {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -42,10 +51,23 @@ impl Arrangement for DefaultLayout {
|
|||||||
layout_options: Option<LayoutOptions>,
|
layout_options: Option<LayoutOptions>,
|
||||||
latest_layout: &[Rect],
|
latest_layout: &[Rect],
|
||||||
) -> Vec<Rect> {
|
) -> Vec<Rect> {
|
||||||
|
// Trace layout_options for debugging
|
||||||
|
if let Some(ref opts) = layout_options {
|
||||||
|
tracing::debug!(
|
||||||
|
"Layout {:?} - layout_options received: column_ratios={:?}, row_ratios={:?}",
|
||||||
|
self,
|
||||||
|
opts.column_ratios,
|
||||||
|
opts.row_ratios
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tracing::debug!("Layout {:?} - no layout_options provided", self);
|
||||||
|
}
|
||||||
|
|
||||||
let len = usize::from(len);
|
let len = usize::from(len);
|
||||||
let mut dimensions = match self {
|
let mut dimensions = match self {
|
||||||
Self::Scrolling => {
|
Self::Scrolling => {
|
||||||
let column_count = layout_options
|
let column_count = layout_options
|
||||||
|
.as_ref()
|
||||||
.and_then(|o| o.scrolling.map(|s| s.columns))
|
.and_then(|o| o.scrolling.map(|s| s.columns))
|
||||||
.unwrap_or(3);
|
.unwrap_or(3);
|
||||||
|
|
||||||
@@ -54,6 +76,7 @@ impl Arrangement for DefaultLayout {
|
|||||||
|
|
||||||
let visible_columns = area.right / column_width;
|
let visible_columns = area.right / column_width;
|
||||||
let keep_centered = layout_options
|
let keep_centered = layout_options
|
||||||
|
.as_ref()
|
||||||
.and_then(|o| {
|
.and_then(|o| {
|
||||||
o.scrolling
|
o.scrolling
|
||||||
.map(|s| s.center_focused_column.unwrap_or_default())
|
.map(|s| s.center_focused_column.unwrap_or_default())
|
||||||
@@ -118,6 +141,15 @@ impl Arrangement for DefaultLayout {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Last visible column absorbs any remainder from integer division
|
||||||
|
// so that visible columns tile the full area width without gaps
|
||||||
|
let width_remainder = area.right - column_width * visible_columns;
|
||||||
|
if width_remainder > 0 {
|
||||||
|
let last_visible_idx =
|
||||||
|
(first_visible as usize + visible_columns as usize - 1).min(len - 1);
|
||||||
|
layouts[last_visible_idx].right += width_remainder;
|
||||||
|
}
|
||||||
|
|
||||||
let adjustment = calculate_scrolling_adjustment(resize_dimensions);
|
let adjustment = calculate_scrolling_adjustment(resize_dimensions);
|
||||||
layouts
|
layouts
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
@@ -131,15 +163,30 @@ impl Arrangement for DefaultLayout {
|
|||||||
|
|
||||||
layouts
|
layouts
|
||||||
}
|
}
|
||||||
Self::BSP => recursive_fibonacci(
|
Self::BSP => {
|
||||||
0,
|
let column_split_ratio = layout_options
|
||||||
len,
|
.and_then(|o| o.column_ratios)
|
||||||
area,
|
.and_then(|r| r[0])
|
||||||
layout_flip,
|
.unwrap_or(DEFAULT_RATIO)
|
||||||
calculate_resize_adjustments(resize_dimensions),
|
.clamp(MIN_RATIO, MAX_RATIO);
|
||||||
),
|
let row_split_ratio = layout_options
|
||||||
|
.and_then(|o| o.row_ratios)
|
||||||
|
.and_then(|r| r[0])
|
||||||
|
.unwrap_or(DEFAULT_RATIO)
|
||||||
|
.clamp(MIN_RATIO, MAX_RATIO);
|
||||||
|
recursive_fibonacci(
|
||||||
|
0,
|
||||||
|
len,
|
||||||
|
area,
|
||||||
|
layout_flip,
|
||||||
|
calculate_resize_adjustments(resize_dimensions),
|
||||||
|
column_split_ratio,
|
||||||
|
row_split_ratio,
|
||||||
|
)
|
||||||
|
}
|
||||||
Self::Columns => {
|
Self::Columns => {
|
||||||
let mut layouts = columns(area, len);
|
let ratios = layout_options.and_then(|o| o.column_ratios);
|
||||||
|
let mut layouts = columns_with_ratios(area, len, ratios);
|
||||||
|
|
||||||
let adjustment = calculate_columns_adjustment(resize_dimensions);
|
let adjustment = calculate_columns_adjustment(resize_dimensions);
|
||||||
layouts
|
layouts
|
||||||
@@ -163,7 +210,8 @@ impl Arrangement for DefaultLayout {
|
|||||||
layouts
|
layouts
|
||||||
}
|
}
|
||||||
Self::Rows => {
|
Self::Rows => {
|
||||||
let mut layouts = rows(area, len);
|
let ratios = layout_options.and_then(|o| o.row_ratios);
|
||||||
|
let mut layouts = rows_with_ratios(area, len, ratios);
|
||||||
|
|
||||||
let adjustment = calculate_rows_adjustment(resize_dimensions);
|
let adjustment = calculate_rows_adjustment(resize_dimensions);
|
||||||
layouts
|
layouts
|
||||||
@@ -189,9 +237,17 @@ impl Arrangement for DefaultLayout {
|
|||||||
Self::VerticalStack => {
|
Self::VerticalStack => {
|
||||||
let mut layouts: Vec<Rect> = vec![];
|
let mut layouts: Vec<Rect> = vec![];
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
let primary_right = match len {
|
let primary_right = match len {
|
||||||
1 => area.right,
|
1 => area.right,
|
||||||
_ => area.right / 2,
|
_ => {
|
||||||
|
let ratio = layout_options
|
||||||
|
.and_then(|o| o.column_ratios)
|
||||||
|
.and_then(|r| r[0])
|
||||||
|
.unwrap_or(DEFAULT_RATIO)
|
||||||
|
.clamp(MIN_RATIO, MAX_RATIO);
|
||||||
|
(area.right as f32 * ratio) as i32
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let main_left = area.left;
|
let main_left = area.left;
|
||||||
@@ -206,7 +262,8 @@ impl Arrangement for DefaultLayout {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if len > 1 {
|
if len > 1 {
|
||||||
layouts.append(&mut rows(
|
let row_ratios = layout_options.and_then(|o| o.row_ratios);
|
||||||
|
layouts.append(&mut rows_with_ratios(
|
||||||
&Rect {
|
&Rect {
|
||||||
left: stack_left,
|
left: stack_left,
|
||||||
top: area.top,
|
top: area.top,
|
||||||
@@ -214,6 +271,7 @@ impl Arrangement for DefaultLayout {
|
|||||||
bottom: area.bottom,
|
bottom: area.bottom,
|
||||||
},
|
},
|
||||||
len - 1,
|
len - 1,
|
||||||
|
row_ratios,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,9 +315,17 @@ impl Arrangement for DefaultLayout {
|
|||||||
// Shamelessly borrowed from LeftWM: https://github.com/leftwm/leftwm/commit/f673851745295ae7584a102535566f559d96a941
|
// Shamelessly borrowed from LeftWM: https://github.com/leftwm/leftwm/commit/f673851745295ae7584a102535566f559d96a941
|
||||||
let mut layouts: Vec<Rect> = vec![];
|
let mut layouts: Vec<Rect> = vec![];
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
let primary_width = match len {
|
let primary_width = match len {
|
||||||
1 => area.right,
|
1 => area.right,
|
||||||
_ => area.right / 2,
|
_ => {
|
||||||
|
let ratio = layout_options
|
||||||
|
.and_then(|o| o.column_ratios)
|
||||||
|
.and_then(|r| r[0])
|
||||||
|
.unwrap_or(DEFAULT_RATIO)
|
||||||
|
.clamp(MIN_RATIO, MAX_RATIO);
|
||||||
|
(area.right as f32 * ratio) as i32
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let primary_left = match len {
|
let primary_left = match len {
|
||||||
@@ -276,7 +342,8 @@ impl Arrangement for DefaultLayout {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if len > 1 {
|
if len > 1 {
|
||||||
layouts.append(&mut rows(
|
let row_ratios = layout_options.and_then(|o| o.row_ratios);
|
||||||
|
layouts.append(&mut rows_with_ratios(
|
||||||
&Rect {
|
&Rect {
|
||||||
left: area.left,
|
left: area.left,
|
||||||
top: area.top,
|
top: area.top,
|
||||||
@@ -284,6 +351,7 @@ impl Arrangement for DefaultLayout {
|
|||||||
bottom: area.bottom,
|
bottom: area.bottom,
|
||||||
},
|
},
|
||||||
len - 1,
|
len - 1,
|
||||||
|
row_ratios,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,9 +394,17 @@ impl Arrangement for DefaultLayout {
|
|||||||
Self::HorizontalStack => {
|
Self::HorizontalStack => {
|
||||||
let mut layouts: Vec<Rect> = vec![];
|
let mut layouts: Vec<Rect> = vec![];
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
let bottom = match len {
|
let bottom = match len {
|
||||||
1 => area.bottom,
|
1 => area.bottom,
|
||||||
_ => area.bottom / 2,
|
_ => {
|
||||||
|
let ratio = layout_options
|
||||||
|
.and_then(|o| o.row_ratios)
|
||||||
|
.and_then(|r| r[0])
|
||||||
|
.unwrap_or(DEFAULT_RATIO)
|
||||||
|
.clamp(MIN_RATIO, MAX_RATIO);
|
||||||
|
(area.bottom as f32 * ratio) as i32
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let main_top = area.top;
|
let main_top = area.top;
|
||||||
@@ -343,7 +419,8 @@ impl Arrangement for DefaultLayout {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if len > 1 {
|
if len > 1 {
|
||||||
layouts.append(&mut columns(
|
let col_ratios = layout_options.and_then(|o| o.column_ratios);
|
||||||
|
layouts.append(&mut columns_with_ratios(
|
||||||
&Rect {
|
&Rect {
|
||||||
left: area.left,
|
left: area.left,
|
||||||
top: stack_top,
|
top: stack_top,
|
||||||
@@ -351,6 +428,7 @@ impl Arrangement for DefaultLayout {
|
|||||||
bottom: area.bottom - bottom,
|
bottom: area.bottom - bottom,
|
||||||
},
|
},
|
||||||
len - 1,
|
len - 1,
|
||||||
|
col_ratios,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -393,15 +471,28 @@ impl Arrangement for DefaultLayout {
|
|||||||
Self::UltrawideVerticalStack => {
|
Self::UltrawideVerticalStack => {
|
||||||
let mut layouts: Vec<Rect> = vec![];
|
let mut layouts: Vec<Rect> = vec![];
|
||||||
|
|
||||||
|
// Get ratios: [0] = primary (center), [1] = secondary (left), remainder = tertiary (right)
|
||||||
|
let ratios = layout_options.and_then(|o| o.column_ratios);
|
||||||
|
let primary_ratio = ratios
|
||||||
|
.and_then(|r| r[0])
|
||||||
|
.unwrap_or(DEFAULT_RATIO)
|
||||||
|
.clamp(MIN_RATIO, MAX_RATIO);
|
||||||
|
let secondary_ratio = ratios
|
||||||
|
.and_then(|r| r[1])
|
||||||
|
.unwrap_or(DEFAULT_SECONDARY_RATIO)
|
||||||
|
.clamp(MIN_RATIO, MAX_RATIO);
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
let primary_right = match len {
|
let primary_right = match len {
|
||||||
1 => area.right,
|
1 => area.right,
|
||||||
_ => area.right / 2,
|
_ => (area.right as f32 * primary_ratio) as i32,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
let secondary_right = match len {
|
let secondary_right = match len {
|
||||||
1 => 0,
|
1 => 0,
|
||||||
2 => area.right - primary_right,
|
2 => area.right - primary_right,
|
||||||
_ => (area.right - primary_right) / 2,
|
_ => (area.right as f32 * secondary_ratio) as i32,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (primary_left, secondary_left, stack_left) = match len {
|
let (primary_left, secondary_left, stack_left) = match len {
|
||||||
@@ -438,14 +529,18 @@ impl Arrangement for DefaultLayout {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if len > 2 {
|
if len > 2 {
|
||||||
layouts.append(&mut rows(
|
// Tertiary column gets remaining space after primary and secondary
|
||||||
|
let tertiary_right = area.right - primary_right - secondary_right;
|
||||||
|
let row_ratios = layout_options.and_then(|o| o.row_ratios);
|
||||||
|
layouts.append(&mut rows_with_ratios(
|
||||||
&Rect {
|
&Rect {
|
||||||
left: stack_left,
|
left: stack_left,
|
||||||
top: area.top,
|
top: area.top,
|
||||||
right: secondary_right,
|
right: tertiary_right,
|
||||||
bottom: area.bottom,
|
bottom: area.bottom,
|
||||||
},
|
},
|
||||||
len - 2,
|
len - 2,
|
||||||
|
row_ratios,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -514,13 +609,94 @@ impl Arrangement for DefaultLayout {
|
|||||||
|
|
||||||
let len = len as i32;
|
let len = len as i32;
|
||||||
|
|
||||||
let row_constraint = layout_options.and_then(|o| o.grid.map(|g| g.rows));
|
let row_constraint = layout_options.as_ref().and_then(|o| o.grid.map(|g| g.rows));
|
||||||
|
let column_ratios = layout_options.and_then(|o| o.column_ratios);
|
||||||
|
|
||||||
|
// Count defined column ratios (already validated at deserialization to sum < 1.0)
|
||||||
|
let defined_ratios = column_ratios
|
||||||
|
.as_ref()
|
||||||
|
.map(|r| r.iter().filter(|x| x.is_some()).count())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
let num_cols = if let Some(rows) = row_constraint {
|
let num_cols = if let Some(rows) = row_constraint {
|
||||||
((len as f32) / (rows as f32)).ceil() as i32
|
((len as f32) / (rows as f32)).ceil() as i32
|
||||||
} else {
|
} else {
|
||||||
(len as f32).sqrt().ceil() as i32
|
(len as f32).sqrt().ceil() as i32
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Pre-calculate column widths and left positions using same logic as columns_with_ratios
|
||||||
|
let mut col_widths: Vec<i32> = Vec::with_capacity(num_cols as usize);
|
||||||
|
let mut col_lefts: Vec<i32> = Vec::with_capacity(num_cols as usize);
|
||||||
|
let mut current_left = area.left;
|
||||||
|
|
||||||
|
for col in 0..num_cols {
|
||||||
|
let col_idx = col as usize;
|
||||||
|
let width = if let Some(ref ratios) = column_ratios {
|
||||||
|
// Only apply ratio if there's at least one more column after this
|
||||||
|
// The last column always gets the remaining space
|
||||||
|
let should_apply_ratio =
|
||||||
|
col_idx < MAX_RATIOS && col_idx < defined_ratios && col < num_cols - 1;
|
||||||
|
|
||||||
|
if should_apply_ratio {
|
||||||
|
if let Some(ratio) = ratios[col_idx] {
|
||||||
|
(area.right as f32 * ratio) as i32
|
||||||
|
} else {
|
||||||
|
let used: f32 = (0..col_idx).filter_map(|j| ratios[j]).sum();
|
||||||
|
let remaining_space =
|
||||||
|
area.right - (area.right as f32 * used) as i32;
|
||||||
|
let remaining_cols = num_cols - col;
|
||||||
|
remaining_space / remaining_cols
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Beyond defined ratios or last column - split remaining space equally
|
||||||
|
// Only count ratios that were actually applied (up to defined_ratios, but not beyond num_cols - 1)
|
||||||
|
let ratios_applied = defined_ratios.min((num_cols - 1) as usize);
|
||||||
|
let used: f32 = (0..ratios_applied).filter_map(|j| ratios[j]).sum();
|
||||||
|
let remaining_space = area.right - (area.right as f32 * used) as i32;
|
||||||
|
let remaining_cols = (num_cols as usize - ratios_applied) as i32;
|
||||||
|
if remaining_cols > 0 {
|
||||||
|
remaining_space / remaining_cols
|
||||||
|
} else {
|
||||||
|
remaining_space
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
area.right / num_cols
|
||||||
|
};
|
||||||
|
|
||||||
|
col_lefts.push(current_left);
|
||||||
|
col_widths.push(width);
|
||||||
|
current_left += width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last column absorbs any remainder from integer division
|
||||||
|
// so that columns tile the full area width without gaps
|
||||||
|
let total_width: i32 = col_widths.iter().sum();
|
||||||
|
let width_remainder = area.right - total_width;
|
||||||
|
if width_remainder > 0
|
||||||
|
&& let Some(last) = col_widths.last_mut()
|
||||||
|
{
|
||||||
|
*last += width_remainder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-calculate flipped column positions: same widths laid out
|
||||||
|
// in reverse order so that the last column sits at area.left
|
||||||
|
let flipped_col_lefts = if matches!(
|
||||||
|
layout_flip,
|
||||||
|
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||||
|
) {
|
||||||
|
let n = num_cols as usize;
|
||||||
|
let mut flipped = vec![0i32; n];
|
||||||
|
let mut fl = area.left;
|
||||||
|
for i in (0..n).rev() {
|
||||||
|
flipped[i] = fl;
|
||||||
|
fl += col_widths[i];
|
||||||
|
}
|
||||||
|
flipped
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
let mut iter = layouts.iter_mut().enumerate().peekable();
|
let mut iter = layouts.iter_mut().enumerate().peekable();
|
||||||
|
|
||||||
for col in 0..num_cols {
|
for col in 0..num_cols {
|
||||||
@@ -534,26 +710,47 @@ impl Arrangement for DefaultLayout {
|
|||||||
remaining_windows / remaining_columns
|
remaining_windows / remaining_columns
|
||||||
};
|
};
|
||||||
|
|
||||||
let win_height = area.bottom / num_rows_in_this_col;
|
// Rows within each column: base height from integer division,
|
||||||
let win_width = area.right / num_cols;
|
// last row absorbs any remainder to cover the full area height
|
||||||
|
let base_height = area.bottom / num_rows_in_this_col;
|
||||||
|
let height_remainder = area.bottom - base_height * num_rows_in_this_col;
|
||||||
|
|
||||||
|
let col_idx = col as usize;
|
||||||
|
let win_width = col_widths[col_idx];
|
||||||
|
let col_left = col_lefts[col_idx];
|
||||||
|
|
||||||
for row in 0..num_rows_in_this_col {
|
for row in 0..num_rows_in_this_col {
|
||||||
if let Some((_idx, win)) = iter.next() {
|
if let Some((_idx, win)) = iter.next() {
|
||||||
let mut left = area.left + win_width * col;
|
let is_last_row = row == num_rows_in_this_col - 1;
|
||||||
let mut top = area.top + win_height * row;
|
let win_height = if is_last_row {
|
||||||
|
base_height + height_remainder
|
||||||
|
} else {
|
||||||
|
base_height
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut left = col_left;
|
||||||
|
let mut top = area.top + base_height * row;
|
||||||
|
|
||||||
match layout_flip {
|
match layout_flip {
|
||||||
Some(Axis::Horizontal) => {
|
Some(Axis::Horizontal) => {
|
||||||
left = area.right - win_width * (col + 1) + area.left;
|
left = flipped_col_lefts[col_idx];
|
||||||
}
|
}
|
||||||
Some(Axis::Vertical) => {
|
Some(Axis::Vertical) => {
|
||||||
top = area.bottom - win_height * (row + 1) + area.top;
|
top = if is_last_row {
|
||||||
|
area.top
|
||||||
|
} else {
|
||||||
|
area.top + area.bottom - base_height * (row + 1)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Some(Axis::HorizontalAndVertical) => {
|
Some(Axis::HorizontalAndVertical) => {
|
||||||
left = area.right - win_width * (col + 1) + area.left;
|
left = flipped_col_lefts[col_idx];
|
||||||
top = area.bottom - win_height * (row + 1) + area.top;
|
top = if is_last_row {
|
||||||
|
area.top
|
||||||
|
} else {
|
||||||
|
area.top + area.bottom - base_height * (row + 1)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
None => {} // No flip
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
win.bottom = win_height;
|
win.bottom = win_height;
|
||||||
@@ -576,6 +773,7 @@ impl Arrangement for DefaultLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
impl Arrangement for CustomLayout {
|
impl Arrangement for CustomLayout {
|
||||||
fn calculate(
|
fn calculate(
|
||||||
&self,
|
&self,
|
||||||
@@ -714,14 +912,68 @@ pub enum Axis {
|
|||||||
HorizontalAndVertical,
|
HorizontalAndVertical,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn columns(area: &Rect, len: usize) -> Vec<Rect> {
|
fn columns(area: &Rect, len: usize) -> Vec<Rect> {
|
||||||
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
|
columns_with_ratios(area, len, None)
|
||||||
let right = area.right / len as i32;
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn columns_with_ratios(
|
||||||
|
area: &Rect,
|
||||||
|
len: usize,
|
||||||
|
ratios: Option<[Option<f32>; MAX_RATIOS]>,
|
||||||
|
) -> Vec<Rect> {
|
||||||
|
tracing::debug!(
|
||||||
|
"columns_with_ratios called: len={}, ratios={:?}",
|
||||||
|
len,
|
||||||
|
ratios
|
||||||
|
);
|
||||||
|
let mut layouts: Vec<Rect> = vec![];
|
||||||
let mut left = 0;
|
let mut left = 0;
|
||||||
|
|
||||||
let mut layouts: Vec<Rect> = vec![];
|
// Count how many ratios are defined (already validated at deserialization to sum < 1.0)
|
||||||
for _ in 0..len {
|
let defined_ratios = ratios
|
||||||
|
.as_ref()
|
||||||
|
.map(|r| r.iter().filter(|x| x.is_some()).count())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
for i in 0..len {
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
let right = if let Some(ref r) = ratios {
|
||||||
|
// Only apply ratio[i] if there's at least one more column after this (i < len - 1)
|
||||||
|
// The last column always gets the remaining space
|
||||||
|
let should_apply_ratio = i < MAX_RATIOS && i < defined_ratios && i < len - 1;
|
||||||
|
|
||||||
|
if should_apply_ratio {
|
||||||
|
if let Some(ratio) = r[i] {
|
||||||
|
(area.right as f32 * ratio) as i32
|
||||||
|
} else {
|
||||||
|
let used: f32 = (0..i).filter_map(|j| r[j]).sum();
|
||||||
|
let remaining_space = area.right - (area.right as f32 * used) as i32;
|
||||||
|
let remaining_columns = len - i;
|
||||||
|
remaining_space / remaining_columns as i32
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Last column or beyond defined ratios - split remaining space equally
|
||||||
|
let ratios_applied = i.min(defined_ratios).min(len.saturating_sub(1));
|
||||||
|
let used: f32 = (0..ratios_applied).filter_map(|j| r[j]).sum();
|
||||||
|
let remaining_space = area.right - (area.right as f32 * used) as i32;
|
||||||
|
let remaining_columns = len - ratios_applied;
|
||||||
|
if remaining_columns > 0 {
|
||||||
|
remaining_space / remaining_columns as i32
|
||||||
|
} else {
|
||||||
|
remaining_space
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Equal width columns (original behavior)
|
||||||
|
#[allow(clippy::cast_possible_wrap)]
|
||||||
|
{
|
||||||
|
area.right / len as i32
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
layouts.push(Rect {
|
layouts.push(Rect {
|
||||||
left: area.left + left,
|
left: area.left + left,
|
||||||
top: area.top,
|
top: area.top,
|
||||||
@@ -732,17 +984,77 @@ fn columns(area: &Rect, len: usize) -> Vec<Rect> {
|
|||||||
left += right;
|
left += right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Last column absorbs any remainder from integer division
|
||||||
|
// so that columns tile the full area width without gaps
|
||||||
|
let total_width: i32 = layouts.iter().map(|r| r.right).sum();
|
||||||
|
let remainder = area.right - total_width;
|
||||||
|
if remainder > 0
|
||||||
|
&& let Some(last) = layouts.last_mut()
|
||||||
|
{
|
||||||
|
last.right += remainder;
|
||||||
|
}
|
||||||
|
|
||||||
layouts
|
layouts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn rows(area: &Rect, len: usize) -> Vec<Rect> {
|
fn rows(area: &Rect, len: usize) -> Vec<Rect> {
|
||||||
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
|
rows_with_ratios(area, len, None)
|
||||||
let bottom = area.bottom / len as i32;
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn rows_with_ratios(
|
||||||
|
area: &Rect,
|
||||||
|
len: usize,
|
||||||
|
ratios: Option<[Option<f32>; MAX_RATIOS]>,
|
||||||
|
) -> Vec<Rect> {
|
||||||
|
tracing::debug!("rows_with_ratios called: len={}, ratios={:?}", len, ratios);
|
||||||
|
let mut layouts: Vec<Rect> = vec![];
|
||||||
let mut top = 0;
|
let mut top = 0;
|
||||||
|
|
||||||
let mut layouts: Vec<Rect> = vec![];
|
// Count how many ratios are defined (already validated at deserialization to sum < 1.0)
|
||||||
for _ in 0..len {
|
let defined_ratios = ratios
|
||||||
|
.as_ref()
|
||||||
|
.map(|r| r.iter().filter(|x| x.is_some()).count())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
for i in 0..len {
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
let bottom = if let Some(ref r) = ratios {
|
||||||
|
// Only apply ratio[i] if there's at least one more row after this (i < len - 1)
|
||||||
|
// The last row always gets the remaining space
|
||||||
|
let should_apply_ratio = i < MAX_RATIOS && i < defined_ratios && i < len - 1;
|
||||||
|
|
||||||
|
if should_apply_ratio {
|
||||||
|
if let Some(ratio) = r[i] {
|
||||||
|
(area.bottom as f32 * ratio) as i32
|
||||||
|
} else {
|
||||||
|
let used: f32 = (0..i).filter_map(|j| r[j]).sum();
|
||||||
|
let remaining_space = area.bottom - (area.bottom as f32 * used) as i32;
|
||||||
|
let remaining_rows = len - i;
|
||||||
|
remaining_space / remaining_rows as i32
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Last row or beyond defined ratios - split remaining space equally
|
||||||
|
let ratios_applied = i.min(defined_ratios).min(len.saturating_sub(1));
|
||||||
|
let used: f32 = (0..ratios_applied).filter_map(|j| r[j]).sum();
|
||||||
|
let remaining_space = area.bottom - (area.bottom as f32 * used) as i32;
|
||||||
|
let remaining_rows = len - ratios_applied;
|
||||||
|
if remaining_rows > 0 {
|
||||||
|
remaining_space / remaining_rows as i32
|
||||||
|
} else {
|
||||||
|
remaining_space
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Equal height rows (original behavior)
|
||||||
|
#[allow(clippy::cast_possible_wrap)]
|
||||||
|
{
|
||||||
|
area.bottom / len as i32
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
layouts.push(Rect {
|
layouts.push(Rect {
|
||||||
left: area.left,
|
left: area.left,
|
||||||
top: area.top + top,
|
top: area.top + top,
|
||||||
@@ -753,6 +1065,16 @@ fn rows(area: &Rect, len: usize) -> Vec<Rect> {
|
|||||||
top += bottom;
|
top += bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Last row absorbs any remainder from integer division
|
||||||
|
// so that rows tile the full area height without gaps
|
||||||
|
let total_height: i32 = layouts.iter().map(|r| r.bottom).sum();
|
||||||
|
let remainder = area.bottom - total_height;
|
||||||
|
if remainder > 0
|
||||||
|
&& let Some(last) = layouts.last_mut()
|
||||||
|
{
|
||||||
|
last.bottom += remainder;
|
||||||
|
}
|
||||||
|
|
||||||
layouts
|
layouts
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -862,6 +1184,8 @@ fn recursive_fibonacci(
|
|||||||
area: &Rect,
|
area: &Rect,
|
||||||
layout_flip: Option<Axis>,
|
layout_flip: Option<Axis>,
|
||||||
resize_adjustments: Vec<Option<Rect>>,
|
resize_adjustments: Vec<Option<Rect>>,
|
||||||
|
column_split_ratio: f32,
|
||||||
|
row_split_ratio: f32,
|
||||||
) -> Vec<Rect> {
|
) -> Vec<Rect> {
|
||||||
let mut a = *area;
|
let mut a = *area;
|
||||||
|
|
||||||
@@ -875,41 +1199,41 @@ fn recursive_fibonacci(
|
|||||||
*area
|
*area
|
||||||
};
|
};
|
||||||
|
|
||||||
let half_width = area.right / 2;
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
let half_height = area.bottom / 2;
|
let primary_resized_width = (resized.right as f32 * column_split_ratio) as i32;
|
||||||
let half_resized_width = resized.right / 2;
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
let half_resized_height = resized.bottom / 2;
|
let primary_resized_height = (resized.bottom as f32 * row_split_ratio) as i32;
|
||||||
|
|
||||||
let (main_x, alt_x, alt_y, main_y);
|
let (main_x, alt_x, alt_y, main_y);
|
||||||
|
|
||||||
if let Some(flip) = layout_flip {
|
if let Some(flip) = layout_flip {
|
||||||
match flip {
|
match flip {
|
||||||
Axis::Horizontal => {
|
Axis::Horizontal => {
|
||||||
main_x = resized.left + half_width + (half_width - half_resized_width);
|
main_x = resized.left + (area.right - primary_resized_width);
|
||||||
alt_x = resized.left;
|
alt_x = resized.left;
|
||||||
|
|
||||||
alt_y = resized.top + half_resized_height;
|
alt_y = resized.top + primary_resized_height;
|
||||||
main_y = resized.top;
|
main_y = resized.top;
|
||||||
}
|
}
|
||||||
Axis::Vertical => {
|
Axis::Vertical => {
|
||||||
main_y = resized.top + half_height + (half_height - half_resized_height);
|
main_y = resized.top + (area.bottom - primary_resized_height);
|
||||||
alt_y = resized.top;
|
alt_y = resized.top;
|
||||||
|
|
||||||
main_x = resized.left;
|
main_x = resized.left;
|
||||||
alt_x = resized.left + half_resized_width;
|
alt_x = resized.left + primary_resized_width;
|
||||||
}
|
}
|
||||||
Axis::HorizontalAndVertical => {
|
Axis::HorizontalAndVertical => {
|
||||||
main_x = resized.left + half_width + (half_width - half_resized_width);
|
main_x = resized.left + (area.right - primary_resized_width);
|
||||||
alt_x = resized.left;
|
alt_x = resized.left;
|
||||||
main_y = resized.top + half_height + (half_height - half_resized_height);
|
main_y = resized.top + (area.bottom - primary_resized_height);
|
||||||
alt_y = resized.top;
|
alt_y = resized.top;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
main_x = resized.left;
|
main_x = resized.left;
|
||||||
alt_x = resized.left + half_resized_width;
|
alt_x = resized.left + primary_resized_width;
|
||||||
main_y = resized.top;
|
main_y = resized.top;
|
||||||
alt_y = resized.top + half_resized_height;
|
alt_y = resized.top + primary_resized_height;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::if_not_else)]
|
#[allow(clippy::if_not_else)]
|
||||||
@@ -927,7 +1251,7 @@ fn recursive_fibonacci(
|
|||||||
left: resized.left,
|
left: resized.left,
|
||||||
top: main_y,
|
top: main_y,
|
||||||
right: resized.right,
|
right: resized.right,
|
||||||
bottom: half_resized_height,
|
bottom: primary_resized_height,
|
||||||
}];
|
}];
|
||||||
res.append(&mut recursive_fibonacci(
|
res.append(&mut recursive_fibonacci(
|
||||||
idx + 1,
|
idx + 1,
|
||||||
@@ -936,17 +1260,19 @@ fn recursive_fibonacci(
|
|||||||
left: area.left,
|
left: area.left,
|
||||||
top: alt_y,
|
top: alt_y,
|
||||||
right: area.right,
|
right: area.right,
|
||||||
bottom: area.bottom - half_resized_height,
|
bottom: area.bottom - primary_resized_height,
|
||||||
},
|
},
|
||||||
layout_flip,
|
layout_flip,
|
||||||
resize_adjustments,
|
resize_adjustments,
|
||||||
|
column_split_ratio,
|
||||||
|
row_split_ratio,
|
||||||
));
|
));
|
||||||
res
|
res
|
||||||
} else {
|
} else {
|
||||||
let mut res = vec![Rect {
|
let mut res = vec![Rect {
|
||||||
left: main_x,
|
left: main_x,
|
||||||
top: resized.top,
|
top: resized.top,
|
||||||
right: half_resized_width,
|
right: primary_resized_width,
|
||||||
bottom: resized.bottom,
|
bottom: resized.bottom,
|
||||||
}];
|
}];
|
||||||
res.append(&mut recursive_fibonacci(
|
res.append(&mut recursive_fibonacci(
|
||||||
@@ -955,11 +1281,13 @@ fn recursive_fibonacci(
|
|||||||
&Rect {
|
&Rect {
|
||||||
left: alt_x,
|
left: alt_x,
|
||||||
top: area.top,
|
top: area.top,
|
||||||
right: area.right - half_resized_width,
|
right: area.right - primary_resized_width,
|
||||||
bottom: area.bottom,
|
bottom: area.bottom,
|
||||||
},
|
},
|
||||||
layout_flip,
|
layout_flip,
|
||||||
resize_adjustments,
|
resize_adjustments,
|
||||||
|
column_split_ratio,
|
||||||
|
row_split_ratio,
|
||||||
));
|
));
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
@@ -1267,3 +1595,7 @@ fn resize_top(rect: &mut Rect, resize: i32) {
|
|||||||
fn resize_bottom(rect: &mut Rect, resize: i32) {
|
fn resize_bottom(rect: &mut Rect, resize: i32) {
|
||||||
rect.bottom += resize / 2;
|
rect.bottom += resize / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "arrangement_tests.rs"]
|
||||||
|
mod tests;
|
||||||
1845
komorebi-layouts/src/arrangement_tests.rs
Normal file
1845
komorebi-layouts/src/arrangement_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -8,8 +10,53 @@ use super::OperationDirection;
|
|||||||
use super::Rect;
|
use super::Rect;
|
||||||
use super::Sizing;
|
use super::Sizing;
|
||||||
|
|
||||||
|
/// Maximum number of ratio values that can be specified for column_ratios and row_ratios
|
||||||
|
pub const MAX_RATIOS: usize = 5;
|
||||||
|
|
||||||
|
/// Minimum allowed ratio value (prevents zero-sized windows)
|
||||||
|
pub const MIN_RATIO: f32 = 0.1;
|
||||||
|
|
||||||
|
/// Maximum allowed ratio value (ensures space for remaining windows)
|
||||||
|
pub const MAX_RATIO: f32 = 0.9;
|
||||||
|
|
||||||
|
/// Default ratio value when none is specified
|
||||||
|
pub const DEFAULT_RATIO: f32 = 0.5;
|
||||||
|
|
||||||
|
/// Default secondary ratio value for UltrawideVerticalStack layout
|
||||||
|
pub const DEFAULT_SECONDARY_RATIO: f32 = 0.25;
|
||||||
|
|
||||||
|
/// Validates and converts a Vec of ratios into a fixed-size array.
|
||||||
|
/// - Clamps values to MIN_RATIO..MAX_RATIO range
|
||||||
|
/// - Truncates when cumulative sum reaches or exceeds 1.0
|
||||||
|
/// - Limits to MAX_RATIOS values
|
||||||
|
#[must_use]
|
||||||
|
pub fn validate_ratios(ratios: &[f32]) -> [Option<f32>; MAX_RATIOS] {
|
||||||
|
let mut arr = [None; MAX_RATIOS];
|
||||||
|
let mut cumulative_sum = 0.0_f32;
|
||||||
|
|
||||||
|
for (i, &val) in ratios.iter().take(MAX_RATIOS).enumerate() {
|
||||||
|
let clamped_val = val.clamp(MIN_RATIO, MAX_RATIO);
|
||||||
|
|
||||||
|
// Only add this ratio if cumulative sum stays below 1.0
|
||||||
|
if cumulative_sum + clamped_val < 1.0 {
|
||||||
|
arr[i] = Some(clamped_val);
|
||||||
|
cumulative_sum += clamped_val;
|
||||||
|
} else {
|
||||||
|
// Stop adding ratios - cumulative sum would reach or exceed 1.0
|
||||||
|
tracing::debug!(
|
||||||
|
"Truncating ratios at index {} - cumulative sum {} + {} would reach/exceed 1.0",
|
||||||
|
i,
|
||||||
|
cumulative_sum,
|
||||||
|
clamped_val
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
arr
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Display, EnumString, ValueEnum,
|
Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Display, EnumString, ValueEnum,
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
/// A predefined komorebi layout
|
/// A predefined komorebi layout
|
||||||
@@ -112,7 +159,43 @@ pub enum DefaultLayout {
|
|||||||
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
|
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
/// Helper to deserialize a variable-length array into a fixed [Option<f32>; MAX_RATIOS]
|
||||||
|
/// Ratios are truncated when their cumulative sum reaches or exceeds 1.0 to ensure
|
||||||
|
/// there's always remaining space for additional windows.
|
||||||
|
fn deserialize_ratios<'de, D>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> Result<Option<[Option<f32>; MAX_RATIOS]>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let opt: Option<Vec<f32>> = Option::deserialize(deserializer)?;
|
||||||
|
Ok(opt.map(|vec| validate_ratios(&vec)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to serialize [Option<f32>; MAX_RATIOS] as a compact array (without trailing nulls)
|
||||||
|
fn serialize_ratios<S>(
|
||||||
|
value: &Option<[Option<f32>; MAX_RATIOS]>,
|
||||||
|
serializer: S,
|
||||||
|
) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
match value {
|
||||||
|
None => serializer.serialize_none(),
|
||||||
|
Some(arr) => {
|
||||||
|
// Find last non-None index
|
||||||
|
let last_idx = arr
|
||||||
|
.iter()
|
||||||
|
.rposition(|x| x.is_some())
|
||||||
|
.map(|i| i + 1)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let vec: Vec<f32> = arr.iter().take(last_idx).filter_map(|&x| x).collect();
|
||||||
|
serializer.serialize_some(&vec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
/// Options for specific layouts
|
/// Options for specific layouts
|
||||||
pub struct LayoutOptions {
|
pub struct LayoutOptions {
|
||||||
@@ -120,6 +203,35 @@ pub struct LayoutOptions {
|
|||||||
pub scrolling: Option<ScrollingLayoutOptions>,
|
pub scrolling: Option<ScrollingLayoutOptions>,
|
||||||
/// Options related to the Grid layout
|
/// Options related to the Grid layout
|
||||||
pub grid: Option<GridLayoutOptions>,
|
pub grid: Option<GridLayoutOptions>,
|
||||||
|
/// Column width ratios (up to MAX_RATIOS values between 0.1 and 0.9)
|
||||||
|
///
|
||||||
|
/// - Used by Columns layout: ratios for each column width
|
||||||
|
/// - Used by Grid layout: ratios for column widths
|
||||||
|
/// - Used by BSP, VerticalStack, RightMainVerticalStack: column_ratios[0] as primary split ratio
|
||||||
|
/// - Used by HorizontalStack: column_ratios[0] as primary split ratio (top area height)
|
||||||
|
/// - Used by UltrawideVerticalStack: column_ratios[0] as center ratio, column_ratios[1] as left ratio
|
||||||
|
///
|
||||||
|
/// Columns without a ratio share remaining space equally.
|
||||||
|
/// Example: `[0.3, 0.4, 0.3]` for 30%-40%-30% columns
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
deserialize_with = "deserialize_ratios",
|
||||||
|
serialize_with = "serialize_ratios"
|
||||||
|
)]
|
||||||
|
pub column_ratios: Option<[Option<f32>; MAX_RATIOS]>,
|
||||||
|
/// Row height ratios (up to MAX_RATIOS values between 0.1 and 0.9)
|
||||||
|
///
|
||||||
|
/// - Used by Rows layout: ratios for each row height
|
||||||
|
/// - Used by Grid layout: ratios for row heights
|
||||||
|
///
|
||||||
|
/// Rows without a ratio share remaining space equally.
|
||||||
|
/// Example: `[0.5, 0.5]` for 50%-50% rows
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
deserialize_with = "deserialize_ratios",
|
||||||
|
serialize_with = "serialize_ratios"
|
||||||
|
)]
|
||||||
|
pub row_ratios: Option<[Option<f32>; MAX_RATIOS]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
@@ -140,6 +252,21 @@ pub struct GridLayoutOptions {
|
|||||||
pub rows: usize,
|
pub rows: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Per-layout default options entry for the `layout_defaults` global setting.
|
||||||
|
/// Contains both base layout options and threshold-based layout options rules.
|
||||||
|
pub struct LayoutDefaultEntry {
|
||||||
|
/// Default layout options for this layout
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub layout_options: Option<LayoutOptions>,
|
||||||
|
/// Threshold-based layout options rules in the format of threshold => options.
|
||||||
|
/// When container count >= threshold, the highest matching threshold's options
|
||||||
|
/// fully replace the base `layout_options`.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub layout_options_rules: Option<HashMap<usize, LayoutOptions>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl DefaultLayout {
|
impl DefaultLayout {
|
||||||
pub fn leftmost_index(&self, len: usize) -> usize {
|
pub fn leftmost_index(&self, len: usize) -> usize {
|
||||||
match self {
|
match self {
|
||||||
@@ -308,3 +435,7 @@ impl DefaultLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "default_layout_tests.rs"]
|
||||||
|
mod tests;
|
||||||
954
komorebi-layouts/src/default_layout_tests.rs
Normal file
954
komorebi-layouts/src/default_layout_tests.rs
Normal file
@@ -0,0 +1,954 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
// Helper to create LayoutOptions with column ratios
|
||||||
|
fn layout_options_with_column_ratios(ratios: &[f32]) -> LayoutOptions {
|
||||||
|
let mut arr = [None; MAX_RATIOS];
|
||||||
|
for (i, &r) in ratios.iter().take(MAX_RATIOS).enumerate() {
|
||||||
|
arr[i] = Some(r);
|
||||||
|
}
|
||||||
|
LayoutOptions {
|
||||||
|
scrolling: None,
|
||||||
|
grid: None,
|
||||||
|
column_ratios: Some(arr),
|
||||||
|
row_ratios: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to create LayoutOptions with row ratios
|
||||||
|
fn layout_options_with_row_ratios(ratios: &[f32]) -> LayoutOptions {
|
||||||
|
let mut arr = [None; MAX_RATIOS];
|
||||||
|
for (i, &r) in ratios.iter().take(MAX_RATIOS).enumerate() {
|
||||||
|
arr[i] = Some(r);
|
||||||
|
}
|
||||||
|
LayoutOptions {
|
||||||
|
scrolling: None,
|
||||||
|
grid: None,
|
||||||
|
column_ratios: None,
|
||||||
|
row_ratios: Some(arr),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to create LayoutOptions with both column and row ratios
|
||||||
|
fn layout_options_with_ratios(column_ratios: &[f32], row_ratios: &[f32]) -> LayoutOptions {
|
||||||
|
let mut col_arr = [None; MAX_RATIOS];
|
||||||
|
for (i, &r) in column_ratios.iter().take(MAX_RATIOS).enumerate() {
|
||||||
|
col_arr[i] = Some(r);
|
||||||
|
}
|
||||||
|
let mut row_arr = [None; MAX_RATIOS];
|
||||||
|
for (i, &r) in row_ratios.iter().take(MAX_RATIOS).enumerate() {
|
||||||
|
row_arr[i] = Some(r);
|
||||||
|
}
|
||||||
|
LayoutOptions {
|
||||||
|
scrolling: None,
|
||||||
|
grid: None,
|
||||||
|
column_ratios: Some(col_arr),
|
||||||
|
row_ratios: Some(row_arr),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod deserialize_ratios_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_valid_ratios() {
|
||||||
|
let json = r#"{"column_ratios": [0.3, 0.4, 0.2]}"#;
|
||||||
|
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
let ratios = opts.column_ratios.unwrap();
|
||||||
|
assert_eq!(ratios[0], Some(0.3));
|
||||||
|
assert_eq!(ratios[1], Some(0.4));
|
||||||
|
assert_eq!(ratios[2], Some(0.2));
|
||||||
|
assert_eq!(ratios[3], None);
|
||||||
|
assert_eq!(ratios[4], None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_clamps_values_to_min() {
|
||||||
|
// Values below MIN_RATIO should be clamped
|
||||||
|
let json = r#"{"column_ratios": [0.05]}"#;
|
||||||
|
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
let ratios = opts.column_ratios.unwrap();
|
||||||
|
assert_eq!(ratios[0], Some(MIN_RATIO)); // Clamped to 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_clamps_values_to_max() {
|
||||||
|
// Values above MAX_RATIO should be clamped
|
||||||
|
let json = r#"{"column_ratios": [0.95]}"#;
|
||||||
|
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
let ratios = opts.column_ratios.unwrap();
|
||||||
|
// 0.9 is the max, so it should be clamped
|
||||||
|
assert!(ratios[0].unwrap() <= MAX_RATIO);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_truncates_when_sum_exceeds_one() {
|
||||||
|
// Sum of ratios should not reach 1.0
|
||||||
|
// [0.5, 0.4] = 0.9, then 0.3 would make it 1.2, so it should be truncated
|
||||||
|
let json = r#"{"column_ratios": [0.5, 0.4, 0.3]}"#;
|
||||||
|
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
let ratios = opts.column_ratios.unwrap();
|
||||||
|
assert_eq!(ratios[0], Some(0.5));
|
||||||
|
assert_eq!(ratios[1], Some(0.4));
|
||||||
|
// Third ratio should be truncated because 0.5 + 0.4 + 0.3 >= 1.0
|
||||||
|
assert_eq!(ratios[2], None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_truncates_at_max_ratios() {
|
||||||
|
// More than MAX_RATIOS values should be truncated
|
||||||
|
let json = r#"{"column_ratios": [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]}"#;
|
||||||
|
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
let ratios = opts.column_ratios.unwrap();
|
||||||
|
// Only MAX_RATIOS (5) values should be stored
|
||||||
|
for item in ratios.iter().take(MAX_RATIOS) {
|
||||||
|
assert_eq!(*item, Some(0.1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_empty_array() {
|
||||||
|
let json = r#"{"column_ratios": []}"#;
|
||||||
|
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
let ratios = opts.column_ratios.unwrap();
|
||||||
|
for item in ratios.iter().take(MAX_RATIOS) {
|
||||||
|
assert_eq!(*item, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_null() {
|
||||||
|
let json = r#"{"column_ratios": null}"#;
|
||||||
|
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||||
|
assert!(opts.column_ratios.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_row_ratios() {
|
||||||
|
let json = r#"{"row_ratios": [0.3, 0.5]}"#;
|
||||||
|
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
let ratios = opts.row_ratios.unwrap();
|
||||||
|
assert_eq!(ratios[0], Some(0.3));
|
||||||
|
assert_eq!(ratios[1], Some(0.5));
|
||||||
|
assert_eq!(ratios[2], None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod serialize_ratios_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_ratios_compact() {
|
||||||
|
let opts = layout_options_with_column_ratios(&[0.3, 0.4]);
|
||||||
|
let json = serde_json::to_string(&opts).unwrap();
|
||||||
|
|
||||||
|
// Should serialize ratios as compact array without trailing nulls in the ratios array
|
||||||
|
assert!(json.contains("0.3") && json.contains("0.4"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_none_ratios() {
|
||||||
|
let opts = LayoutOptions {
|
||||||
|
scrolling: None,
|
||||||
|
grid: None,
|
||||||
|
column_ratios: None,
|
||||||
|
row_ratios: None,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&opts).unwrap();
|
||||||
|
|
||||||
|
// None values should serialize as null or be omitted
|
||||||
|
assert!(!json.contains("["));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_roundtrip_serialization() {
|
||||||
|
let original = layout_options_with_column_ratios(&[0.3, 0.4, 0.2]);
|
||||||
|
let json = serde_json::to_string(&original).unwrap();
|
||||||
|
let deserialized: LayoutOptions = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(original.column_ratios, deserialized.column_ratios);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_row_ratios() {
|
||||||
|
let opts = layout_options_with_row_ratios(&[0.3, 0.5]);
|
||||||
|
let json = serde_json::to_string(&opts).unwrap();
|
||||||
|
|
||||||
|
assert!(json.contains("row_ratios"));
|
||||||
|
assert!(json.contains("0.3") && json.contains("0.5"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_roundtrip_row_ratios() {
|
||||||
|
let original = layout_options_with_row_ratios(&[0.4, 0.3]);
|
||||||
|
let json = serde_json::to_string(&original).unwrap();
|
||||||
|
let deserialized: LayoutOptions = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(original.row_ratios, deserialized.row_ratios);
|
||||||
|
assert!(original.column_ratios.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_roundtrip_both_ratios() {
|
||||||
|
let original = layout_options_with_ratios(&[0.3, 0.4], &[0.5, 0.3]);
|
||||||
|
let json = serde_json::to_string(&original).unwrap();
|
||||||
|
let deserialized: LayoutOptions = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(original.column_ratios, deserialized.column_ratios);
|
||||||
|
assert_eq!(original.row_ratios, deserialized.row_ratios);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod ratio_constants_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_constants_valid_ranges() {
|
||||||
|
const {
|
||||||
|
assert!(MIN_RATIO > 0.0);
|
||||||
|
assert!(MIN_RATIO < MAX_RATIO);
|
||||||
|
assert!(MAX_RATIO < 1.0);
|
||||||
|
assert!(DEFAULT_RATIO >= MIN_RATIO && DEFAULT_RATIO <= MAX_RATIO);
|
||||||
|
assert!(DEFAULT_SECONDARY_RATIO >= MIN_RATIO && DEFAULT_SECONDARY_RATIO <= MAX_RATIO);
|
||||||
|
assert!(MAX_RATIOS >= 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_ratio_is_half() {
|
||||||
|
assert_eq!(DEFAULT_RATIO, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_max_ratios_is_five() {
|
||||||
|
assert_eq!(MAX_RATIOS, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod layout_options_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_layout_options_default_values() {
|
||||||
|
let json = r#"{}"#;
|
||||||
|
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
assert!(opts.scrolling.is_none());
|
||||||
|
assert!(opts.grid.is_none());
|
||||||
|
assert!(opts.column_ratios.is_none());
|
||||||
|
assert!(opts.row_ratios.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_layout_options_with_all_fields() {
|
||||||
|
let json = r#"{
|
||||||
|
"scrolling": {"columns": 3},
|
||||||
|
"grid": {"rows": 2},
|
||||||
|
"column_ratios": [0.3, 0.4],
|
||||||
|
"row_ratios": [0.5]
|
||||||
|
}"#;
|
||||||
|
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
assert!(opts.scrolling.is_some());
|
||||||
|
assert_eq!(opts.scrolling.unwrap().columns, 3);
|
||||||
|
assert!(opts.grid.is_some());
|
||||||
|
assert_eq!(opts.grid.unwrap().rows, 2);
|
||||||
|
assert!(opts.column_ratios.is_some());
|
||||||
|
assert!(opts.row_ratios.is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod default_layout_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cycle_next_covers_all_layouts() {
|
||||||
|
let start = DefaultLayout::BSP;
|
||||||
|
let mut current = start;
|
||||||
|
let mut visited = vec![current];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
current = current.cycle_next();
|
||||||
|
if current == start {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
!visited.contains(¤t),
|
||||||
|
"Cycle contains duplicate: {:?}",
|
||||||
|
current
|
||||||
|
);
|
||||||
|
visited.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have visited all layouts
|
||||||
|
assert_eq!(visited.len(), 9); // 9 layouts total
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cycle_previous_is_inverse_of_next() {
|
||||||
|
// Note: cycle_previous has some inconsistencies in the current implementation
|
||||||
|
// This test documents the expected behavior for most layouts
|
||||||
|
let layouts_with_correct_inverse = [
|
||||||
|
DefaultLayout::Columns,
|
||||||
|
DefaultLayout::Rows,
|
||||||
|
DefaultLayout::VerticalStack,
|
||||||
|
DefaultLayout::HorizontalStack,
|
||||||
|
DefaultLayout::UltrawideVerticalStack,
|
||||||
|
DefaultLayout::Grid,
|
||||||
|
DefaultLayout::RightMainVerticalStack,
|
||||||
|
];
|
||||||
|
|
||||||
|
for layout in layouts_with_correct_inverse {
|
||||||
|
let next = layout.cycle_next();
|
||||||
|
assert_eq!(
|
||||||
|
next.cycle_previous(),
|
||||||
|
layout,
|
||||||
|
"cycle_previous should be inverse of cycle_next for {:?}",
|
||||||
|
layout
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_leftmost_index_standard_layouts() {
|
||||||
|
assert_eq!(DefaultLayout::BSP.leftmost_index(5), 0);
|
||||||
|
assert_eq!(DefaultLayout::Columns.leftmost_index(5), 0);
|
||||||
|
assert_eq!(DefaultLayout::Rows.leftmost_index(5), 0);
|
||||||
|
assert_eq!(DefaultLayout::VerticalStack.leftmost_index(5), 0);
|
||||||
|
assert_eq!(DefaultLayout::HorizontalStack.leftmost_index(5), 0);
|
||||||
|
assert_eq!(DefaultLayout::Grid.leftmost_index(5), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_leftmost_index_ultrawide() {
|
||||||
|
assert_eq!(DefaultLayout::UltrawideVerticalStack.leftmost_index(1), 0);
|
||||||
|
assert_eq!(DefaultLayout::UltrawideVerticalStack.leftmost_index(2), 1);
|
||||||
|
assert_eq!(DefaultLayout::UltrawideVerticalStack.leftmost_index(5), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_leftmost_index_right_main() {
|
||||||
|
assert_eq!(DefaultLayout::RightMainVerticalStack.leftmost_index(1), 0);
|
||||||
|
assert_eq!(DefaultLayout::RightMainVerticalStack.leftmost_index(2), 1);
|
||||||
|
assert_eq!(DefaultLayout::RightMainVerticalStack.leftmost_index(5), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rightmost_index_standard_layouts() {
|
||||||
|
assert_eq!(DefaultLayout::BSP.rightmost_index(5), 4);
|
||||||
|
assert_eq!(DefaultLayout::Columns.rightmost_index(5), 4);
|
||||||
|
assert_eq!(DefaultLayout::Rows.rightmost_index(5), 4);
|
||||||
|
assert_eq!(DefaultLayout::VerticalStack.rightmost_index(5), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rightmost_index_right_main() {
|
||||||
|
assert_eq!(DefaultLayout::RightMainVerticalStack.rightmost_index(1), 0);
|
||||||
|
assert_eq!(DefaultLayout::RightMainVerticalStack.rightmost_index(5), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rightmost_index_ultrawide() {
|
||||||
|
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(1), 0);
|
||||||
|
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(2), 0);
|
||||||
|
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(3), 2);
|
||||||
|
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(5), 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod layout_options_rules_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hashmap_deserialization_ratios_only() {
|
||||||
|
// layout_options_rules entries with only ratios
|
||||||
|
// Note: ratios must sum to < 1.0 to avoid truncation by validate_ratios
|
||||||
|
let json = r#"{
|
||||||
|
"2": {"column_ratios": [0.7]},
|
||||||
|
"3": {"column_ratios": [0.55]},
|
||||||
|
"5": {"column_ratios": [0.3, 0.3, 0.3]}
|
||||||
|
}"#;
|
||||||
|
let rules: std::collections::HashMap<usize, LayoutOptions> =
|
||||||
|
serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(rules.len(), 3);
|
||||||
|
assert_eq!(rules[&2].column_ratios.unwrap()[0], Some(0.7));
|
||||||
|
assert_eq!(rules[&3].column_ratios.unwrap()[0], Some(0.55));
|
||||||
|
let r5 = rules[&5].column_ratios.unwrap();
|
||||||
|
assert_eq!(r5[0], Some(0.3));
|
||||||
|
assert_eq!(r5[1], Some(0.3));
|
||||||
|
assert_eq!(r5[2], Some(0.3));
|
||||||
|
// No scrolling/grid in these entries
|
||||||
|
assert!(rules[&2].scrolling.is_none());
|
||||||
|
assert!(rules[&2].grid.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hashmap_deserialization_full_options() {
|
||||||
|
// layout_options_rules entries with full options including scrolling/grid
|
||||||
|
let json = r#"{
|
||||||
|
"2": {"column_ratios": [0.7], "scrolling": {"columns": 3}},
|
||||||
|
"5": {"column_ratios": [0.3, 0.3, 0.3], "grid": {"rows": 2}}
|
||||||
|
}"#;
|
||||||
|
let rules: std::collections::HashMap<usize, LayoutOptions> =
|
||||||
|
serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(rules.len(), 2);
|
||||||
|
assert_eq!(rules[&2].scrolling.unwrap().columns, 3);
|
||||||
|
assert!(rules[&2].grid.is_none());
|
||||||
|
assert!(rules[&5].scrolling.is_none());
|
||||||
|
assert_eq!(rules[&5].grid.unwrap().rows, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rule_entry_with_all_fields() {
|
||||||
|
let json = r#"{
|
||||||
|
"column_ratios": [0.6, 0.3],
|
||||||
|
"scrolling": {"columns": 4, "center_focused_column": true},
|
||||||
|
"grid": {"rows": 2},
|
||||||
|
"row_ratios": [0.5]
|
||||||
|
}"#;
|
||||||
|
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||||
|
let col = opts.column_ratios.unwrap();
|
||||||
|
assert_eq!(col[0], Some(0.6));
|
||||||
|
assert_eq!(col[1], Some(0.3));
|
||||||
|
let row = opts.row_ratios.unwrap();
|
||||||
|
assert_eq!(row[0], Some(0.5));
|
||||||
|
assert_eq!(opts.scrolling.unwrap().columns, 4);
|
||||||
|
assert_eq!(opts.scrolling.unwrap().center_focused_column, Some(true));
|
||||||
|
assert_eq!(opts.grid.unwrap().rows, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rule_entry_empty_object_gives_defaults() {
|
||||||
|
let json = r#"{}"#;
|
||||||
|
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||||
|
assert!(opts.column_ratios.is_none());
|
||||||
|
assert!(opts.row_ratios.is_none());
|
||||||
|
assert!(opts.scrolling.is_none());
|
||||||
|
assert!(opts.grid.is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod layout_default_entry_tests {
|
||||||
|
use super::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_layout_as_hashmap_key() {
|
||||||
|
let mut map: HashMap<DefaultLayout, &str> = HashMap::new();
|
||||||
|
map.insert(DefaultLayout::BSP, "bsp");
|
||||||
|
map.insert(DefaultLayout::VerticalStack, "vstack");
|
||||||
|
map.insert(DefaultLayout::Columns, "cols");
|
||||||
|
|
||||||
|
assert_eq!(map.len(), 3);
|
||||||
|
assert_eq!(map[&DefaultLayout::BSP], "bsp");
|
||||||
|
assert_eq!(map[&DefaultLayout::VerticalStack], "vstack");
|
||||||
|
assert_eq!(map[&DefaultLayout::Columns], "cols");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_layout_hash_consistency() {
|
||||||
|
// Same variant inserted twice should overwrite
|
||||||
|
let mut map: HashMap<DefaultLayout, i32> = HashMap::new();
|
||||||
|
map.insert(DefaultLayout::Grid, 1);
|
||||||
|
map.insert(DefaultLayout::Grid, 2);
|
||||||
|
assert_eq!(map.len(), 1);
|
||||||
|
assert_eq!(map[&DefaultLayout::Grid], 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_layout_default_entry_deserialize_full() {
|
||||||
|
let json = r#"{
|
||||||
|
"layout_options": {"column_ratios": [0.7]},
|
||||||
|
"layout_options_rules": {
|
||||||
|
"2": {"column_ratios": [0.7]},
|
||||||
|
"3": {"column_ratios": [0.55]},
|
||||||
|
"5": {"column_ratios": [0.3, 0.3, 0.3]}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
let entry: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
let base = entry.layout_options.unwrap();
|
||||||
|
assert_eq!(base.column_ratios.unwrap()[0], Some(0.7));
|
||||||
|
|
||||||
|
let rules = entry.layout_options_rules.unwrap();
|
||||||
|
assert_eq!(rules.len(), 3);
|
||||||
|
assert_eq!(rules[&2].column_ratios.unwrap()[0], Some(0.7));
|
||||||
|
assert_eq!(rules[&3].column_ratios.unwrap()[0], Some(0.55));
|
||||||
|
let r5 = rules[&5].column_ratios.unwrap();
|
||||||
|
assert_eq!(r5[0], Some(0.3));
|
||||||
|
assert_eq!(r5[1], Some(0.3));
|
||||||
|
assert_eq!(r5[2], Some(0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_layout_default_entry_deserialize_only_base() {
|
||||||
|
let json = r#"{
|
||||||
|
"layout_options": {"column_ratios": [0.6]}
|
||||||
|
}"#;
|
||||||
|
let entry: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
assert!(entry.layout_options.is_some());
|
||||||
|
assert_eq!(
|
||||||
|
entry.layout_options.unwrap().column_ratios.unwrap()[0],
|
||||||
|
Some(0.6)
|
||||||
|
);
|
||||||
|
assert!(entry.layout_options_rules.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_layout_default_entry_deserialize_only_rules() {
|
||||||
|
let json = r#"{
|
||||||
|
"layout_options_rules": {
|
||||||
|
"3": {"column_ratios": [0.4]}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
let entry: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
assert!(entry.layout_options.is_none());
|
||||||
|
let rules = entry.layout_options_rules.unwrap();
|
||||||
|
assert_eq!(rules.len(), 1);
|
||||||
|
assert_eq!(rules[&3].column_ratios.unwrap()[0], Some(0.4));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_layout_default_entry_deserialize_empty() {
|
||||||
|
let json = r#"{}"#;
|
||||||
|
let entry: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
|
||||||
|
assert!(entry.layout_options.is_none());
|
||||||
|
assert!(entry.layout_options_rules.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_layout_default_entry_roundtrip() {
|
||||||
|
let json = r#"{
|
||||||
|
"layout_options": {"column_ratios": [0.7]},
|
||||||
|
"layout_options_rules": {
|
||||||
|
"2": {"column_ratios": [0.6]},
|
||||||
|
"5": {"column_ratios": [0.3, 0.3, 0.3]}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
let original: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
|
||||||
|
let serialized = serde_json::to_string(&original).unwrap();
|
||||||
|
let deserialized: LayoutDefaultEntry = serde_json::from_str(&serialized).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
original.layout_options.unwrap().column_ratios,
|
||||||
|
deserialized.layout_options.unwrap().column_ratios
|
||||||
|
);
|
||||||
|
let orig_rules = original.layout_options_rules.unwrap();
|
||||||
|
let deser_rules = deserialized.layout_options_rules.unwrap();
|
||||||
|
assert_eq!(orig_rules.len(), deser_rules.len());
|
||||||
|
for (key, orig_opts) in &orig_rules {
|
||||||
|
let deser_opts = &deser_rules[key];
|
||||||
|
assert_eq!(orig_opts.column_ratios, deser_opts.column_ratios);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_layout_defaults_full_config_deserialize() {
|
||||||
|
// Simulate the top-level layout_defaults field
|
||||||
|
let json = r#"{
|
||||||
|
"VerticalStack": {
|
||||||
|
"layout_options": {"column_ratios": [0.7]},
|
||||||
|
"layout_options_rules": {
|
||||||
|
"2": {"column_ratios": [0.7]},
|
||||||
|
"3": {"column_ratios": [0.55]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HorizontalStack": {
|
||||||
|
"layout_options": {"column_ratios": [0.6]}
|
||||||
|
},
|
||||||
|
"Columns": {
|
||||||
|
"layout_options_rules": {
|
||||||
|
"4": {"column_ratios": [0.3, 0.3, 0.3]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
let defaults: HashMap<DefaultLayout, LayoutDefaultEntry> =
|
||||||
|
serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(defaults.len(), 3);
|
||||||
|
|
||||||
|
// VerticalStack: has both base and rules
|
||||||
|
let vs = &defaults[&DefaultLayout::VerticalStack];
|
||||||
|
assert!(vs.layout_options.is_some());
|
||||||
|
assert_eq!(vs.layout_options_rules.as_ref().unwrap().len(), 2);
|
||||||
|
|
||||||
|
// HorizontalStack: has only base
|
||||||
|
let hs = &defaults[&DefaultLayout::HorizontalStack];
|
||||||
|
assert!(hs.layout_options.is_some());
|
||||||
|
assert!(hs.layout_options_rules.is_none());
|
||||||
|
|
||||||
|
// Columns: has only rules
|
||||||
|
let cols = &defaults[&DefaultLayout::Columns];
|
||||||
|
assert!(cols.layout_options.is_none());
|
||||||
|
assert_eq!(cols.layout_options_rules.as_ref().unwrap().len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_layout_default_entry_with_scrolling_and_grid() {
|
||||||
|
let json = r#"{
|
||||||
|
"layout_options": {
|
||||||
|
"column_ratios": [0.5],
|
||||||
|
"scrolling": {"columns": 3},
|
||||||
|
"grid": {"rows": 2}
|
||||||
|
},
|
||||||
|
"layout_options_rules": {
|
||||||
|
"4": {
|
||||||
|
"scrolling": {"columns": 5, "center_focused_column": true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
let entry: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
let base = entry.layout_options.unwrap();
|
||||||
|
assert_eq!(base.scrolling.unwrap().columns, 3);
|
||||||
|
assert_eq!(base.grid.unwrap().rows, 2);
|
||||||
|
|
||||||
|
let rules = entry.layout_options_rules.unwrap();
|
||||||
|
let r4 = &rules[&4];
|
||||||
|
assert_eq!(r4.scrolling.unwrap().columns, 5);
|
||||||
|
assert_eq!(r4.scrolling.unwrap().center_focused_column, Some(true));
|
||||||
|
// Rule doesn't inherit base fields - full replacement
|
||||||
|
assert!(r4.column_ratios.is_none());
|
||||||
|
assert!(r4.grid.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_layout_default_entry_skip_serializing_none() {
|
||||||
|
// When both fields are None, they should not appear in output
|
||||||
|
let entry = LayoutDefaultEntry {
|
||||||
|
layout_options: None,
|
||||||
|
layout_options_rules: None,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&entry).unwrap();
|
||||||
|
assert!(!json.contains("layout_options"));
|
||||||
|
assert!(!json.contains("layout_options_rules"));
|
||||||
|
assert_eq!(json, "{}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests for the complete-replacement cascade logic.
|
||||||
|
///
|
||||||
|
/// This mirrors the resolution algorithm in workspace.rs::update():
|
||||||
|
/// - If the workspace defines EITHER layout_options OR layout_options_rules,
|
||||||
|
/// it completely replaces the global layout_defaults for this layout.
|
||||||
|
/// - Global defaults are only used when the workspace has NEITHER setting.
|
||||||
|
/// - Within the effective source (workspace or global):
|
||||||
|
/// 1. Try threshold match from rules (highest matching threshold wins)
|
||||||
|
/// 2. If a rule matches -> use it (full replacement of base)
|
||||||
|
/// 3. Else -> use the base layout_options
|
||||||
|
///
|
||||||
|
/// Since the actual cascade is in workspace.rs (which has heavy WM dependencies),
|
||||||
|
/// we test the pure algorithm here using the same data structures.
|
||||||
|
mod cascade_resolution_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Simulates the cascade resolution logic from workspace.rs::update().
|
||||||
|
/// This is a pure function equivalent of the inline code in update().
|
||||||
|
fn resolve_effective_options(
|
||||||
|
container_count: usize,
|
||||||
|
workspace_base: Option<LayoutOptions>,
|
||||||
|
workspace_rules: &[(usize, LayoutOptions)], // sorted by threshold ascending
|
||||||
|
global_base: Option<LayoutOptions>,
|
||||||
|
global_rules: &[(usize, LayoutOptions)], // sorted by threshold ascending
|
||||||
|
) -> Option<LayoutOptions> {
|
||||||
|
let has_workspace_overrides = workspace_base.is_some() || !workspace_rules.is_empty();
|
||||||
|
|
||||||
|
let (effective_base, effective_rules): (Option<LayoutOptions>, &[(usize, LayoutOptions)]) =
|
||||||
|
if has_workspace_overrides {
|
||||||
|
(workspace_base, workspace_rules)
|
||||||
|
} else {
|
||||||
|
(global_base, global_rules)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try threshold match from effective rules
|
||||||
|
let mut matched = None;
|
||||||
|
for (threshold, opts) in effective_rules {
|
||||||
|
if container_count >= *threshold {
|
||||||
|
matched = Some(*opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a rule matched, use it (full replacement); otherwise use effective base
|
||||||
|
if matched.is_some() {
|
||||||
|
matched
|
||||||
|
} else {
|
||||||
|
effective_base
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opts_with_ratio(ratio: f32) -> LayoutOptions {
|
||||||
|
layout_options_with_column_ratios(&[ratio])
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- No overrides ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_workspace_no_global_returns_none() {
|
||||||
|
let result = resolve_effective_options(3, None, &[], None, &[]);
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Base-only scenarios ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_workspace_base_only() {
|
||||||
|
let ws_base = opts_with_ratio(0.7);
|
||||||
|
let result = resolve_effective_options(3, Some(ws_base), &[], None, &[]);
|
||||||
|
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_global_base_only() {
|
||||||
|
let global_base = opts_with_ratio(0.6);
|
||||||
|
let result = resolve_effective_options(3, None, &[], Some(global_base), &[]);
|
||||||
|
assert_eq!(result.unwrap().column_ratios, global_base.column_ratios);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_workspace_base_overrides_all_globals() {
|
||||||
|
// Workspace has base → globals (both base and rules) are ignored entirely
|
||||||
|
let ws_base = opts_with_ratio(0.7);
|
||||||
|
let global_base = opts_with_ratio(0.6);
|
||||||
|
let global_rules = vec![(2, opts_with_ratio(0.5))];
|
||||||
|
let result =
|
||||||
|
resolve_effective_options(3, Some(ws_base), &[], Some(global_base), &global_rules);
|
||||||
|
// Workspace base wins; global rules are NOT used even though they would match
|
||||||
|
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Rules-only scenarios ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_global_rules_match() {
|
||||||
|
let global_rules = vec![(2, opts_with_ratio(0.6)), (4, opts_with_ratio(0.5))];
|
||||||
|
// 3 containers: matches threshold 2, not 4
|
||||||
|
let result = resolve_effective_options(3, None, &[], None, &global_rules);
|
||||||
|
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_global_rules_highest_matching_threshold_wins() {
|
||||||
|
let global_rules = vec![(2, opts_with_ratio(0.6)), (4, opts_with_ratio(0.5))];
|
||||||
|
// 5 containers: matches both thresholds 2 and 4; highest (4) wins
|
||||||
|
let result = resolve_effective_options(5, None, &[], None, &global_rules);
|
||||||
|
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_global_rules_no_match_falls_through_to_none() {
|
||||||
|
let global_rules = vec![(5, opts_with_ratio(0.5))];
|
||||||
|
// 3 containers: doesn't match threshold 5
|
||||||
|
let result = resolve_effective_options(3, None, &[], None, &global_rules);
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_global_rules_no_match_falls_through_to_global_base() {
|
||||||
|
let global_base = opts_with_ratio(0.6);
|
||||||
|
let global_rules = vec![(5, opts_with_ratio(0.5))];
|
||||||
|
// 3 containers: doesn't match threshold 5, falls back to global base
|
||||||
|
let result = resolve_effective_options(3, None, &[], Some(global_base), &global_rules);
|
||||||
|
assert_eq!(result.unwrap().column_ratios, global_base.column_ratios);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_workspace_rules_override_global_rules() {
|
||||||
|
let ws_rules = vec![(2, opts_with_ratio(0.8))];
|
||||||
|
let global_rules = vec![(2, opts_with_ratio(0.6))];
|
||||||
|
// Workspace has rules → global rules are ignored entirely
|
||||||
|
let result = resolve_effective_options(3, None, &ws_rules, None, &global_rules);
|
||||||
|
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.8));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Complete replacement: workspace having EITHER setting disables ALL globals ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_workspace_rules_disable_global_base() {
|
||||||
|
// Workspace has rules but no base. Global has base.
|
||||||
|
// Since workspace has a setting, globals are completely replaced.
|
||||||
|
let ws_rules = vec![(2, opts_with_ratio(0.8))];
|
||||||
|
let global_base = opts_with_ratio(0.6);
|
||||||
|
// Rule matches → use it. Global base is NOT available as fallback.
|
||||||
|
let result = resolve_effective_options(3, None, &ws_rules, Some(global_base), &[]);
|
||||||
|
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.8));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_workspace_rules_no_match_does_not_fall_to_global_base() {
|
||||||
|
// Workspace has rules (but they don't match). Global has base.
|
||||||
|
// Since workspace has a setting, globals are completely replaced → returns None.
|
||||||
|
let ws_rules = vec![(5, opts_with_ratio(0.8))];
|
||||||
|
let global_base = opts_with_ratio(0.6);
|
||||||
|
let result = resolve_effective_options(3, None, &ws_rules, Some(global_base), &[]);
|
||||||
|
// No workspace base, no rule match, globals ignored → None
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_workspace_base_disables_global_rules() {
|
||||||
|
// Workspace has base but no rules. Global has rules.
|
||||||
|
// Since workspace has a setting, globals are completely replaced.
|
||||||
|
let ws_base = opts_with_ratio(0.7);
|
||||||
|
let global_rules = vec![(2, opts_with_ratio(0.5))];
|
||||||
|
// No workspace rules → no rule match → use workspace base. Global rules ignored.
|
||||||
|
let result = resolve_effective_options(3, Some(ws_base), &[], None, &global_rules);
|
||||||
|
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_workspace_base_disables_global_rules_and_base() {
|
||||||
|
// Workspace has base. Global has both rules and base.
|
||||||
|
// Since workspace has a setting, all globals are completely replaced.
|
||||||
|
let ws_base = opts_with_ratio(0.7);
|
||||||
|
let global_base = opts_with_ratio(0.6);
|
||||||
|
let global_rules = vec![(2, opts_with_ratio(0.5))];
|
||||||
|
let result =
|
||||||
|
resolve_effective_options(3, Some(ws_base), &[], Some(global_base), &global_rules);
|
||||||
|
// Only workspace base is used; global rules and base are both ignored
|
||||||
|
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_workspace_rules_disable_global_rules_and_base() {
|
||||||
|
// Workspace has rules. Global has both rules and base.
|
||||||
|
// Since workspace has a setting, all globals are completely replaced.
|
||||||
|
let ws_rules = vec![(2, opts_with_ratio(0.8))];
|
||||||
|
let global_base = opts_with_ratio(0.6);
|
||||||
|
let global_rules = vec![(2, opts_with_ratio(0.5))];
|
||||||
|
let result =
|
||||||
|
resolve_effective_options(3, None, &ws_rules, Some(global_base), &global_rules);
|
||||||
|
// Workspace rule matches → 0.8. Global base and rules both ignored.
|
||||||
|
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.8));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Full replacement semantics (rule match replaces base) ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rule_match_is_full_replacement_not_merge() {
|
||||||
|
// When a rule matches, its options FULLY REPLACE the base.
|
||||||
|
// Fields not specified in the rule default to their standard defaults.
|
||||||
|
let ws_base = layout_options_with_ratios(&[0.7], &[0.4]);
|
||||||
|
let rule_opts = layout_options_with_column_ratios(&[0.5]);
|
||||||
|
// rule_opts has column_ratios but no row_ratios
|
||||||
|
let ws_rules = vec![(2, rule_opts)];
|
||||||
|
let result = resolve_effective_options(3, Some(ws_base), &ws_rules, None, &[]);
|
||||||
|
let effective = result.unwrap();
|
||||||
|
// Column ratios come from the rule
|
||||||
|
assert_eq!(effective.column_ratios.unwrap()[0], Some(0.5));
|
||||||
|
// Row ratios are NOT inherited from ws_base - they're None (full replacement)
|
||||||
|
assert!(effective.row_ratios.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Edge cases ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_exact_threshold_match() {
|
||||||
|
let rules = vec![(3, opts_with_ratio(0.6))];
|
||||||
|
let result = resolve_effective_options(3, None, &rules, None, &[]);
|
||||||
|
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_container_count_one_below_threshold() {
|
||||||
|
let rules = vec![(3, opts_with_ratio(0.6))];
|
||||||
|
let result = resolve_effective_options(2, None, &rules, None, &[]);
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_zero_containers() {
|
||||||
|
let ws_base = opts_with_ratio(0.7);
|
||||||
|
let rules = vec![(1, opts_with_ratio(0.5))];
|
||||||
|
let result = resolve_effective_options(0, Some(ws_base), &rules, None, &[]);
|
||||||
|
// 0 containers doesn't match threshold 1 → falls back to workspace base
|
||||||
|
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_many_thresholds_correct_match() {
|
||||||
|
let rules = vec![
|
||||||
|
(1, opts_with_ratio(0.8)),
|
||||||
|
(3, opts_with_ratio(0.6)),
|
||||||
|
(5, opts_with_ratio(0.4)),
|
||||||
|
(8, opts_with_ratio(0.3)),
|
||||||
|
];
|
||||||
|
// 6 containers: matches 1, 3, 5 but not 8. Highest match is 5.
|
||||||
|
let result = resolve_effective_options(6, None, &rules, None, &[]);
|
||||||
|
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.4));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_workspace_rules_disable_global_rules_even_if_ws_rules_dont_match() {
|
||||||
|
// Key behavior: if workspace has ANY setting, globals are entirely ignored.
|
||||||
|
// Even if workspace rules don't match, we don't fall back to global rules.
|
||||||
|
let ws_rules = vec![(10, opts_with_ratio(0.8))]; // threshold too high
|
||||||
|
let global_rules = vec![(2, opts_with_ratio(0.5))]; // would match
|
||||||
|
let result = resolve_effective_options(3, None, &ws_rules, None, &global_rules);
|
||||||
|
// Workspace has rules → all globals ignored. WS rules don't match → None.
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_all_four_sources_present_rules_match() {
|
||||||
|
// All four sources present: workspace base, workspace rules, global base, global rules
|
||||||
|
let ws_base = opts_with_ratio(0.7);
|
||||||
|
let ws_rules = vec![(2, opts_with_ratio(0.8))];
|
||||||
|
let global_base = opts_with_ratio(0.6);
|
||||||
|
let global_rules = vec![(2, opts_with_ratio(0.5))];
|
||||||
|
let result = resolve_effective_options(
|
||||||
|
3,
|
||||||
|
Some(ws_base),
|
||||||
|
&ws_rules,
|
||||||
|
Some(global_base),
|
||||||
|
&global_rules,
|
||||||
|
);
|
||||||
|
// Workspace has settings → uses workspace only. Rule matches → 0.8
|
||||||
|
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.8));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_all_four_sources_present_rules_no_match() {
|
||||||
|
// All four sources present, but workspace rules don't match
|
||||||
|
let ws_base = opts_with_ratio(0.7);
|
||||||
|
let ws_rules = vec![(10, opts_with_ratio(0.8))]; // threshold too high
|
||||||
|
let global_base = opts_with_ratio(0.6);
|
||||||
|
let global_rules = vec![(10, opts_with_ratio(0.5))]; // also too high
|
||||||
|
let result = resolve_effective_options(
|
||||||
|
3,
|
||||||
|
Some(ws_base),
|
||||||
|
&ws_rules,
|
||||||
|
Some(global_base),
|
||||||
|
&global_rules,
|
||||||
|
);
|
||||||
|
// Workspace has settings → uses workspace only. No rule match → workspace base 0.7
|
||||||
|
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Workspace with both base and rules ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_workspace_both_rule_matches() {
|
||||||
|
let ws_base = opts_with_ratio(0.7);
|
||||||
|
let ws_rules = vec![(2, opts_with_ratio(0.5))];
|
||||||
|
let result = resolve_effective_options(3, Some(ws_base), &ws_rules, None, &[]);
|
||||||
|
// Rule matches → use rule (full replacement), not ws_base
|
||||||
|
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_workspace_both_rule_no_match() {
|
||||||
|
let ws_base = opts_with_ratio(0.7);
|
||||||
|
let ws_rules = vec![(10, opts_with_ratio(0.5))];
|
||||||
|
let result = resolve_effective_options(3, Some(ws_base), &ws_rules, None, &[]);
|
||||||
|
// Rule doesn't match → fall back to ws_base
|
||||||
|
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
use super::DefaultLayout;
|
use super::DefaultLayout;
|
||||||
use super::OperationDirection;
|
use super::OperationDirection;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
use super::custom_layout::Column;
|
use super::custom_layout::Column;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
use super::custom_layout::ColumnSplit;
|
use super::custom_layout::ColumnSplit;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
use super::custom_layout::ColumnSplitWithCapacity;
|
use super::custom_layout::ColumnSplitWithCapacity;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
use super::custom_layout::CustomLayout;
|
use super::custom_layout::CustomLayout;
|
||||||
use crate::default_layout::LayoutOptions;
|
use crate::default_layout::LayoutOptions;
|
||||||
|
|
||||||
@@ -400,6 +404,7 @@ fn grid_neighbor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
impl Direction for CustomLayout {
|
impl Direction for CustomLayout {
|
||||||
fn index_in_direction(
|
fn index_in_direction(
|
||||||
&self,
|
&self,
|
||||||
@@ -2,6 +2,7 @@ use serde::Deserialize;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use super::Arrangement;
|
use super::Arrangement;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
use super::CustomLayout;
|
use super::CustomLayout;
|
||||||
use super::DefaultLayout;
|
use super::DefaultLayout;
|
||||||
use super::Direction;
|
use super::Direction;
|
||||||
@@ -10,6 +11,7 @@ use super::Direction;
|
|||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub enum Layout {
|
pub enum Layout {
|
||||||
Default(DefaultLayout),
|
Default(DefaultLayout),
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
Custom(CustomLayout),
|
Custom(CustomLayout),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +20,7 @@ impl Layout {
|
|||||||
pub fn as_boxed_direction(&self) -> Box<dyn Direction> {
|
pub fn as_boxed_direction(&self) -> Box<dyn Direction> {
|
||||||
match self {
|
match self {
|
||||||
Layout::Default(layout) => Box::new(*layout),
|
Layout::Default(layout) => Box::new(*layout),
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
Layout::Custom(layout) => Box::new(layout.clone()),
|
Layout::Custom(layout) => Box::new(layout.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +29,7 @@ impl Layout {
|
|||||||
pub fn as_boxed_arrangement(&self) -> Box<dyn Arrangement> {
|
pub fn as_boxed_arrangement(&self) -> Box<dyn Arrangement> {
|
||||||
match self {
|
match self {
|
||||||
Layout::Default(layout) => Box::new(*layout),
|
Layout::Default(layout) => Box::new(*layout),
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
Layout::Custom(layout) => Box::new(layout.clone()),
|
Layout::Custom(layout) => Box::new(layout.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
30
komorebi-layouts/src/lib.rs
Normal file
30
komorebi-layouts/src/lib.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#![warn(clippy::all)]
|
||||||
|
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
|
||||||
|
|
||||||
|
//! Layout system for the komorebi window manager.
|
||||||
|
//!
|
||||||
|
//! This crate provides the core layout algorithms and types for arranging windows
|
||||||
|
//! in various configurations. It includes optional Windows-specific functionality
|
||||||
|
//! behind the `win32` feature flag.
|
||||||
|
|
||||||
|
pub mod arrangement;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
|
pub mod custom_layout;
|
||||||
|
pub mod cycle_direction;
|
||||||
|
pub mod default_layout;
|
||||||
|
pub mod direction;
|
||||||
|
pub mod layout;
|
||||||
|
pub mod operation_direction;
|
||||||
|
pub mod rect;
|
||||||
|
pub mod sizing;
|
||||||
|
|
||||||
|
pub use arrangement::*;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
|
pub use custom_layout::*;
|
||||||
|
pub use cycle_direction::*;
|
||||||
|
pub use default_layout::*;
|
||||||
|
pub use direction::*;
|
||||||
|
pub use layout::*;
|
||||||
|
pub use operation_direction::*;
|
||||||
|
pub use rect::*;
|
||||||
|
pub use sizing::*;
|
||||||
@@ -1,7 +1,18 @@
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
use windows::Win32::Foundation::RECT;
|
use windows::Win32::Foundation::RECT;
|
||||||
|
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
use objc2_core_foundation::CGFloat;
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
use objc2_core_foundation::CGPoint;
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
use objc2_core_foundation::CGRect;
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
use objc2_core_foundation::CGSize;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
|
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
/// Rectangle dimensions
|
/// Rectangle dimensions
|
||||||
@@ -16,6 +27,7 @@ pub struct Rect {
|
|||||||
pub bottom: i32,
|
pub bottom: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
impl From<RECT> for Rect {
|
impl From<RECT> for Rect {
|
||||||
fn from(rect: RECT) -> Self {
|
fn from(rect: RECT) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -27,6 +39,7 @@ impl From<RECT> for Rect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
impl From<Rect> for RECT {
|
impl From<Rect> for RECT {
|
||||||
fn from(rect: Rect) -> Self {
|
fn from(rect: Rect) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -38,6 +51,53 @@ impl From<Rect> for RECT {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
impl From<CGSize> for Rect {
|
||||||
|
fn from(value: CGSize) -> Self {
|
||||||
|
Self {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: value.width as i32,
|
||||||
|
bottom: value.height as i32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
impl From<CGRect> for Rect {
|
||||||
|
fn from(value: CGRect) -> Self {
|
||||||
|
Self {
|
||||||
|
left: value.origin.x as i32,
|
||||||
|
top: value.origin.y as i32,
|
||||||
|
right: value.size.width as i32,
|
||||||
|
bottom: value.size.height as i32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
impl From<&Rect> for CGRect {
|
||||||
|
fn from(value: &Rect) -> Self {
|
||||||
|
Self {
|
||||||
|
origin: CGPoint {
|
||||||
|
x: value.left as CGFloat,
|
||||||
|
y: value.top as CGFloat,
|
||||||
|
},
|
||||||
|
size: CGSize {
|
||||||
|
width: value.right as CGFloat,
|
||||||
|
height: value.bottom as CGFloat,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
impl From<Rect> for CGRect {
|
||||||
|
fn from(value: Rect) -> Self {
|
||||||
|
CGRect::from(&value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Rect {
|
impl Rect {
|
||||||
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
|
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
|
||||||
self.right == rhs.right && self.bottom == rhs.bottom
|
self.right == rhs.right && self.bottom == rhs.bottom
|
||||||
@@ -96,6 +156,7 @@ impl Rect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn rect(&self) -> RECT {
|
pub const fn rect(&self) -> RECT {
|
||||||
RECT {
|
RECT {
|
||||||
@@ -105,4 +166,19 @@ impl Rect {
|
|||||||
bottom: self.top + self.bottom,
|
bottom: self.top + self.bottom,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn percentage_within_horizontal_bounds(&self, other: &Rect) -> f64 {
|
||||||
|
let overlap_left = self.left.max(other.left);
|
||||||
|
let overlap_right = (self.left + self.right).min(other.left + other.right);
|
||||||
|
|
||||||
|
let overlap_width = overlap_right - overlap_left;
|
||||||
|
|
||||||
|
if overlap_width <= 0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
(overlap_width as f64) / (other.right as f64) * 100.0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
31
komorebi-layouts/src/sizing.rs
Normal file
31
komorebi-layouts/src/sizing.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
use clap::ValueEnum;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use strum::Display;
|
||||||
|
use strum::EnumString;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Sizing
|
||||||
|
pub enum Sizing {
|
||||||
|
/// Increase
|
||||||
|
Increase,
|
||||||
|
/// Decrease
|
||||||
|
Decrease,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sizing {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn adjust_by(&self, value: i32, adjustment: i32) -> i32 {
|
||||||
|
match self {
|
||||||
|
Self::Increase => value + adjustment,
|
||||||
|
Self::Decrease => {
|
||||||
|
if value > 0 && value - adjustment >= 0 {
|
||||||
|
value - adjustment
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi-themes"
|
name = "komorebi-themes"
|
||||||
version = "0.1.40"
|
version = "0.1.41"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "b9e26b31f7a0e7ed239b14e5317e95d1bdc544bd" }
|
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "3f157904c641f0dc80f043449fe0214fc4182425" }
|
||||||
#catppuccin-egui = { version = "5", default-features = false, features = ["egui32"] }
|
#catppuccin-egui = { version = "5", default-features = false, features = ["egui32"] }
|
||||||
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "b2f95cbf441d1dd99f3c955ef10dcb84ce23c20a", default-features = false, features = [
|
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "b2f95cbf441d1dd99f3c955ef10dcb84ce23c20a", default-features = false, features = [
|
||||||
"egui33",
|
"egui33",
|
||||||
@@ -15,7 +15,7 @@ serde = { workspace = true }
|
|||||||
serde_variant = "0.1"
|
serde_variant = "0.1"
|
||||||
strum = { workspace = true }
|
strum = { workspace = true }
|
||||||
hex_color = { version = "3", features = ["serde"] }
|
hex_color = { version = "3", features = ["serde"] }
|
||||||
flavours = { git = "https://github.com/LGUG2Z/flavours", version = "0.7.2" }
|
flavours = { git = "https://github.com/LGUG2Z/flavours", rev = "24518c129918fe3260aa559eded7657e50752cb1" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["schemars"]
|
default = ["schemars"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi"
|
name = "komorebi"
|
||||||
version = "0.1.40"
|
version = "0.1.41"
|
||||||
description = "A tiling window manager for Windows"
|
description = "A tiling window manager for Windows"
|
||||||
repository = "https://github.com/LGUG2Z/komorebi"
|
repository = "https://github.com/LGUG2Z/komorebi"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
@@ -8,6 +8,7 @@ edition = "2024"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
komorebi-layouts = { path = "../komorebi-layouts", features = ["win32"] }
|
||||||
komorebi-themes = { path = "../komorebi-themes" }
|
komorebi-themes = { path = "../komorebi-themes" }
|
||||||
|
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
@@ -50,7 +51,7 @@ windows-numerics = { workspace = true }
|
|||||||
windows-implement = { workspace = true }
|
windows-implement = { workspace = true }
|
||||||
windows-interface = { workspace = true }
|
windows-interface = { workspace = true }
|
||||||
winput = "0.2"
|
winput = "0.2"
|
||||||
winreg = "0.55"
|
winreg = "0.56"
|
||||||
serde_with = { version = "3.12", features = ["schemars_1"] }
|
serde_with = { version = "3.12", features = ["schemars_1"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
@@ -63,4 +64,4 @@ uuid = { version = "1", features = ["v4"] }
|
|||||||
[features]
|
[features]
|
||||||
default = ["schemars"]
|
default = ["schemars"]
|
||||||
deadlock_detection = ["parking_lot/deadlock_detection"]
|
deadlock_detection = ["parking_lot/deadlock_detection"]
|
||||||
schemars = ["dep:schemars"]
|
schemars = ["dep:schemars", "komorebi-layouts/schemars"]
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ use crate::core::Rect;
|
|||||||
use crate::windows_api;
|
use crate::windows_api;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use windows::Win32::Foundation::FALSE;
|
use windows::Win32::Foundation::FALSE;
|
||||||
@@ -56,6 +58,7 @@ use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
|
|||||||
use windows::Win32::UI::WindowsAndMessaging::IDC_ARROW;
|
use windows::Win32::UI::WindowsAndMessaging::IDC_ARROW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
|
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::PostMessageW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
|
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::SetCursor;
|
use windows::Win32::UI::WindowsAndMessaging::SetCursor;
|
||||||
@@ -65,11 +68,16 @@ use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
|
|||||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
|
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WM_SETCURSOR;
|
use windows::Win32::UI::WindowsAndMessaging::WM_SETCURSOR;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WM_USER;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||||
use windows_core::BOOL;
|
use windows_core::BOOL;
|
||||||
use windows_core::PCWSTR;
|
use windows_core::PCWSTR;
|
||||||
use windows_numerics::Matrix3x2;
|
use windows_numerics::Matrix3x2;
|
||||||
|
|
||||||
|
/// Custom WM_USER message that tells the border window thread to call update_brushes() on itself,
|
||||||
|
/// avoiding a data race between the border manager thread and the border's message loop thread.
|
||||||
|
pub const WM_UPDATE_BRUSHES: u32 = WM_USER + 1;
|
||||||
|
|
||||||
pub struct RenderFactory(ID2D1Factory);
|
pub struct RenderFactory(ID2D1Factory);
|
||||||
unsafe impl Sync for RenderFactory {}
|
unsafe impl Sync for RenderFactory {}
|
||||||
unsafe impl Send for RenderFactory {}
|
unsafe impl Send for RenderFactory {}
|
||||||
@@ -126,6 +134,7 @@ pub struct Border {
|
|||||||
pub brush_properties: D2D1_BRUSH_PROPERTIES,
|
pub brush_properties: D2D1_BRUSH_PROPERTIES,
|
||||||
pub rounded_rect: D2D1_ROUNDED_RECT,
|
pub rounded_rect: D2D1_ROUNDED_RECT,
|
||||||
pub brushes: HashMap<WindowKind, ID2D1SolidColorBrush>,
|
pub brushes: HashMap<WindowKind, ID2D1SolidColorBrush>,
|
||||||
|
pub is_destroying: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<isize> for Border {
|
impl From<isize> for Border {
|
||||||
@@ -144,6 +153,7 @@ impl From<isize> for Border {
|
|||||||
brush_properties: D2D1_BRUSH_PROPERTIES::default(),
|
brush_properties: D2D1_BRUSH_PROPERTIES::default(),
|
||||||
rounded_rect: D2D1_ROUNDED_RECT::default(),
|
rounded_rect: D2D1_ROUNDED_RECT::default(),
|
||||||
brushes: HashMap::new(),
|
brushes: HashMap::new(),
|
||||||
|
is_destroying: Arc::new(AtomicBool::new(false)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,6 +202,7 @@ impl Border {
|
|||||||
brush_properties: Default::default(),
|
brush_properties: Default::default(),
|
||||||
rounded_rect: Default::default(),
|
rounded_rect: Default::default(),
|
||||||
brushes: HashMap::new(),
|
brushes: HashMap::new(),
|
||||||
|
is_destroying: Arc::new(AtomicBool::new(false)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let border_pointer = &raw mut border;
|
let border_pointer = &raw mut border;
|
||||||
@@ -313,14 +324,31 @@ impl Border {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn destroy(&self) -> color_eyre::Result<()> {
|
pub fn destroy(&self) -> color_eyre::Result<()> {
|
||||||
// clear user data **BEFORE** closing window
|
// signal that we're destroying - prevents new render operations from starting
|
||||||
// pending messages will see a null pointer and exit early
|
self.is_destroying.store(true, Ordering::Release);
|
||||||
unsafe {
|
|
||||||
SetWindowLongPtrW(self.hwnd(), GWLP_USERDATA, 0);
|
// small delay to allow in-flight render operations to complete
|
||||||
}
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
|
|
||||||
|
// WM_DESTROY will clear GWLP_USERDATA and drop the render target before D2D
|
||||||
|
// frees its internal HwndPresenter during WM_NCDESTROY
|
||||||
WindowsApi::close_window(self.hwnd)
|
WindowsApi::close_window(self.hwnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Post a message to the border's own message loop thread requesting a brush update.
|
||||||
|
/// This ensures update_brushes() always runs on the window thread that owns the D2D
|
||||||
|
/// render target, preventing a data race with concurrent WndProc render operations.
|
||||||
|
pub fn request_brush_update(&self) {
|
||||||
|
let _ = unsafe {
|
||||||
|
PostMessageW(
|
||||||
|
Option::from(self.hwnd()),
|
||||||
|
WM_UPDATE_BRUSHES,
|
||||||
|
WPARAM(0),
|
||||||
|
LPARAM(0),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_position(&self, rect: &Rect, reference_hwnd: isize) -> color_eyre::Result<()> {
|
pub fn set_position(&self, rect: &Rect, reference_hwnd: isize) -> color_eyre::Result<()> {
|
||||||
let mut rect = *rect;
|
let mut rect = *rect;
|
||||||
rect.add_margin(self.width);
|
rect.add_margin(self.width);
|
||||||
@@ -386,6 +414,10 @@ impl Border {
|
|||||||
return LRESULT(0);
|
return LRESULT(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
|
||||||
|
return LRESULT(0);
|
||||||
|
}
|
||||||
|
|
||||||
let reference_hwnd = (*border_pointer).tracking_hwnd;
|
let reference_hwnd = (*border_pointer).tracking_hwnd;
|
||||||
|
|
||||||
let old_rect = (*border_pointer).window_rect;
|
let old_rect = (*border_pointer).window_rect;
|
||||||
@@ -400,6 +432,11 @@ impl Border {
|
|||||||
if (!rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect))
|
if (!rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect))
|
||||||
&& let Some(render_target) = (*border_pointer).render_target.as_ref()
|
&& let Some(render_target) = (*border_pointer).render_target.as_ref()
|
||||||
{
|
{
|
||||||
|
// double-check destruction flag before rendering
|
||||||
|
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
|
||||||
|
return LRESULT(0);
|
||||||
|
}
|
||||||
|
|
||||||
let border_width = (*border_pointer).width;
|
let border_width = (*border_pointer).width;
|
||||||
let border_offset = (*border_pointer).offset;
|
let border_offset = (*border_pointer).offset;
|
||||||
|
|
||||||
@@ -468,6 +505,10 @@ impl Border {
|
|||||||
return LRESULT(0);
|
return LRESULT(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
|
||||||
|
return LRESULT(0);
|
||||||
|
}
|
||||||
|
|
||||||
let reference_hwnd = (*border_pointer).tracking_hwnd;
|
let reference_hwnd = (*border_pointer).tracking_hwnd;
|
||||||
|
|
||||||
// Update position to update the ZOrder
|
// Update position to update the ZOrder
|
||||||
@@ -481,6 +522,11 @@ impl Border {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
|
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
|
||||||
|
// double-check destruction flag before rendering
|
||||||
|
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
|
||||||
|
return LRESULT(0);
|
||||||
|
}
|
||||||
|
|
||||||
(*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed);
|
(*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed);
|
||||||
(*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed);
|
(*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed);
|
||||||
|
|
||||||
@@ -547,8 +593,27 @@ impl Border {
|
|||||||
let _ = ValidateRect(Option::from(window), None);
|
let _ = ValidateRect(Option::from(window), None);
|
||||||
LRESULT(0)
|
LRESULT(0)
|
||||||
}
|
}
|
||||||
|
WM_UPDATE_BRUSHES => {
|
||||||
|
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||||
|
if border_pointer.is_null() {
|
||||||
|
return LRESULT(0);
|
||||||
|
}
|
||||||
|
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
|
||||||
|
return LRESULT(0);
|
||||||
|
}
|
||||||
|
if let Err(error) = (*border_pointer).update_brushes() {
|
||||||
|
tracing::error!("failed to update brushes: {error}");
|
||||||
|
}
|
||||||
|
(*border_pointer).invalidate();
|
||||||
|
LRESULT(0)
|
||||||
|
}
|
||||||
WM_DESTROY => {
|
WM_DESTROY => {
|
||||||
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
|
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||||
|
if !border_pointer.is_null() {
|
||||||
|
(*border_pointer).render_target = None;
|
||||||
|
(*border_pointer).brushes.clear();
|
||||||
|
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
|
||||||
|
}
|
||||||
PostQuitMessage(0);
|
PostQuitMessage(0);
|
||||||
LRESULT(0)
|
LRESULT(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -451,8 +451,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
} else if matches!(notification, Notification::ForceUpdate) {
|
} else if matches!(notification, Notification::ForceUpdate) {
|
||||||
// Update the border brushes if there was a forced update
|
// Update the border brushes if there was a forced update
|
||||||
// notification and this is not a new border (new border's
|
// notification and this is not a new border (new border's
|
||||||
// already have their brushes updated on creation)
|
// already have their brushes updated on creation).
|
||||||
border.update_brushes()?;
|
// Post to the border's own thread to avoid a data race between
|
||||||
|
// this thread dropping the old render target and the window
|
||||||
|
// thread mid-render holding a reference to it.
|
||||||
|
border.request_brush_update();
|
||||||
}
|
}
|
||||||
|
|
||||||
border.invalidate();
|
border.invalidate();
|
||||||
@@ -616,8 +619,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
if forced_update && !new_border {
|
if forced_update && !new_border {
|
||||||
// Update the border brushes if there was a forced update
|
// Update the border brushes if there was a forced update
|
||||||
// notification and this is not a new border (new border's
|
// notification and this is not a new border (new border's
|
||||||
// already have their brushes updated on creation)
|
// already have their brushes updated on creation).
|
||||||
border.update_brushes()?;
|
// Post to the border's own thread to avoid a data race between
|
||||||
|
// this thread dropping the old render target and the window
|
||||||
|
// thread mid-render holding a reference to it.
|
||||||
|
border.request_brush_update();
|
||||||
}
|
}
|
||||||
border.set_position(&rect, focused_window_hwnd)?;
|
border.set_position(&rect, focused_window_hwnd)?;
|
||||||
border.invalidate();
|
border.invalidate();
|
||||||
@@ -699,8 +705,11 @@ fn handle_floating_borders(
|
|||||||
if forced_update && !new_border {
|
if forced_update && !new_border {
|
||||||
// Update the border brushes if there was a forced update
|
// Update the border brushes if there was a forced update
|
||||||
// notification and this is not a new border (new border's
|
// notification and this is not a new border (new border's
|
||||||
// already have their brushes updated on creation)
|
// already have their brushes updated on creation).
|
||||||
border.update_brushes()?;
|
// Post to the border's own thread to avoid a data race between
|
||||||
|
// this thread dropping the old render target and the window
|
||||||
|
// thread mid-render holding a reference to it.
|
||||||
|
border.request_brush_update();
|
||||||
}
|
}
|
||||||
border.set_position(&rect, window.hwnd)?;
|
border.set_position(&rect, window.hwnd)?;
|
||||||
border.invalidate();
|
border.invalidate();
|
||||||
@@ -767,12 +776,6 @@ fn remove_border(
|
|||||||
fn destroy_border(border: Box<Border>) -> color_eyre::Result<()> {
|
fn destroy_border(border: Box<Border>) -> color_eyre::Result<()> {
|
||||||
let raw_pointer = Box::into_raw(border);
|
let raw_pointer = Box::into_raw(border);
|
||||||
unsafe {
|
unsafe {
|
||||||
// release d2d resources **BEFORE** destroying window
|
|
||||||
// this drops render_target and brushes while HWND is still valid
|
|
||||||
// prevents EndDraw() from accessing freed HWND resources
|
|
||||||
(*raw_pointer).render_target = None;
|
|
||||||
(*raw_pointer).brushes.clear();
|
|
||||||
|
|
||||||
// Now safe to destroy window
|
// Now safe to destroy window
|
||||||
(*raw_pointer).destroy()?;
|
(*raw_pointer).destroy()?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ pub enum AnimationStyle {
|
|||||||
EaseInOutBounce,
|
EaseInOutBounce,
|
||||||
#[cfg_attr(feature = "schemars", schemars(title = "CubicBezier"))]
|
#[cfg_attr(feature = "schemars", schemars(title = "CubicBezier"))]
|
||||||
#[value(skip)]
|
#[value(skip)]
|
||||||
/// Custom Cubic Bézier function
|
/// Custom Cubic Bezier function
|
||||||
CubicBezier(f64, f64, f64, f64),
|
CubicBezier(f64, f64, f64, f64),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,37 +15,45 @@ use strum::EnumString;
|
|||||||
|
|
||||||
use crate::KomorebiTheme;
|
use crate::KomorebiTheme;
|
||||||
use crate::animation::prefix::AnimationPrefix;
|
use crate::animation::prefix::AnimationPrefix;
|
||||||
|
use crate::state::State;
|
||||||
|
|
||||||
|
// Re-export everything from komorebi-layouts
|
||||||
|
pub use komorebi_layouts::Arrangement;
|
||||||
|
pub use komorebi_layouts::Axis;
|
||||||
|
pub use komorebi_layouts::Column;
|
||||||
|
pub use komorebi_layouts::ColumnSplit;
|
||||||
|
pub use komorebi_layouts::ColumnSplitWithCapacity;
|
||||||
|
pub use komorebi_layouts::ColumnWidth;
|
||||||
|
pub use komorebi_layouts::CustomLayout;
|
||||||
|
pub use komorebi_layouts::CycleDirection;
|
||||||
|
pub use komorebi_layouts::DEFAULT_RATIO;
|
||||||
|
pub use komorebi_layouts::DEFAULT_SECONDARY_RATIO;
|
||||||
|
pub use komorebi_layouts::DefaultLayout;
|
||||||
|
pub use komorebi_layouts::Direction;
|
||||||
|
pub use komorebi_layouts::GridLayoutOptions;
|
||||||
|
pub use komorebi_layouts::Layout;
|
||||||
|
pub use komorebi_layouts::LayoutDefaultEntry;
|
||||||
|
pub use komorebi_layouts::LayoutOptions;
|
||||||
|
pub use komorebi_layouts::MAX_RATIO;
|
||||||
|
pub use komorebi_layouts::MAX_RATIOS;
|
||||||
|
pub use komorebi_layouts::MIN_RATIO;
|
||||||
|
pub use komorebi_layouts::OperationDirection;
|
||||||
|
pub use komorebi_layouts::Rect;
|
||||||
|
pub use komorebi_layouts::ScrollingLayoutOptions;
|
||||||
|
pub use komorebi_layouts::Sizing;
|
||||||
|
pub use komorebi_layouts::validate_ratios;
|
||||||
|
|
||||||
|
// Local modules and exports
|
||||||
pub use animation::AnimationStyle;
|
pub use animation::AnimationStyle;
|
||||||
pub use arrangement::Arrangement;
|
|
||||||
pub use arrangement::Axis;
|
|
||||||
pub use custom_layout::Column;
|
|
||||||
pub use custom_layout::ColumnSplit;
|
|
||||||
pub use custom_layout::ColumnSplitWithCapacity;
|
|
||||||
pub use custom_layout::ColumnWidth;
|
|
||||||
pub use custom_layout::CustomLayout;
|
|
||||||
pub use cycle_direction::CycleDirection;
|
|
||||||
pub use default_layout::*;
|
|
||||||
pub use direction::Direction;
|
|
||||||
pub use layout::Layout;
|
|
||||||
pub use operation_direction::OperationDirection;
|
|
||||||
pub use pathext::PathExt;
|
pub use pathext::PathExt;
|
||||||
pub use pathext::ResolvedPathBuf;
|
pub use pathext::ResolvedPathBuf;
|
||||||
pub use pathext::replace_env_in_path;
|
pub use pathext::replace_env_in_path;
|
||||||
pub use pathext::resolve_option_hashmap_usize_path;
|
pub use pathext::resolve_option_hashmap_usize_path;
|
||||||
pub use rect::Rect;
|
|
||||||
|
|
||||||
pub mod animation;
|
pub mod animation;
|
||||||
pub mod arrangement;
|
|
||||||
pub mod asc;
|
pub mod asc;
|
||||||
pub mod config_generation;
|
pub mod config_generation;
|
||||||
pub mod custom_layout;
|
|
||||||
pub mod cycle_direction;
|
|
||||||
pub mod default_layout;
|
|
||||||
pub mod direction;
|
|
||||||
pub mod layout;
|
|
||||||
pub mod operation_direction;
|
|
||||||
pub mod pathext;
|
pub mod pathext;
|
||||||
pub mod rect;
|
|
||||||
|
|
||||||
// serde_as must be before derive
|
// serde_as must be before derive
|
||||||
#[serde_with::serde_as]
|
#[serde_with::serde_as]
|
||||||
@@ -113,6 +121,7 @@ pub enum SocketMessage {
|
|||||||
AdjustWorkspacePadding(Sizing, i32),
|
AdjustWorkspacePadding(Sizing, i32),
|
||||||
ChangeLayout(DefaultLayout),
|
ChangeLayout(DefaultLayout),
|
||||||
CycleLayout(CycleDirection),
|
CycleLayout(CycleDirection),
|
||||||
|
LayoutRatios(Option<Vec<f32>>, Option<Vec<f32>>),
|
||||||
ScrollingLayoutColumns(NonZeroUsize),
|
ScrollingLayoutColumns(NonZeroUsize),
|
||||||
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||||
FlipLayout(Axis),
|
FlipLayout(Axis),
|
||||||
@@ -249,6 +258,8 @@ pub enum SocketMessage {
|
|||||||
StaticConfigSchema,
|
StaticConfigSchema,
|
||||||
GenerateStaticConfig,
|
GenerateStaticConfig,
|
||||||
DebugWindow(isize),
|
DebugWindow(isize),
|
||||||
|
// low level commands
|
||||||
|
ApplyState(State),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SocketMessage {
|
impl SocketMessage {
|
||||||
@@ -545,32 +556,6 @@ pub enum OperationBehaviour {
|
|||||||
NoOp,
|
NoOp,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
||||||
/// Sizing
|
|
||||||
pub enum Sizing {
|
|
||||||
/// Increase
|
|
||||||
Increase,
|
|
||||||
/// Decrease
|
|
||||||
Decrease,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sizing {
|
|
||||||
#[must_use]
|
|
||||||
pub const fn adjust_by(&self, value: i32, adjustment: i32) -> i32 {
|
|
||||||
match self {
|
|
||||||
Self::Increase => value + adjustment,
|
|
||||||
Self::Decrease => {
|
|
||||||
if value > 0 && value - adjustment >= 0 {
|
|
||||||
value - adjustment
|
|
||||||
} else {
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||||
)]
|
)]
|
||||||
|
|||||||
@@ -238,6 +238,9 @@ lazy_static! {
|
|||||||
static ref FLOATING_WINDOW_TOGGLE_ASPECT_RATIO: Arc<Mutex<AspectRatio>> = Arc::new(Mutex::new(AspectRatio::Predefined(PredefinedAspectRatio::Widescreen)));
|
static ref FLOATING_WINDOW_TOGGLE_ASPECT_RATIO: Arc<Mutex<AspectRatio>> = Arc::new(Mutex::new(AspectRatio::Predefined(PredefinedAspectRatio::Widescreen)));
|
||||||
|
|
||||||
static ref CURRENT_VIRTUAL_DESKTOP: Arc<Mutex<Option<Vec<u8>>>> = Arc::new(Mutex::new(None));
|
static ref CURRENT_VIRTUAL_DESKTOP: Arc<Mutex<Option<Vec<u8>>>> = Arc::new(Mutex::new(None));
|
||||||
|
|
||||||
|
pub static ref LAYOUT_DEFAULTS: Arc<Mutex<HashMap<DefaultLayout, LayoutDefaultEntry>>> =
|
||||||
|
Arc::new(Mutex::new(HashMap::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
|
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||||
@@ -322,7 +325,7 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
|||||||
// the latter case, if the user desires this validation after initiating the task view, komorebi
|
// the latter case, if the user desires this validation after initiating the task view, komorebi
|
||||||
// should be restarted, and then when this // fn runs again for the first time, it will pick up
|
// should be restarted, and then when this // fn runs again for the first time, it will pick up
|
||||||
// the value of CurrentVirtualDesktop and validate against it accordingly
|
// the value of CurrentVirtualDesktop and validate against it accordingly
|
||||||
current
|
current.map(|current| current.to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -307,12 +307,10 @@ impl Monitor {
|
|||||||
DefaultLayout::RightMainVerticalStack => {
|
DefaultLayout::RightMainVerticalStack => {
|
||||||
workspace.add_container_to_front(container);
|
workspace.add_container_to_front(container);
|
||||||
}
|
}
|
||||||
DefaultLayout::UltrawideVerticalStack => {
|
DefaultLayout::UltrawideVerticalStack
|
||||||
if workspace.containers().len() == 1 {
|
if workspace.containers().len() == 1 =>
|
||||||
workspace.insert_container_at_idx(0, container);
|
{
|
||||||
} else {
|
workspace.insert_container_at_idx(0, container);
|
||||||
workspace.add_container_to_back(container);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
workspace.add_container_to_back(container);
|
workspace.add_container_to_back(container);
|
||||||
@@ -332,12 +330,10 @@ impl Monitor {
|
|||||||
|
|
||||||
match layout {
|
match layout {
|
||||||
DefaultLayout::RightMainVerticalStack
|
DefaultLayout::RightMainVerticalStack
|
||||||
| DefaultLayout::UltrawideVerticalStack => {
|
| DefaultLayout::UltrawideVerticalStack
|
||||||
if workspace.containers().len() == 1 {
|
if workspace.containers().len() == 1 =>
|
||||||
workspace.add_container_to_back(container);
|
{
|
||||||
} else {
|
workspace.add_container_to_back(container);
|
||||||
workspace.insert_container_at_idx(target_index, container);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
workspace.insert_container_at_idx(target_index, container);
|
workspace.insert_container_at_idx(target_index, container);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ use std::collections::HashMap;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::sync::atomic::AtomicI64;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
pub mod hidden;
|
pub mod hidden;
|
||||||
@@ -44,6 +45,10 @@ pub enum MonitorNotification {
|
|||||||
|
|
||||||
static ACTIVE: AtomicBool = AtomicBool::new(true);
|
static ACTIVE: AtomicBool = AtomicBool::new(true);
|
||||||
|
|
||||||
|
/// Timestamp (epoch millis) of the last DisplayConnectionChange notification.
|
||||||
|
/// Used to suppress OS-initiated window minimizes during transient display events.
|
||||||
|
static LAST_DISPLAY_CHANGE_TIMESTAMP: AtomicI64 = AtomicI64::new(0);
|
||||||
|
|
||||||
static CHANNEL: OnceLock<(Sender<MonitorNotification>, Receiver<MonitorNotification>)> =
|
static CHANNEL: OnceLock<(Sender<MonitorNotification>, Receiver<MonitorNotification>)> =
|
||||||
OnceLock::new();
|
OnceLock::new();
|
||||||
|
|
||||||
@@ -62,11 +67,40 @@ fn event_rx() -> Receiver<MonitorNotification> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_notification(notification: MonitorNotification) {
|
pub fn send_notification(notification: MonitorNotification) {
|
||||||
|
if matches!(
|
||||||
|
notification,
|
||||||
|
MonitorNotification::DisplayConnectionChange
|
||||||
|
| MonitorNotification::ResumingFromSuspendedState
|
||||||
|
| MonitorNotification::SessionUnlocked
|
||||||
|
) {
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis() as i64;
|
||||||
|
LAST_DISPLAY_CHANGE_TIMESTAMP.store(now, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
if event_tx().try_send(notification).is_err() {
|
if event_tx().try_send(notification).is_err() {
|
||||||
tracing::warn!("channel is full; dropping notification")
|
tracing::warn!("channel is full; dropping notification")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if a display connection change event was received within the
|
||||||
|
/// last `grace_period` duration. This is used by the event processor to avoid
|
||||||
|
/// treating OS-initiated minimizes (caused by transient monitor disconnects)
|
||||||
|
/// as user-initiated minimizes.
|
||||||
|
pub fn display_change_in_progress(grace_period: std::time::Duration) -> bool {
|
||||||
|
let last = LAST_DISPLAY_CHANGE_TIMESTAMP.load(Ordering::SeqCst);
|
||||||
|
if last == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis() as i64;
|
||||||
|
(now - last) < grace_period.as_millis() as i64
|
||||||
|
}
|
||||||
|
|
||||||
pub fn insert_in_monitor_cache(serial_or_device_id: &str, monitor: Monitor) {
|
pub fn insert_in_monitor_cache(serial_or_device_id: &str, monitor: Monitor) {
|
||||||
let dip = DISPLAY_INDEX_PREFERENCES.read();
|
let dip = DISPLAY_INDEX_PREFERENCES.read();
|
||||||
let mut dip_ids = dip.values();
|
let mut dip_ids = dip.values();
|
||||||
@@ -89,7 +123,41 @@ where
|
|||||||
F: Fn() -> I + Copy,
|
F: Fn() -> I + Copy,
|
||||||
I: Iterator<Item = Result<win32_display_data::Device, win32_display_data::Error>>,
|
I: Iterator<Item = Result<win32_display_data::Device, win32_display_data::Error>>,
|
||||||
{
|
{
|
||||||
let all_displays = display_provider().flatten().collect::<Vec<_>>();
|
let mut attempts = 0;
|
||||||
|
|
||||||
|
let (displays, errors) = loop {
|
||||||
|
let (displays, errors): (Vec<_>, Vec<_>) = display_provider().partition(Result::is_ok);
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
break (displays, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
for err in &errors {
|
||||||
|
if let Err(e) = err {
|
||||||
|
tracing::warn!(
|
||||||
|
"enumerating display in reconciliator (attempt {}): {:?}",
|
||||||
|
attempts + 1,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempts < 5 {
|
||||||
|
attempts += 1;
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(150));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break (displays, errors);
|
||||||
|
};
|
||||||
|
|
||||||
|
if !errors.is_empty() {
|
||||||
|
return Err(color_eyre::eyre::eyre!(
|
||||||
|
"could not successfully enumerate all displays"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let all_displays = displays.into_iter().map(Result::unwrap).collect::<Vec<_>>();
|
||||||
|
|
||||||
let mut serial_id_map = HashMap::new();
|
let mut serial_id_map = HashMap::new();
|
||||||
|
|
||||||
@@ -203,6 +271,8 @@ where
|
|||||||
border_manager::send_notification(None);
|
border_manager::send_notification(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep reference to Arc for potential re-locking
|
||||||
|
let wm_arc = Arc::clone(&wm);
|
||||||
let mut wm = wm.lock();
|
let mut wm = wm.lock();
|
||||||
|
|
||||||
let initial_state = State::from(wm.as_ref());
|
let initial_state = State::from(wm.as_ref());
|
||||||
@@ -346,12 +416,180 @@ where
|
|||||||
continue 'receiver;
|
continue 'receiver;
|
||||||
}
|
}
|
||||||
|
|
||||||
if initial_monitor_count > attached_devices.len() {
|
// Handle potential monitor removal with verification
|
||||||
|
let attached_devices = if initial_monitor_count > attached_devices.len() {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"monitor count mismatch ({initial_monitor_count} vs {}), removing disconnected monitors",
|
"potential monitor removal detected ({initial_monitor_count} vs {}), verifying in 3s",
|
||||||
attached_devices.len()
|
attached_devices.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Release locks before waiting
|
||||||
|
drop(wm);
|
||||||
|
drop(monitor_cache);
|
||||||
|
|
||||||
|
// Wait 3 seconds for display state to stabilize
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||||
|
|
||||||
|
// Re-query the Win32 display APIs
|
||||||
|
let re_queried_devices = match attached_display_devices(display_provider) {
|
||||||
|
Ok(devices) => devices,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("failed to re-query display devices: {}", e);
|
||||||
|
continue 'receiver;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"after verification: wm had {} monitors, initial query found {}, re-query found {}",
|
||||||
|
initial_monitor_count,
|
||||||
|
attached_devices.len(),
|
||||||
|
re_queried_devices.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// If monitors are back, the removal was transient (spurious event)
|
||||||
|
// Still try to restore state since windows might have been minimized
|
||||||
|
if re_queried_devices.len() >= initial_monitor_count {
|
||||||
|
tracing::info!(
|
||||||
|
"monitor removal was transient (spurious event), attempting state restoration. Initial: {}, Re-queried: {}",
|
||||||
|
initial_monitor_count,
|
||||||
|
re_queried_devices.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Re-acquire locks for state restoration
|
||||||
|
wm = wm_arc.lock();
|
||||||
|
|
||||||
|
// Update Win32 data for all monitors
|
||||||
|
for monitor in wm.monitors_mut() {
|
||||||
|
for attached in &re_queried_devices {
|
||||||
|
let serial_number_ids_match =
|
||||||
|
if let (Some(attached_snid), Some(m_snid)) =
|
||||||
|
(&attached.serial_number_id, &monitor.serial_number_id)
|
||||||
|
{
|
||||||
|
attached_snid.eq(m_snid)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if serial_number_ids_match
|
||||||
|
|| attached.device_id.eq(&monitor.device_id)
|
||||||
|
{
|
||||||
|
monitor.id = attached.id;
|
||||||
|
monitor.device = attached.device.clone();
|
||||||
|
monitor.device_id = attached.device_id.clone();
|
||||||
|
monitor.serial_number_id = attached.serial_number_id.clone();
|
||||||
|
monitor.name = attached.name.clone();
|
||||||
|
monitor.size = attached.size;
|
||||||
|
monitor.work_area_size = attached.work_area_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to restore windows that might have been minimized
|
||||||
|
let offset = wm.work_area_offset;
|
||||||
|
for monitor in wm.monitors_mut() {
|
||||||
|
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||||
|
|
||||||
|
for (idx, workspace) in monitor.workspaces_mut().iter_mut().enumerate()
|
||||||
|
{
|
||||||
|
let is_focused_workspace = idx == focused_workspace_idx;
|
||||||
|
|
||||||
|
if is_focused_workspace {
|
||||||
|
// Restore containers
|
||||||
|
for container in workspace.containers_mut() {
|
||||||
|
if let Some(window) = container.focused_window()
|
||||||
|
&& WindowsApi::is_window(window.hwnd)
|
||||||
|
{
|
||||||
|
tracing::debug!(
|
||||||
|
"restoring window after transient removal: {}",
|
||||||
|
window.hwnd
|
||||||
|
);
|
||||||
|
WindowsApi::restore_window(window.hwnd);
|
||||||
|
} else if let Some(window) = container.focused_window() {
|
||||||
|
tracing::debug!(
|
||||||
|
"skipping restore of invalid window: {}",
|
||||||
|
window.hwnd
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore maximized window
|
||||||
|
if let Some(window) = &workspace.maximized_window
|
||||||
|
&& WindowsApi::is_window(window.hwnd)
|
||||||
|
{
|
||||||
|
WindowsApi::restore_window(window.hwnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore monocle container
|
||||||
|
if let Some(container) = &workspace.monocle_container
|
||||||
|
&& let Some(window) = container.focused_window()
|
||||||
|
&& WindowsApi::is_window(window.hwnd)
|
||||||
|
{
|
||||||
|
WindowsApi::restore_window(window.hwnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore floating windows
|
||||||
|
for window in workspace.floating_windows() {
|
||||||
|
if WindowsApi::is_window(window.hwnd) {
|
||||||
|
WindowsApi::restore_window(window.hwnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monitor.update_focused_workspace(offset)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
border_manager::send_notification(None);
|
||||||
|
continue 'receiver;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If monitors are still missing, proceed with actual removal logic
|
||||||
|
tracing::info!(
|
||||||
|
"verified monitor removal ({initial_monitor_count} vs {}), removing disconnected monitors",
|
||||||
|
re_queried_devices.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Re-acquire locks for removal processing
|
||||||
|
wm = wm_arc.lock();
|
||||||
|
monitor_cache = MONITOR_CACHE
|
||||||
|
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||||
|
.lock();
|
||||||
|
|
||||||
|
// Make sure that in our state any attached displays have the latest Win32 data
|
||||||
|
// We must do this again because we dropped the lock and are working with new data
|
||||||
|
for monitor in wm.monitors_mut() {
|
||||||
|
for attached in &re_queried_devices {
|
||||||
|
let serial_number_ids_match =
|
||||||
|
if let (Some(attached_snid), Some(m_snid)) =
|
||||||
|
(&attached.serial_number_id, &monitor.serial_number_id)
|
||||||
|
{
|
||||||
|
attached_snid.eq(m_snid)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if serial_number_ids_match || attached.device_id.eq(&monitor.device_id)
|
||||||
|
{
|
||||||
|
monitor.id = attached.id;
|
||||||
|
monitor.device = attached.device.clone();
|
||||||
|
monitor.device_id = attached.device_id.clone();
|
||||||
|
monitor.serial_number_id = attached.serial_number_id.clone();
|
||||||
|
monitor.name = attached.name.clone();
|
||||||
|
monitor.size = attached.size;
|
||||||
|
monitor.work_area_size = attached.work_area_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use re-queried devices for remaining logic
|
||||||
|
re_queried_devices
|
||||||
|
} else {
|
||||||
|
attached_devices
|
||||||
|
};
|
||||||
|
|
||||||
|
if initial_monitor_count > attached_devices.len() {
|
||||||
|
tracing::info!("removing disconnected monitors");
|
||||||
|
|
||||||
// Windows to remove from `known_hwnds`
|
// Windows to remove from `known_hwnds`
|
||||||
let mut windows_to_remove = Vec::new();
|
let mut windows_to_remove = Vec::new();
|
||||||
|
|
||||||
@@ -584,7 +822,9 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
if is_focused_workspace {
|
if is_focused_workspace {
|
||||||
if let Some(window) = container.focused_window() {
|
if let Some(window) = container.focused_window()
|
||||||
|
&& WindowsApi::is_window(window.hwnd)
|
||||||
|
{
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"restoring window: {}",
|
"restoring window: {}",
|
||||||
window.hwnd
|
window.hwnd
|
||||||
@@ -596,7 +836,9 @@ where
|
|||||||
// first window and show that one
|
// first window and show that one
|
||||||
container.focus_window(0);
|
container.focus_window(0);
|
||||||
|
|
||||||
if let Some(window) = container.focused_window() {
|
if let Some(window) = container.focused_window()
|
||||||
|
&& WindowsApi::is_window(window.hwnd)
|
||||||
|
{
|
||||||
WindowsApi::restore_window(window.hwnd);
|
WindowsApi::restore_window(window.hwnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -617,7 +859,9 @@ where
|
|||||||
|| known_hwnds.contains_key(&window.hwnd)
|
|| known_hwnds.contains_key(&window.hwnd)
|
||||||
{
|
{
|
||||||
workspace.maximized_window = None;
|
workspace.maximized_window = None;
|
||||||
} else if is_focused_workspace {
|
} else if is_focused_workspace
|
||||||
|
&& WindowsApi::is_window(window.hwnd)
|
||||||
|
{
|
||||||
WindowsApi::restore_window(window.hwnd);
|
WindowsApi::restore_window(window.hwnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -631,7 +875,9 @@ where
|
|||||||
if container.windows().is_empty() {
|
if container.windows().is_empty() {
|
||||||
workspace.monocle_container = None;
|
workspace.monocle_container = None;
|
||||||
} else if is_focused_workspace {
|
} else if is_focused_workspace {
|
||||||
if let Some(window) = container.focused_window() {
|
if let Some(window) = container.focused_window()
|
||||||
|
&& WindowsApi::is_window(window.hwnd)
|
||||||
|
{
|
||||||
WindowsApi::restore_window(window.hwnd);
|
WindowsApi::restore_window(window.hwnd);
|
||||||
} else {
|
} else {
|
||||||
// If the focused window was moved or removed by
|
// If the focused window was moved or removed by
|
||||||
@@ -639,7 +885,9 @@ where
|
|||||||
// first window and show that one
|
// first window and show that one
|
||||||
container.focus_window(0);
|
container.focus_window(0);
|
||||||
|
|
||||||
if let Some(window) = container.focused_window() {
|
if let Some(window) = container.focused_window()
|
||||||
|
&& WindowsApi::is_window(window.hwnd)
|
||||||
|
{
|
||||||
WindowsApi::restore_window(window.hwnd);
|
WindowsApi::restore_window(window.hwnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -653,7 +901,9 @@ where
|
|||||||
|
|
||||||
if is_focused_workspace {
|
if is_focused_workspace {
|
||||||
for window in workspace.floating_windows() {
|
for window in workspace.floating_windows() {
|
||||||
WindowsApi::restore_window(window.hwnd);
|
if WindowsApi::is_window(window.hwnd) {
|
||||||
|
WindowsApi::restore_window(window.hwnd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,9 +60,11 @@ use crate::core::Axis;
|
|||||||
use crate::core::BorderImplementation;
|
use crate::core::BorderImplementation;
|
||||||
use crate::core::FocusFollowsMouseImplementation;
|
use crate::core::FocusFollowsMouseImplementation;
|
||||||
use crate::core::Layout;
|
use crate::core::Layout;
|
||||||
|
use crate::core::LayoutOptions;
|
||||||
use crate::core::MoveBehaviour;
|
use crate::core::MoveBehaviour;
|
||||||
use crate::core::OperationDirection;
|
use crate::core::OperationDirection;
|
||||||
use crate::core::Rect;
|
use crate::core::Rect;
|
||||||
|
use crate::core::ScrollingLayoutOptions;
|
||||||
use crate::core::Sizing;
|
use crate::core::Sizing;
|
||||||
use crate::core::SocketMessage;
|
use crate::core::SocketMessage;
|
||||||
use crate::core::StateQuery;
|
use crate::core::StateQuery;
|
||||||
@@ -72,8 +74,6 @@ use crate::core::config_generation::IdWithIdentifier;
|
|||||||
use crate::core::config_generation::MatchingRule;
|
use crate::core::config_generation::MatchingRule;
|
||||||
use crate::core::config_generation::MatchingStrategy;
|
use crate::core::config_generation::MatchingStrategy;
|
||||||
use crate::current_virtual_desktop;
|
use crate::current_virtual_desktop;
|
||||||
use crate::default_layout::LayoutOptions;
|
|
||||||
use crate::default_layout::ScrollingLayoutOptions;
|
|
||||||
use crate::monitor::MonitorInformation;
|
use crate::monitor::MonitorInformation;
|
||||||
use crate::notify_subscribers;
|
use crate::notify_subscribers;
|
||||||
use crate::stackbar_manager;
|
use crate::stackbar_manager;
|
||||||
@@ -947,6 +947,8 @@ impl WindowManager {
|
|||||||
center_focused_column: Default::default(),
|
center_focused_column: Default::default(),
|
||||||
}),
|
}),
|
||||||
grid: None,
|
grid: None,
|
||||||
|
column_ratios: None,
|
||||||
|
row_ratios: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -955,6 +957,29 @@ impl WindowManager {
|
|||||||
}
|
}
|
||||||
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
|
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
|
||||||
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
|
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
|
||||||
|
SocketMessage::LayoutRatios(ref columns, ref rows) => {
|
||||||
|
use crate::core::validate_ratios;
|
||||||
|
|
||||||
|
let focused_workspace = self.focused_workspace_mut()?;
|
||||||
|
|
||||||
|
let mut options = focused_workspace.layout_options.unwrap_or(LayoutOptions {
|
||||||
|
scrolling: None,
|
||||||
|
grid: None,
|
||||||
|
column_ratios: None,
|
||||||
|
row_ratios: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(cols) = columns {
|
||||||
|
options.column_ratios = Some(validate_ratios(cols));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(rws) = rows {
|
||||||
|
options.row_ratios = Some(validate_ratios(rws));
|
||||||
|
}
|
||||||
|
|
||||||
|
focused_workspace.layout_options = Some(options);
|
||||||
|
self.update_focused_workspace(false, false)?;
|
||||||
|
}
|
||||||
SocketMessage::ChangeLayoutCustom(ref path) => {
|
SocketMessage::ChangeLayoutCustom(ref path) => {
|
||||||
self.change_workspace_custom_layout(path)?;
|
self.change_workspace_custom_layout(path)?;
|
||||||
}
|
}
|
||||||
@@ -2271,6 +2296,9 @@ if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue))
|
|||||||
SocketMessage::Theme(ref theme) => {
|
SocketMessage::Theme(ref theme) => {
|
||||||
theme_manager::send_notification(*theme.clone());
|
theme_manager::send_notification(*theme.clone());
|
||||||
}
|
}
|
||||||
|
SocketMessage::ApplyState(ref state) => {
|
||||||
|
self.apply_state(state.clone());
|
||||||
|
}
|
||||||
// Deprecated commands
|
// Deprecated commands
|
||||||
SocketMessage::AltFocusHack(_)
|
SocketMessage::AltFocusHack(_)
|
||||||
| SocketMessage::IdentifyBorderOverflowApplication(_, _) => {}
|
| SocketMessage::IdentifyBorderOverflowApplication(_, _) => {}
|
||||||
|
|||||||
@@ -266,18 +266,33 @@ impl WindowManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowManagerEvent::Minimize(_, window) => {
|
WindowManagerEvent::Minimize(_, window) => {
|
||||||
let mut hide = false;
|
// During transient display connection changes (e.g. monitor
|
||||||
|
// briefly disconnecting and reconnecting), Windows may fire
|
||||||
|
// SystemMinimizeStart for windows on the affected monitor.
|
||||||
|
// We must not treat these OS-initiated minimizes as user
|
||||||
|
// actions, otherwise the window gets removed from the
|
||||||
|
// workspace and the reconciliator cannot restore it.
|
||||||
|
if crate::monitor_reconciliator::display_change_in_progress(
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
) {
|
||||||
|
tracing::debug!(
|
||||||
|
"ignoring minimize during display connection change for hwnd: {}",
|
||||||
|
window.hwnd
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let mut hide = false;
|
||||||
|
|
||||||
{
|
{
|
||||||
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
||||||
if !programmatically_hidden_hwnds.contains(&window.hwnd) {
|
if !programmatically_hidden_hwnds.contains(&window.hwnd) {
|
||||||
hide = true;
|
hide = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if hide {
|
if hide {
|
||||||
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
|
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
|
||||||
self.update_focused_workspace(false, false)?;
|
self.update_focused_workspace(false, false)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowManagerEvent::Hide(_, window) => {
|
WindowManagerEvent::Hide(_, window) => {
|
||||||
@@ -431,6 +446,24 @@ impl WindowManager {
|
|||||||
proceed = false;
|
proceed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// after enforce_workspace_rules() has run, check if window exists in ANY workspace
|
||||||
|
// to prevent duplication when workspace rules move windows across workspaces
|
||||||
|
if proceed {
|
||||||
|
let window_already_managed = self
|
||||||
|
.monitors()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|m| m.workspaces())
|
||||||
|
.any(|ws| ws.contains_window(window.hwnd));
|
||||||
|
|
||||||
|
if window_already_managed {
|
||||||
|
tracing::debug!(
|
||||||
|
"skipping window addition, already managed after workspace rule enforcement"
|
||||||
|
);
|
||||||
|
|
||||||
|
proceed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if proceed {
|
if proceed {
|
||||||
let behaviour = self.window_management_behaviour(
|
let behaviour = self.window_management_behaviour(
|
||||||
focused_monitor_idx,
|
focused_monitor_idx,
|
||||||
|
|||||||
@@ -28,12 +28,10 @@ pub fn listen_for_movements(wm: Arc<Mutex<WindowManager>>) {
|
|||||||
Action::Press => ignore_movement = true,
|
Action::Press => ignore_movement = true,
|
||||||
Action::Release => ignore_movement = false,
|
Action::Release => ignore_movement = false,
|
||||||
},
|
},
|
||||||
Event::MouseMoveRelative { .. } => {
|
Event::MouseMoveRelative { .. } if !ignore_movement => {
|
||||||
if !ignore_movement {
|
match wm.lock().raise_window_at_cursor_pos() {
|
||||||
match wm.lock().raise_window_at_cursor_pos() {
|
Ok(()) => {}
|
||||||
Ok(()) => {}
|
Err(error) => tracing::error!("{}", error),
|
||||||
Err(error) => tracing::error!("{}", error),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@@ -253,6 +253,9 @@ impl From<&WindowManager> for State {
|
|||||||
layout: workspace.layout.clone(),
|
layout: workspace.layout.clone(),
|
||||||
layout_options: workspace.layout_options,
|
layout_options: workspace.layout_options,
|
||||||
layout_rules: workspace.layout_rules.clone(),
|
layout_rules: workspace.layout_rules.clone(),
|
||||||
|
layout_options_rules: workspace.layout_options_rules.clone(),
|
||||||
|
layout_defaults_cache: workspace.layout_defaults_cache.clone(),
|
||||||
|
work_area_offset_rules: workspace.work_area_offset_rules.clone(),
|
||||||
layout_flip: workspace.layout_flip,
|
layout_flip: workspace.layout_flip,
|
||||||
workspace_padding: workspace.workspace_padding,
|
workspace_padding: workspace.workspace_padding,
|
||||||
container_padding: workspace.container_padding,
|
container_padding: workspace.container_padding,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use crate::FloatingLayerBehaviour;
|
|||||||
use crate::HIDING_BEHAVIOUR;
|
use crate::HIDING_BEHAVIOUR;
|
||||||
use crate::IGNORE_IDENTIFIERS;
|
use crate::IGNORE_IDENTIFIERS;
|
||||||
use crate::LAYERED_WHITELIST;
|
use crate::LAYERED_WHITELIST;
|
||||||
|
use crate::LAYOUT_DEFAULTS;
|
||||||
use crate::MANAGE_IDENTIFIERS;
|
use crate::MANAGE_IDENTIFIERS;
|
||||||
use crate::MONITOR_INDEX_PREFERENCES;
|
use crate::MONITOR_INDEX_PREFERENCES;
|
||||||
use crate::NO_TITLEBAR;
|
use crate::NO_TITLEBAR;
|
||||||
@@ -53,6 +54,8 @@ use crate::core::DefaultLayout;
|
|||||||
use crate::core::FocusFollowsMouseImplementation;
|
use crate::core::FocusFollowsMouseImplementation;
|
||||||
use crate::core::HidingBehaviour;
|
use crate::core::HidingBehaviour;
|
||||||
use crate::core::Layout;
|
use crate::core::Layout;
|
||||||
|
use crate::core::LayoutDefaultEntry;
|
||||||
|
use crate::core::LayoutOptions;
|
||||||
use crate::core::MoveBehaviour;
|
use crate::core::MoveBehaviour;
|
||||||
use crate::core::OperationBehaviour;
|
use crate::core::OperationBehaviour;
|
||||||
use crate::core::Rect;
|
use crate::core::Rect;
|
||||||
@@ -67,7 +70,6 @@ use crate::core::config_generation::ApplicationOptions;
|
|||||||
use crate::core::config_generation::MatchingRule;
|
use crate::core::config_generation::MatchingRule;
|
||||||
use crate::core::config_generation::MatchingStrategy;
|
use crate::core::config_generation::MatchingStrategy;
|
||||||
use crate::current_virtual_desktop;
|
use crate::current_virtual_desktop;
|
||||||
use crate::default_layout::LayoutOptions;
|
|
||||||
use crate::monitor;
|
use crate::monitor;
|
||||||
use crate::monitor::Monitor;
|
use crate::monitor::Monitor;
|
||||||
use crate::monitor_reconciliator;
|
use crate::monitor_reconciliator;
|
||||||
@@ -215,6 +217,12 @@ pub struct WorkspaceConfig {
|
|||||||
/// Layout-specific options
|
/// Layout-specific options
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub layout_options: Option<LayoutOptions>,
|
pub layout_options: Option<LayoutOptions>,
|
||||||
|
/// Threshold-based layout options rules in the format of threshold => options.
|
||||||
|
/// When container count >= threshold, the highest matching threshold's options
|
||||||
|
/// fully replace the base `layout_options`.
|
||||||
|
/// This follows the same threshold logic as `layout_rules`.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub layout_options_rules: Option<HashMap<usize, LayoutOptions>>,
|
||||||
/// END OF LIFE FEATURE: Custom Layout
|
/// END OF LIFE FEATURE: Custom Layout
|
||||||
#[deprecated(note = "End of life feature")]
|
#[deprecated(note = "End of life feature")]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -223,6 +231,9 @@ pub struct WorkspaceConfig {
|
|||||||
/// Layout rules in the format of threshold => layout
|
/// Layout rules in the format of threshold => layout
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub layout_rules: Option<HashMap<usize, DefaultLayout>>,
|
pub layout_rules: Option<HashMap<usize, DefaultLayout>>,
|
||||||
|
/// Work area offset rules in the format of threshold => Rect (default: None)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub work_area_offset_rules: Option<HashMap<usize, Rect>>,
|
||||||
/// END OF LIFE FEATURE: Custom layout rules
|
/// END OF LIFE FEATURE: Custom layout rules
|
||||||
#[deprecated(note = "End of life feature")]
|
#[deprecated(note = "End of life feature")]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -287,6 +298,13 @@ impl From<&Workspace> for WorkspaceConfig {
|
|||||||
}
|
}
|
||||||
let layout_rules = (!layout_rules.is_empty()).then_some(layout_rules);
|
let layout_rules = (!layout_rules.is_empty()).then_some(layout_rules);
|
||||||
|
|
||||||
|
let mut work_area_offset_rules = HashMap::new();
|
||||||
|
for (threshold, offset) in &value.work_area_offset_rules {
|
||||||
|
work_area_offset_rules.insert(*threshold, *offset);
|
||||||
|
}
|
||||||
|
let work_area_offset_rules =
|
||||||
|
(!work_area_offset_rules.is_empty()).then_some(work_area_offset_rules);
|
||||||
|
|
||||||
let mut window_container_behaviour_rules = HashMap::new();
|
let mut window_container_behaviour_rules = HashMap::new();
|
||||||
for (threshold, behaviour) in value.window_container_behaviour_rules.iter().flatten() {
|
for (threshold, behaviour) in value.window_container_behaviour_rules.iter().flatten() {
|
||||||
window_container_behaviour_rules.insert(*threshold, *behaviour);
|
window_container_behaviour_rules.insert(*threshold, *behaviour);
|
||||||
@@ -325,7 +343,18 @@ impl From<&Workspace> for WorkspaceConfig {
|
|||||||
Layout::Custom(_) => None,
|
Layout::Custom(_) => None,
|
||||||
})
|
})
|
||||||
.flatten(),
|
.flatten(),
|
||||||
layout_options: value.layout_options,
|
layout_options: {
|
||||||
|
tracing::debug!(
|
||||||
|
"Parsing workspace config - layout_options: {:?}",
|
||||||
|
value.layout_options
|
||||||
|
);
|
||||||
|
value.layout_options
|
||||||
|
},
|
||||||
|
layout_options_rules: if value.layout_options_rules.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(value.layout_options_rules.iter().copied().collect())
|
||||||
|
},
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
custom_layout: value
|
custom_layout: value
|
||||||
.workspace_config
|
.workspace_config
|
||||||
@@ -347,6 +376,7 @@ impl From<&Workspace> for WorkspaceConfig {
|
|||||||
.workspace_config
|
.workspace_config
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|c| c.workspace_rules.clone()),
|
.and_then(|c| c.workspace_rules.clone()),
|
||||||
|
work_area_offset_rules,
|
||||||
work_area_offset: value.work_area_offset,
|
work_area_offset: value.work_area_offset,
|
||||||
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset),
|
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset),
|
||||||
window_container_behaviour: value.window_container_behaviour,
|
window_container_behaviour: value.window_container_behaviour,
|
||||||
@@ -445,7 +475,7 @@ pub enum AppSpecificConfigurationPath {
|
|||||||
#[serde_with::serde_as]
|
#[serde_with::serde_as]
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
/// The `komorebi.json` static configuration file reference for `v0.1.40`
|
/// The `komorebi.json` static configuration file reference for `v0.1.41`
|
||||||
pub struct StaticConfig {
|
pub struct StaticConfig {
|
||||||
/// DEPRECATED from v0.1.22: no longer required
|
/// DEPRECATED from v0.1.22: no longer required
|
||||||
#[deprecated(note = "No longer required")]
|
#[deprecated(note = "No longer required")]
|
||||||
@@ -565,6 +595,11 @@ pub struct StaticConfig {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = DEFAULT_CONTAINER_PADDING)))]
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = DEFAULT_CONTAINER_PADDING)))]
|
||||||
pub default_container_padding: Option<i32>,
|
pub default_container_padding: Option<i32>,
|
||||||
|
/// Per-layout default options and rules, keyed by layout name.
|
||||||
|
/// Applied as fallback when a workspace does not define its own layout_options or layout_options_rules.
|
||||||
|
/// If a workspace defines either setting, all global defaults for that layout are completely replaced.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub layout_defaults: Option<HashMap<DefaultLayout, LayoutDefaultEntry>>,
|
||||||
/// Monitor and workspace configurations
|
/// Monitor and workspace configurations
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub monitors: Option<Vec<MonitorConfig>>,
|
pub monitors: Option<Vec<MonitorConfig>>,
|
||||||
@@ -874,6 +909,14 @@ impl From<&WindowManager> for StaticConfig {
|
|||||||
default_container_padding: Option::from(
|
default_container_padding: Option::from(
|
||||||
DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst),
|
DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst),
|
||||||
),
|
),
|
||||||
|
layout_defaults: {
|
||||||
|
let guard = LAYOUT_DEFAULTS.lock();
|
||||||
|
if guard.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(guard.clone())
|
||||||
|
}
|
||||||
|
},
|
||||||
monitors: Option::from(monitors),
|
monitors: Option::from(monitors),
|
||||||
window_hiding_behaviour: Option::from(*HIDING_BEHAVIOUR.lock()),
|
window_hiding_behaviour: Option::from(*HIDING_BEHAVIOUR.lock()),
|
||||||
global_work_area_offset: value.work_area_offset,
|
global_work_area_offset: value.work_area_offset,
|
||||||
@@ -989,6 +1032,12 @@ impl StaticConfig {
|
|||||||
DEFAULT_WORKSPACE_PADDING.store(workspace, Ordering::SeqCst);
|
DEFAULT_WORKSPACE_PADDING.store(workspace, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(defaults) = &self.layout_defaults {
|
||||||
|
*LAYOUT_DEFAULTS.lock() = defaults.clone();
|
||||||
|
} else {
|
||||||
|
LAYOUT_DEFAULTS.lock().clear();
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(border_width) = self.border_width {
|
if let Some(border_width) = self.border_width {
|
||||||
border_manager::BORDER_WIDTH.store(border_width, Ordering::SeqCst);
|
border_manager::BORDER_WIDTH.store(border_width, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
@@ -1397,7 +1446,7 @@ impl StaticConfig {
|
|||||||
workspace_config.layout = Some(DefaultLayout::Columns);
|
workspace_config.layout = Some(DefaultLayout::Columns);
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.load_static_config(workspace_config)?;
|
ws.load_static_config(workspace_config, value.layout_defaults.as_ref())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1480,7 +1529,10 @@ impl StaticConfig {
|
|||||||
|
|
||||||
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
||||||
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
|
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
|
||||||
ws.load_static_config(workspace_config)?;
|
ws.load_static_config(
|
||||||
|
workspace_config,
|
||||||
|
value.layout_defaults.as_ref(),
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1562,7 +1614,7 @@ impl StaticConfig {
|
|||||||
|
|
||||||
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
|
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
|
||||||
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
|
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
|
||||||
ws.load_static_config(workspace_config)?;
|
ws.load_static_config(workspace_config, value.layout_defaults.as_ref())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1645,7 +1697,10 @@ impl StaticConfig {
|
|||||||
|
|
||||||
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
||||||
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
|
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
|
||||||
ws.load_static_config(workspace_config)?;
|
ws.load_static_config(
|
||||||
|
workspace_config,
|
||||||
|
value.layout_defaults.as_ref(),
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1913,7 +1968,7 @@ mod tests {
|
|||||||
let docs = vec![
|
let docs = vec![
|
||||||
"0.1.20", "0.1.21", "0.1.22", "0.1.23", "0.1.24", "0.1.25", "0.1.26", "0.1.27",
|
"0.1.20", "0.1.21", "0.1.22", "0.1.23", "0.1.24", "0.1.25", "0.1.26", "0.1.27",
|
||||||
"0.1.28", "0.1.29", "0.1.30", "0.1.31", "0.1.32", "0.1.33", "0.1.34", "0.1.35",
|
"0.1.28", "0.1.29", "0.1.30", "0.1.31", "0.1.32", "0.1.33", "0.1.34", "0.1.35",
|
||||||
"0.1.36", "0.1.37", "0.1.38",
|
"0.1.36", "0.1.37", "0.1.38", "0.1.39",
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut versions = vec![];
|
let mut versions = vec![];
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ use crate::animation::AnimationEngine;
|
|||||||
use crate::core::Arrangement;
|
use crate::core::Arrangement;
|
||||||
use crate::core::Axis;
|
use crate::core::Axis;
|
||||||
use crate::core::BorderImplementation;
|
use crate::core::BorderImplementation;
|
||||||
|
use crate::core::CustomLayout;
|
||||||
use crate::core::CycleDirection;
|
use crate::core::CycleDirection;
|
||||||
use crate::core::DefaultLayout;
|
use crate::core::DefaultLayout;
|
||||||
use crate::core::FocusFollowsMouseImplementation;
|
use crate::core::FocusFollowsMouseImplementation;
|
||||||
@@ -40,7 +41,6 @@ use crate::core::Sizing;
|
|||||||
use crate::core::WindowContainerBehaviour;
|
use crate::core::WindowContainerBehaviour;
|
||||||
use crate::core::WindowManagementBehaviour;
|
use crate::core::WindowManagementBehaviour;
|
||||||
use crate::core::config_generation::MatchingRule;
|
use crate::core::config_generation::MatchingRule;
|
||||||
use crate::core::custom_layout::CustomLayout;
|
|
||||||
|
|
||||||
use crate::CrossBoundaryBehaviour;
|
use crate::CrossBoundaryBehaviour;
|
||||||
use crate::DATA_DIR;
|
use crate::DATA_DIR;
|
||||||
@@ -239,21 +239,30 @@ impl WindowManager {
|
|||||||
let mouse_follows_focus = self.mouse_follows_focus;
|
let mouse_follows_focus = self.mouse_follows_focus;
|
||||||
for (monitor_idx, monitor) in self.monitors_mut().iter_mut().enumerate() {
|
for (monitor_idx, monitor) in self.monitors_mut().iter_mut().enumerate() {
|
||||||
let mut focused_workspace = 0;
|
let mut focused_workspace = 0;
|
||||||
for (workspace_idx, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
|
if let Some(state_monitor) = state.monitors.elements().get(monitor_idx) {
|
||||||
if let Some(state_monitor) = state.monitors.elements().get(monitor_idx)
|
monitor
|
||||||
&& let Some(state_workspace) = state_monitor.workspaces().get(workspace_idx)
|
.workspaces_mut()
|
||||||
|
.resize(state_monitor.workspaces().len(), Workspace::default());
|
||||||
|
|
||||||
|
for (workspace_idx, workspace) in
|
||||||
|
monitor.workspaces_mut().iter_mut().enumerate()
|
||||||
{
|
{
|
||||||
// to make sure padding changes get applied for users after a quick restart
|
if let Some(state_workspace) = state_monitor.workspaces().get(workspace_idx)
|
||||||
let container_padding = workspace.container_padding;
|
{
|
||||||
let workspace_padding = workspace.workspace_padding;
|
// to make sure padding and layout_options changes get applied for users after a quick restart
|
||||||
|
let container_padding = workspace.container_padding;
|
||||||
|
let workspace_padding = workspace.workspace_padding;
|
||||||
|
let layout_options = workspace.layout_options;
|
||||||
|
|
||||||
*workspace = state_workspace.clone();
|
*workspace = state_workspace.clone();
|
||||||
|
|
||||||
workspace.container_padding = container_padding;
|
workspace.container_padding = container_padding;
|
||||||
workspace.workspace_padding = workspace_padding;
|
workspace.workspace_padding = workspace_padding;
|
||||||
|
workspace.layout_options = layout_options;
|
||||||
|
|
||||||
if state_monitor.focused_workspace_idx() == workspace_idx {
|
if state_monitor.focused_workspace_idx() == workspace_idx {
|
||||||
focused_workspace = workspace_idx;
|
focused_workspace = workspace_idx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2101,12 +2110,19 @@ impl WindowManager {
|
|||||||
|
|
||||||
tracing::info!("focusing container");
|
tracing::info!("focusing container");
|
||||||
|
|
||||||
let new_idx =
|
if workspace.monocle_container.is_some() {
|
||||||
if workspace.maximized_window.is_some() || workspace.monocle_container.is_some() {
|
let cycle_direction = match direction {
|
||||||
None
|
OperationDirection::Left | OperationDirection::Down => CycleDirection::Previous,
|
||||||
} else {
|
OperationDirection::Right | OperationDirection::Up => CycleDirection::Next,
|
||||||
workspace.new_idx_for_direction(direction)
|
|
||||||
};
|
};
|
||||||
|
return self.cycle_monocle(cycle_direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_idx = if workspace.maximized_window.is_some() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
workspace.new_idx_for_direction(direction)
|
||||||
|
};
|
||||||
|
|
||||||
let mut cross_monitor_monocle_or_max = false;
|
let mut cross_monitor_monocle_or_max = false;
|
||||||
|
|
||||||
@@ -3091,6 +3107,27 @@ impl WindowManager {
|
|||||||
workspace.reintegrate_monocle_container()
|
workspace.reintegrate_monocle_container()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub fn cycle_monocle(&mut self, direction: CycleDirection) -> eyre::Result<()> {
|
||||||
|
tracing::info!("cycling monocle container");
|
||||||
|
|
||||||
|
if self.focused_workspace()?.containers().is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.focused_workspace_mut()?
|
||||||
|
.cycle_monocle_container(direction)?;
|
||||||
|
|
||||||
|
for container in self.focused_workspace_mut()?.containers_mut() {
|
||||||
|
container.hide(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// borders were getting funny during cycles, can't be bothered to root cause it
|
||||||
|
border_manager::destroy_all_borders()?;
|
||||||
|
|
||||||
|
self.update_focused_workspace(true, true)
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub fn toggle_maximize(&mut self) -> eyre::Result<()> {
|
pub fn toggle_maximize(&mut self) -> eyre::Result<()> {
|
||||||
self.handle_unmanaged_window_behaviour()?;
|
self.handle_unmanaged_window_behaviour()?;
|
||||||
@@ -3339,7 +3376,7 @@ impl WindowManager {
|
|||||||
let rules: &mut Vec<(usize, Layout)> = &mut workspace.layout_rules;
|
let rules: &mut Vec<(usize, Layout)> = &mut workspace.layout_rules;
|
||||||
rules.retain(|pair| pair.0 != at_container_count);
|
rules.retain(|pair| pair.0 != at_container_count);
|
||||||
rules.push((at_container_count, Layout::Default(layout)));
|
rules.push((at_container_count, Layout::Default(layout)));
|
||||||
rules.sort_by(|a, b| a.0.cmp(&b.0));
|
rules.sort_by_key(|a| a.0);
|
||||||
|
|
||||||
// If this is the focused workspace on a non-focused screen, let's update it
|
// If this is the focused workspace on a non-focused screen, let's update it
|
||||||
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
|
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
|
||||||
@@ -3382,7 +3419,7 @@ impl WindowManager {
|
|||||||
let rules: &mut Vec<(usize, Layout)> = &mut workspace.layout_rules;
|
let rules: &mut Vec<(usize, Layout)> = &mut workspace.layout_rules;
|
||||||
rules.retain(|pair| pair.0 != at_container_count);
|
rules.retain(|pair| pair.0 != at_container_count);
|
||||||
rules.push((at_container_count, Layout::Custom(layout)));
|
rules.push((at_container_count, Layout::Custom(layout)));
|
||||||
rules.sort_by(|a, b| a.0.cmp(&b.0));
|
rules.sort_by_key(|a| a.0);
|
||||||
|
|
||||||
// If this is the focused workspace on a non-focused screen, let's update it
|
// If this is the focused workspace on a non-focused screen, let's update it
|
||||||
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
|
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
@@ -25,9 +26,10 @@ use crate::core::CustomLayout;
|
|||||||
use crate::core::CycleDirection;
|
use crate::core::CycleDirection;
|
||||||
use crate::core::DefaultLayout;
|
use crate::core::DefaultLayout;
|
||||||
use crate::core::Layout;
|
use crate::core::Layout;
|
||||||
|
use crate::core::LayoutDefaultEntry;
|
||||||
|
use crate::core::LayoutOptions;
|
||||||
use crate::core::OperationDirection;
|
use crate::core::OperationDirection;
|
||||||
use crate::core::Rect;
|
use crate::core::Rect;
|
||||||
use crate::default_layout::LayoutOptions;
|
|
||||||
use crate::lockable_sequence::LockableSequence;
|
use crate::lockable_sequence::LockableSequence;
|
||||||
use crate::ring::Ring;
|
use crate::ring::Ring;
|
||||||
use crate::should_act;
|
use crate::should_act;
|
||||||
@@ -61,6 +63,15 @@ pub struct Workspace {
|
|||||||
pub layout: Layout,
|
pub layout: Layout,
|
||||||
pub layout_options: Option<LayoutOptions>,
|
pub layout_options: Option<LayoutOptions>,
|
||||||
pub layout_rules: Vec<(usize, Layout)>,
|
pub layout_rules: Vec<(usize, Layout)>,
|
||||||
|
/// Threshold-based layout options rules (container_count >= threshold -> use these options).
|
||||||
|
/// Sorted by threshold ascending at load time.
|
||||||
|
#[serde(default)]
|
||||||
|
pub layout_options_rules: Vec<(usize, LayoutOptions)>,
|
||||||
|
/// Cached per-layout defaults from the global `layout_defaults` config setting.
|
||||||
|
/// Pre-sorted at config load time; used as fallback when workspace has no overrides.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub(crate) layout_defaults_cache: HashMap<DefaultLayout, CachedLayoutDefault>,
|
||||||
|
pub work_area_offset_rules: Vec<(usize, Rect)>,
|
||||||
pub layout_flip: Option<Axis>,
|
pub layout_flip: Option<Axis>,
|
||||||
pub workspace_padding: Option<i32>,
|
pub workspace_padding: Option<i32>,
|
||||||
pub container_padding: Option<i32>,
|
pub container_padding: Option<i32>,
|
||||||
@@ -118,6 +129,9 @@ impl Default for Workspace {
|
|||||||
layout: Layout::Default(DefaultLayout::BSP),
|
layout: Layout::Default(DefaultLayout::BSP),
|
||||||
layout_options: None,
|
layout_options: None,
|
||||||
layout_rules: vec![],
|
layout_rules: vec![],
|
||||||
|
layout_options_rules: vec![],
|
||||||
|
layout_defaults_cache: HashMap::new(),
|
||||||
|
work_area_offset_rules: vec![],
|
||||||
layout_flip: None,
|
layout_flip: None,
|
||||||
workspace_padding: Option::from(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)),
|
workspace_padding: Option::from(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)),
|
||||||
container_padding: Option::from(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)),
|
container_padding: Option::from(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)),
|
||||||
@@ -163,8 +177,49 @@ pub struct WorkspaceGlobals {
|
|||||||
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
|
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
/// Cached per-layout default options (pre-sorted rules) derived from the global `layout_defaults`.
|
||||||
|
pub(crate) struct CachedLayoutDefault {
|
||||||
|
pub layout_options: Option<LayoutOptions>,
|
||||||
|
/// Threshold-based rules, sorted by threshold ascending at load time
|
||||||
|
pub layout_options_rules: Vec<(usize, LayoutOptions)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert an optional HashMap of threshold-based layout options rules into a Vec sorted by
|
||||||
|
/// threshold ascending.
|
||||||
|
fn sorted_layout_options_rules(
|
||||||
|
rules: Option<&HashMap<usize, LayoutOptions>>,
|
||||||
|
) -> Vec<(usize, LayoutOptions)> {
|
||||||
|
match rules {
|
||||||
|
Some(rules) => {
|
||||||
|
let mut sorted: Vec<(usize, LayoutOptions)> =
|
||||||
|
rules.iter().map(|(t, o)| (*t, *o)).collect();
|
||||||
|
sorted.sort_by_key(|(t, _)| *t);
|
||||||
|
sorted
|
||||||
|
}
|
||||||
|
None => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the highest matching threshold rule for the given container count.
|
||||||
|
/// Rules must be sorted by threshold ascending.
|
||||||
|
fn resolve_threshold_match(
|
||||||
|
rules: &[(usize, LayoutOptions)],
|
||||||
|
container_count: usize,
|
||||||
|
) -> Option<LayoutOptions> {
|
||||||
|
rules
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|(threshold, _)| container_count >= *threshold)
|
||||||
|
.map(|(_, opts)| *opts)
|
||||||
|
}
|
||||||
|
|
||||||
impl Workspace {
|
impl Workspace {
|
||||||
pub fn load_static_config(&mut self, config: &WorkspaceConfig) -> eyre::Result<()> {
|
pub fn load_static_config(
|
||||||
|
&mut self,
|
||||||
|
config: &WorkspaceConfig,
|
||||||
|
layout_defaults: Option<&HashMap<DefaultLayout, LayoutDefaultEntry>>,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
self.name = Option::from(config.name.clone());
|
self.name = Option::from(config.name.clone());
|
||||||
|
|
||||||
self.container_padding = config.container_padding;
|
self.container_padding = config.container_padding;
|
||||||
@@ -213,6 +268,15 @@ impl Workspace {
|
|||||||
self.layout_rules = all_layout_rules;
|
self.layout_rules = all_layout_rules;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut all_work_area_offset_rules = vec![];
|
||||||
|
if let Some(work_area_offset_rules) = &config.work_area_offset_rules {
|
||||||
|
for (count, rect) in work_area_offset_rules {
|
||||||
|
all_work_area_offset_rules.push((*count, *rect));
|
||||||
|
}
|
||||||
|
all_work_area_offset_rules.sort_by_key(|(i, _)| *i);
|
||||||
|
self.work_area_offset_rules = all_work_area_offset_rules;
|
||||||
|
}
|
||||||
|
|
||||||
self.work_area_offset = config.work_area_offset;
|
self.work_area_offset = config.work_area_offset;
|
||||||
|
|
||||||
self.apply_window_based_work_area_offset =
|
self.apply_window_based_work_area_offset =
|
||||||
@@ -240,13 +304,78 @@ impl Workspace {
|
|||||||
self.layout_flip = config.layout_flip;
|
self.layout_flip = config.layout_flip;
|
||||||
self.floating_layer_behaviour = config.floating_layer_behaviour;
|
self.floating_layer_behaviour = config.floating_layer_behaviour;
|
||||||
self.wallpaper = config.wallpaper.clone();
|
self.wallpaper = config.wallpaper.clone();
|
||||||
|
|
||||||
|
// Load layout options directly (LayoutOptions is used in both config and runtime)
|
||||||
self.layout_options = config.layout_options;
|
self.layout_options = config.layout_options;
|
||||||
|
|
||||||
|
// Load threshold-based layout options rules, sorted by threshold ascending
|
||||||
|
self.layout_options_rules =
|
||||||
|
sorted_layout_options_rules(config.layout_options_rules.as_ref());
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"Workspace '{}' loaded layout_options: {:?}, layout_options_rules: {} entries",
|
||||||
|
self.name.as_deref().unwrap_or("unnamed"),
|
||||||
|
self.layout_options,
|
||||||
|
self.layout_options_rules.len(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cache per-layout defaults from global layout_defaults, pre-sorting rules
|
||||||
|
self.layout_defaults_cache = if let Some(defaults) = layout_defaults {
|
||||||
|
defaults
|
||||||
|
.iter()
|
||||||
|
.map(|(layout, entry)| {
|
||||||
|
(
|
||||||
|
*layout,
|
||||||
|
CachedLayoutDefault {
|
||||||
|
layout_options: entry.layout_options,
|
||||||
|
layout_options_rules: sorted_layout_options_rules(
|
||||||
|
entry.layout_options_rules.as_ref(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
HashMap::new()
|
||||||
|
};
|
||||||
|
|
||||||
self.workspace_config = Some(config.clone());
|
self.workspace_config = Some(config.clone());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compute effective layout options using the complete-replacement cascade:
|
||||||
|
///
|
||||||
|
/// If the workspace defines EITHER `layout_options` OR `layout_options_rules`,
|
||||||
|
/// it completely replaces the global `layout_defaults` for this layout.
|
||||||
|
/// Global defaults are only used when the workspace has NEITHER setting.
|
||||||
|
///
|
||||||
|
/// Within the effective source (workspace or global):
|
||||||
|
/// 1. Try threshold match from rules (highest matching threshold wins)
|
||||||
|
/// 2. If a rule matches -> use it (full replacement of base)
|
||||||
|
/// 3. Else -> use the base `layout_options`
|
||||||
|
fn effective_layout_options(&self) -> Option<LayoutOptions> {
|
||||||
|
let container_count = self.containers().len();
|
||||||
|
|
||||||
|
let has_workspace_overrides =
|
||||||
|
self.layout_options.is_some() || !self.layout_options_rules.is_empty();
|
||||||
|
|
||||||
|
let (effective_base, effective_rules): (Option<LayoutOptions>, &[(usize, LayoutOptions)]) =
|
||||||
|
if has_workspace_overrides {
|
||||||
|
(self.layout_options, &self.layout_options_rules)
|
||||||
|
} else {
|
||||||
|
match &self.layout {
|
||||||
|
Layout::Default(dl) => match self.layout_defaults_cache.get(dl) {
|
||||||
|
Some(entry) => (entry.layout_options, &entry.layout_options_rules),
|
||||||
|
None => (None, &[]),
|
||||||
|
},
|
||||||
|
Layout::Custom(_) => (None, &[]),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
resolve_threshold_match(effective_rules, container_count).or(effective_base)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn hide(&mut self, omit: Option<isize>) {
|
pub fn hide(&mut self, omit: Option<isize>) {
|
||||||
for window in self.floating_windows_mut().iter_mut().rev() {
|
for window in self.floating_windows_mut().iter_mut().rev() {
|
||||||
let mut should_hide = omit.is_none();
|
let mut should_hide = omit.is_none();
|
||||||
@@ -473,9 +602,27 @@ impl Workspace {
|
|||||||
let border_width = self.globals.border_width;
|
let border_width = self.globals.border_width;
|
||||||
let border_offset = self.globals.border_offset;
|
let border_offset = self.globals.border_offset;
|
||||||
let work_area = self.globals.work_area;
|
let work_area = self.globals.work_area;
|
||||||
let work_area_offset = self.work_area_offset.or(self.globals.work_area_offset);
|
|
||||||
let window_based_work_area_offset = self.globals.window_based_work_area_offset;
|
let window_based_work_area_offset = self.globals.window_based_work_area_offset;
|
||||||
let window_based_work_area_offset_limit = self.globals.window_based_work_area_offset_limit;
|
let window_based_work_area_offset_limit = self.globals.window_based_work_area_offset_limit;
|
||||||
|
let mut rules_work_area_offset = None;
|
||||||
|
|
||||||
|
if !self.work_area_offset_rules.is_empty() {
|
||||||
|
let count = if self.monocle_container.is_some() {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
self.containers().len()
|
||||||
|
};
|
||||||
|
|
||||||
|
for (threshold, work_area_offset_rule) in &self.work_area_offset_rules {
|
||||||
|
if count >= *threshold {
|
||||||
|
rules_work_area_offset = Some(*work_area_offset_rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let work_area_offset = rules_work_area_offset
|
||||||
|
.or(self.work_area_offset)
|
||||||
|
.or(self.globals.work_area_offset);
|
||||||
|
|
||||||
let mut adjusted_work_area = work_area_offset.map_or_else(
|
let mut adjusted_work_area = work_area_offset.map_or_else(
|
||||||
|| work_area,
|
|| work_area,
|
||||||
@@ -489,7 +636,6 @@ impl Workspace {
|
|||||||
with_offset
|
with_offset
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (self.containers().len() <= window_based_work_area_offset_limit as usize
|
if (self.containers().len() <= window_based_work_area_offset_limit as usize
|
||||||
|| self.monocle_container.is_some() && window_based_work_area_offset_limit > 0)
|
|| self.monocle_container.is_some() && window_based_work_area_offset_limit > 0)
|
||||||
&& self.apply_window_based_work_area_offset
|
&& self.apply_window_based_work_area_offset
|
||||||
@@ -550,6 +696,15 @@ impl Workspace {
|
|||||||
} else if let Some(window) = &mut self.maximized_window {
|
} else if let Some(window) = &mut self.maximized_window {
|
||||||
window.maximize();
|
window.maximize();
|
||||||
} else if !self.containers().is_empty() {
|
} else if !self.containers().is_empty() {
|
||||||
|
let effective_layout_options = self.effective_layout_options();
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"Workspace '{}' update() - effective_layout_options: {:?} (base: {:?}, rules: {})",
|
||||||
|
self.name.as_deref().unwrap_or("unnamed"),
|
||||||
|
effective_layout_options,
|
||||||
|
self.layout_options,
|
||||||
|
self.layout_options_rules.len(),
|
||||||
|
);
|
||||||
let mut layouts = self.layout.as_boxed_arrangement().calculate(
|
let mut layouts = self.layout.as_boxed_arrangement().calculate(
|
||||||
&adjusted_work_area,
|
&adjusted_work_area,
|
||||||
NonZeroUsize::new(self.containers().len()).ok_or_eyre(
|
NonZeroUsize::new(self.containers().len()).ok_or_eyre(
|
||||||
@@ -559,7 +714,7 @@ impl Workspace {
|
|||||||
self.layout_flip,
|
self.layout_flip,
|
||||||
&self.resize_dimensions,
|
&self.resize_dimensions,
|
||||||
self.focused_container_idx(),
|
self.focused_container_idx(),
|
||||||
self.layout_options,
|
effective_layout_options,
|
||||||
&self.latest_layout,
|
&self.latest_layout,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1504,6 +1659,23 @@ impl Workspace {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cycle_monocle_container(&mut self, direction: CycleDirection) -> eyre::Result<()> {
|
||||||
|
if self.containers().is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.reintegrate_monocle_container()?;
|
||||||
|
|
||||||
|
let new_idx = self
|
||||||
|
.new_idx_for_cycle_direction(direction)
|
||||||
|
.ok_or_eyre("there is no container to cycle monocle to")?;
|
||||||
|
|
||||||
|
self.focus_container(new_idx);
|
||||||
|
self.new_monocle_container()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new_maximized_window(&mut self) -> eyre::Result<()> {
|
pub fn new_maximized_window(&mut self) -> eyre::Result<()> {
|
||||||
let focused_idx = self.focused_container_idx();
|
let focused_idx = self.focused_container_idx();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebic-no-console"
|
name = "komorebic-no-console"
|
||||||
version = "0.1.40"
|
version = "0.1.41"
|
||||||
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
|
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
|
||||||
repository = "https://github.com/LGUG2Z/komorebi"
|
repository = "https://github.com/LGUG2Z/komorebi"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebic"
|
name = "komorebic"
|
||||||
version = "0.1.40"
|
version = "0.1.41"
|
||||||
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
|
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
|
||||||
repository = "https://github.com/LGUG2Z/komorebi"
|
repository = "https://github.com/LGUG2Z/komorebi"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|||||||
@@ -1001,6 +1001,16 @@ struct ScrollingLayoutColumns {
|
|||||||
count: NonZeroUsize,
|
count: NonZeroUsize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct LayoutRatios {
|
||||||
|
/// Column width ratios (space-separated values between 0.1 and 0.9)
|
||||||
|
#[clap(short, long, num_args = 1..)]
|
||||||
|
columns: Option<Vec<f32>>,
|
||||||
|
/// Row height ratios (space-separated values between 0.1 and 0.9)
|
||||||
|
#[clap(short, long, num_args = 1..)]
|
||||||
|
rows: Option<Vec<f32>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
struct License {
|
struct License {
|
||||||
/// Email address associated with an Individual Commercial Use License
|
/// Email address associated with an Individual Commercial Use License
|
||||||
@@ -1267,6 +1277,8 @@ enum SubCommand {
|
|||||||
/// Set the number of visible columns for the Scrolling layout on the focused workspace
|
/// Set the number of visible columns for the Scrolling layout on the focused workspace
|
||||||
#[clap(arg_required_else_help = true)]
|
#[clap(arg_required_else_help = true)]
|
||||||
ScrollingLayoutColumns(ScrollingLayoutColumns),
|
ScrollingLayoutColumns(ScrollingLayoutColumns),
|
||||||
|
/// Set the layout column and row ratios for the focused workspace
|
||||||
|
LayoutRatios(LayoutRatios),
|
||||||
/// Load a custom layout from file for the focused workspace
|
/// Load a custom layout from file for the focused workspace
|
||||||
#[clap(hide = true)]
|
#[clap(hide = true)]
|
||||||
#[clap(arg_required_else_help = true)]
|
#[clap(arg_required_else_help = true)]
|
||||||
@@ -1912,13 +1924,11 @@ fn main() -> eyre::Result<()> {
|
|||||||
"Application specific configuration file path has not been set. Try running 'komorebic fetch-asc'\n"
|
"Application specific configuration file path has not been set. Try running 'komorebic fetch-asc'\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Some(AppSpecificConfigurationPath::Single(path)) => {
|
Some(AppSpecificConfigurationPath::Single(path)) if !path.exists() => {
|
||||||
if !path.exists() {
|
println!(
|
||||||
println!(
|
"Application specific configuration file path '{}' does not exist. Try running 'komorebic fetch-asc'\n",
|
||||||
"Application specific configuration file path '{}' does not exist. Try running 'komorebic fetch-asc'\n",
|
path.display()
|
||||||
path.display()
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -2934,6 +2944,15 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
|
|||||||
SubCommand::ScrollingLayoutColumns(args) => {
|
SubCommand::ScrollingLayoutColumns(args) => {
|
||||||
send_message(&SocketMessage::ScrollingLayoutColumns(args.count))?;
|
send_message(&SocketMessage::ScrollingLayoutColumns(args.count))?;
|
||||||
}
|
}
|
||||||
|
SubCommand::LayoutRatios(args) => {
|
||||||
|
if args.columns.is_none() && args.rows.is_none() {
|
||||||
|
println!(
|
||||||
|
"No ratios provided, nothing to change. Use --columns or --rows to specify ratios."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
send_message(&SocketMessage::LayoutRatios(args.columns, args.rows))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
SubCommand::LoadCustomLayout(args) => {
|
SubCommand::LoadCustomLayout(args) => {
|
||||||
send_message(&SocketMessage::ChangeLayoutCustom(args.path))?;
|
send_message(&SocketMessage::ChangeLayoutCustom(args.path))?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ nav:
|
|||||||
- common-workflows/mouse-follows-focus.md
|
- common-workflows/mouse-follows-focus.md
|
||||||
- common-workflows/dynamic-layout-switching.md
|
- common-workflows/dynamic-layout-switching.md
|
||||||
- common-workflows/multiple-bar-instances.md
|
- common-workflows/multiple-bar-instances.md
|
||||||
|
- common-workflows/bar.md
|
||||||
|
- common-workflows/bar-widgets/systray.md
|
||||||
- common-workflows/multi-monitor-setup.md
|
- common-workflows/multi-monitor-setup.md
|
||||||
- CLI reference:
|
- CLI reference:
|
||||||
- cli/quickstart.md
|
- cli/quickstart.md
|
||||||
|
|||||||
1762
schema.bar.json
1762
schema.bar.json
File diff suppressed because it is too large
Load Diff
104
schema.json
104
schema.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"title": "StaticConfig",
|
"title": "StaticConfig",
|
||||||
"description": "The `komorebi.json` static configuration file reference for `v0.1.40`",
|
"description": "The `komorebi.json` static configuration file reference for `v0.1.41`",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"animation": {
|
"animation": {
|
||||||
@@ -304,6 +304,16 @@
|
|||||||
"$ref": "#/$defs/MatchingRule"
|
"$ref": "#/$defs/MatchingRule"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"layout_defaults": {
|
||||||
|
"description": "Per-layout default options and rules, keyed by layout name.\nApplied as fallback when a workspace does not define its own layout_options or layout_options_rules.\nIf a workspace defines either setting, all global defaults for that layout are completely replaced.",
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/LayoutDefaultEntry"
|
||||||
|
}
|
||||||
|
},
|
||||||
"manage_rules": {
|
"manage_rules": {
|
||||||
"description": "Individual window force-manage rules",
|
"description": "Individual window force-manage rules",
|
||||||
"type": [
|
"type": [
|
||||||
@@ -703,7 +713,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "CubicBezier",
|
"title": "CubicBezier",
|
||||||
"description": "Custom Cubic Bézier function",
|
"description": "Custom Cubic Bezier function",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"CubicBezier": {
|
"CubicBezier": {
|
||||||
@@ -3290,10 +3300,57 @@
|
|||||||
"colours"
|
"colours"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"LayoutDefaultEntry": {
|
||||||
|
"description": "Per-layout default options entry for the `layout_defaults` global setting.\nContains both base layout options and threshold-based layout options rules.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"layout_options": {
|
||||||
|
"description": "Default layout options for this layout",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/LayoutOptions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"layout_options_rules": {
|
||||||
|
"description": "Threshold-based layout options rules in the format of threshold => options.\nWhen container count >= threshold, the highest matching threshold's options\nfully replace the base `layout_options`.",
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {
|
||||||
|
"^\\d+$": {
|
||||||
|
"$ref": "#/$defs/LayoutOptions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"LayoutOptions": {
|
"LayoutOptions": {
|
||||||
"description": "Options for specific layouts",
|
"description": "Options for specific layouts",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"column_ratios": {
|
||||||
|
"description": "Column width ratios (up to MAX_RATIOS values between 0.1 and 0.9)\n\n- Used by Columns layout: ratios for each column width\n- Used by Grid layout: ratios for column widths\n- Used by BSP, VerticalStack, RightMainVerticalStack: column_ratios[0] as primary split ratio\n- Used by HorizontalStack: column_ratios[0] as primary split ratio (top area height)\n- Used by UltrawideVerticalStack: column_ratios[0] as center ratio, column_ratios[1] as left ratio\n\nColumns without a ratio share remaining space equally.\nExample: `[0.3, 0.4, 0.3]` for 30%-40%-30% columns",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"items": {
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "float"
|
||||||
|
},
|
||||||
|
"maxItems": 5,
|
||||||
|
"minItems": 5
|
||||||
|
},
|
||||||
"grid": {
|
"grid": {
|
||||||
"description": "Options related to the Grid layout",
|
"description": "Options related to the Grid layout",
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
@@ -3305,6 +3362,23 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"row_ratios": {
|
||||||
|
"description": "Row height ratios (up to MAX_RATIOS values between 0.1 and 0.9)\n\n- Used by Rows layout: ratios for each row height\n- Used by Grid layout: ratios for row heights\n\nRows without a ratio share remaining space equally.\nExample: `[0.5, 0.5]` for 50%-50% rows",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"items": {
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "float"
|
||||||
|
},
|
||||||
|
"maxItems": 5,
|
||||||
|
"minItems": 5
|
||||||
|
},
|
||||||
"scrolling": {
|
"scrolling": {
|
||||||
"description": "Options related to the Scrolling layout",
|
"description": "Options related to the Scrolling layout",
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
@@ -4180,6 +4254,19 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"layout_options_rules": {
|
||||||
|
"description": "Threshold-based layout options rules in the format of threshold => options.\nWhen container count >= threshold, the highest matching threshold's options\nfully replace the base `layout_options`.\nThis follows the same threshold logic as `layout_rules`.",
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {
|
||||||
|
"^\\d+$": {
|
||||||
|
"$ref": "#/$defs/LayoutOptions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"layout_rules": {
|
"layout_rules": {
|
||||||
"description": "Layout rules in the format of threshold => layout",
|
"description": "Layout rules in the format of threshold => layout",
|
||||||
"type": [
|
"type": [
|
||||||
@@ -4252,6 +4339,19 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"work_area_offset_rules": {
|
||||||
|
"description": "Work area offset rules in the format of threshold => Rect (default: None)",
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {
|
||||||
|
"^\\d+$": {
|
||||||
|
"$ref": "#/$defs/Rect"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"workspace_padding": {
|
"workspace_padding": {
|
||||||
"description": "Workspace padding (default: global)",
|
"description": "Workspace padding (default: global)",
|
||||||
"type": [
|
"type": [
|
||||||
|
|||||||
Reference in New Issue
Block a user