Files
DankMaterialShell/quickshell/Common/ShellLayout.qml
T

201 lines
8.9 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Services
import "LayoutResolver.js" as Resolver
Singleton {
id: root
readonly property var screens: Quickshell.screens.map(screen => ({
name: screen.name,
model: screen.model,
x: screen.x,
y: screen.y,
width: screen.width,
height: screen.height,
scale: CompositorService.getScreenScale(screen)
}))
readonly property var primaryBar: SettingsData.getPrimaryBarConfig()
readonly property var layouts: screens.map(screen => Resolver.resolveScreen(SettingsData.barConfigs.map(config => ({
config,
barThickness: Theme.barThickness(config.innerPadding ?? 2, screen.scale),
wingSize: config.gothCornersEnabled && root.barSpansEdge(config) ? Math.max(0, config.gothCornerRadiusOverride ? config.gothCornerRadiusValue ?? 12 : Theme.windowRadius) : 0,
popupThickness: Theme.barThickness(Resolver.option(config, "innerPadding", root.primaryBar, 2), screen.scale),
islandThickness: Resolver.islandThickness(SettingsData.islandSettings(config), SettingsData.islandDefaultsFor(config)),
islandReserve: Resolver.islandReserve(SettingsData.islandSettings(config), SettingsData.islandDefaultsFor(config)),
islandFloating: SettingsData.islandSetting(config, "islandFloating"),
islandPlacement: SettingsData.islandFreePlacement(config) ? "free" : "edge",
islandSatellites: SettingsData.islandSetting(config, "islandSatellitesEnabled")
})), screen, {
screens: root.screens,
displayNameMode: SettingsData.displayNameMode,
framePreferences: SettingsData.frameScreenPreferences,
effectiveFrameEnabled: FrameTransitionState.effectiveFrameEnabled,
effectiveConnected: FrameTransitionState.effectiveConnectedFrameModeActive,
frameThickness: SettingsData.frameThickness,
frameBarSize: SettingsData.frameBarSize
}))
readonly property var islandKeys: layouts.reduce((keys, layout) => keys.concat(layout.instances.filter(instance => instance.hostsIsland && !instance.free).map(instance => instance.key)), [])
readonly property var freeIslandKeys: layouts.reduce((keys, layout) => keys.concat(layout.instances.filter(instance => instance.free).map(instance => instance.key)), [])
function barSpansEdge(config) {
return (config?.barLengthMode ?? "full") === "full" && (config?.barLengthPadding ?? 0) <= 0;
}
function forScreen(screen) {
const name = typeof screen === "string" ? screen : screen?.name;
return layouts.find(layout => layout.screen.name === name) ?? null;
}
function screenForName(name) {
return Quickshell.screens.find(screen => screen.name === name) ?? null;
}
function instance(key) {
const [screenName, barId] = JSON.parse(key);
return forScreen(screenName)?.instances.find(instance => instance.barId === barId) ?? null;
}
function forConfig(screen, barId) {
return forScreen(screen)?.instances.find(instance => instance.barId === barId) ?? null;
}
function edge(screen, side) {
return forScreen(screen)?.edges[side] ?? null;
}
function assignedScreens(config) {
return Quickshell.screens.filter(screen => coversScreen(config, screen));
}
function coversScreen(config, screen) {
return Resolver.coversScreen(config, screen, screens, SettingsData.displayNameMode);
}
function standaloneScreens(barId) {
return Quickshell.screens.filter(screen => forScreen(screen)?.instances.some(instance => instance.barId === barId && instance.kind === "bar"));
}
function hostedScreens(barId) {
return Quickshell.screens.filter(screen => forScreen(screen)?.instances.some(instance => instance.barId === barId && Resolver.hostsBarWindow(instance)));
}
function frameKeys(screen) {
const instances = forScreen(screen)?.instances ?? [];
const order = ["left", "right", "top", "bottom"];
return instances.filter(instance => instance.kind === "frame").sort((a, b) => order.indexOf(a.edge) - order.indexOf(b.edge)).map(instance => instance.key);
}
function barEdges(screen, overlayOnly) {
return Resolver.edges.filter(side => edge(screen, side)?.bars.some(config => !overlayOnly || config.useOverlayLayer));
}
function islandConfigs(screen) {
return forScreen(screen)?.islands.map(input => input.config) ?? [];
}
function frameReservation(screen, side) {
const band = edge(screen, side);
if (SettingsData.frameShowOnOverview && CompositorService.overviewActiveOnScreen(screen))
return band?.overviewFrameReservation ?? 0;
return band?.frameReservation ?? 0;
}
function frameContentInset(screen, barId, side) {
const layout = forScreen(screen);
const band = layout?.edges[side];
if (!band?.occupancy)
return null;
const instance = forConfig(screen, barId);
return Math.max(0, band.occupancy - (instance?.kind === "frame" ? 0 : instance?.margins[side] ?? 0));
}
function adjacentBar(screen, side, config) {
return Resolver.adjacentBar(forScreen(screen), side, config);
}
function dockAdjacentThickness(screen, side) {
const layout = forScreen(screen);
return (layout?.instances ?? []).filter(instance => instance.edge === side).reduce((sum, instance) => sum + instance.reservation, 0);
}
// non-manual layouts let the compositor stack exclusive zones, so the bar that was mapped later is the tucked one
function adjacentCover(screen, side, config, length) {
const layout = forScreen(screen);
const neighbour = adjacentBar(screen, side, config);
if (!layout || !neighbour)
return null;
const opposite = {
top: "bottom",
bottom: "top",
left: "right",
right: "left"
}[side];
const near = layout.edges[side].reservation;
const far = adjacentBar(screen, opposite, config) ? layout.edges[opposite].reservation : 0;
const deficit = (side === "top" || side === "bottom" ? layout.screen.height : layout.screen.width) - length;
const order = id => layout.instances.find(instance => instance.barId === id)?.configOrder ?? 0;
let tucked;
if (deficit >= near + far - 1)
tucked = true;
else if (deficit < near - 1)
tucked = false;
else if (Math.abs(near - far) > 1)
tucked = Math.abs(deficit - near) <= 1;
else
tucked = order(neighbour.config.id) < order(config?.id);
return {
reach: dockAdjacentThickness(screen, side),
wing: neighbour.wingSize ?? 0,
tucked
};
}
function adjacentInfo(screen, config) {
return Resolver.adjacentInfo(forScreen(screen), config, primaryBar);
}
function barBounds(screen, thickness, position, config) {
const appearance = config ?? primaryBar;
const radius = (appearance?.gothCornerRadiusOverride ?? false) ? (appearance?.gothCornerRadiusValue ?? 12) : Theme.windowRadius;
const wing = (appearance?.gothCornersEnabled ?? false) && barSpansEdge(appearance) ? Math.max(0, radius) : 0;
const edge = position === undefined ? (primaryBar?.position ?? 0) : position;
return Resolver.barBounds(forScreen(screen), thickness, edge, config, primaryBar, SettingsData.connectedFrameModeActive, wing);
}
function surfaceOrigin(screen, width, height, anchors, margins) {
return Resolver.surfaceOrigin(forScreen(screen) ?? {
screen
}, width, height, anchors, margins);
}
function popupTrigger(pos, screen, thickness, width, spacing, position, config) {
const edge = position === undefined ? (primaryBar?.position ?? 0) : position;
const gap = spacing === undefined ? (primaryBar?.spacing ?? 4) : spacing;
const trigger = Resolver.popupTrigger(pos, screen, thickness, width, gap, edge, config, primaryBar, SettingsData.connectedFrameModeActive);
const offset = forScreen(screen)?.instances.find(instance => instance.barId === config?.id)?.rowOffset ?? 0;
switch (edge) {
case 0:
trigger.y += offset;
break;
case 1:
trigger.y -= offset;
break;
case 2:
trigger.x += offset;
break;
case 3:
trigger.x -= offset;
break;
}
return trigger;
}
function positionChoices(config) {
return Resolver.availablePositions(layouts.filter(layout => coversScreen(config, layout.screen)), config);
}
}