mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-05-19 18:26:56 +02:00
feat(wm): add threshold-based layout_options_rules for workspaces
Adds layout_options_rules to workspace configuration, allowing layout_options to dynamically change based on container count. Uses the same threshold semantics as layout_rules: when container count >= threshold, the highest matching rule fully replaces the base layout_options.
This commit is contained in:
@@ -172,6 +172,71 @@ consistently to all splits of that type throughout the layout. Additional values
|
|||||||
- Unspecified ratios default to sharing the remaining space equally
|
- 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
|
- 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.
|
||||||
|
|
||||||
## Progressive Ratio Behavior
|
## Progressive Ratio Behavior
|
||||||
|
|
||||||
Ratios are applied progressively as windows are added. For example, with `row_ratios: [0.3, 0.5]` in a VerticalStack:
|
Ratios are applied progressively as windows are added. For example, with `row_ratios: [0.3, 0.5]` in a VerticalStack:
|
||||||
|
|||||||
@@ -420,370 +420,5 @@ impl DefaultLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
#[path = "default_layout_tests.rs"]
|
||||||
use super::*;
|
mod tests;
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,435 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -253,6 +253,7 @@ 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(),
|
||||||
work_area_offset_rules: workspace.work_area_offset_rules.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,
|
||||||
|
|||||||
@@ -215,6 +215,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")]
|
||||||
@@ -342,6 +348,11 @@ impl From<&Workspace> for WorkspaceConfig {
|
|||||||
);
|
);
|
||||||
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
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ 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)>,
|
||||||
pub work_area_offset_rules: Vec<(usize, Rect)>,
|
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>,
|
||||||
@@ -119,6 +123,7 @@ 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![],
|
||||||
work_area_offset_rules: vec![],
|
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)),
|
||||||
@@ -251,12 +256,25 @@ 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 = if let Some(rules) = &config.layout_options_rules {
|
||||||
|
let mut sorted_rules: Vec<(usize, LayoutOptions)> =
|
||||||
|
rules.iter().map(|(t, o)| (*t, *o)).collect();
|
||||||
|
sorted_rules.sort_by_key(|(t, _)| *t);
|
||||||
|
sorted_rules
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Workspace '{}' loaded layout_options: {:?}",
|
"Workspace '{}' loaded layout_options: {:?}, layout_options_rules: {} entries",
|
||||||
self.name.as_deref().unwrap_or("unnamed"),
|
self.name.as_deref().unwrap_or("unnamed"),
|
||||||
self.layout_options
|
self.layout_options,
|
||||||
|
self.layout_options_rules.len(),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.workspace_config = Some(config.clone());
|
self.workspace_config = Some(config.clone());
|
||||||
@@ -584,10 +602,34 @@ 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!(
|
// Compute effective layout options:
|
||||||
"Workspace '{}' update() - self.layout_options before calculate: {:?}",
|
// 1. If layout_options_rules has a matching threshold (container_count >= threshold),
|
||||||
self.name.as_deref().unwrap_or("unnamed"),
|
// use the highest matching threshold's options (full replacement)
|
||||||
|
// 2. Otherwise, use the workspace base layout_options
|
||||||
|
let effective_layout_options = if !self.layout_options_rules.is_empty() {
|
||||||
|
let container_count = self.containers().len();
|
||||||
|
let mut matched = None;
|
||||||
|
for (threshold, opts) in &self.layout_options_rules {
|
||||||
|
if container_count >= *threshold {
|
||||||
|
matched = Some(*opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If a rule matched, use it entirely (replaces base layout_options)
|
||||||
|
if matched.is_some() {
|
||||||
|
matched
|
||||||
|
} else {
|
||||||
|
self.layout_options
|
||||||
|
}
|
||||||
|
} else {
|
||||||
self.layout_options
|
self.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,
|
||||||
@@ -598,7 +640,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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9683,6 +9683,26 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"layout_options_rules": {
|
||||||
|
"description": "Threshold-based layout options rules (container_count >= threshold -> use these options).\nSorted by threshold ascending at load time.",
|
||||||
|
"type": "array",
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"maxItems": 2,
|
||||||
|
"minItems": 2,
|
||||||
|
"prefixItems": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/LayoutOptions"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"layout_rules": {
|
"layout_rules": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
@@ -9985,6 +10005,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": [
|
||||||
|
|||||||
+13
@@ -4214,6 +4214,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": [
|
||||||
|
|||||||
Reference in New Issue
Block a user