summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCrucible <crucible@localhost>2026-10-22 19:17:45 -0500
committerCrucible <crucible@localhost>2026-10-22 19:17:45 -0500
commitb4a1c96b776780b6ed75aa034ed4f3f64be9199c (patch)
tree0c94b8abcf0367ebd73a5ab930c790038d86dbf2
parent2eaf3b0e17c56ca25d67211bfd1e18a1a9926736 (diff)
downloadsrcux-b4a1c96b776780b6ed75aa034ed4f3f64be9199c.tar.gz
srcux-b4a1c96b776780b6ed75aa034ed4f3f64be9199c.tar.xz
srcux-b4a1c96b776780b6ed75aa034ed4f3f64be9199c.zip
Publish srcux candidate 0000000823-20261022-191401
-rw-r--r--gui-apps/ambient-background/ambient-background-1.0.0-r18.ebuild (renamed from gui-apps/ambient-background/ambient-background-1.0.0-r17.ebuild)2
-rw-r--r--gui-apps/ambient-background/files/ambient-background-1.0.0-idle-timer.patch243
-rw-r--r--gui-libs/pixdecor/files/pixdecor-corner-resize.patch18
-rw-r--r--gui-libs/pixdecor/pixdecor-0.1.0_p20261006-r2.ebuild (renamed from gui-libs/pixdecor/pixdecor-0.1.0_p20261006-r1.ebuild)1
-rw-r--r--gui-libs/pixdecor/pixdecor-9999-r6.ebuild (renamed from gui-libs/pixdecor/pixdecor-9999-r5.ebuild)1
-rw-r--r--gui-libs/wayfire-plugins-custom/files/window-state-preview-budget-v1.patch35
-rw-r--r--gui-libs/wayfire-plugins-custom/wayfire-plugins-custom-0.1.0-r12.ebuild (renamed from gui-libs/wayfire-plugins-custom/wayfire-plugins-custom-0.1.0-r11.ebuild)1
-rw-r--r--gui-libs/wayfire-plugins-extra/files/wayfire-plugins-extra-0.11.2-follow-focus-explicit.patch130
-rw-r--r--gui-libs/wayfire-plugins-extra/wayfire-plugins-extra-0.11.2-r6.ebuild (renamed from gui-libs/wayfire-plugins-extra/wayfire-plugins-extra-0.11.2-r5.ebuild)1
-rw-r--r--gui-wm/wayfire/files/wayfire-0.11.0-r71-cube-teardown-backgrounds.patch149
-rw-r--r--gui-wm/wayfire/files/wayfire-0.11.0-r71-stitch-noop-reload.patch170
-rw-r--r--gui-wm/wayfire/files/wayfire-0.11.0-r71-switch-tile-exclusivity.patch325
-rw-r--r--gui-wm/wayfire/wayfire-0.11.0-r71.ebuild (renamed from gui-wm/wayfire/wayfire-0.11.0-r70.ebuild)3
13 files changed, 1079 insertions, 0 deletions
diff --git a/gui-apps/ambient-background/ambient-background-1.0.0-r17.ebuild b/gui-apps/ambient-background/ambient-background-1.0.0-r18.ebuild
index db547dbb907b..38d2e9f66be3 100644
--- a/gui-apps/ambient-background/ambient-background-1.0.0-r17.ebuild
+++ b/gui-apps/ambient-background/ambient-background-1.0.0-r18.ebuild
@@ -33,6 +33,8 @@ RDEPEND="
>=gui-wm/wayfire-0.11.0-r60:0=
"
+PATCHES=( "${FILESDIR}/${P}-idle-timer.patch" )
+
src_unpack() {
mkdir -p "${S}" || die
cp -a "${FILESDIR}/${P}-r16/." "${S}/" || die
diff --git a/gui-apps/ambient-background/files/ambient-background-1.0.0-idle-timer.patch b/gui-apps/ambient-background/files/ambient-background-1.0.0-idle-timer.patch
new file mode 100644
index 000000000000..2e695ec81e11
--- /dev/null
+++ b/gui-apps/ambient-background/files/ambient-background-1.0.0-idle-timer.patch
@@ -0,0 +1,243 @@
+diff --git a/core/ambient-background.cpp b/core/ambient-background.cpp
+index 2fb658d..60a9213 100644
+--- a/core/ambient-background.cpp
++++ b/core/ambient-background.cpp
+@@ -96,6 +96,9 @@ class ambient_plugin_t : public wf::plugin_interface_t {
+ std::map<wf::output_t*, output_state_t> outputs;
+ wf::scene::node_ptr shield;
+ wf::wl_timer<true> timer;
++ wf::wl_idle_call rearm;
++ unsigned timer_interval = 0;
++ bool in_tick = false;
+ bool manual = false, suspended = false, daemon_started = false, cursor_hidden = false;
+ unsigned reset_generation = 0;
+ double last_tick = now(), heartbeat = 0, restore_until = 0, last_launch = 0;
+@@ -131,6 +134,7 @@ class ambient_plugin_t : public wf::plugin_interface_t {
+ auto it = std::find(children.begin(), children.end(), anchor);
+ children.insert(it, node);
+ root->set_children_list(children);
++ wf::scene::update(root, wf::scene::update_flag::CHILDREN_LIST);
+ }
+ bool locked() const {
+ for (auto& [o, s] : outputs) if (o->is_inhibited()) return true;
+@@ -141,7 +145,7 @@ class ambient_plugin_t : public wf::plugin_interface_t {
+ if (!stitch->zones.empty()) return stitch->zones;
+ return {{o->to_string(), o->get_relative_geometry()}};
+ }
+- void update_zones(wf::output_t *o, output_state_t& state) {
++ bool update_zones(wf::output_t *o, output_state_t& state) {
+ auto old = state.zones;
+ state.zones.clear();
+ for (auto z : targets(o)) {
+@@ -151,13 +155,17 @@ class ambient_plugin_t : public wf::plugin_interface_t {
+ }
+ state.zones.push_back(zone);
+ }
++ if (old.size() != state.zones.size()) return true;
++ for (size_t i = 0; i < old.size(); i++)
++ if (old[i].name != state.zones[i].name || old[i].box != state.zones[i].box) return true;
++ return false;
+ }
+ void add_output(wf::output_t *o) {
+ if (outputs.count(o)) return;
+ auto& state = outputs[o];
+ update_zones(o, state);
+- state.hyperspace = std::make_unique<ambient_hyperspace_background>([o] { o->render->damage_whole(); });
+- state.nebula = std::make_unique<ambient_nebula_background>([o] { o->render->damage_whole(); });
++ state.hyperspace = std::make_unique<ambient_hyperspace_background>([this,o] { o->render->damage_whole(); schedule(); });
++ state.nebula = std::make_unique<ambient_nebula_background>([this,o] { o->render->damage_whole(); schedule(); });
+ state.nebula->set_active(active() && !locked() && std::string(backend) == "nebula");
+ state.hyperspace->set_active(active() && !locked() && std::string(backend) == "hyperspace");
+ o->add_activator(toggle_binding, &toggle);
+@@ -268,7 +276,9 @@ class ambient_plugin_t : public wf::plugin_interface_t {
+ if (auto view=previous_focus.lock()) if (view->is_mapped()) wf::get_core().seat->focus_view(view->self());
+ previous_focus.reset();
+ }
++ for (auto& [o,s]:outputs) o->render->damage_whole();
+ update_shield();
++ schedule();
+ }
+ wf::activator_callback toggle = [this] (auto) { if (!active() || locked()) return false; set_manual(!manual); return true; };
+ wf::activator_callback toggle_background = [this] (auto) {
+@@ -285,6 +295,8 @@ class ambient_plugin_t : public wf::plugin_interface_t {
+ void paint(wf::output_t *o,const wf::render_target_t& target,const wf::regionf_t& damage) {
+ auto it=outputs.find(o); if (it==outputs.end() || !enabled || locked()) return;
+ auto& state=it->second;
++ // Unlocking has no signal; resume animation on the first repaint.
++ schedule();
+ GL_CALL(glDisable(GL_DEPTH_TEST)); GL_CALL(glDisable(GL_CULL_FACE));
+ wf::gles::for_each_scissor_rect(target,damage,[&] {
+ OpenGL::render_rectangle(o->get_relative_geometry(),{0,0,0,1},wf::gles::render_target_orthographic_projection(target));
+@@ -332,31 +344,80 @@ class ambient_plugin_t : public wf::plugin_interface_t {
+ }
+ GL_CALL(glDisable(GL_BLEND));
+ }
++ bool stale(double t) const { return heartbeat ? t-heartbeat>3 : daemon_started && t-last_launch>3; }
++ float zone_target(const zone_t& z, double t, bool is_stale) const {
++ return manual ? 0 : (is_stale || t<restore_until || locked() ? 1 : z.target);
++ }
++ bool effect_animating(output_state_t& s) {
++ bool visible=active() && !locked();
++ auto b=std::string(backend);
++ s.hyperspace->set_active(visible && b=="hyperspace");
++ s.nebula->set_active(visible && b=="nebula");
++ if (!visible) return false;
++ if (b=="hyperspace") return s.hyperspace->animated();
++ if (b=="nebula") return s.nebula->animated();
++ return true; // Digital rain frames come from its client buffers.
++ }
++ unsigned frame_interval() const {
++ if (std::string(backend)=="hyperspace" && !outputs.empty())
++ return outputs.begin()->second.hyperspace->frame_interval();
++ return 33;
++ }
++ // Frames only while an effect animates or a zone fades. Otherwise, while
++ // enabled, poll slowly for the daemon heartbeat, lock and zone changes.
++ unsigned wanted_interval() {
++ if (!enabled) return 0;
++ double t=now(); bool is_stale=stale(t);
++ bool busy=manual || t<restore_until;
++ for (auto& [o,s]:outputs) {
++ busy |= effect_animating(s);
++ for (auto& z:s.zones) busy |= z.opacity != zone_target(z,t,is_stale);
++ }
++ return busy ? frame_interval() : 1000;
++ }
++ void schedule() {
++ if (in_tick) return; // tick() re-evaluates when it returns
++ unsigned interval=wanted_interval();
++ if (interval == timer_interval) return;
++ timer.disconnect(); timer_interval=interval; last_tick=now();
++ if (interval) timer.set_timeout(interval,[this]{return tick();});
++ }
+ bool tick() {
++ in_tick=true;
+ double t=now(), dt=std::clamp(t-last_tick,0.0,.1); last_tick=t;
++ // A slow poll tick starts any new fade with a single frame step.
++ if (timer_interval > frame_interval()) dt=std::min(dt,frame_interval()/1000.0);
+ if (locked() && manual) set_manual(false);
+- bool stale=(heartbeat ? t-heartbeat>3 : daemon_started && t-last_launch>3);
+- if (stale && manual) set_manual(false);
++ bool is_stale=stale(t);
++ if (is_stale && manual) set_manual(false);
+ for (auto& [o,s]:outputs) {
+- update_zones(o,s);
+- s.hyperspace->set_active(active() && !locked() && std::string(backend)=="hyperspace");
+- s.nebula->set_active(active() && !locked() && std::string(backend)=="nebula");
++ // The locker owns animation while protected. Do not keep waking
++ // the desktop renderer behind a black or powered-off lock screen.
++ bool visible=active() && !locked();
++ if (update_zones(o,s) && visible) o->render->damage_whole();
++ bool animating=effect_animating(s);
+ for (auto& z:s.zones) {
+- float target = manual ? 0 : (stale || t<restore_until || locked() ? 1 : z.target);
++ auto before=z;
++ float target=zone_target(z,t,is_stale);
+ double duration=manual ? double(manual_seconds) : double(wake_seconds);
+- z.opacity += std::clamp(target-z.opacity,float(-dt/std::max(.05,duration)),float(dt/std::max(.05,duration)));
++ float step=float(dt/std::max(.05,duration));
++ z.opacity=std::abs(target-z.opacity)<=step ? target : z.opacity+std::clamp(target-z.opacity,-step,step);
+ z.opacity=std::clamp(z.opacity,0.f,1.f);
+- if (stale) { z.target=1;z.black=0;z.effect=1; }
++ if (is_stale) { z.target=1;z.black=0;z.effect=1; }
++ if (visible && !animating && (z.opacity!=before.opacity || z.black!=before.black || z.effect!=before.effect))
++ o->render->damage(z.box);
+ }
+- // The locker owns animation while protected. Do not keep waking
+- // the desktop renderer behind a black or powered-off lock screen.
+- if (active() && !locked()) o->render->damage_whole();
++ if (animating) o->render->damage_whole();
+ }
+ update_shield();
+- if (enabled && stale && !std::string(controller).empty() && t-last_launch>10) {
++ if (enabled && is_stale && !std::string(controller).empty() && t-last_launch>10) {
+ last_launch=t;wf::get_core().run(std::string(controller));
+ }
+- return true;
++ in_tick=false;
++ if (wanted_interval() == timer_interval) return true;
++ // The timer cannot be re-armed from its own callback.
++ timer_interval=0; rearm.run_once([this]{schedule();});
++ return false;
+ }
+ wf::json_t status() {
+ wf::json_t j=wf::ipc::json_ok();
+@@ -431,6 +492,7 @@ class ambient_plugin_t : public wf::plugin_interface_t {
+ if (enabled && !daemon_started && !std::string(controller).empty()) {
+ daemon_started=true;last_launch=now();wf::get_core().run(std::string(controller));
+ }
++ schedule();
+ }
+ wf::signal::connection_t<wf::ambient_query_signal> query = [this] (auto ev) {
+ ev->enabled=enabled && outputs.count(ev->output);
+@@ -441,7 +503,12 @@ class ambient_plugin_t : public wf::plugin_interface_t {
+ wf::signal::connection_t<wf::ambient_transition_signal> transition = [this] (auto ev) {
+ if (outputs.count(ev->output)) outputs.at(ev->output).hyperspace->set_transition(ev->moving);
+ };
+- wf::signal::connection_t<wf::output_added_signal> added = [this] (auto ev) { add_output(ev->output); };
++ // Background and wallpaper options are read on demand without callbacks.
++ wf::signal::connection_t<wf::reload_config_signal> reloaded = [this] (auto) {
++ if (active()) for (auto& [o,s]:outputs) o->render->damage_whole();
++ schedule();
++ };
++ wf::signal::connection_t<wf::output_added_signal> added = [this] (auto ev) { add_output(ev->output); schedule(); };
+ wf::signal::connection_t<wf::output_pre_remove_signal> removed = [this] (auto ev) {
+ if (outputs.count(ev->output)) {
+ ev->output->rem_binding(&toggle);ev->output->rem_binding(&restore);
+@@ -450,6 +517,7 @@ class ambient_plugin_t : public wf::plugin_interface_t {
+ if(s.node)wf::scene::remove_child(s.node);
+ if(s.fade_node)wf::scene::remove_child(s.fade_node);
+ outputs.erase(ev->output);
++ schedule();
+ }
+ };
+ public:
+@@ -467,7 +535,7 @@ class ambient_plugin_t : public wf::plugin_interface_t {
+ for (auto o:wf::get_core().output_layout->get_outputs()) add_output(o);
+ wf::get_core().output_layout->connect(&added);wf::get_core().output_layout->connect(&removed);
+ wf::get_core().connect(&query);wf::get_core().connect(&render);wf::get_core().connect(&transition);
+- wf::get_core().connect(&on_key);
++ wf::get_core().connect(&on_key);wf::get_core().connect(&reloaded);
+ enabled.set_callback([this]{suspended=false;changed();});backend.set_callback([this]{changed();});
+ repo->register_method("ambient/status",[this](wf::json_t){return status();});
+ repo->register_method("ambient/toggle",[this](wf::json_t){set_manual(!manual);return status();});
+@@ -490,19 +558,21 @@ class ambient_plugin_t : public wf::plugin_interface_t {
+ for (size_t i=0;i<data["outputs"].size();i++) {
+ wf::json_t item=data["outputs"][i];
+ for(auto& [o,s]:outputs)for(auto& z:s.zones)if(z.name==item["name"].as_string()) {
+- z.target=item["opacity"].as_double();z.black=item["black"].as_double();z.effect=item["effect"].as_double();
++ float black=item["black"].as_double(), effect=item["effect"].as_double();
++ if ((black!=z.black || effect!=z.effect) && active() && !locked()) o->render->damage(z.box);
++ z.target=item["opacity"].as_double();z.black=black;z.effect=effect;
+ }
+ }
+- heartbeat=now();return wf::ipc::json_ok();
++ heartbeat=now();schedule();return wf::ipc::json_ok();
+ });
+- changed();timer.set_timeout(33,[this]{return tick();});
++ changed();
+ }
+ void fini() override {
+- timer.disconnect();enabled.set_callback(nullptr);backend.set_callback(nullptr);
+- set_manual(false);
++ enabled.set_callback(nullptr);backend.set_callback(nullptr);
++ set_manual(false);timer.disconnect();rearm.disconnect();timer_interval=0;
+ if(cursor_hidden) { wf::get_core().unhide_cursor();cursor_hidden=false; }
+ if(shield) {wf::scene::remove_child(shield);shield.reset();}
+- query.disconnect();render.disconnect();transition.disconnect();on_key.disconnect();added.disconnect();removed.disconnect();
++ query.disconnect();render.disconnect();transition.disconnect();on_key.disconnect();reloaded.disconnect();added.disconnect();removed.disconnect();
+ for(auto& [o,s]:outputs) {
+ o->rem_binding(&toggle);o->rem_binding(&restore);
+ o->rem_binding(&toggle_background);
+diff --git a/effects/nebula/nebula-background.hpp b/effects/nebula/nebula-background.hpp
+index 51df5eb..c0eec86 100644
+--- a/effects/nebula/nebula-background.hpp
++++ b/effects/nebula/nebula-background.hpp
+@@ -31,6 +31,7 @@ class __attribute__((visibility("hidden"))) ambient_nebula_background {
+ explicit ambient_nebula_background(std::function<void()> changed);
+ ~ambient_nebula_background();
+ void set_active(bool enabled);
++ bool animated() const { return active && double(speed)>0; }
+ void invalidate() { dirty=true; }
+ bool has_scene() const { return configured_seed>=0; }
+ std::array<double,2> scene() const { return {double(scene_seed),phase}; }
diff --git a/gui-libs/pixdecor/files/pixdecor-corner-resize.patch b/gui-libs/pixdecor/files/pixdecor-corner-resize.patch
new file mode 100644
index 000000000000..48c5b1ce13ab
--- /dev/null
+++ b/gui-libs/pixdecor/files/pixdecor-corner-resize.patch
@@ -0,0 +1,18 @@
+diff --git a/src/deco-subsurface.cpp b/src/deco-subsurface.cpp
+index f626204..8eb57e9 100644
+--- a/src/deco-subsurface.cpp
++++ b/src/deco-subsurface.cpp
+@@ -263,7 +263,12 @@ class simple_decoration_node_t : public wf::scene::node_t, public wf::pointer_in
+ const double x = local.x - shadow, y = local.y - shadow;
+ const double dx = std::max({radius - x, 0.0, x - (w - radius)});
+ const double dy = std::max({radius - y, 0.0, y - (h - radius)});
+- if (dx > 0 && dy > 0 && dx * dx + dy * dy > radius * radius)
++ // Keep the corner resize handles (where the layout's edge
++ // handles overlap, see calculate_resize_edges()) hittable.
++ const double handle = theme.get_input_size();
++ const bool corner_handle = std::min(x, w - x) < handle &&
++ std::min(y, h - y) < handle;
++ if (!corner_handle && dx > 0 && dy > 0 && dx * dx + dy * dy > radius * radius)
+ {
+ return {};
+ }
diff --git a/gui-libs/pixdecor/pixdecor-0.1.0_p20261006-r1.ebuild b/gui-libs/pixdecor/pixdecor-0.1.0_p20261006-r2.ebuild
index eb0d7eb34303..be5306c3d66b 100644
--- a/gui-libs/pixdecor/pixdecor-0.1.0_p20261006-r1.ebuild
+++ b/gui-libs/pixdecor/pixdecor-0.1.0_p20261006-r2.ebuild
@@ -20,6 +20,7 @@ PATCHES=(
"${FILESDIR}/${PN}-shade-deco-lifetime.patch"
"${FILESDIR}/${PN}-shade-output-lifetime.patch"
"${FILESDIR}/${PN}-theme-ipc-validate.patch"
+ "${FILESDIR}/${PN}-corner-resize.patch"
)
LICENSE="MIT"
diff --git a/gui-libs/pixdecor/pixdecor-9999-r5.ebuild b/gui-libs/pixdecor/pixdecor-9999-r6.ebuild
index f64605697e03..5ec0d321a8c7 100644
--- a/gui-libs/pixdecor/pixdecor-9999-r5.ebuild
+++ b/gui-libs/pixdecor/pixdecor-9999-r6.ebuild
@@ -20,6 +20,7 @@ PATCHES=(
"${FILESDIR}/${PN}-shade-deco-lifetime.patch"
"${FILESDIR}/${PN}-shade-output-lifetime.patch"
"${FILESDIR}/${PN}-theme-ipc-validate.patch"
+ "${FILESDIR}/${PN}-corner-resize.patch"
)
LICENSE="MIT"
diff --git a/gui-libs/wayfire-plugins-custom/files/window-state-preview-budget-v1.patch b/gui-libs/wayfire-plugins-custom/files/window-state-preview-budget-v1.patch
new file mode 100644
index 000000000000..f1c3f4024cdb
--- /dev/null
+++ b/gui-libs/wayfire-plugins-custom/files/window-state-preview-budget-v1.patch
@@ -0,0 +1,35 @@
+diff --git a/plugins/window-state/src/spatial.hpp b/plugins/window-state/src/spatial.hpp
+index eedaf43..639ef78 100644
+--- a/plugins/window-state/src/spatial.hpp
++++ b/plugins/window-state/src/spatial.hpp
+@@ -2,6 +2,7 @@
+ #include <wayfire/txn/transaction-manager.hpp>
+ #include <wayfire/seat.hpp>
+ #include <deque>
++#include <cmath>
+ #include "previews.hpp"
+
+ namespace
+@@ -338,6 +339,13 @@ class SpatialOwner
+ if (!tokens.is_array() || tokens.empty() || tokens.size()>(hover?1:12)) throw std::runtime_error("preview batch exceeds limit");
+ if (Clock::now()-last_preview<std::chrono::milliseconds(180)) throw std::runtime_error("preview refresh is rate limited");
+ last_preview=Clock::now();Json frames=Json::array();
++ // Bound the whole reply, not only each frame: twelve 160x90 RGBA
++ // frames encode to ~920 KB, past the nonblocking socket's send
++ // buffer (~208 KiB default; Wayfire without ipc-backpressure drops
++ // the client on a short write) and near the 1 MiB IPC message cap.
++ // Batches shrink to two full frames' worth (~150 KB encoded).
++ const double fit=std::min(1.0,std::sqrt(2.0/tokens.size()));
++ const int batch_width=int(160*fit),batch_height=int(90*fit);
+ for (const auto& token : tokens)
+ {
+ auto row=find(rows,token);auto v=wf::toplevel_cast(wf::ipc::find_view_by_id(row["id"].get<int>()));
+@@ -345,7 +353,7 @@ class SpatialOwner
+ throw std::runtime_error("previews unavailable while locked or desktop interaction is active");
+ // Wayfire writes IPC responses synchronously to a nonblocking socket.
+ // A raw 480x300 RGBA frame can exceed its socket send buffer.
+- auto frame=capture_preview(v,hover?240:160,hover?150:90);
++ auto frame=capture_preview(v,hover?240:batch_width,hover?150:batch_height);
+ frame["token"]=row["token"];frame["version"]=row["version"];frames.push_back(std::move(frame));
+ }
+ return {{"instance",instance},{"topology-revision",topo["revision"]},{"frames",frames}};
diff --git a/gui-libs/wayfire-plugins-custom/wayfire-plugins-custom-0.1.0-r11.ebuild b/gui-libs/wayfire-plugins-custom/wayfire-plugins-custom-0.1.0-r12.ebuild
index d7796a0a0645..8d382ed8fd2b 100644
--- a/gui-libs/wayfire-plugins-custom/wayfire-plugins-custom-0.1.0-r11.ebuild
+++ b/gui-libs/wayfire-plugins-custom/wayfire-plugins-custom-0.1.0-r12.ebuild
@@ -26,6 +26,7 @@ src_prepare() {
eapply "${FILESDIR}/window-state-control-exclusion-v1.patch"
eapply "${FILESDIR}/window-state-preview-ipc-budget-v1.patch"
eapply "${FILESDIR}/window-state-settings-exclusion-v1.patch"
+ eapply "${FILESDIR}/window-state-preview-budget-v1.patch"
}
RDEPEND="
diff --git a/gui-libs/wayfire-plugins-extra/files/wayfire-plugins-extra-0.11.2-follow-focus-explicit.patch b/gui-libs/wayfire-plugins-extra/files/wayfire-plugins-extra-0.11.2-follow-focus-explicit.patch
new file mode 100644
index 000000000000..22e4d994261e
--- /dev/null
+++ b/gui-libs/wayfire-plugins-extra/files/wayfire-plugins-extra-0.11.2-follow-focus-explicit.patch
@@ -0,0 +1,130 @@
+diff --git a/src/follow-focus.cpp b/src/follow-focus.cpp
+index ced6a92..b271452 100644
+--- a/src/follow-focus.cpp
++++ b/src/follow-focus.cpp
+@@ -28,10 +28,12 @@
+ #include <wayfire/output-layout.hpp>
+ #include <wayfire/per-output-plugin.hpp>
+ #include <wayfire/view-helpers.hpp>
++#include <wayfire/workspace-set.hpp>
+ #include <wayfire/view.hpp>
+ #include <wayfire/util.hpp>
+ #include <wayfire/seat.hpp>
+ #include <wayfire/signal-definitions.hpp>
++#include <wayfire/plugins/common/util.hpp>
+
+ namespace follow_focus
+ {
+@@ -44,8 +46,10 @@ class wayfire_follow_focus : public wf::per_output_plugin_instance_t
+ wayfire_view focus_view = nullptr;
+ wf::wl_timer<false> change_output_focus, change_view_focus;
+ wf::pointf_t last_output_coords, last_view_coords;
+- wf::wl_idle_call workspace_focus_idle;
++ wf::wl_idle_call workspace_focus_idle, workspace_switch_idle;
+ bool workspace_focus_pending = false;
++ bool workspace_switch_started = false;
++ uint64_t workspace_switch_focus_ts = 0;
+ wf::option_wrapper_t<bool> focus_on_workspace_change{"follow-focus/focus_on_workspace_change"};
+
+ wf::option_wrapper_t<bool> should_change_view{"follow-focus/change_view"};
+@@ -172,7 +176,8 @@ class wayfire_follow_focus : public wf::per_output_plugin_instance_t
+ if (!should_change_view || !focus_on_workspace_change ||
+ output != wf::get_core().seat->get_active_output() ||
+ !output->can_activate_plugin(wf::CAPABILITY_MANAGE_COMPOSITOR) ||
+- !view || view != focus_view || view->get_output() != output)
++ !view || view != focus_view || view->get_output() != output ||
++ workspace_focus_overridden())
+ {
+ return false;
+ }
+@@ -183,6 +188,29 @@ class wayfire_follow_focus : public wf::per_output_plugin_instance_t
+ });
+ }
+
++ // Remember the seat's focus timestamp when a workspace switch begins, so
++ // focus given explicitly as part of the switch (scale, switcher,
++ // activation requests, a window carried along) can be told apart from
++ // the core's refocus after the switch, which does not update timestamps.
++ // Requests are serviced synchronously, so the mark expires on idle.
++ void mark_workspace_switch_start()
++ {
++ if (!workspace_switch_started && !workspace_focus_pending)
++ {
++ workspace_switch_started = true;
++ workspace_switch_focus_ts = wf::get_core().seat->get_last_focus_timestamp();
++ workspace_switch_idle.run_once([=] () { workspace_switch_started = false; });
++ }
++ }
++
++ bool workspace_focus_overridden()
++ {
++ auto active = wf::toplevel_cast(wf::get_core().seat->get_active_view());
++ return active && (active->get_output() == output) &&
++ (wf::get_focus_timestamp(active) > workspace_switch_focus_ts) &&
++ output->wset()->view_visible_on(active, output->wset()->get_current_workspace());
++ }
++
+ // A workspace switch can put a window under a stationary pointer. Wait
+ // until the switcher's animation/grab is finished, then use the normal
+ // follow-focus delay and raise policy without requiring pointer movement.
+@@ -195,8 +223,11 @@ class wayfire_follow_focus : public wf::per_output_plugin_instance_t
+ return;
+ }
+
++ // A lock (output inhibited) or an explicit focus since the switch
++ // makes the pending pointer focus stale; drop it.
+ if (!should_change_view || !focus_on_workspace_change ||
+- output != wf::get_core().seat->get_active_output())
++ output != wf::get_core().seat->get_active_output() ||
++ output->is_inhibited() || workspace_focus_overridden())
+ {
+ workspace_focus_pending = false;
+ return;
+@@ -221,12 +252,30 @@ class wayfire_follow_focus : public wf::per_output_plugin_instance_t
+ {
+ if (ev->old_viewport != ev->new_viewport)
+ {
++ mark_workspace_switch_start();
++ workspace_switch_started = false;
+ change_view_focus.disconnect();
+ workspace_focus_pending = should_change_view && focus_on_workspace_change;
+ schedule_workspace_focus();
+ }
+ };
+
++ wf::signal::connection_t<wf::workspace_change_request_signal> workspace_change_request =
++ [=] (wf::workspace_change_request_signal *ev)
++ {
++ if (ev->old_viewport != ev->new_viewport)
++ {
++ mark_workspace_switch_start();
++ }
++ };
++
++ // Emitted for views carried along with a switch, before they are focused.
++ wf::signal::connection_t<wf::view_change_workspace_signal> view_change_workspace =
++ [=] (wf::view_change_workspace_signal *ev)
++ {
++ mark_workspace_switch_start();
++ };
++
+ wf::signal::connection_t<wf::output_plugin_activated_changed_signal> plugin_changed =
+ [=] (wf::output_plugin_activated_changed_signal *ev)
+ {
+@@ -256,6 +305,8 @@ class wayfire_follow_focus : public wf::per_output_plugin_instance_t
+ {
+ wf::get_core().connect(&pointer_motion);
+ wf::get_core().connect(&pointer_motion_absolute);
++ output->connect(&workspace_change_request);
++ output->connect(&view_change_workspace);
+ output->connect(&workspace_changed);
+ output->connect(&plugin_changed);
+ }
+@@ -263,6 +314,7 @@ class wayfire_follow_focus : public wf::per_output_plugin_instance_t
+ void fini() override
+ {
+ workspace_focus_idle.disconnect();
++ workspace_switch_idle.disconnect();
+ change_output_focus.disconnect();
+ change_view_focus.disconnect();
+ }
diff --git a/gui-libs/wayfire-plugins-extra/wayfire-plugins-extra-0.11.2-r5.ebuild b/gui-libs/wayfire-plugins-extra/wayfire-plugins-extra-0.11.2-r6.ebuild
index 02fe0199f8af..c25403fc7b21 100644
--- a/gui-libs/wayfire-plugins-extra/wayfire-plugins-extra-0.11.2-r5.ebuild
+++ b/gui-libs/wayfire-plugins-extra/wayfire-plugins-extra-0.11.2-r6.ebuild
@@ -54,6 +54,7 @@ PATCHES=(
"${FILESDIR}/wayfire-plugins-extra-0.11.0-force-fullscreen-teardown.patch"
"${FILESDIR}/wayfire-plugins-extra-0.11.2-force-fullscreen-physical-scope.patch"
"${FILESDIR}/wayfire-plugins-extra-0.11.2-join-views-multi-view.patch"
+ "${FILESDIR}/wayfire-plugins-extra-0.11.2-follow-focus-explicit.patch"
)
src_install() {
diff --git a/gui-wm/wayfire/files/wayfire-0.11.0-r71-cube-teardown-backgrounds.patch b/gui-wm/wayfire/files/wayfire-0.11.0-r71-cube-teardown-backgrounds.patch
new file mode 100644
index 000000000000..6ae91ff09170
--- /dev/null
+++ b/gui-wm/wayfire/files/wayfire-0.11.0-r71-cube-teardown-backgrounds.patch
@@ -0,0 +1,149 @@
+diff --git a/plugins/cube/cube-background.hpp b/plugins/cube/cube-background.hpp
+index c6e3f48..500be89 100644
+--- a/plugins/cube/cube-background.hpp
++++ b/plugins/cube/cube-background.hpp
+@@ -9,6 +9,14 @@ class wf_cube_background_base
+ public:
+ virtual void render_frame(const wf::render_target_t& fb,
+ wf_cube_animation_attribs& attribs) = 0;
++ // Backgrounds without a camera-independent pose ignore follow_cube.
++ virtual void render_frame(const wf::render_target_t& fb,
++ wf_cube_animation_attribs& attribs, bool follow_cube)
++ {
++ (void)follow_cube;
++ render_frame(fb, attribs);
++ }
++
+ virtual ~wf_cube_background_base() = default;
+ };
+
+diff --git a/plugins/cube/cube.cpp b/plugins/cube/cube.cpp
+index 3b4b857..ce9614b 100644
+--- a/plugins/cube/cube.cpp
++++ b/plugins/cube/cube.cpp
+@@ -1316,7 +1316,12 @@ class wayfire_cube : public wf::per_output_plugin_instance_t, public wf::pointer
+ return;
+ }
+
+- wf::scene::remove_child(render_node);
++ // A render pause may already have removed the node.
++ if (render_node)
++ {
++ wf::scene::remove_child(render_node);
++ }
++
+ output->render->damage_whole();
+
+ render_node = nullptr;
+@@ -1712,6 +1717,29 @@ class wayfire_cube : public wf::per_output_plugin_instance_t, public wf::pointer
+ {
+ hyperspace_background->render(data.target, data.damage,
+ wf::dimensions(output->get_relative_geometry()), opacity);
++ } else if (opacity > 0 && background && (std::string(background_mode) == "skydome" ||
++ std::string(background_mode) == "cubemap"))
++ {
++ // The textures are opaque, so fade them with a constant alpha.
++ GL_CALL(glDisable(GL_DEPTH_TEST));
++ GL_CALL(glDisable(GL_CULL_FACE));
++ if (opacity < 1.0f)
++ {
++ GL_CALL(glEnable(GL_BLEND));
++ GL_CALL(glBlendColor(0, 0, 0, opacity));
++ GL_CALL(glBlendFunc(GL_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_ALPHA));
++ } else
++ {
++ GL_CALL(glDisable(GL_BLEND));
++ }
++
++ // At rest the pose is identity and both modes agree; while
++ // moving, follow the cube's yaw and pitch as upstream does.
++ wf::gles::for_each_scissor_rect(data.target, data.damage, [&]
++ {
++ background->render_frame(data.target, animation, !resting_renderer);
++ });
++ GL_CALL(glDisable(GL_BLEND));
+ } else if (opacity > 0)
+ {
+ // None clears the owned pixels even when the image opacity
+@@ -1981,12 +2009,28 @@ class wayfire_cube : public wf::per_output_plugin_instance_t, public wf::pointer
+ {
+ shutting_down = true;
+ background_timer.disconnect();
+- clear_post_move_window_dim();
+ if (output->is_plugin_active(grab_interface.name))
+ {
+ deactivate();
+ }
++
++ // deactivate() keeps the carried window hidden and dimmed while the
++ // modifiers are held, but the listeners that release it die with this
++ // instance. Restore it and drop every node of ours from the scene.
++ clear_fixed_workspace_overlay(false);
++ clear_post_move_window_dim();
+ remove_rest_renderer();
++ if (render_node)
++ {
++ wf::scene::remove_child(render_node);
++ render_node = nullptr;
++ }
++
++ if (fixed_top_node)
++ {
++ wf::scene::remove_child(fixed_top_node);
++ fixed_top_node = nullptr;
++ }
+
+ wf::gles::run_in_context_if_gles([&]
+ {
+diff --git a/plugins/cube/cubemap.hpp b/plugins/cube/cubemap.hpp
+index e68c8e1..6360612 100644
+--- a/plugins/cube/cubemap.hpp
++++ b/plugins/cube/cubemap.hpp
+@@ -9,12 +9,12 @@ class wf_cube_background_cubemap : public wf_cube_background_base
+ wf_cube_background_cubemap();
+ virtual void render_frame(const wf::render_target_t& fb,
+ wf_cube_animation_attribs& attribs) override;
++ void render_frame(const wf::render_target_t& fb,
++ wf_cube_animation_attribs& attribs, bool follow_cube) override;
+
+ ~wf_cube_background_cubemap();
+
+ private:
+- void render_frame(const wf::render_target_t& fb,
+- wf_cube_animation_attribs& attribs, bool follow_cube);
+ void reload_texture();
+ void create_program();
+
+diff --git a/plugins/cube/simple-background.hpp b/plugins/cube/simple-background.hpp
+index 470aadf..7769269 100644
+--- a/plugins/cube/simple-background.hpp
++++ b/plugins/cube/simple-background.hpp
+@@ -9,6 +9,7 @@ class wf_cube_simple_background : public wf_cube_background_base
+
+ public:
+ wf_cube_simple_background();
++ using wf_cube_background_base::render_frame;
+ virtual void render_frame(const wf::render_target_t& fb,
+ wf_cube_animation_attribs& attribs) override;
+ };
+diff --git a/plugins/cube/skydome.hpp b/plugins/cube/skydome.hpp
+index a64bce9..9df12d0 100644
+--- a/plugins/cube/skydome.hpp
++++ b/plugins/cube/skydome.hpp
+@@ -11,14 +11,14 @@ class wf_cube_background_skydome : public wf_cube_background_base
+ wf_cube_background_skydome(wf::output_t *output);
+ virtual void render_frame(const wf::render_target_t& fb,
+ wf_cube_animation_attribs& attribs) override;
++ void render_frame(const wf::render_target_t& fb,
++ wf_cube_animation_attribs& attribs, bool follow_cube) override;
+
+ virtual ~wf_cube_background_skydome();
+
+ private:
+ wf::output_t *output;
+
+- void render_frame(const wf::render_target_t& fb,
+- wf_cube_animation_attribs& attribs, bool follow_cube);
+ void load_program();
+ void fill_vertices();
+ void reload_texture();
diff --git a/gui-wm/wayfire/files/wayfire-0.11.0-r71-stitch-noop-reload.patch b/gui-wm/wayfire/files/wayfire-0.11.0-r71-stitch-noop-reload.patch
new file mode 100644
index 000000000000..169861ff907a
--- /dev/null
+++ b/gui-wm/wayfire/files/wayfire-0.11.0-r71-stitch-noop-reload.patch
@@ -0,0 +1,170 @@
+diff --git a/src/core/output-layout.cpp b/src/core/output-layout.cpp
+index 1aeb6be..d481851 100644
+--- a/src/core/output-layout.cpp
++++ b/src/core/output-layout.cpp
+@@ -1411,6 +1411,38 @@ class output_layout_t::impl
+ wf::option_wrapper_t<bool> stitch_enabled{"core/stitch_enabled"};
+ wf::option_wrapper_t<std::string> stitch_outputs{"core/stitch_outputs"};
+
++ /* Inputs and result of the last stitched reconfiguration, so that a
++ * config reload which changes nothing relevant does not rebuild the
++ * group and re-scope every view. */
++ struct applied_stitch_t
++ {
++ std::string members;
++ int max_buffer_size;
++ output_configuration_t requested;
++ output_configuration_t applied;
++ std::string scope_settings;
++ };
++ std::optional<applied_stitch_t> applied_stitch;
++
++ /** Options read by window scoping, which reload re-applies to views. */
++ std::string window_scope_settings()
++ {
++ std::string key;
++ for (auto name : {"core/window_scope", "core/window_scope_rules", "grid/zone_gap"})
++ {
++ auto opt = get_core().config->get_option(name);
++ key += (opt ? opt->get_value_str() : "") + '\n';
++ }
++
++ for (auto& [handle, lo] : outputs)
++ {
++ auto opt = lo->config_section ? lo->config_section->get_option_or("zone_gap") : nullptr;
++ key += std::string(handle->name) + '=' + (opt ? opt->get_value_str() : "") + '\n';
++ }
++
++ return key;
++ }
++
+ void release_mirror_buffers()
+ {
+ // A source may be removed in the same transaction. Release its cursor
+@@ -1805,6 +1837,11 @@ class output_layout_t::impl
+ }
+
+ outputs.erase(to_remove);
++ if (applied_stitch)
++ {
++ applied_stitch->requested.erase(to_remove);
++ applied_stitch->applied.erase(to_remove);
++ }
+
+ /* If no physical outputs, then at least the noop output */
+ assert(get_outputs().size() || is_shutting_down());
+@@ -1992,7 +2029,31 @@ class output_layout_t::impl
+ {
+ configuration[output].source = OUTPUT_IMAGE_SOURCE_NONE;
+ }
++ }
++
++ std::string members = stitch_outputs;
++ int max_buffer_size = wf::option_wrapper_t<int>("workarounds/max_buffer_size");
++ if (stitch_output && !removing && stitch_enabled && applied_stitch &&
++ (applied_stitch->members == members) && (applied_stitch->max_buffer_size == max_buffer_size) &&
++ (applied_stitch->requested == configuration) &&
++ (applied_stitch->applied == get_current_configuration()))
++ {
++ // Group, member slices and all other outputs are already as
++ // configured. Only re-scope views if window scope options changed.
++ auto scope_settings = window_scope_settings();
++ if (applied_stitch->scope_settings != scope_settings)
++ {
++ applied_stitch->scope_settings = std::move(scope_settings);
++ rescope_views(scoped_views);
++ }
+
++ return;
++ }
++
++ applied_stitch.reset();
++ auto requested = configuration;
++ for (auto& [output, layout_output] : this->outputs)
++ {
+ layout_output->stitch_source_box.reset();
+ }
+
+@@ -2004,11 +2065,13 @@ class output_layout_t::impl
+
+ // The public mirror state does not include source rectangles; reapply
+ // active groups even when only member order or slice geometry changed.
++ bool applied = false;
+ if (stitch_output || configuration != get_current_configuration())
+ {
+ if (test_configuration(configuration))
+ {
+ apply_configuration(configuration);
++ applied = true;
+ }
+ }
+
+@@ -2022,6 +2085,12 @@ class output_layout_t::impl
+ LOGI("Combined desktop: ", stitch_output->width, "x", stitch_output->height,
+ " on VIRTUAL-1");
+ }
++
++ if (applied && wo)
++ {
++ applied_stitch = applied_stitch_t{members, max_buffer_size, std::move(requested),
++ get_current_configuration(), window_scope_settings()};
++ }
+ } else
+ {
+ for (auto& [handle, lo] : outputs)
+@@ -2032,6 +2101,11 @@ class output_layout_t::impl
+
+ destroy_stitch_output();
+ }
++ rescope_views(scoped_views);
++ }
++
++ void rescope_views(std::vector<std::pair<wayfire_toplevel_view, wf::point_t>>& scoped_views)
++ {
+ for (auto& [view, workspace] : scoped_views)
+ {
+ if (!view->is_mapped() || !view->get_output()) continue;
+@@ -2339,6 +2413,16 @@ class output_layout_t::impl
+ wlr_configuration);
+ }
+
++ void set_stitch_member_power(output_layout_output_t& lo, bool enabled)
++ {
++ lo.stitch_power_off = !enabled;
++ lo.apply_state(lo.current_state);
++ if (auto source = find_output(lo.current_state.mirror_from))
++ {
++ source->render->damage_whole();
++ }
++ }
++
+ void set_power_mode(wlr_output_power_v1_set_mode_event *ev)
+ {
+ LOGD("output: ", ev->output->name, " power mode: ", ev->mode);
+@@ -2358,7 +2442,10 @@ class output_layout_t::impl
+ }
+ if (stitch_output && outputs.at(target)->stitch_source_box)
+ {
+- target = stitch_output;
++ // Power only this member's slice. wlroots reports the member's
++ // new mode to clients when the connector commits enabled/disabled.
++ set_stitch_member_power(*outputs.at(target), !wants_dpms);
++ return;
+ }
+
+ config[target].source = (wants_dpms ? OUTPUT_IMAGE_SOURCE_DPMS : OUTPUT_IMAGE_SOURCE_SELF);
+@@ -2386,13 +2473,7 @@ class output_layout_t::impl
+
+ if (lo->stitch_source_box)
+ {
+- lo->stitch_power_off = !enabled;
+- lo->apply_state(lo->current_state);
+- if (auto source = find_output(lo->current_state.mirror_from))
+- {
+- source->render->damage_whole();
+- }
+-
++ set_stitch_member_power(*lo, enabled);
+ return true;
+ }
+
diff --git a/gui-wm/wayfire/files/wayfire-0.11.0-r71-switch-tile-exclusivity.patch b/gui-wm/wayfire/files/wayfire-0.11.0-r71-switch-tile-exclusivity.patch
new file mode 100644
index 000000000000..53489523c71b
--- /dev/null
+++ b/gui-wm/wayfire/files/wayfire-0.11.0-r71-switch-tile-exclusivity.patch
@@ -0,0 +1,325 @@
+diff --git a/plugins/cube/cube.cpp b/plugins/cube/cube.cpp
+index 3b4b857..813a459 100644
+--- a/plugins/cube/cube.cpp
++++ b/plugins/cube/cube.cpp
+@@ -907,13 +907,13 @@ class wayfire_cube : public wf::per_output_plugin_instance_t, public wf::pointer
+ output->render->schedule_redraw();
+ }
+
+- void animate_workspace_delta(wf::point_t delta,
++ bool animate_workspace_delta(wf::point_t delta,
+ wf::point_t anchor_workspace,
+ wayfire_toplevel_view fixed_view)
+ {
+ if (!activate())
+ {
+- return;
++ return false;
+ }
+
+ end_mouse_overview();
+@@ -957,14 +957,15 @@ class wayfire_cube : public wf::per_output_plugin_instance_t, public wf::pointer
+ start_animation();
+ update_view_matrix();
+ output->render->schedule_redraw();
++ return true;
+ }
+
+ wf::signal::connection_t<cube_workspace_animation_signal>
+ on_cube_workspace_animation = [=] (cube_workspace_animation_signal *ev)
+ {
+- animate_workspace_delta(ev->delta, ev->anchor_workspace,
+- ev->fixed_view);
+- ev->carried_out = true;
++ // Report failure so vswitch can fall back to its own animation.
++ ev->carried_out = animate_workspace_delta(ev->delta,
++ ev->anchor_workspace, ev->fixed_view);
+ };
+
+ wf::signal::connection_t<cube_render_pause_signal>
+diff --git a/plugins/cube/meson.build b/plugins/cube/meson.build
+index 48ea276..af57b9a 100644
+--- a/plugins/cube/meson.build
++++ b/plugins/cube/meson.build
+@@ -2,5 +2,7 @@ animiate = shared_module('cube',
+ ['cube.cpp', 'cubemap.cpp', 'skydome.cpp', 'simple-background.cpp', 'warp-background.cpp', 'hyperspace-background.cpp'],
+ include_directories: [wayfire_api_inc, wayfire_conf_inc, plugins_common_inc, ipc_include_dirs],
+ dependencies: [wlroots, pixman, wfconfig, json, plugin_pch_dep],
++ link_args: ['-Wl,--version-script=' + meson.project_source_root() / 'plugins/common/plugin-exports.map'],
++ link_depends: '../../plugins/common/plugin-exports.map',
+ install: true,
+ install_dir: join_paths(get_option('libdir'), 'wayfire'))
+diff --git a/plugins/grid/grid.cpp b/plugins/grid/grid.cpp
+index ed24b42..46bf79e 100644
+--- a/plugins/grid/grid.cpp
++++ b/plugins/grid/grid.cpp
+@@ -294,6 +294,8 @@ class wayfire_grid : public wf::plugin_interface_t, public wf::per_output_tracke
+ public:
+ void init() override
+ {
++ wf::get_core().store_data(std::make_unique<wf::custom_data_t>(),
++ wf::grid::grid_loaded_marker);
+ init_output_tracking();
+ // Older metadata leaves new ini options untyped, or absent. Promote
+ // them before loading activators, preserving any configured binding.
+@@ -465,6 +467,7 @@ class wayfire_grid : public wf::plugin_interface_t, public wf::per_output_tracke
+ void fini() override
+ {
+ fini_output_tracking();
++ wf::get_core().erase_data(wf::grid::grid_loaded_marker);
+ }
+
+ bool can_adjust_view(wayfire_toplevel_view view)
+diff --git a/plugins/grid/meson.build b/plugins/grid/meson.build
+index e107495..71c5746 100644
+--- a/plugins/grid/meson.build
++++ b/plugins/grid/meson.build
+@@ -5,6 +5,8 @@ all_deps = [wlroots, pixman, wfconfig, wftouch, cairo, json, plugin_pch_dep]
+ shared_module('grid', ['grid.cpp'],
+ include_directories: all_include_dirs,
+ dependencies: all_deps,
++ link_args: ['-Wl,--version-script=' + meson.project_source_root() / 'plugins/common/plugin-exports.map'],
++ link_depends: '../../plugins/common/plugin-exports.map',
+ install: true,
+ install_dir: conf_data.get('PLUGIN_PATH'))
+
+diff --git a/plugins/grid/wayfire/plugins/grid.hpp b/plugins/grid/wayfire/plugins/grid.hpp
+index d981735..1da956e 100644
+--- a/plugins/grid/wayfire/plugins/grid.hpp
++++ b/plugins/grid/wayfire/plugins/grid.hpp
+@@ -23,6 +23,13 @@ enum move_op_t
+ MOVE_OP_FINISH_PREVIEW = 3,
+ };
+
++/**
++ * Core custom data name present while the Grid plugin is loaded, so that
++ * move-snap leaves view_tile_request_signal to Grid regardless of load order.
++ * Checked by name only: custom data types do not compare across plugin builds.
++ */
++static constexpr const char *grid_loaded_marker = "wf-grid-plugin-loaded";
++
+ /**
+ * name: request
+ * on: core
+diff --git a/plugins/single_plugins/meson.build b/plugins/single_plugins/meson.build
+index f2acc10..716b824 100644
+--- a/plugins/single_plugins/meson.build
++++ b/plugins/single_plugins/meson.build
+@@ -18,11 +18,19 @@ extra_libs = {
+ 'vswipe': [workspace_wall],
+ }
+
++# Export only the plugin entry points, so that live-swapped revisions do not
++# bind to each other's inline/template symbols.
++export_map = 'plugins/common/plugin-exports.map'
++export_map_plugins = ['move']
++
+ foreach plugin : plugins
+ plugin_module = shared_module(plugin, plugin + '.cpp',
+ include_directories: all_include_dirs,
+ dependencies: all_deps,
+ link_with: extra_libs.get(plugin, []),
++ link_args: plugin in export_map_plugins ?
++ ['-Wl,--version-script=' + meson.project_source_root() / export_map] : [],
++ link_depends: plugin in export_map_plugins ? '../../' + export_map : [],
+ install: true,
+ install_dir: conf_data.get('PLUGIN_PATH'))
+
+diff --git a/plugins/single_plugins/move-snap.hpp b/plugins/single_plugins/move-snap.hpp
+index ddcb70f..2c2fce5 100644
+--- a/plugins/single_plugins/move-snap.hpp
++++ b/plugins/single_plugins/move-snap.hpp
+@@ -9,6 +9,7 @@
+ #include <wayfire/workspace-set.hpp>
+ #include <wayfire/toplevel.hpp>
+ #include <wayfire/plugins/common/move-drag-interface.hpp>
++#include <wayfire/plugins/grid.hpp>
+ #include <wayfire/plugins/wobbly/wobbly-signal.hpp>
+ #include <chrono>
+ #include "move-snap-map.hpp"
+@@ -300,7 +301,8 @@ class move_snap_t : public wf::per_output_tracker_mixin_t<>
+ // Without Grid, ordinary tiling reflows still preserve half/quarter sizes.
+ wf::signal::connection_t<wf::view_tile_request_signal> on_tile = [this] (auto *ev)
+ {
+- if (ev->carried_out || !eligible(ev->view) || !ev->edges) return;
++ if (ev->carried_out || wf::get_core().has_data(wf::grid::grid_loaded_marker)) return;
++ if (!eligible(ev->view) || !ev->edges) return;
+ int slot = wf::grid::get_slot_from_tiled_edges(ev->edges);
+ if (!slot) return;
+ auto area = wf::window_scope_workarea(ev->view);
+diff --git a/plugins/tile/tile-plugin.cpp b/plugins/tile/tile-plugin.cpp
+index 5d66c4d..ab03b2f 100644
+--- a/plugins/tile/tile-plugin.cpp
++++ b/plugins/tile/tile-plugin.cpp
+@@ -183,10 +183,26 @@ class tile_output_plugin_t : public wf::pointer_interaction_t, public wf::custom
+ ev->carried_out = true;
+ stop_controller(true);
+ auto& data = tile_workspace_set_data_t::get(ev->view->get_wset());
+- data.reconcile_view(ev->view, ev->workspace);
++ reconcile_requested_view(data, ev->view, ev->workspace);
+ data.update_root_size();
+ };
+
++ /**
++ * Most tile/fullscreen requests carry the output's current workspace, not
++ * the view's. Only an explicit window-scope reflow may move the node to
++ * another workspace; otherwise just reconcile its region on its own one.
++ */
++ void reconcile_requested_view(tile_workspace_set_data_t& data,
++ wayfire_toplevel_view view, wf::point_t requested)
++ {
++ auto vp = view->has_data<wf::window_scope_reflow_t>() ?
++ std::optional<wf::point_t>{requested} : data.workspace_of(view);
++ if (vp)
++ {
++ data.reconcile_view(view, *vp);
++ }
++ }
++
+ wf::signal::connection_t<view_fullscreen_request_signal> on_fullscreen_request =
+ [=] (view_fullscreen_request_signal *ev)
+ {
+@@ -196,8 +212,9 @@ class tile_output_plugin_t : public wf::pointer_interaction_t, public wf::custom
+ }
+
+ ev->carried_out = true;
+- tile_workspace_set_data_t::get(ev->view->get_wset()).reconcile_view(ev->view, ev->workspace);
+- tile_workspace_set_data_t::get(ev->view->get_wset()).set_view_fullscreen(ev->view, ev->state);
++ auto& data = tile_workspace_set_data_t::get(ev->view->get_wset());
++ reconcile_requested_view(data, ev->view, ev->workspace);
++ data.set_view_fullscreen(ev->view, ev->state);
+ };
+
+ void change_view_workspace(wayfire_toplevel_view view, std::optional<wf::point_t> vp = {})
+diff --git a/plugins/tile/tile-wset.hpp b/plugins/tile/tile-wset.hpp
+index a695bc3..6a6f17a 100644
+--- a/plugins/tile/tile-wset.hpp
++++ b/plugins/tile/tile-wset.hpp
+@@ -117,8 +117,27 @@ class tile_workspace_set_data_t : public wf::custom_data_t
+ return output->workarea->get_workarea();
+ }
+
++ wf::point_t clamp_workspace(wf::point_t vp)
++ {
++ auto size = wset.lock()->get_workspace_grid_size();
++ return {std::clamp(vp.x, 0, size.width - 1), std::clamp(vp.y, 0, size.height - 1)};
++ }
++
++ /** The workspace whose (virtual or physical) root holds the view's node. */
++ std::optional<wf::point_t> workspace_of(wayfire_toplevel_view view)
++ {
++ auto node = tile::view_node_t::get_node(view);
++ if (!node) return {};
++ auto root = tile::get_root(node);
++ std::optional<wf::point_t> result;
++ for_each_root([&] (auto& candidate, auto vp, const auto&)
++ { if (candidate.get() == root.get()) result = vp; });
++ return result;
++ }
++
+ std::unique_ptr<tile::tree_node_t>& root_at(wf::point_t vp, const std::string& monitor = "")
+ {
++ vp = clamp_workspace(vp);
+ if (monitor.empty()) return roots[vp.x][vp.y];
+ auto& grid = physical_roots[monitor];
+ auto size = wset.lock()->get_workspace_grid_size();
+@@ -166,6 +185,7 @@ class tile_workspace_set_data_t : public wf::custom_data_t
+ {
+ auto node = tile::view_node_t::get_node(view);
+ if (!node) return;
++ vp = clamp_workspace(vp);
+ auto& destination = root_at(vp, region_for(view));
+ if (tile::get_root(node).get() == destination.get()) return;
+ autocommit_transaction_t tx;
+diff --git a/plugins/vswitch/vswitch.cpp b/plugins/vswitch/vswitch.cpp
+index 34b63be..3ad44e8 100644
+--- a/plugins/vswitch/vswitch.cpp
++++ b/plugins/vswitch/vswitch.cpp
+@@ -387,6 +387,15 @@ class vswitch : public wf::per_output_plugin_instance_t
+ on_done();
+ }
+
++ /** The workspace has already changed: slide in from @old_ws. */
++ void slide_from(wf::point_t old_ws)
++ {
++ auto cws = output->wset()->get_current_workspace();
++ animation.dx.set(old_ws.x - cws.x, 0);
++ animation.dy.set(old_ws.y - cws.y, 0);
++ animation.start();
++ }
++
+ private:
+ std::function<void()> on_done;
+ };
+@@ -665,27 +674,36 @@ class vswitch : public wf::per_output_plugin_instance_t
+
+ if (!is_active())
+ {
++ // Respect plugin exclusivity (expo, scale, ...) like upstream's
++ // start_switch() did.
++ if (!output->can_activate_plugin(&grab_interface))
++ {
++ return false;
++ }
++
+ if (fixed_view)
+ {
+ clear_held_window_dim();
+ wf::vswitch::move_view(fixed_view, target_ws);
+ output->wset()->set_workspace(target_ws);
+ wf::get_core().seat->focus_view(view);
+-
+- cube_workspace_animation_signal ev;
+- ev.delta = animation_delta;
+- ev.anchor_workspace = cws;
+- ev.fixed_view = fixed_view;
+- output->emit(&ev);
+- return true;
++ } else
++ {
++ output->wset()->set_workspace(target_ws);
+ }
+
+- output->wset()->set_workspace(target_ws);
+ cube_workspace_animation_signal ev;
+ ev.delta = animation_delta;
+ ev.anchor_workspace = cws;
+- ev.fixed_view = nullptr;
++ ev.fixed_view = fixed_view;
+ output->emit(&ev);
++
++ // Without a cube to animate it, use vswitch's own slide.
++ if (!ev.carried_out && start_switch())
++ {
++ algorithm->slide_from(cws);
++ }
++
+ return true;
+ }
+
+@@ -695,8 +713,7 @@ class vswitch : public wf::per_output_plugin_instance_t
+ }
+
+ algorithm->set_overlay_view(toplevel_cast(view));
+- algorithm->set_target_workspace(
+- output->wset()->get_current_workspace() + delta);
++ algorithm->set_target_workspace(target_ws);
+
+ return true;
+ }
+@@ -781,15 +798,6 @@ class vswitch : public wf::per_output_plugin_instance_t
+ return;
+ }
+
+- if (!ev->fixed_views.empty())
+- {
+- cube_workspace_animation_signal cube_ev;
+- cube_ev.delta = ev->new_viewport - ev->old_viewport;
+- cube_ev.anchor_workspace = ev->old_viewport;
+- cube_ev.fixed_view = ev->fixed_views[0];
+- output->emit(&cube_ev);
+- }
+-
+ if (is_active())
+ {
+ ev->carried_out = add_direction(ev->new_viewport - ev->old_viewport);
diff --git a/gui-wm/wayfire/wayfire-0.11.0-r70.ebuild b/gui-wm/wayfire/wayfire-0.11.0-r71.ebuild
index 8efecefbb78f..ce38e25cb609 100644
--- a/gui-wm/wayfire/wayfire-0.11.0-r70.ebuild
+++ b/gui-wm/wayfire/wayfire-0.11.0-r71.ebuild
@@ -156,6 +156,9 @@ PATCHES=(
"${FILESDIR}"/${P}-r67-grid-gap.patch
"${FILESDIR}"/${P}-r68-ipc-control.patch
"${FILESDIR}"/${P}-ipc-backpressure.patch
+ "${FILESDIR}"/${P}-r71-switch-tile-exclusivity.patch
+ "${FILESDIR}"/${P}-r71-cube-teardown-backgrounds.patch
+ "${FILESDIR}"/${P}-r71-stitch-noop-reload.patch
)
# The local patches change the plugin ABI (scene layer indices, class layouts).