Workspaces now show up on their appropriate monitors. (#681)

* Workspaces now show up on their appropriate monitors.

* Fixed undefined rules showing up.
This commit is contained in:
Jas Singh
2024-12-31 01:33:40 -08:00
committed by GitHub
parent d2e02f553a
commit 5f72b4f5e1
17 changed files with 576 additions and 425 deletions

View File

@@ -1,4 +1,4 @@
import { bind, exec } from 'astal';
import { bind, GLib } from 'astal';
import { Gtk } from 'astal/gtk3';
import options from 'src/options.js';
import { normalizePath, isAnImage } from 'src/lib/utils.js';
@@ -28,7 +28,8 @@ const ProfileName = (): JSX.Element => {
halign={Gtk.Align.CENTER}
label={bind(name).as((profileName) => {
if (profileName === 'system') {
return exec('bash -c whoami');
const username = GLib.get_user_name();
return username;
}
return profileName;
})}

View File

@@ -1,97 +1,215 @@
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
import options from 'src/options';
import { bash } from 'src/lib/utils';
import { globalEventBoxes } from 'src/globals/dropdown';
import { GLib } from 'astal';
import { EventBox } from 'astal/gtk3/widget';
const hyprland = AstalHyprland.get_default();
import { hyprlandService } from 'src/lib/constants/services';
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
const { location } = options.theme.bar;
const { scalingPriority } = options;
export const calculateMenuPosition = async (pos: number[], windowName: string): Promise<void> => {
/**
* Retrieves the dropdown EventBox widget from the global event boxes map using the provided window name.
*
* @param windowName - A string identifier for the window whose EventBox you want to retrieve.
* @returns The EventBox object if found; otherwise, `undefined`.
*/
function getDropdownEventBox(windowName: string): EventBox | undefined {
return globalEventBoxes.get()[windowName];
}
/**
* Finds and returns the currently focused Hyprland monitor object.
*
* @returns The focused Hyprland monitor, or `undefined` if no match is found.
*/
function getFocusedHyprlandMonitor(): AstalHyprland.Monitor | undefined {
const allMonitors = hyprlandService.get_monitors();
return allMonitors.find((monitor) => monitor.id === hyprlandService.focusedMonitor.id);
}
/**
* Computes the scaled monitor dimensions based on user configuration and environment variables.
*
* This function applies:
* 1. GDK scaling (from the `GDK_SCALE` environment variable).
* 2. Hyprland scaling (from the monitor's scale).
*
* The order in which scaling is applied depends on `scalingPriority`:
* - 'both': Apply GDK scale first, then Hyprland scale.
* - 'gdk': Apply GDK scale only.
* - Otherwise: Apply Hyprland scale only.
*
* @param width - The original width of the focused Hyprland monitor.
* @param height - The original height of the focused Hyprland monitor.
* @param monitorScaling - The scale factor reported by Hyprland for this monitor.
* @returns An object containing `adjustedWidth` and `adjustedHeight` after scaling is applied.
*/
function applyMonitorScaling(width: number, height: number, monitorScaling: number): MonitorScaling {
const gdkEnvScale = GLib.getenv('GDK_SCALE') || '1';
const userScalingPriority = scalingPriority.get();
let adjustedWidth = width;
let adjustedHeight = height;
if (userScalingPriority === 'both') {
const gdkScaleValue = parseFloat(gdkEnvScale);
adjustedWidth /= gdkScaleValue;
adjustedHeight /= gdkScaleValue;
adjustedWidth /= monitorScaling;
adjustedHeight /= monitorScaling;
} else if (/^\d+(\.\d+)?$/.test(gdkEnvScale) && userScalingPriority === 'gdk') {
const gdkScaleValue = parseFloat(gdkEnvScale);
adjustedWidth /= gdkScaleValue;
adjustedHeight /= gdkScaleValue;
} else {
adjustedWidth /= monitorScaling;
adjustedHeight /= monitorScaling;
}
return { adjustedWidth, adjustedHeight };
}
/**
* Corrects monitor dimensions if the monitor is rotated (vertical orientation),
* which requires swapping the width and height.
*
* @param monitorWidth - The monitor width after scaling.
* @param monitorHeight - The monitor height after scaling.
* @param isVertical - Whether the monitor transform indicates a vertical rotation.
* @returns The appropriately adjusted width and height.
*/
function adjustForVerticalTransform(
monitorWidth: number,
monitorHeight: number,
isVertical: boolean,
): TransformedDimensions {
if (!isVertical) {
return { finalWidth: monitorWidth, finalHeight: monitorHeight };
}
return { finalWidth: monitorHeight, finalHeight: monitorWidth };
}
/**
* Calculates the left and right margins required to place the dropdown in the correct position
* relative to the monitor width and the desired anchor X coordinate.
*
* @param monitorWidth - The width of the monitor (already scaled).
* @param dropdownWidth - The width of the dropdown widget.
* @param anchorX - The X coordinate (in scaled pixels) around which the dropdown should be placed.
* @returns An object containing `leftMargin` and `rightMargin`, ensuring they do not go below 0.
*/
function calculateHorizontalMargins(monitorWidth: number, dropdownWidth: number, anchorX: number): HorizontalMargins {
const minimumSpacing = 0;
let rightMarginSpacing = monitorWidth - dropdownWidth / 2;
rightMarginSpacing -= anchorX;
let leftMarginSpacing = monitorWidth - dropdownWidth - rightMarginSpacing;
if (rightMarginSpacing < minimumSpacing) {
rightMarginSpacing = minimumSpacing;
leftMarginSpacing = monitorWidth - dropdownWidth - minimumSpacing;
}
if (leftMarginSpacing < minimumSpacing) {
leftMarginSpacing = minimumSpacing;
rightMarginSpacing = monitorWidth - dropdownWidth - minimumSpacing;
}
return { leftMargin: leftMarginSpacing, rightMargin: rightMarginSpacing };
}
/**
* Positions the dropdown vertically based on the global bar location setting.
* If the bar is positioned at the top, the dropdown is placed at the top (margin_top = 0).
* Otherwise, it's placed at the bottom.
*
* @param dropdownEventBox - The EventBox representing the dropdown.
* @param monitorHeight - The scaled (and possibly swapped) monitor height.
* @param dropdownHeight - The height of the dropdown widget.
*/
function setVerticalPosition(dropdownEventBox: EventBox, monitorHeight: number, dropdownHeight: number): void {
if (location.get() === 'top') {
dropdownEventBox.set_margin_top(0);
dropdownEventBox.set_margin_bottom(monitorHeight);
} else {
dropdownEventBox.set_margin_bottom(0);
dropdownEventBox.set_margin_top(monitorHeight - dropdownHeight);
}
}
/**
* Adjusts the position of a dropdown menu (event box) based on the focused monitor, scaling preferences,
* and the bar location setting. It ensures the dropdown is accurately placed either at the top or bottom
* of the screen within monitor boundaries, respecting both GDK scaling and Hyprland scaling.
*
* @param positionCoordinates - An array of `[x, y]` values representing the anchor position at which
* the dropdown should ideally appear (only the X coordinate is used here).
* @param windowName - A string that identifies the window in the globalEventBoxes map.
*
* @returns A Promise that resolves once the dropdown position is fully calculated and set.
*/
export const calculateMenuPosition = async (positionCoordinates: number[], windowName: string): Promise<void> => {
try {
const self = globalEventBoxes.get()[windowName] as EventBox;
const dropdownEventBox = getDropdownEventBox(windowName);
const curHyprlandMonitor = hyprland.get_monitors().find((m) => m.id === hyprland.focusedMonitor.id);
const dropdownWidth = self.get_child()?.get_allocation().width ?? 0;
const dropdownHeight = self.get_child()?.get_allocation().height ?? 0;
let hyprScaling = 1;
const monitorInfo = await bash('hyprctl monitors -j');
const parsedMonitorInfo = JSON.parse(monitorInfo);
const foundMonitor = parsedMonitorInfo.find(
(monitor: AstalHyprland.Monitor) => monitor.id === hyprland.focusedMonitor.id,
);
hyprScaling = foundMonitor?.scale || 1;
let monWidth = curHyprlandMonitor?.width;
let monHeight = curHyprlandMonitor?.height;
if (monWidth === undefined || monHeight === undefined || hyprScaling === undefined) {
if (!dropdownEventBox) {
return;
}
// If GDK Scaling is applied, then get divide width by scaling
// to get the proper coordinates.
// Ex: On a 2860px wide monitor... if scaling is set to 2, then the right
// end of the monitor is the 1430th pixel.
const gdkScale = GLib.getenv('GDK_SCALE') || '1';
const focusedHyprlandMonitor = getFocusedHyprlandMonitor();
if (scalingPriority.get() === 'both') {
const scale = parseFloat(gdkScale);
monWidth = monWidth / scale;
monHeight = monHeight / scale;
monWidth = monWidth / hyprScaling;
monHeight = monHeight / hyprScaling;
} else if (/^\d+(.\d+)?$/.test(gdkScale) && scalingPriority.get() === 'gdk') {
const scale = parseFloat(gdkScale);
monWidth = monWidth / scale;
monHeight = monHeight / scale;
} else {
monWidth = monWidth / hyprScaling;
monHeight = monHeight / hyprScaling;
if (!focusedHyprlandMonitor) {
return;
}
// If monitor is vertical (transform = 1 || 3) swap height and width
const isVertical = curHyprlandMonitor?.transform !== undefined ? curHyprlandMonitor.transform % 2 !== 0 : false;
const dropdownWidth = dropdownEventBox.get_child()?.get_allocation().width ?? 0;
const dropdownHeight = dropdownEventBox.get_child()?.get_allocation().height ?? 0;
if (isVertical) {
[monWidth, monHeight] = [monHeight, monWidth];
const monitorScaling = focusedHyprlandMonitor.scale || 1;
const { width: rawMonitorWidth, height: rawMonitorHeight, transform } = focusedHyprlandMonitor;
if (!rawMonitorWidth || !rawMonitorHeight) {
return;
}
let marginRight = monWidth - dropdownWidth / 2;
marginRight = marginRight - pos[0];
let marginLeft = monWidth - dropdownWidth - marginRight;
const { adjustedWidth, adjustedHeight } = applyMonitorScaling(
rawMonitorWidth,
rawMonitorHeight,
monitorScaling,
);
const minimumMargin = 0;
const isVertical = transform !== undefined ? transform % 2 !== 0 : false;
const { finalWidth, finalHeight } = adjustForVerticalTransform(adjustedWidth, adjustedHeight, isVertical);
if (marginRight < minimumMargin) {
marginRight = minimumMargin;
marginLeft = monWidth - dropdownWidth - minimumMargin;
}
const { leftMargin, rightMargin } = calculateHorizontalMargins(
finalWidth,
dropdownWidth,
positionCoordinates[0],
);
if (marginLeft < minimumMargin) {
marginLeft = minimumMargin;
marginRight = monWidth - dropdownWidth - minimumMargin;
}
dropdownEventBox.set_margin_left(leftMargin);
dropdownEventBox.set_margin_right(rightMargin);
self.set_margin_left(marginLeft);
self.set_margin_right(marginRight);
if (location.get() === 'top') {
self.set_margin_top(0);
self.set_margin_bottom(monHeight);
} else {
self.set_margin_bottom(0);
self.set_margin_top(monHeight - dropdownHeight);
}
} catch (error) {
console.error(`Error getting menu position: ${error}`);
setVerticalPosition(dropdownEventBox, finalHeight, dropdownHeight);
} catch (caughtError) {
console.error(`Error getting menu position: ${caughtError}`);
}
};
type HorizontalMargins = {
leftMargin: number;
rightMargin: number;
};
type MonitorScaling = {
adjustedWidth: number;
adjustedHeight: number;
};
type TransformedDimensions = {
finalWidth: number;
finalHeight: number;
};