Commit 075d167f7479
tarball5 files changed+187-21
@@ -1340,6 +1340,35 @@ mod tests {
13401340
}13411341
13421342
#[test]1343
+ fn a_title_set_after_mapping_brings_its_rules_in() {1344
+ let loaded = Config::parse(1345
+ r#"1346
+ [[rules]]1347
+ app_id = "^gimp$"1348
+ tag = "art"1349
+ [[rules]]1350
+ app_id = "^gimp$"1351
+ title = "^Tool Options$"1352
+ floating = true1353
+ opacity = 0.91354
+ "#,1355
+ )1356
+ .unwrap();1357
+ let before = loaded1358
+ .config1359
+ .placement(&subject("gimp", "GNU Image Manipulation Program"));1360
+ let after = Subject {1361
+ title: "Tool Options",1362
+ initial_title: "GNU Image Manipulation Program",1363
+ ..subject("gimp", "")1364
+ };1365
+ let after = loaded.config.placement(&after);1366
+ assert_eq!((before.floating, before.opacity), (None, None));1367
+ assert_eq!((after.floating, after.opacity), (Some(true), Some(0.9)));1368
+ assert_eq!(before.tag, after.tag);1369
+ }1370
+1371
+ #[test]13431372
fn size_bounds_treat_zero_as_open() {13441373
let p = Placement {13451374
min_size: Some((200, 0)),@@ -76,7 +76,6 @@ impl CompositorHandler for State {
7676
}7777
self.count_commit(&root);7878
if let Some(id) = self.window_id(&root) {79
- self.refresh_identity(id);8079
self.realign(id);8180
if self.services.captures.wants_damage() {8281
self.services.captures.window_damaged(id);@@ -3,7 +3,8 @@
33
44
use crate::state::State;55
use crate::wm::identity;6
-use monolith_config::Subject;6
+use monolith_config::{Placement, Subject};7
+use monolith_layout::{OutputId, WindowId};78
use smithay::desktop::Window;89
use smithay::xwayland::xwm::WmWindowType;910
@@ -60,3 +61,125 @@ impl State {
6061
}6162
}6263
}64
+65
+/// Takes the rules that follow a window for its whole life from `latest`,66
+/// leaving what was only meant for mapping as it was.67
+fn follow_effects(rule: &mut Placement, latest: &Placement) {68
+ rule.opacity = latest.opacity;69
+ rule.idle_inhibit = latest.idle_inhibit;70
+ rule.vrr = latest.vrr;71
+ rule.tearing = latest.tearing;72
+}73
+74
+impl State {75
+ pub fn layout_output_named(&self, name: &str) -> Option<OutputId> {76
+ let mut outputs = self.wm.layout.outputs();77
+ outputs.find(|(_, o)| o.name == name).map(|(id, _)| id)78
+ }79
+80
+ /// Matches a window's rules again after its title or app_id changed.81
+ /// Many clients only set the real ones after their first commit, so until82
+ /// the window has drawn everything is applied again. After that only the83
+ /// effects follow.84
+ pub fn rematch(&mut self, id: WindowId) {85
+ let Some(s) = self.wm.get(id) else {86
+ return;87
+ };88
+ let facts = self.facts(&s.window, Some(&s.initial_title));89
+ let new = self.settings.config.placement(&facts.subject());90
+ if new == s.rule {91
+ return;92
+ }93
+ let awaiting = s.awaiting_buffer;94
+ let Some(s) = self.wm.get_mut(id) else {95
+ return;96
+ };97
+ if awaiting {98
+ let old = std::mem::replace(&mut s.rule, new.clone());99
+ s.placed_by_rule = new.tag.is_some() || new.output.is_some();100
+ self.replace_placement(id, &old, &new);101
+ } else {102
+ follow_effects(&mut s.rule, &new);103
+ }104
+ self.update_inhibit();105
+ self.dirty.redraw = true;106
+ }107
+108
+ /// Moves a window that has not drawn yet from what `old` put it in to109
+ /// what `new` asks. A field `new` leaves out keeps what it has.110
+ fn replace_placement(&mut self, id: WindowId, old: &Placement, new: &Placement) {111
+ let output = new112
+ .output113
+ .as_deref()114
+ .and_then(|n| self.layout_output_named(n));115
+ let layout = &mut self.wm.layout;116
+ let Some(win) = layout.window(id) else {117
+ return;118
+ };119
+ let (floating, sticky, fullscreen) = (win.floating, win.sticky, win.fullscreen);120
+ if new.output != old.output121
+ && let Some(o) = output122
+ {123
+ layout.send_to_output(id, o);124
+ }125
+ if new.tag != old.tag126
+ && let Some(name) = &new.tag127
+ {128
+ let tag = layout.tag(name);129
+ layout.relocate(id, None, tag);130
+ }131
+ let flip = |was: Option<bool>, now: Option<bool>, current: bool| {132
+ was != now && now.is_some_and(|n| n != current)133
+ };134
+ if flip(old.floating, new.floating, floating) {135
+ layout.toggle_floating(id);136
+ }137
+ if flip(old.fullscreen, new.fullscreen, fullscreen) {138
+ layout.toggle_fullscreen(id);139
+ }140
+ if flip(old.sticky, new.sticky, sticky) {141
+ layout.toggle_sticky(id);142
+ }143
+ if new.scratchpad != old.scratchpad144
+ && let Some(name) = &new.scratchpad145
+ {146
+ layout.scratchpad_add(id, name);147
+ }148
+ self.view_if_first(id);149
+ if let Some(s) = self.wm.get_mut(id)150
+ && new.floating.is_some()151
+ {152
+ s.reclassify = false;153
+ }154
+ self.float_by_rule(id);155
+ self.apply_layout();156
+ }157
+}158
+159
+#[cfg(test)]160
+mod tests {161
+ use super::*;162
+ use monolith_config::IdleInhibit;163
+164
+ #[test]165
+ fn after_placement_only_effects_follow_the_latest_rules() {166
+ let mut rule = Placement {167
+ size: Some((10, 20)),168
+ focus: Some(false),169
+ opacity: Some(0.5),170
+ vrr: Some(true),171
+ ..Placement::default()172
+ };173
+ let latest = Placement {174
+ size: Some((30, 40)),175
+ idle_inhibit: Some(IdleInhibit::Visible),176
+ ..Placement::default()177
+ };178
+ follow_effects(&mut rule, &latest);179
+ assert_eq!(rule.size, Some((10, 20)));180
+ assert_eq!(rule.focus, Some(false));181
+ assert_eq!(rule.opacity, None);182
+ assert_eq!(rule.vrr, None);183
+ assert_eq!(rule.idle_inhibit, Some(IdleInhibit::Visible));184
+ }185
+}@@ -29,13 +29,7 @@ impl State {
2929
let output = rule3030
.output3131
.as_deref()32
- .and_then(|name| {33
- self.wm34
- .layout35
- .outputs()36
- .find(|(_, o)| o.name == name)37
- .map(|(id, _)| id)38
- })32
+ .and_then(|name| self.layout_output_named(name))3933
.or(fullscreen_output.and_then(|o| self.layout_output(&o)))4034
.or(launch.0);4135
let tag = match rule.tag.as_deref() {@@ -72,20 +66,15 @@ impl State {
7266
}7367
self.view_if_first(id);7468
let by_rule = rule.tag.is_some() || rule.output.is_some();75
- let natural_size =76
- floating && !fullscreen && window.toplevel().is_some() && rule.size.is_none();69
+ let natural_size = floating && !fullscreen && window.toplevel().is_some();7770
let reclassify = !floating && rule.floating.is_none() && window.toplevel().is_some();7871
let focus = rule.focus != Some(false);79
- let shaped = rule.at.is_some() || rule.min_size.is_some() || rule.max_size.is_some();80
- let positioned = rule.size.is_some() || (shaped && !natural_size && !fullscreen);8172
self.wm.insert(id, window, by_rule, app_id, title, rule);8273
if let Some(s) = self.wm.get_mut(id) {8374
s.natural_size = natural_size;8475
s.reclassify = reclassify;8576
}86
- if positioned {87
- self.float_by_rule(id);88
- }77
+ self.float_by_rule(id);8978
self.await_first_buffer(id);9079
if focus && self.wm.layout.visible(id) && !self.wm.layout.covered(id) {9180
self.wm.layout.focus(id);@@ -330,14 +319,27 @@ impl State {
330319
}331320
332321
/// Gives a floating window the size and spot its rules ask for, keeping333
- /// whichever of the two they leave out.334
- fn float_by_rule(&mut self, id: WindowId) {322
+ /// whichever of the two they leave out. One still waiting for its client323
+ /// to pick a size gets its spot when the size arrives, unless the rule324
+ /// has a size to give it right away.325
+ pub(crate) fn float_by_rule(&mut self, id: WindowId) {335326
let Some(win) = self.wm.layout.window(id).filter(|win| win.floating) else {336327
return;337328
};338
- let held = win.float_rect.map(|r| (r.w, r.h));339
- let size = self.wm.get(id).and_then(|s| s.rule.size);340
- let Some((w, h)) = size.map(|(w, h)| (f64::from(w), f64::from(h))).or(held) else {329
+ let (held, fullscreen) = (win.float_rect.map(|r| (r.w, r.h)), win.fullscreen);330
+ let Some(s) = self.wm.get_mut(id) else {331
+ return;332
+ };333
+ let rule = &s.rule;334
+ let size = rule.size.map(|(w, h)| (f64::from(w), f64::from(h)));335
+ let shaped = rule.at.is_some() || rule.min_size.is_some() || rule.max_size.is_some();336
+ if size.is_some() {337
+ s.natural_size = false;338
+ s.natural_after = None;339
+ } else if !shaped || s.natural_size || fullscreen {340
+ return;341
+ }342
+ let Some((w, h)) = size.or(held) else {341343
return;342344
};343345
if let Some(rect) = self.float_rect_for(id, w, h) {@@ -500,6 +502,7 @@ impl State {
500502
w.app_id = app_id;501503
w.title = title;502504
self.dirty.snapshot = true;505
+ self.rematch(id);503506
}504507
}505508
@@ -66,6 +66,18 @@ impl XdgShellHandler for State {
6666
}6767
}6868
69
+ fn title_changed(&mut self, surface: ToplevelSurface) {70
+ if let Some(id) = self.window_id(surface.wl_surface()) {71
+ self.refresh_identity(id);72
+ }73
+ }74
+75
+ fn app_id_changed(&mut self, surface: ToplevelSurface) {76
+ if let Some(id) = self.window_id(surface.wl_surface()) {77
+ self.refresh_identity(id);78
+ }79
+ }80
+6981
fn fullscreen_request(&mut self, surface: ToplevelSurface, output: Option<WlOutput>) {7082
self.set_fullscreen(&surface, true, output);7183
}