Skip to content

Add multimonitor support for movewindow#178

Open
ojasookert wants to merge 6 commits into
outfoxxed:masterfrom
ojasookert:master
Open

Add multimonitor support for movewindow#178
ojasookert wants to merge 6 commits into
outfoxxed:masterfrom
ojasookert:master

Conversation

@ojasookert

Copy link
Copy Markdown

This PR makes progress on resolving #162 by enabling hy3:movewindow to move focused windows between monitors. While it functions as expected in most cases, it does not always ensure the window is placed exactly at the edge of the next monitor.

It has been working well for my personal use cases, but further testing is required to validate its behavior across different setups. Feedback and additional testing would be greatly appreciated!

Looking forward to any insights or suggestions. 🚀

@theammir

Copy link
Copy Markdown

i'm not sure if i have the plugin set up correctly, but it seemingly makes no difference on my machine. I'll try to investigate further

@outfoxxed

Copy link
Copy Markdown
Owner

I can't get it to shift across monitors either.

@outfoxxed

Copy link
Copy Markdown
Owner

Actually I was testing it wrong. It does work, kind of. Focus gets locked into an odd state that doesn't match what's happening visually after a move, and I would like windows placed at matching monitor edges where possible.

@outfoxxed

Copy link
Copy Markdown
Owner

Also a single window can't be moved across monitors. It needs at least 2 for some reason.

@ojasookert

Copy link
Copy Markdown
Author

Thanks for retesting @outfoxxed, I should have noted that I have cursor warping enabled with configuration, which avoids the focus issue for me:

# Move window with mainMod SHIFT+ arrow keys
bind = $mainMod SHIFT, left, hy3:movewindow, l
bind = $mainMod SHIFT, left, hy3:warpcursor 
[...]

@ojasookert
ojasookert marked this pull request as draft February 1, 2025 10:40
@ojasookert

Copy link
Copy Markdown
Author

Marking this as a draft until noted issues are fixed.

@ojasookert

Copy link
Copy Markdown
Author

Added changes to detect edge placement, it should now place windows at the correct edge when moving across monitors.

@ojasookert

Copy link
Copy Markdown
Author

Ready for another review.

