const { Window, Box, CenterBox, Icon, Revealer, EventBox } = ags.Widget; const { closeWindow } = ags.App; const { execAsync } = ags.Utils; const { Hyprland } = ags.Service; const { Gtk, Gdk } = imports.gi; import Cairo from 'cairo'; import { Button } from '../misc/cursorbox.js'; import { PopUp } from '../misc/popup.js'; const WORKSPACE_PER_ROW = 6; const SCALE = 0.11; const ICON_SCALE = 0.8; const MARGIN = 8; const SCREEN = { X: 1920, Y: 1200, } const DEFAULT_SPECIAL = { SIZE_X: 1524, SIZE_Y: 908, POS_X: 197, POS_Y: 170, }; const IconStyle = app => `min-width: ${app.size[0] * SCALE - MARGIN}px; min-height: ${app.size[1] * SCALE - MARGIN}px; font-size: ${Math.min(app.size[0] * SCALE - MARGIN, app.size[1] * SCALE - MARGIN) * ICON_SCALE}px;`; Array.prototype.remove = function (el) { this.splice(this.indexOf(el), 1) }; const TARGET = [Gtk.TargetEntry.new('text/plain', Gtk.TargetFlags.SAME_APP, 0)]; export function createSurfaceFromWidget(widget) { const alloc = widget.get_allocation(); const surface = new Cairo.ImageSurface( Cairo.Format.ARGB32, alloc.width, alloc.height, ); const cr = new Cairo.Context(surface); cr.setSourceRGBA(255, 255, 255, 0); cr.rectangle(0, 0, alloc.width, alloc.height); cr.fill(); widget.draw(cr); return surface; } const WorkspaceRow = (className, i) => Revealer({ transition: 'slide_down', connections: [[Hyprland, rev => { rev.revealChild = Hyprland.workspaces.some(ws => ws.id > i * WORKSPACE_PER_ROW && (ws.windows > 0 || ws.id === Hyprland.active.workspace.id)); }]], child: CenterBox({ children: [null, Box({ className: className, }), null], }), }); const OverviewWidget = Box({ className: 'overview', vertical: true, children: [ Box({ vertical: true, children: [ WorkspaceRow('normal', 0), ], }), Box({ vertical: true, children: [ WorkspaceRow('special', 0), ], }), ], connections: [ [Hyprland, box => { box._getWorkspaces(box); box._updateWs(box); box._updateApps(box); }], ], properties: [ ['workspaces'], ['getWorkspaces', box => { let children = []; box.children.forEach(type => { type.children.forEach(row => { row.child.centerWidget.children.forEach(ch => { children.push(ch); }); }); }); box._workspaces = children.sort((a, b) => a._id - b._id); }], ['updateWs', box => { Hyprland.workspaces.forEach(ws => { let currentWs = box._workspaces.find(ch => ch._id == ws.id); if (!currentWs) { var type = 0; var rowNo = 0; if (ws.id < 0) { // This means it's a special workspace type = 1; } else { rowNo = Math.floor((ws.id - 1) / WORKSPACE_PER_ROW); if (rowNo >= box.children[type].children.length) { for (let i = box.children[type].children.length; i <= rowNo; ++i) { box.children[type].add(WorkspaceRow('normal', i)); } } } var row = box.children[type].children[rowNo].child.centerWidget; currentWs = Revealer({ transition: 'slide_right', properties: [ ['id', ws.id], ], connections: [[Hyprland, box => { let active = Hyprland.active.workspace.id === box._id; box.child.child.toggleClassName('active', active); box.revealChild = Hyprland.getWorkspace(box._id)?.windows > 0 || active; }]], child: EventBox({ tooltipText: `Workspace: ${ws.id}`, child: Box({ className: 'workspace', style: `min-width: ${SCREEN.X * SCALE}px; min-height: ${SCREEN.Y * SCALE}px;`, setup: eventbox => { eventbox.drag_dest_set(Gtk.DestDefaults.ALL, TARGET, Gdk.DragAction.COPY); eventbox.connect('drag-data-received', (_w, _c, _x, _y, data) => { execAsync(`hyprctl dispatch movetoworkspacesilent ${ws.id},address:${data.get_text()}`) .catch(print); }); }, child: ags.Widget({ type: Gtk.Fixed, }), }), }), }); row.add(currentWs); } }); box.show_all(); // Make sure the order is correct box._workspaces.forEach((workspace, i) => { workspace.get_parent().reorder_child(workspace, i) }); }], ['updateApps', box => { ags.Utils.execAsync('hyprctl clients -j') .then(result => { let clients = JSON.parse(result).filter(client => client.class) box._workspaces.forEach(workspace => { let fixed = workspace.child.child.children[0]; let toRemove = fixed.get_children(); clients.filter(app => app.workspace.id == workspace._id).forEach(app => { let active = ''; if (app.address == Hyprland.active.client.address) { active = 'active'; } // Special workspaces that haven't been opened yet // return a size of 0. We need to set them to default // values to show the workspace properly if (app.size[0] === 0) { app.size[0] = DEFAULT_SPECIAL.SIZE_X; app.size[1] = DEFAULT_SPECIAL.SIZE_Y; app.at[0] = DEFAULT_SPECIAL.POS_X; app.at[1] = DEFAULT_SPECIAL.POS_Y; } let existingApp = fixed.get_children().find(ch => ch._address == app.address); toRemove.remove(existingApp); if (existingApp) { fixed.move( existingApp, app.at[0] * SCALE, app.at[1] * SCALE, ); existingApp.child.child.className = `window ${active}`; existingApp.child.child.style = IconStyle(app); } else { fixed.put( Revealer({ transition: 'crossfade', setup: rev => { rev.revealChild = true; }, properties: [ ['address', app.address], ['toDestroy', false] ], child: Button({ onSecondaryClickRelease: () => { execAsync(`hyprctl dispatch closewindow address:${address}`) .catch(print) }, onPrimaryClickRelease: () => { if (app.class === 'thunderbird' || app.class === 'Spotify') execAsync(['bash', '-c', `$AGS_PATH/launch-app.sh ${app.class}`]) .then(() => closeWindow('overview')) .catch(print); else execAsync(`hyprctl dispatch focuswindow address:${app.address}`) .then(() => closeWindow('overview')) .catch(print); }, setup: button => { button.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, TARGET, Gdk.DragAction.COPY); button.connect('drag-data-get', (_w, _c, data) => data.set_text(app.address, app.address.length)); button.connect('drag-begin', (_, context) => { Gtk.drag_set_icon_surface(context, createSurfaceFromWidget(button)); button.get_parent().revealChild = false; }); button.connect('drag-end', () => button.get_parent().revealChild = true); }, child: Icon({ className: `window ${active}`, style: IconStyle(app), icon: app.class, }), }), }), app.at[0] * SCALE, app.at[1] * SCALE, ); } }); fixed.show_all(); toRemove.forEach(ch => { if (ch._toDestroy) { ch.destroy(); } else { ch.revealChild = false; ch._toDestroy = true; } }); }); }).catch(print); }], ], }); export const Overview = Window({ name: 'overview', layer: 'overlay', child: PopUp({ name: 'overview', transition: 'crossfade', child: OverviewWidget, }), });