Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/common/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ pub struct AppWorkspaceRule {
/// non-empty string and will be compared against the accessibility subrole
/// reported by the AX APIs for a window (exact string match).
pub ax_subrole: Option<String>,
/// When true and floating is false, the layout will size the window's tile/column
/// to the window's current natural size rather than stretching it to fill.
#[serde(default)]
pub wrap_size: bool,
}

impl Default for VirtualWorkspaceSettings {
Expand Down Expand Up @@ -1536,4 +1540,26 @@ mod tests {
let (s, _maybe_dep) = suggestion.unwrap();
assert_eq!(s, "toggle_stack");
}

#[test]
fn parse_wrap_size_in_app_rules() {
let toml = r#"
[settings]
animate = false

[virtual_workspaces]
app_rules = [
{ app_id = "com.example.wrap", wrap_size = true },
{ app_id = "com.example.nofloat", floating = false, wrap_size = true },
]

[keys]
"#;

let cfg = Config::parse(toml).unwrap();
assert_eq!(cfg.virtual_workspaces.app_rules.len(), 2);
assert!(cfg.virtual_workspaces.app_rules[0].wrap_size);
assert!(cfg.virtual_workspaces.app_rules[1].wrap_size);
assert!(!cfg.virtual_workspaces.app_rules[1].floating);
}
}
143 changes: 129 additions & 14 deletions src/layout_engine/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1166,20 +1166,6 @@ impl LayoutEngine {
max_size,
) in windows_with_titles
{
self.window_layout_constraints.insert(
wid,
WindowLayoutConstraints {
is_resizable,
locked_width: size_hint.width,
locked_height: size_hint.height,
min_width: min_size.map_or(0.0, |s| s.width),
min_height: min_size.map_or(0.0, |s| s.height),
max_width: max_size.map_or(0.0, |s| s.width),
max_height: max_size.map_or(0.0, |s| s.height),
}
.normalized(),
);

let title_ref = title_opt.as_deref();
let ax_role_ref = ax_role_opt.as_deref();
let ax_subrole_ref = ax_subrole_opt.as_deref();
Expand All @@ -1204,6 +1190,7 @@ impl LayoutEngine {
workspace_id: ws,
floating: was_floating,
prev_rule_decision: false,
wrap_size: false,
}),
Err(_) => {
warn!(
Expand All @@ -1220,11 +1207,29 @@ impl LayoutEngine {
workspace_id: assigned_workspace,
floating: rule_says_float,
prev_rule_decision,
wrap_size: rule_wrap_size,
} = match assignment {
Some(assign) => assign,
None => continue,
};

let has_valid_size = size_hint.width > 0.0 && size_hint.height > 0.0;
let effective_wrap = rule_wrap_size && has_valid_size;
self.window_layout_constraints.insert(
wid,
WindowLayoutConstraints {
is_resizable: if effective_wrap { false } else { is_resizable },
locked_width: size_hint.width,
locked_height: size_hint.height,
min_width: min_size.map_or(0.0, |s| s.width),
min_height: min_size.map_or(0.0, |s| s.height),
max_width: max_size.map_or(0.0, |s| s.width),
max_height: max_size.map_or(0.0, |s| s.height),
wrap_size: effective_wrap,
}
.normalized(),
);

let should_float = rule_says_float || (!prev_rule_decision && was_floating);

if should_float {
Expand Down Expand Up @@ -1300,6 +1305,13 @@ impl LayoutEngine {
new_frame,
screens,
} => {
// Update wrap_size constraints so calculate_layout sees the new size immediately.
if let Some(c) = self.window_layout_constraints.get_mut(&wid) {
if c.wrap_size {
c.locked_width = new_frame.size.width;
c.locked_height = new_frame.size.height;
}
}
for (space, screen_frame, display_uuid) in screens {
let Some((ws_id, layout)) = self.workspace_and_layout(space) else {
debug!(
Expand Down Expand Up @@ -3163,4 +3175,107 @@ mod tests {
before
);
}

#[test]
fn windows_on_screen_updated_sets_wrap_size_constraints() {
let mut engine = test_engine();
let space = SpaceId::new(42);
let screen = CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(1000.0, 800.0));
let pid = 1;
let wid = WindowId::new(pid, 1);

// Configure app rule with wrap_size
let mut settings = VirtualWorkspaceSettings::default();
settings.app_rules = vec![crate::common::config::AppWorkspaceRule {
app_id: Some("com.example.wrap".into()),
workspace: None,
floating: false,
manage: true,
app_name: None,
title_regex: None,
title_substring: None,
ax_role: None,
ax_subrole: None,
wrap_size: true,
}];
engine.update_virtual_workspace_settings(&settings);

let app_info = crate::actor::app::AppInfo {
bundle_id: Some("com.example.wrap".into()),
localized_name: None,
};

let _ = engine.handle_event(LayoutEvent::SpaceExposed(space, screen.size));
let _ = engine.handle_event(LayoutEvent::WindowsOnScreenUpdated(
space,
pid,
vec![(
wid,
None,
None,
None,
true,
CGSize::new(400.0, 600.0),
None,
None,
)],
Some(app_info),
));

let c = engine
.window_layout_constraints
.get(&wid)
.copied()
.expect("constraint missing");
assert!(c.wrap_size);
assert!(!c.is_resizable);
assert_eq!(c.locked_width, 400.0);
assert_eq!(c.locked_height, 600.0);
}

#[test]
fn window_resized_updates_wrap_size_locked_dimensions() {
let mut engine = test_engine();
let space = SpaceId::new(42);
let screen = CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(1000.0, 800.0));
let pid = 1;
let wid = WindowId::new(pid, 1);

let _ = engine.handle_event(LayoutEvent::SpaceExposed(space, screen.size));
let _ = engine.handle_event(LayoutEvent::WindowsOnScreenUpdated(
space,
pid,
vec![(
wid,
None,
None,
None,
true,
CGSize::new(400.0, 600.0),
None,
None,
)],
None,
));

// Manually mark as wrap_size to simulate what the app rule would have done
if let Some(c) = engine.window_layout_constraints.get_mut(&wid) {
c.wrap_size = true;
}

let _ = engine.handle_event(LayoutEvent::WindowResized {
wid,
old_frame: CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(400.0, 600.0)),
new_frame: CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(500.0, 700.0)),
screens: vec![(space, screen, None)],
});

let c = engine
.window_layout_constraints
.get(&wid)
.copied()
.expect("constraint missing");
assert_eq!(c.locked_width, 500.0);
assert_eq!(c.locked_height, 700.0);
}
}
14 changes: 10 additions & 4 deletions src/layout_engine/systems.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub struct WindowLayoutConstraints {
pub min_height: f64,
pub max_width: f64,
pub max_height: f64,
pub wrap_size: bool,
}

impl WindowLayoutConstraints {
Expand All @@ -40,6 +41,7 @@ impl WindowLayoutConstraints {
min_height,
max_width,
max_height,
wrap_size: self.wrap_size,
}
}

Expand Down Expand Up @@ -192,7 +194,8 @@ mod tests {
min_height: 470.0,
max_width: 723.0,
max_height: 0.0,
}
wrap_size: false,
}
.normalized();