@ojasookert
ojasookert marked this pull request as ready for review February 1, 2025 14:00
@ojasookert ojasookert changed the title Improve Window Transition Between Monitors (Partial Fix for #162) Add multimonitor support for movewindow Feb 1, 2025
@theammir

theammir commented Feb 1, 2025

Copy link
Copy Markdown

I use hyprsplit, and my secondary monitor is on top of the primary.
There is a single window on the first workspace of the primary display, and two windows on the second.

When the first workspace of the second display is active, and I'm moving a window upwards like so:
image
it moves to the first workspace of the primary display instead.

I'll try with regular workspaces later.

@ojasookert

Copy link
Copy Markdown
Author

@theammir I tried to reproduce the issue with hyprsplit but sadly could not - it works as expected. Perhaps you could share your configs in a gist so I could try to reproduce the issue?

@theammir

theammir commented Feb 3, 2025

Copy link
Copy Markdown

Here's the minimal setup. The plugins are ojasookert/hy3 on master and shezdy/hyprsplit on main

hyprland -v
Hyprland 0.47.0 built from branch main at commit 1d3904c3e7a8b74ea83669f73ee408bd38390b11  (configmgr: properly free glob memory).
Date: Tue Jan 28 12:19:52 2025
Tag: v0.47.0-9-g1d3904c3, commits: 5751
built against:
 aquamarine 0.7.1
 hyprlang 0.6.0
 hyprutils 0.5.0
 hyprcursor 0.1.11
 hyprgraphics 0.1.1


no flags were set
hyprland.conf
monitor = eDP-1,    1920x1080@144, 0x0,     1
monitor = HDMI-A-1, 1920x1080@60,  0x-1080, 1

exec-once = hyprpm reload -nn

plugin {
    hy3 {
        autotile {
            enable = true
        }
    }

    hyprsplit {
        num_workspaces = 5
        persistent_workspaces = true
    }
}

general {
    layout = hy3
}


$mainMod = SUPER
$shiftMod = SUPERSHIFT
$altMod = SUPERALT

bind = $mainMod, h, hy3:movefocus, l
bind = $mainMod, l, hy3:movefocus, r
bind = $mainMod, k, hy3:movefocus, u
bind = $mainMod, j, hy3:movefocus, d

bind = $shiftMod, h, hy3:movewindow, l
bind = $shiftMod, l, hy3:movewindow, r
bind = $shiftMod, k, hy3:movewindow, u
bind = $shiftMod, j, hy3:movewindow, d

bind = $mainMod, 1, split:workspace, 1
bind = $mainMod, 2, split:workspace, 2
bind = $mainMod, 3, split:workspace, 3
bind = $mainMod, 4, split:workspace, 4
bind = $mainMod, 5, split:workspace, 5

bind = $shiftMod, 1, hy3:movetoworkspace, 1, follow
bind = $shiftMod, 2, hy3:movetoworkspace, 2, follow
bind = $shiftMod, 3, hy3:movetoworkspace, 3, follow
bind = $shiftMod, 4, hy3:movetoworkspace, 4, follow
bind = $shiftMod, 5, hy3:movetoworkspace, 5, follow

@ojasookert

ojasookert commented Feb 5, 2025

Copy link
Copy Markdown
Author

@theammir Thank you for providing the minimal configuration. Sadly, I still could not reproduce the issue - the focused window hops monitors perfectly using hy3:movewindow even with the persistent workspaces defined in hyprsplit config.

I have to admit, I am not quite sure what's the expected behaviour of hy3:movetoworkspace when using hyprsplit - are you perhaps trying to use that dispatcher to move the window? Note that I found mention of a similar issue you are describing here.

Can you verify that both plugins are up to date?

@theammir

theammir commented Feb 5, 2025

Copy link
Copy Markdown

@ojasookert Thanks for mentioning movetoworkspace, but it does work as expected when moving windows within a single monitor with hy3's dispatcher since the very pull request you linked.
The issue I'm describing occurs with hy3:movewindow, but I thought I'd still include all the binds I use to control windows.

Expanding on the description of behaviour I'm experiencing, it comes to this: when moving a window towards the secondary monitor, it moves to the equally-numbered workspace of the primary instead. Similar thing happens when moving downwards. At the same time, no movement occurs at all when both monitors are on equal workspaces or when I'm trying to move the only window of a workspace.

Regarding the plugin versions, I do find it really odd how hyprpm list doesn't output neither full remote urls nor current revisions, however hyprpm update tells me this:

✔ Headers up to date.

→ checking for updates for hyprsplit                                                                                                                                                                                
→ Cloning https://github.com/shezdy/hyprsplit                                                                                                                                                                       
→ Plugin has revision set, resetting: main                                                                                                                                                                          
✔ repository hyprsplit is up-to-date.                                                                                                                                                                               
→ checking for updates for hy3                                                                                                                                                                                      
→ Cloning https://github.com/ojasookert/hy3                                                                                                                                                                         
→ Plugin has revision set, resetting: 5f5e9550cca1242cc01c0c66537aa25f4a99b5db                                                                                                                                      
✔ repository hy3 is up-to-date.                                                                                                                                                                                     
 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  6 / 6  Done!                                                                                                                                                   
✔ Ensuring plugin load state
✔ Plugin load state ensured

At this point, I'm not sure what's the issue on my side. Am I missing something?

@daniel-hernandez-sony

Copy link
Copy Markdown

Very much looking forward to this. Thank you everyone for working on this and testing. Let me know if another opinion / test could be useful

@Dimfred

Dimfred commented Feb 27, 2025

Copy link
Copy Markdown

@ojasookert thank you!
Checked out your branch and am running it. Works fine. But it seems that building hy3 on my own, kinda broke the font and some other things which is a little weird.
Any ideas?

@Dimfred

Dimfred commented Apr 6, 2025

Copy link
Copy Markdown

Any progress on this?

@niteria

niteria commented Mar 21, 2026

Copy link
Copy Markdown

I had bots write a workaround for me: https://gist.github.com/niteria/af6de2044204462d7978f68de2013cf2

@rofrol

rofrol commented Jul 20, 2026

Copy link
Copy Markdown

Maybe try this patch

diff --git a/src/Hy3Layout.cpp b/src/Hy3Layout.cpp
index 5a45a5b..3bc7399 100644
--- a/src/Hy3Layout.cpp
+++ b/src/Hy3Layout.cpp
@@ -967,7 +967,13 @@ bool Hy3Layout::shiftMonitor(Hy3Node& node, ShiftDirection direction, bool follo
 		g_pCompositor->setActiveMonitor(next_monitor);
 		auto next_workspace = next_monitor->activeWorkspace;
 		if (next_workspace) {
-			moveNodeToWorkspace(node.workspace.get(), next_workspace->m_szName, follow, false);
+			// `direction` must be forwarded so the node lands on the correct
+			// edge of the target monitor's tree (front vs. back insertion in
+			// moveNodeToWorkspace). Previously this defaulted to
+			// ShiftDirection::Right regardless of the actual move direction,
+			// which is why moving up/left could land the window on the wrong
+			// workspace/edge.
+			moveNodeToWorkspace(node.workspace.get(), next_workspace->m_szName, follow, true, direction);
 			return true;
 		}
 	}
@@ -1752,40 +1758,44 @@ bool shiftMatchesLayout(Hy3GroupLayout layout, ShiftDirection direction) {
 }
 
 bool Hy3Layout::isAtEdgeWithNoMovement(Hy3Node& node, ShiftDirection direction) {
-    auto* parent = node.parent;
-    if (!parent) return true; // No parent means we're definitely at an edge
-    
-    auto& group = parent->data.as_group();
-    auto& children = group.children;
-    
-    // First check if we're at the edge of our current group
-    bool at_immediate_edge = (shiftIsForward(direction) && &node == children.back()) ||
-                           (!shiftIsForward(direction) && &node == children.front());
-    
-    if (!at_immediate_edge) return false; // If we're not at the edge, movement is possible
-    
-    // If we're in a group that matches our movement direction
-    if (shiftMatchesLayout(group.layout, direction)) {
-        // If we're at root, we're at a true edge
-        if (parent->parent == nullptr) return true;
-        
-        // Check if our parent group has any siblings in the direction we want to move
-        auto* grandparent = parent->parent;
-        auto& grandparent_group = grandparent->data.as_group();
-        auto parent_iter = std::find(grandparent_group.children.begin(), 
-                                   grandparent_group.children.end(), 
-                                   parent);
-        
-        if (shiftIsForward(direction)) {
-            return parent_iter == std::prev(grandparent_group.children.end());
-        } else {
-            return parent_iter == grandparent_group.children.begin();
+    Hy3Node* origin = &node;
+    auto* parent = origin->parent;
+
+    if (!parent) return true; // lone node on the workspace, nowhere else to go
+
+    // Walk all the way up to the workspace's outermost group, mirroring the
+    // break-out loop further down in shiftOrGetFocus(). Previously this only
+    // inspected the immediate grandparent, which meant deeply nested trees
+    // (3+ levels) could report "at edge" even when an ancestor further up
+    // still had room to shift into, causing a premature jump to the next
+    // monitor instead of a normal same-monitor move.
+    while (true) {
+        auto& group = parent->data.as_group();
+
+        if (group.layout != Hy3GroupLayout::Tabbed && shiftMatchesLayout(group.layout, direction)) {
+            bool at_edge_here = (shiftIsForward(direction) && origin == group.children.back())
+                              || (!shiftIsForward(direction) && origin == group.children.front());
+
+            // Not at the edge of a group whose orientation matches the shift
+            // direction -> there's room to move within the current monitor.
+            if (!at_edge_here) return false;
         }
+        // Groups whose orientation doesn't match the shift direction (or
+        // tabbed groups) are transparent for this check: hy3 always breaks
+        // straight through them, so we keep walking up regardless of
+        // position within them.
+
+        if (parent->parent == nullptr) {
+            // Reached the workspace's outermost group. We're only truly at
+            // a workspace edge if this group's own orientation matches the
+            // shift direction; otherwise shiftOrGetFocus() will just wrap it
+            // in a new group to make room, so it isn't a real dead end.
+            return group.layout != Hy3GroupLayout::Tabbed && shiftMatchesLayout(group.layout, direction);
+        }
+
+        origin = parent;
+        parent = origin->parent;
     }
-    
-    // If we're in a group that doesn't match our movement direction,
-    // check if there are any possible movements in the parent's layout
-    return group.children.size() <= 1;
 }
 
 Hy3Node* Hy3Layout::shiftOrGetFocus(
@@ -1796,19 +1806,26 @@ Hy3Node* Hy3Layout::shiftOrGetFocus(
     bool visible
 ) {
 
-    auto* parent = node.parent;
-    if (shift && parent && parent->parent == nullptr &&
-        parent->data.as_group().layout != Hy3GroupLayout::Tabbed &&
-        shiftMatchesLayout(parent->data.as_group().layout, direction)) {
-
-        if (isAtEdgeWithNoMovement(node, direction)) {
-            auto next_monitor = g_pCompositor->getMonitorInDirection(getShiftDirectionChar(direction));
-            if (next_monitor && next_monitor->activeWorkspace) {
-                moveNodeToWorkspace(node.workspace.get(), 
-                                  next_monitor->activeWorkspace->m_szName, 
-                                  true, true, direction);
-                return nullptr;
-            }
+    if (shift) {
+        auto* parent = node.parent;
+        bool tabbed_parent =
+            parent != nullptr && parent->data.as_group().layout == Hy3GroupLayout::Tabbed;
+
+        // isAtEdgeWithNoMovement already returns true when `parent` is null
+        // (a lone window on the workspace with no group yet), and now walks
+        // all the way up to the workspace's outermost group to check for
+        // further movement, so we no longer artificially restrict this to
+        // `parent->parent == nullptr`. That restriction was skipping both
+        // lone windows and windows nested more than one group deep, silently
+        // no-op'ing hy3:movewindow for them.
+        if (!tabbed_parent && isAtEdgeWithNoMovement(node, direction)) {
+            // shiftMonitor() (below) sets the active monitor via
+            // g_pCompositor->setActiveMonitor() before moving the node,
+            // which keeps Hyprland's own focus/monitor state in sync with
+            // the move. The previous inline duplicate of this logic here
+            // didn't do that, which is what caused focus to visually
+            // desync after a cross-monitor move.
+            if (this->shiftMonitor(node, direction, /* follow = */ true)) return nullptr;
         }
     }
 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants