summaryrefslogtreecommitdiff
path: root/gui-wm/wayfire/files/wayfire-0.11.0-r73-stitch-persist.patch
blob: 2c5c09416414aa64c7d58fdd36362a8a3e847909 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
Keep the stitched VIRTUAL-1 while its members are unplugged (apply after
r72-review-followups).

Unplugging a member of core/stitch_outputs (for example a wall-switch power
off, which is a real DRM disconnect) used to break the group up: VIRTUAL-1
was destroyed, windows moved to NOOP-1 or to members re-enabled as standalone
outputs, and back again on return. move_view_to_output() scales positions
but only clamps sizes, so windows came back small, top-left, sometimes on
another workspace.

- output-layout: an unplugged member is remembered (name, make/model/serial,
  configured state, slice). VIRTUAL-1 keeps its size and windows; the other
  members keep mirroring their slices; the missing slice is not scanned out.
  NOOP-1 is not created and no views are transferred, also when every member
  is gone. Members are absent from the output lists clients see.
- While no member is connected VIRTUAL-1 runs at 1 Hz (frame callbacks keep
  coming, slowly); the configured refresh returns with the first member.
- A remembered member that returns with the same monitor (make/model/serial
  where known) and the same configured state is re-attached to its slice
  without a rebuild. It starts powered on, like any new connector.
- A member that returns with another mode or monitor, or any change of
  core/stitch_enabled or core/stitch_outputs, rebuilds the group as before.
  A member that never returns keeps its slice (and VIRTUAL-1 its size)
  indefinitely.
- core: move_view_to_output() clamped the kept workspace to the grid size
  instead of the last column/row.

No installed header or class layout changes; plugin ABI is unchanged.

diff --git a/src/core/core.cpp b/src/core/core.cpp
index db70f67..7eb7452 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -741,8 +741,8 @@ void wf::move_view_to_output(wayfire_toplevel_view v, wf::output_t *new_output,
         {
             auto new_grid_size = new_wset->get_workspace_grid_size();
             new_ws = {
-                std::min(old_ws.x, new_grid_size.width),
-                std::min(old_ws.y, new_grid_size.height)
+                std::min(old_ws.x, new_grid_size.width - 1),
+                std::min(old_ws.y, new_grid_size.height - 1)
             };
         }
 
diff --git a/src/core/output-layout.cpp b/src/core/output-layout.cpp
index 23c0406..a211b6e 100644
--- a/src/core/output-layout.cpp
+++ b/src/core/output-layout.cpp
@@ -1418,6 +1418,22 @@ class output_layout_t::impl
      * virtual output with the noop backend. */
     std::unique_ptr<output_layout_output_t> noop_output;
     wlr_output *stitch_output = nullptr;
+
+    /* Members of the stitched group which were unplugged. VIRTUAL-1 keeps
+     * their slice (and so its size and every window position) until they
+     * return with the same monitor and mode, or until the group changes in
+     * the config. A member which never returns keeps its slice indefinitely. */
+    struct missing_stitch_member_t
+    {
+        std::string name, make, model, serial;
+        output_state_t state;
+        wlr_box slice;
+    };
+    std::vector<missing_stitch_member_t> missing_members;
+    /** core/stitch_outputs when the first missing member was recorded. */
+    std::string missing_members_group;
+    /** VIRTUAL-1 refresh (mHz) while none of its members is connected. */
+    static constexpr int STITCH_IDLE_REFRESH = 1000;
     wf::option_wrapper_t<bool> stitch_enabled{"core/stitch_enabled"};
     wf::option_wrapper_t<std::string> stitch_outputs{"core/stitch_outputs"};
 
@@ -1478,6 +1494,7 @@ class output_layout_t::impl
             }
         }
 
+        missing_members.clear();
         auto handle = stitch_output;
         outputs.at(handle)->destroy_wayfire_output();
         wlr_output_layout_remove(output_layout, handle);
@@ -1825,15 +1842,186 @@ class output_layout_t::impl
             lo->output->render->schedule_redraw();
         });
 
+        if (reattach_stitch_member(lo))
+        {
+            return;
+        }
+
         reconfigure_from_config();
     }
 