assert_eq!(c.fixed_for_axis(true), Some(723.0));
Expand All @@ -212,7 +215,8 @@ mod tests {
min_height: 0.0,
max_width: 0.0,
max_height: 0.0,
}
wrap_size: false,
}
.normalized();

assert_eq!(c.fixed_for_axis(true), None);
Expand All @@ -231,7 +235,8 @@ mod tests {
min_height: 0.0,
max_width: 0.0,
max_height: 0.0,
}
wrap_size: false,
}
.normalized();

assert_eq!(c.fixed_for_axis(true), Some(640.0));
Expand All @@ -251,7 +256,8 @@ mod tests {
min_height: 0.0,
max_width: 600.0,
max_height: 480.0,
}
wrap_size: false,
}
.normalized();

assert_eq!(c.fixed_for_axis(true), None);
Expand Down
2 changes: 2 additions & 0 deletions src/layout_engine/systems/bsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,7 @@ mod tests {
min_height: 0.0,
max_width: 600.0,
max_height: 0.0,
wrap_size: false,
}
.normalized(),
);
Expand Down Expand Up @@ -882,6 +883,7 @@ mod tests {
min_height: 0.0,
max_width: 0.0,
max_height: 200.0,
wrap_size: false,
}
.normalized(),
);
Expand Down
Loading