mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-04-25 10:08:33 +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::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(
|
||||
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`
|
||||
}
|
||||
|
||||
#[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))]
|
||||
/// Options for specific layouts
|
||||
pub struct LayoutOptions {
|
||||
@@ -120,6 +194,35 @@ pub struct LayoutOptions {
|
||||
pub scrolling: Option<ScrollingLayoutOptions>,
|
||||
/// Options related to the Grid layout
|
||||
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)]
|
||||
@@ -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(),
|
||||
}),
|
||||
grid: None,
|
||||
column_ratios: None,
|
||||
row_ratios: None,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -325,7 +325,13 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
Layout::Custom(_) => None,
|
||||
})
|
||||
.flatten(),
|
||||
layout_options: value.layout_options,
|
||||
layout_options: {
|
||||
tracing::debug!(
|
||||
"Parsing workspace config - layout_options: {:?}",
|
||||
value.layout_options
|
||||
);
|
||||
value.layout_options
|
||||
},
|
||||
#[allow(deprecated)]
|
||||
custom_layout: value
|
||||
.workspace_config
|
||||
|
||||
@@ -243,14 +243,16 @@ impl WindowManager {
|
||||
if let Some(state_monitor) = state.monitors.elements().get(monitor_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 workspace_padding = workspace.workspace_padding;
|
||||
let layout_options = workspace.layout_options;
|
||||
|
||||
*workspace = state_workspace.clone();
|
||||
|
||||
workspace.container_padding = container_padding;
|
||||
workspace.workspace_padding = workspace_padding;
|
||||
workspace.layout_options = layout_options;
|
||||
|
||||
if state_monitor.focused_workspace_idx() == workspace_idx {
|
||||
focused_workspace = workspace_idx;
|
||||
|
||||
@@ -242,6 +242,12 @@ impl Workspace {
|
||||
self.wallpaper = config.wallpaper.clone();
|
||||
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());
|
||||
|
||||
Ok(())
|
||||
@@ -550,6 +556,11 @@ impl Workspace {
|
||||
} else if let Some(window) = &mut self.maximized_window {
|
||||
window.maximize();
|
||||
} 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(
|
||||
&adjusted_work_area,
|
||||
NonZeroUsize::new(self.containers().len()).ok_or_eyre(
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"null"
|
||||
],
|
||||
"format": "float",
|
||||
"default": 50.0
|
||||
"default": 50
|
||||
},
|
||||
"icon_scale": {
|
||||
"description": "Scale of the icons relative to the font_size [[1.0-2.0]]",
|
||||
@@ -68,7 +68,7 @@
|
||||
"null"
|
||||
],
|
||||
"format": "float",
|
||||
"default": 1.399999976158142
|
||||
"default": 1.4
|
||||
},
|
||||
"left_widgets": {
|
||||
"description": "Left side widgets (ordered left-to-right)",
|
||||
@@ -99,7 +99,15 @@
|
||||
},
|
||||
"monitor": {
|
||||
"description": "The monitor index or the full monitor options",
|
||||
"$ref": "#/$defs/MonitorConfigOrIndex"
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/MonitorConfigOrIndex"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": 0
|
||||
},
|
||||
"mouse": {
|
||||
"description": "Options for mouse interaction on the bar",
|
||||
@@ -174,7 +182,6 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"monitor",
|
||||
"left_widgets",
|
||||
"right_widgets"
|
||||
],
|
||||
|
||||
34
schema.json
34
schema.json
@@ -3294,6 +3294,23 @@
|
||||
"description": "Options for specific layouts",
|
||||
"type": "object",
|
||||
"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": {
|
||||
"description": "Options related to the Grid layout",
|
||||
"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": {
|
||||
"description": "Options related to the Scrolling layout",
|
||||
"anyOf": [
|
||||
|
||||
Reference in New Issue
Block a user