+    missing_stitch_member_t *find_missing_member(const std::string& name)
+    {
+        for (auto& member : missing_members)
+        {
+            if (member.name == name)
+            {
+                return &member;
+            }
+        }
+
+        return nullptr;
+    }
+
+    /**
+     * A stitch member was unplugged: keep VIRTUAL-1 as it is, stop scanning
+     * this member's slice and remember the member, instead of breaking up the
+     * group (which moved every window to NOOP-1/standalone outputs and back).
+     */
+    bool detach_stitch_member(output_layout_output_t& lo)
+    {
+        if (!stitch_output || !stitch_enabled || !lo.stitch_source_box || !lo.stitch_restore_state ||
+            !outputs.at(stitch_output)->output)
+        {
+            return false;
+        }
+
+        if (missing_members.empty())
+        {
+            missing_members_group = stitch_outputs;
+        }
+
+        missing_stitch_member_t member;
+        member.name   = nonull(lo.handle->name);
+        member.make   = nonull(lo.handle->make);
+        member.model  = nonull(lo.handle->model);
+        member.serial = nonull(lo.handle->serial);
+        member.state  = *lo.stitch_restore_state;
+        member.slice  = *lo.stitch_source_box;
+        missing_members.push_back(member);
+        LOGI("Stitch member ", member.name, " disconnected; keeping VIRTUAL-1 and its slice");
+
+        output_state_t off;
+        off.source = OUTPUT_IMAGE_SOURCE_NONE;
+        lo.apply_state(off);
+        lo.stitch_source_box.reset();
+        lo.stitch_restore_state.reset();
+
+        update_stitch_idle_refresh();
+        idle_update_configuration.run_once([=] ()
+        {
+            send_wlr_configuration();
+        });
+        return true;
+    }
+
+    static bool same_or_unknown(const std::string& a, const char *b)
+    {
+        return a.empty() || !b || !*b || (a == b);
+    }
+
+    /** A remembered member came back: mirror its slice again if nothing changed. */
+    bool reattach_stitch_member(output_layout_output_t *lo)
+    {
+        if (!stitch_output || missing_members.empty())
+        {
+            return false;
+        }
+
+        auto it = std::find_if(missing_members.begin(), missing_members.end(),
+            [&] (auto& m) { return m.name == nonull(lo->handle->name); });
+        if (it == missing_members.end())
+        {
+            return false;
+        }
+
+        auto member = *it;
+        missing_members.erase(it);
+
+        auto configured = lo->load_configured_state();
+        auto& zones     = outputs.at(stitch_output)->stitch_zones;
+        bool slice_kept = std::any_of(zones.begin(), zones.end(), [&] (auto& zone)
+        {
+            return (zone.name == member.name) && (zone.geometry == wf::geometry_t{(double)member.slice.x,
+                (double)member.slice.y, (double)member.slice.width, (double)member.slice.height});
+        });
+        if (!stitch_enabled || (stitch_outputs.value() != missing_members_group) || !slice_kept ||
+            !same_or_unknown(member.make, lo->handle->make) || !same_or_unknown(member.model,
+                lo->handle->model) || !same_or_unknown(member.serial, lo->handle->serial) ||
+            !(configured == member.state))
+        {
+            LOGI("Stitch member ", member.name, " returned with a different monitor or mode; rebuilding");
+            return false;
+        }
+
+        LOGI("Stitch member ", member.name, " reconnected; re-attaching its slice of VIRTUAL-1");
+        lo->stitch_restore_state = configured;
+        lo->stitch_source_box    = member.slice;
+        // Power requests are not carried over: like any new connector, the
+        // member starts powered on (digitalrain-power drops its lease when
+        // the old wlr_output is destroyed).
+
+        // Back to full rate before the member starts mirroring.
+        update_stitch_idle_refresh();
+        auto state = configured;
+        state.source = OUTPUT_IMAGE_SOURCE_MIRROR;
+        state.mirror_from = "VIRTUAL-1";
+        lo->apply_state(state);
+        if (applied_stitch)
+        {
+            applied_stitch->requested[lo->handle] = configured;
+            applied_stitch->applied[lo->handle]   = lo->current_state;
+        }
+
+        if (auto wo = outputs.at(stitch_output)->output.get())
+        {
+            wo->render->damage_whole();
+        }
+
+        idle_update_configuration.run_once([=] ()
+        {
+            send_wlr_configuration();
+        });
+        return true;
+    }
+
+    /**
+     * Nothing scans VIRTUAL-1 out while all its members are unplugged: lower
+     * its refresh so that animations and clients do not render at full rate
+     * into nothing. Frame callbacks continue at 1 Hz, like a slow monitor.
+     */
+    void update_stitch_idle_refresh()
+    {
+        if (!stitch_output || !outputs.count(stitch_output) || !outputs.at(stitch_output)->output)
+        {
+            return;
+        }
+
+        auto& virt = outputs.at(stitch_output);
+        bool connected = std::any_of(outputs.begin(), outputs.end(), [] (auto& entry)
+        {
+            return entry.second->stitch_source_box.has_value();
+        });
+        int refresh = connected ? virt->current_state.mode.refresh : STITCH_IDLE_REFRESH;
+        if ((refresh <= 0) || (stitch_output->refresh == refresh))
+        {
+            return;
+        }
+
+        wlr_output_state state;
+        wlr_output_state_init(&state);
+        wlr_output_state_set_custom_mode(&state, stitch_output->width, stitch_output->height, refresh);
+        if (wlr_output_commit_state(stitch_output, &state))
+        {
+            LOGI("VIRTUAL-1 refresh ", refresh / 1000.0, " Hz (", connected ? "members connected" :
+                "no member connected", ")");
+        } else
+        {
+            LOGE("Failed to change VIRTUAL-1 refresh to ", refresh / 1000.0, " Hz");
+        }
+
+        wlr_output_state_finish(&state);
+    }
+
     void remove_output(wlr_output *to_remove)
     {
         auto active_outputs = get_outputs();
         LOGI("remove output: ", to_remove->name);
 
-        if (stitch_output && !is_shutting_down())
+        if (stitch_output && !is_shutting_down() && detach_stitch_member(*outputs.at(to_remove)))
+        {
+            // VIRTUAL-1 stays; the member's slice waits for it to return.
+        } else if (stitch_output && !is_shutting_down())
         {
             // A disappearing member invalidates the group. Restore surviving
             // connectors before releasing the combined workspace/output.
@@ -1878,6 +2066,8 @@ class output_layout_t::impl
 
         std::istringstream names(stitch_outputs.value());
         std::vector<wlr_output*> members;
+        std::vector<size_t> member_zones;
+        std::optional<output_config::position_t> first_position;
         std::unordered_set<std::string> seen;
         std::string name;
         int width = 0, height = 0, refresh = 0;
@@ -1889,13 +2079,15 @@ class output_layout_t::impl
                 return entry.first != stitch_output && name == entry.first->name;
             });
 
