feat(input): focus follows mouse on niri and mango

This commit is contained in:
purian23 committed 2026-10-12 02:27:21 -04:00
1 parent 80ca0f0ee5
commit ce389d2e87
11 files changed
+37 -42

No files matched your search

+1 -1
View File
@@ -245,7 +245,7 @@ Singleton {
property int mangoLayoutGapsOutOverride: Spec.SPEC.mangoLayoutGapsOutOverride.def
property int mangoLayoutRadiusOverride: Spec.SPEC.mangoLayoutRadiusOverride.def
property int mangoLayoutBorderSize: Spec.SPEC.mangoLayoutBorderSize.def
property int hyprlandFollowMouse: Spec.SPEC.hyprlandFollowMouse.def
property int focusFollowsMouse: Spec.SPEC.focusFollowsMouse.def
property string mouseAccelProfile: Spec.SPEC.mouseAccelProfile.def
property real mouseAccelSpeed: Spec.SPEC.mouseAccelSpeed.def
property bool mouseLeftHanded: Spec.SPEC.mouseLeftHanded.def
+1 -1
View File
@@ -315,7 +315,7 @@ var LOCAL_SPEC = {
def: -1,
onChange: "updateCompositorLayout"
},
hyprlandFollowMouse: {
focusFollowsMouse: {
def: -1,
onChange: "updateCompositorInput"
},
@@ -33,15 +33,16 @@ Item {
iconName: "mouse"
SettingsToggleRow {
visible: CompositorService.isHyprland && HyprlandService.hyprSupports("input:follow_mouse")
enabled: inputInclude.included && !inputInclude.readOnly && HyprlandService.luaConfigActive && HyprlandService.followMouseMode >= 0
tags: ["mouse", "focus", "follow", "pointer", "window", "hyprland"]
settingKey: "hyprlandFollowMouse"
resetKeys: ["hyprlandFollowMouse"]
text: I18n.tr("Focus follows mouse", "Hyprland toggle that focuses a window when the pointer moves over it")
description: I18n.tr("Focus windows when the pointer moves over them.", "Description of the Hyprland focus follows mouse toggle")
checked: (SettingsData.hyprlandFollowMouse < 0 ? HyprlandService.followMouseMode : SettingsData.hyprlandFollowMouse) === 1
onToggled: checked => SettingsData.set("hyprlandFollowMouse", checked ? 1 : 0)
// Only Hyprland can report the live mode; untouched niri and Mango show their defaults
readonly property int inheritedMode: CompositorService.isHyprland ? HyprlandService.followMouseMode : CompositorService.isMango ? 1 : 0
visible: CompositorService.supportsInputConfig && (!CompositorService.isHyprland || HyprlandService.hyprSupports("input:follow_mouse"))
enabled: inputInclude.included && !inputInclude.readOnly && (!CompositorService.isHyprland || (HyprlandService.luaConfigActive && HyprlandService.followMouseMode >= 0))
tags: ["mouse", "focus", "follow", "pointer", "window", "sloppy"]
settingKey: "focusFollowsMouse"
resetKeys: ["focusFollowsMouse"]
text: I18n.tr("Focus follows mouse", "Toggle that focuses a window when the pointer moves over it")
checked: (SettingsData.focusFollowsMouse < 0 ? inheritedMode : SettingsData.focusFollowsMouse) === 1
onToggled: checked => SettingsData.set("focusFollowsMouse", checked ? 1 : 0)
}
SettingsSliderRow {
@@ -483,6 +483,8 @@ Singleton {
function onRawEvent(event) {
if (event.name === "monitoraddedv2" || event.name === "monitorremoved" || event.name === "configreloaded") {
if (event.name === "configreloaded")
HyprlandService.refreshFollowMouse();
root.refreshHyprlandMonitorLayout();
return;
}
+2 -10
View File
@@ -239,14 +239,6 @@ Singleton {
});
}
Connections {
target: CompositorService.isHyprland ? Hyprland : null
function onRawEvent(event) {
if (event.name === "configreloaded")
root.refreshFollowMouse();
}
}
function loadHyprOptionNames() {
if (!CompositorService.isHyprland || hyprOptionsReady || hyprOptionsLoading)
return;
@@ -848,8 +840,8 @@ hl.layer_rule({
lines.push(line);
};
// -1 leaves existing follow_mouse modes untouched until the user operates the toggle.
if (s.hyprlandFollowMouse === 0 || s.hyprlandFollowMouse === 1)
add("input:follow_mouse", `follow_mouse = ${s.hyprlandFollowMouse},`);
if (s.focusFollowsMouse === 0 || s.focusFollowsMouse === 1)
add("input:follow_mouse", `follow_mouse = ${s.focusFollowsMouse},`);
if (s.mouseAccelSpeed !== 0)
add("input:sensitivity", `sensitivity = ${s.mouseAccelSpeed.toFixed(2)},`);
if (s.mouseAccelProfile !== "default")
+1
View File
@@ -780,6 +780,7 @@ cursor_size=${size}`;
add("mouse_middle_button_emulation", flag(s.mouseMiddleEmulation));
add("axis_scroll_factor", s.mouseScrollFactor.toFixed(2));
add("mouse_scroll_method", scrollMethod(s.mouseScrollMethod));
add("sloppy_focus", s.focusFollowsMouse);
add("tap_to_click", flag(s.touchpadTapToClick));
add("tap_and_drag", flag(s.touchpadTapAndDrag));
add("drag_lock", flag(s.touchpadDragLock));
+4
View File
@@ -1748,6 +1748,10 @@ window-rule {
`;
let inputContent = dmsWarning + "input {\n";
// Niri flags take no false, so off can only leave it out
if (typeof SettingsData !== "undefined" && SettingsData.focusFollowsMouse === 1) {
inputContent += " focus-follows-mouse max-scroll-amount=\"0%\"\n";
}
// Mouse block
inputContent += " mouse {\n";
+5 -1
View File
@@ -61,7 +61,7 @@ test("mango input fragment maps choices onto libinput numbers and spells numlock
assert.ok(found, "mangoInputLines not found in MangoService.qml");
const scope = vm.createContext({});
vm.runInContext(found[0], scope);
const defaults = Object.fromEntries(Object.keys(SPEC).filter(key => /^(mouse|touchpad|keyboard)[A-Z]/.test(key)).map(key => [key, SPEC[key].def]));
const defaults = Object.fromEntries(Object.keys(SPEC).filter(key => /^(mouse|touchpad|keyboard)[A-Z]|^focusFollowsMouse$/.test(key)).map(key => [key, SPEC[key].def]));
const lines = (overrides, snake = false) => [...scope.mangoInputLines(Object.assign({}, defaults, overrides), snake)];
const value = (list, key) => list.find(line => line.startsWith(key + "="))?.slice(key.length + 1);
@@ -81,4 +81,8 @@ test("mango input fragment maps choices onto libinput numbers and spells numlock
assert.equal(value(chosen, "repeat_delay"), "300");
assert.equal(value(chosen, "numlockon"), "0");
assert.equal(value(lines({ keyboardNumlock: true }, true), "numlock_on"), "1");
assert.equal(value(stock, "sloppy_focus"), undefined, "untouched focus mode stays Mango's");
for (const mode of [0, 1])
assert.equal(value(lines({ focusFollowsMouse: mode }), "sloppy_focus"), String(mode));
});
+1 -1
View File
@@ -145,7 +145,7 @@ test("input: turning natural scroll off is written so the template's true cannot
});
test("input: focus follows mouse writes both on and off, leaves unmanaged modes alone, and skips unsupported options", () => {
const modeLine = mode => inputLines(input({ hyprlandFollowMouse: mode })).filter(line => line.startsWith("follow_mouse ="));
const modeLine = mode => inputLines(input({ focusFollowsMouse: mode })).filter(line => line.startsWith("follow_mouse ="));
assert.deepEqual(modeLine(1), ["follow_mouse = 1,"]);
assert.deepEqual(modeLine(0), ["follow_mouse = 0,"]);
assert.deepEqual(modeLine(-1), []);
@@ -53,11 +53,11 @@ test("a coerced key normalises a percentage and leaves a unit value alone", () =
test("a focus override persists both off and on, and resetting removes it", () => {
const root = defaultRoot();
for (const mode of [0, 1]) {
root.hyprlandFollowMouse = mode;
root.focusFollowsMouse = mode;
const json = store.toJson(root, new Set());
assert.equal(json.hyprlandFollowMouse, mode);
assert.equal(loaded(json).hyprlandFollowMouse, mode);
assert.equal(json.focusFollowsMouse, mode);
assert.equal(loaded(json).focusFollowsMouse, mode);
}
root.hyprlandFollowMouse = -1;
assert.equal("hyprlandFollowMouse" in store.toJson(root, new Set()), false);
root.focusFollowsMouse = -1;
assert.equal("focusFollowsMouse" in store.toJson(root, new Set()), false);
});
@@ -14376,7 +14376,7 @@
"conditionKey": "pointerCapable"
},
{
"section": "hyprlandFollowMouse",
"section": "focusFollowsMouse",
"label": "Focus follows mouse",
"tabIndex": 44,
"category": "Mouse & touchpad",
@@ -14384,19 +14384,15 @@
"focus",
"follow",
"follows",
"hyprland",
"input",
"mouse",
"moves",
"over",
"pointer",
"sloppy",
"touchpad",
"window",
"windows"
"window"
],
"parentLabel": "Input",
"description": "Focus windows when the pointer moves over them.",
"conditionKey": "isHyprland"
"conditionKey": "inputCapable"
},
{
"section": "mouseLeftHanded",
@@ -14454,19 +14450,14 @@
"category": "Mouse & touchpad",
"keywords": [
"acceleration",
"focus",
"input",
"mouse",
"moves",
"over",
"pointer",
"sensitivity",
"touchpad",
"windows"
"touchpad"
],
"parentLabel": "Input",
"icon": "mouse",
"description": "Focus windows when the pointer moves over them.",
"conditionKey": "inputCapable"
},
{