mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-04-27 15:07:02 +02:00
feat(wm): add layout_options with ratios support
Added customizable split ratios for layouts via layout_options configuration. Users can now specify column_ratios and row_ratios arrays to control window sizing in various layouts. Ratios are validated at config load time: values are clamped between 0.1 and 0.9 to prevent zero-sized windows, and arrays are automatically truncated when their cumulative sum would reach or exceed 1.0. This ensures there's always remaining space for additional windows. Ratio support varies by layout: - Columns and Rows layouts use the full arrays for each column/row width or height - VerticalStack, RightMainVerticalStack, and HorizontalStack use the first ratio for the primary split and the remaining ratios for stack windows - BSP uses the first value from each array for horizontal and vertical splits respectively - Grid only supports column_ratios since row counts vary dynamically. - UltrawideVerticalStack uses the first two column ratios for center and left columns. All ratio-related values are now defined as constants in default_layout.rs: MAX_RATIOS (5), MIN_RATIO (0.1), MAX_RATIO (0.9), DEFAULT_RATIO (0.5), and DEFAULT_SECONDARY_RATIO (0.25 for UltrawideVerticalStack).
This commit is contained in:
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 |
200
docs/common-workflows/layout-ratios.md
Normal file
200
docs/common-workflows/layout-ratios.md
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
## 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.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,21 @@ 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;
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Display, EnumString, ValueEnum,
|
Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Display, EnumString, ValueEnum,
|
||||||
)]
|
)]
|
||||||
@@ -112,7 +127,66 @@ 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| {
|
||||||
|
let mut arr = [None; MAX_RATIOS];
|
||||||
|
let mut cumulative_sum = 0.0_f32;
|
||||||
|
|
||||||
|
for (i, &val) in vec.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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 +194,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)]
|
||||||
@@ -308,3 +411,368 @@ impl DefaultLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
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 i in 0..MAX_RATIOS {
|
||||||
|
assert_eq!(ratios[i], 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 i in 0..MAX_RATIOS {
|
||||||
|
assert_eq!(ratios[i], 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() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -325,7 +325,13 @@ 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
|
||||||
|
},
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
custom_layout: value
|
custom_layout: value
|
||||||
.workspace_config
|
.workspace_config
|
||||||
|
|||||||
@@ -243,14 +243,16 @@ impl WindowManager {
|
|||||||
if let Some(state_monitor) = state.monitors.elements().get(monitor_idx)
|
if let Some(state_monitor) = state.monitors.elements().get(monitor_idx)
|
||||||
&& let Some(state_workspace) = state_monitor.workspaces().get(workspace_idx)
|
&& let Some(state_workspace) = state_monitor.workspaces().get(workspace_idx)
|
||||||
{
|
{
|
||||||
// to make sure padding changes get applied for users after a quick restart
|
// to make sure padding and layout_options changes get applied for users after a quick restart
|
||||||
let container_padding = workspace.container_padding;
|
let container_padding = workspace.container_padding;
|
||||||
let workspace_padding = workspace.workspace_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;
|
||||||
|
|||||||
@@ -242,6 +242,12 @@ impl Workspace {
|
|||||||
self.wallpaper = config.wallpaper.clone();
|
self.wallpaper = config.wallpaper.clone();
|
||||||
self.layout_options = config.layout_options;
|
self.layout_options = config.layout_options;
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"Workspace '{}' loaded layout_options: {:?}",
|
||||||
|
self.name.as_deref().unwrap_or("unnamed"),
|
||||||
|
self.layout_options
|
||||||
|
);
|
||||||
|
|
||||||
self.workspace_config = Some(config.clone());
|
self.workspace_config = Some(config.clone());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -550,6 +556,11 @@ 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() {
|
||||||
|
tracing::debug!(
|
||||||
|
"Workspace '{}' update() - self.layout_options before calculate: {:?}",
|
||||||
|
self.name.as_deref().unwrap_or("unnamed"),
|
||||||
|
self.layout_options
|
||||||
|
);
|
||||||
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(
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
"null"
|
"null"
|
||||||
],
|
],
|
||||||
"format": "float",
|
"format": "float",
|
||||||
"default": 50.0
|
"default": 50
|
||||||
},
|
},
|
||||||
"icon_scale": {
|
"icon_scale": {
|
||||||
"description": "Scale of the icons relative to the font_size [[1.0-2.0]]",
|
"description": "Scale of the icons relative to the font_size [[1.0-2.0]]",
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
"null"
|
"null"
|
||||||
],
|
],
|
||||||
"format": "float",
|
"format": "float",
|
||||||
"default": 1.399999976158142
|
"default": 1.4
|
||||||
},
|
},
|
||||||
"left_widgets": {
|
"left_widgets": {
|
||||||
"description": "Left side widgets (ordered left-to-right)",
|
"description": "Left side widgets (ordered left-to-right)",
|
||||||
@@ -99,7 +99,15 @@
|
|||||||
},
|
},
|
||||||
"monitor": {
|
"monitor": {
|
||||||
"description": "The monitor index or the full monitor options",
|
"description": "The monitor index or the full monitor options",
|
||||||
"$ref": "#/$defs/MonitorConfigOrIndex"
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/MonitorConfigOrIndex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": 0
|
||||||
},
|
},
|
||||||
"mouse": {
|
"mouse": {
|
||||||
"description": "Options for mouse interaction on the bar",
|
"description": "Options for mouse interaction on the bar",
|
||||||
@@ -174,7 +182,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"monitor",
|
|
||||||
"left_widgets",
|
"left_widgets",
|
||||||
"right_widgets"
|
"right_widgets"
|
||||||
],
|
],
|
||||||
|
|||||||
34
schema.json
34
schema.json
@@ -3294,6 +3294,23 @@
|
|||||||
"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 +3322,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": [
|
||||||
|
|||||||
Reference in New Issue
Block a user