-            if ((it == config.end()) || !seen.insert(name).second)
+            // An unplugged member keeps its slice while VIRTUAL-1 exists.
+            auto missing = (it == config.end() && stitch_output) ? find_missing_member(name) : nullptr;
+            if (((it == config.end()) && !missing) || !seen.insert(name).second)
             {
                 LOGW("Cannot stitch: missing or duplicate output ", name);
                 return {};
             }
 
-            auto& state = it->second;
+            auto& state = missing ? missing->state : it->second;
             if ((state.source != OUTPUT_IMAGE_SOURCE_SELF) || (state.scale != 1.0) ||
                 (state.transform != WL_OUTPUT_TRANSFORM_NORMAL) || state.vrr || state.hdr ||
                 (state.depth != 8) || (state.mode.width <= 0) || (state.mode.height <= 0) ||
@@ -1920,10 +2112,19 @@ class output_layout_t::impl
             refresh = refresh ? std::min(refresh, state.mode.refresh) : state.mode.refresh;
             zones.push_back({name, {(double)width, 0.0, (double)state.mode.width, (double)height}});
             width += state.mode.width;
-            members.push_back(it->first);
+            if (!first_position)
+            {
+                first_position = state.position;
+            }
+
+            if (!missing)
+            {
+                members.push_back(it->first);
+                member_zones.push_back(zones.size() - 1);
+            }
         }
 
-        if (members.size() < 2)
+        if (zones.size() < 2)
         {
             LOGW("Cannot stitch: select at least two outputs in core/stitch_outputs.");
             return {};
@@ -1984,7 +2185,7 @@ class output_layout_t::impl
         combined.mode.height = height;
         combined.mode.refresh = refresh;
         combined.uses_custom_mode = true;
-        auto position = config.at(members.front()).position;
+        auto position = *first_position;
         combined.position = position.is_automatic_position() ? output_config::position_t{0, 0} : position;
         config[stitch_output] = combined;
         outputs.at(stitch_output)->stitch_zones = zones;
@@ -1994,7 +2195,7 @@ class output_layout_t::impl
             auto& lo = outputs.at(members[i]);
             auto& state = config.at(members[i]);
             lo->stitch_restore_state = state;
-            const auto& box = zones[i].geometry;
+            const auto& box = zones[member_zones[i]].geometry;
             lo->stitch_source_box = wlr_box{(int)box.x, (int)box.y, (int)box.width, (int)box.height};
             state.source = OUTPUT_IMAGE_SOURCE_MIRROR;
             state.mirror_from = "VIRTUAL-1";
@@ -2024,6 +2225,13 @@ class output_layout_t::impl
                     scoped_views.emplace_back(top, ws);
                 }
         }
+        if (!missing_members.empty() && (!stitch_enabled || (stitch_outputs.value() != missing_members_group)))
+        {
+            // The group itself changed: unplugged members no longer hold slices.
+            LOGI("Stitch group changed; forgetting ", missing_members.size(), " unplugged member(s)");
+            missing_members.clear();
+        }
+
         // Load desired configuration from config file
         output_configuration_t configuration;
         for (auto& [output, layout_output] : this->outputs)
@@ -2374,6 +2582,8 @@ class output_layout_t::impl
             });
         }
 
+        // Re-applying VIRTUAL-1 restores its configured refresh.
+        update_stitch_idle_refresh();
         idle_update_configuration.run_once([=] ()
         {
             send_wlr_configuration();