Files
Sn3aky f4a9cd2b05 Blur: a surface sliding off its layer no longer frosts the whole of it
Hyprland clips the blur to the region's overlap with the surface, and an empty clip means no clip, so a region wholly outside it frosts everything. A hidden bar's pills sat there, covering the screen for as long as it stayed hidden, and the jelly pushed a hidden dock's last row off the edge in brief flashes. The blur regions are now clipped to the surface, so such a region arrives empty and nothing is frosted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:27:50 +01:00

1619 lines
54 KiB
QML

//@ pragma Env QSG_DISTANCEFIELD_ANTIALIASING=gray
// qt's default subpixel text splits into colour fringes on translucent surfaces
import "./lucidbar"
import "./luciddesktop"
import "./luciddocks"
import "./lucidedit"
import "./lucidkeys"
import "./lucidlock"
import "./lucidmoji"
import "./lucidnews"
import "./lucidnotif"
import "./lucidosd"
import "./lucidpolkit"
import "./lucidprefs"
import "./lucidsession"
import "./lucidshot"
import "./lucidswitch"
import "./lucidupdate"
import "./lucidwidgets"
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.lucidicons
ShellRoot {
id: root
// singletons are made lazily, and these have to be up before anything
// asks: the KDE Connect bridge and its ipc target, the bluez extras, the
// idle daemon, which owns hypridle.conf, the environment, which owns
// the gtk and qt appearance files, and the special workspaces, which own
// lucid-specials.lua, the glass mirror, which owns kitty's opacity file,
// the displays, which own lucid-monitors.lua, the bluetooth file
// receiver, which has to be listening before anyone sends something, the
// pairing agent, which bluez asks before letting a device in, the
// Hyprland options, which own lucid-settings.lua and follow the palette
// in border colours,
// the update check, which runs whether or not the settings app is
// ever opened, the clipboard, which owns the wl-paste watchers and so
// has to be up long before the launcher is first opened, and night
// light, whose schedule runs whether or not the System pill is shown,
// the sounds, so their ipc target answers before anything has played, and
// storage, which warns when space runs low and empties old trash
Component.onCompleted: {
void KdeConnect.installed;
void Bt.present;
void BtReceive.status;
void BtAgent.status;
void Net.connectivity;
void Idle.probed;
void Env.probed;
void Specials.moduleProbed;
void Glass.probed;
void Monitors.probed;
void HyprConfig.moduleProbed;
void Startup.moduleProbed;
void Updates.current;
void Notifs.count;
void Clip.probed;
void Users.probed;
void Polkit.registered;
void Chrono.now;
void Agenda.tick;
void Zones.offsets;
void Capture.state;
void NightLight.active;
void LiveWall.wanted;
void Sounds.dir;
void Storage.watching;
void WallTransitions.active;
void Edit.active;
// the combined output is remade at login by the service that keeps it
if (Prefs.audioCombine !== "")
void Audio.sink;
}
// lucid's icon set, picked on the Environment page; lumen reads prefs.json for its own
Binding {
target: LucidIcons
property: "set"
value: Prefs.envIconSet
}
// a capture nobody on screen is showing yet: a card, a notification, or
// nothing but the clipboard, as Settings says
function announceShot(entry) {
if (Prefs.shotPreview === "preview") {
shotCards.show(entry);
return ;
}
if (Prefs.shotPreview !== "notify")
return ;
const where = String(entry.file).replace(Quickshell.env("HOME"), "~");
if (entry.kind === "video")
Quickshell.execDetached(["notify-send", "-a", "LucidShot", "-i", "media-record", "Recording saved", where]);
else
Quickshell.execDetached(["sh", "-c", "a=$(notify-send -a LucidShot -i \"$1\" -A 'edit=Mark up' -A 'open=Open' --wait \"$2\" \"$3\"); case \"$a\" in edit) qs ipc call snap edit \"$1\" ;; open) xdg-open \"$1\" ;; esac; true", "sh", entry.file, entry.temp ? "Screenshot copied" : "Screenshot saved", entry.temp ? "It is on the clipboard only" : where]);
}
// the bar that speaks for the shell, of however many there are
function mainBar() {
const list = bars.instances;
for (let i = 0; i < list.length; i++) {
if (list[i].primary)
return list[i];
}
return list.length > 0 ? list[0] : null;
}
// the modules' own ipc targets, here rather than in each bar, which would
// claim them once per display: each reaches the main bar
IpcHandler {
target: "clock"
// qs ipc call clock open timer
function open(page: string): void {
const b = root.mainBar();
if (b)
b.clockModule.openTab(page === "" ? "today" : page);
}
function toggle(): void {
const b = root.mainBar();
if (b)
b.clockModule.expanded = !b.clockModule.expanded;
}
function close(): void {
const b = root.mainBar();
if (b)
b.clockModule.expanded = false;
}
}
IpcHandler {
target: "media"
function toggle(): void {
const b = root.mainBar();
if (!b)
return ;
if (b.mprisModule.expanded)
b.mprisModule.expanded = false;
else
b.mprisModule.openPanel("player");
}
function open(): void {
const b = root.mainBar();
if (b)
b.mprisModule.openPanel("player");
}
function close(): void {
const b = root.mainBar();
if (b)
b.mprisModule.expanded = false;
}
function identify(): void {
const b = root.mainBar();
if (!b)
return ;
b.mprisModule.openPanel("shazam");
b.mprisModule.startListening();
}
function playPause(): void {
const b = root.mainBar();
if (b)
b.mprisModule.togglePlay();
}
function next(): void {
const b = root.mainBar();
if (b)
b.mprisModule.skip(1);
}
function previous(): void {
const b = root.mainBar();
if (b)
b.mprisModule.skip(-1);
}
}
IpcHandler {
target: "workspaces"
function toggle(): void {
const b = root.mainBar();
if (b)
b.workspacesModule.expanded = !b.workspacesModule.expanded;
}
function open(): void {
const b = root.mainBar();
if (b)
b.workspacesModule.expanded = true;
}
function close(): void {
const b = root.mainBar();
if (b)
b.workspacesModule.expanded = false;
}
}
// one bar, or one on every display when Settings > Displays asks for it
Variants {
id: bars
model: Monitors.barEverywhere ? Quickshell.screens : [""]
PanelWindow {
id: bar
// a display from Quickshell.screens with a bar on every one, or "" for the one bar
required property var modelData
// the bar that speaks for the shell: it alone turns into notification
// popups, raises the privacy toast and answers the modules' ipc
readonly property bool primary: !Monitors.barEverywhere || bar.screen === Monitors.popupScreen
// what the shell's ipc targets reach on the main bar
readonly property var clockModule: clockMod
readonly property var mprisModule: mprisMod
readonly property var workspacesModule: workspacesMod
PaletteFade {
active: bar.visible
}
// the display Settings > Displays picks, or this one's own. gated,
// because unset must leave the choice to hyprland, which null would not
Binding {
target: bar
property: "screen"
value: bar.modelData !== "" ? bar.modelData : Monitors.barPlacement
when: bar.modelData !== "" || Monitors.barPlacement !== null
}
visible: Prefs.loaded && Prefs.barEnabled && Monitors.surfacesUp
property bool laidOut: false
// stand-ins take room in edit mode, so this asks after the modules themselves
readonly property bool anyModuleShown: Prefs.barZones.some((zone) => {
return bar.zoneKeys(zone).some((key) => {
return bar.realWidthOf(key) > 0.5;
});
})
// edit mode: the module in hand, how far it sits off its slot, and the
// arrangement the bar shows while it is held
property real editT: Edit.active ? 1 : 0
// in edit mode the modules part a little, so each gap has room for its clasp
readonly property real editGap: Theme.dp(16) * bar.editT
property string dragKey: ""
property real dragShift: 0
property real dragLift: 0
property var dragGroups: null
property bool dragHeld: false
Behavior on editT {
NumberAnimation {
duration: Theme.barMs(320)
easing.type: Easing.OutCubic
}
}
function xOf(key) {
return bar.placeFor(key) + (key === bar.dragKey ? bar.dragShift : 0);
}
function liftOf(key) {
return key === bar.dragKey ? bar.dragLift : 0;
}
// a right click on a module: its sheet, on the side of the screen away from it
function editModule(key) {
const mod = bar.pillFor[key];
const cx = bar.xOf(key) + (mod ? mod.width : 0) / 2;
Edit.openSheet("module", key, Edit.sideFor(cx, bar.width), bar.screen ? bar.screen.name : "");
}
// the sheet's open button: this bar's module opens as a click would open it
Connections {
function onModuleOpenRequested(id, screen) {
if (screen !== "" ? (!bar.screen || bar.screen.name !== screen) : !bar.primary)
return ;
const mod = bar.pillFor[id];
if (!mod)
return ;
if (id === "start") {
dock.openLauncher("");
} else if (id === "media") {
mod.openPanel("player");
} else {
if (mod.compactClicked)
mod.compactClicked();
mod.expanded = true;
}
}
target: Edit
}
// keys, when given, lets modules joined to the next one sit flush against it
function placeGroup(widths, originX, keys) {
const gap = Theme.dp(Prefs.barSpacing) + bar.editGap;
const out = [];
let x = originX;
let any = false;
let trailing = 0;
for (let i = 0; i < widths.length; i++) {
out.push(x);
const w = widths[i];
if (w > 0.5) {
trailing = keys && bar.joinedAfter(keys, widths, i) ? 0 : (gap > 0 ? gap * Math.min(1, w / gap) : 0);
x += w + trailing;
any = true;
}
}
out.push(any ? Math.max(originX, x - trailing) : originX);
return out;
}
// whether the module at i is joined to the one right after it. both
// have to be in the bar this moment: one that has emptied out (a tray
// with no icons, workspaces off in the overview) parts its group there
function joinedAfter(keys, widths, i) {
return i + 1 < keys.length && widths[i] > 0.5 && widths[i + 1] > 0.5 && bar.realWidthOf(keys[i]) > 0.5 && bar.realWidthOf(keys[i + 1]) > 0.5 && Prefs.barJoinedNow(keys[i], keys[i + 1]);
}
// what a module's pill reads to square off the corners it shares
function joinedLeftOf(key) {
const at = bar.placeMap[key];
if (!at || at.at === 0)
return false;
const keys = bar.zoneKeys(at.zone);
return bar.realWidthOf(key) > 0.5 && bar.realWidthOf(keys[at.at - 1]) > 0.5 && Prefs.barJoinedNow(keys[at.at - 1], key);
}
function joinedRightOf(key) {
const at = bar.placeMap[key];
if (!at)
return false;
const keys = bar.zoneKeys(at.zone);
return at.at + 1 < keys.length && bar.realWidthOf(key) > 0.5 && bar.realWidthOf(keys[at.at + 1]) > 0.5 && Prefs.barJoinedNow(key, keys[at.at + 1]);
}
// per module key, radius caps where a hanging panel meets the islands: {corners, tops, sides, fillets}
readonly property var hangPlan: bar.planHangs()
function surfaceSpan(mod) {
const l = mod.x + (mod.surfaceX !== undefined ? mod.surfaceX : 0);
return [l, l + (mod.surfaceWidth !== undefined ? mod.surfaceWidth : mod.width)];
}
function planHangs() {
const plan = ({
"corners": ({}),
"tops": ({}),
"sides": ({}),
"fillets": []
});
const popups = Prefs.barPopupMode;
if (popups && !Prefs.barPopupNotched)
return plan;
const most = Theme.dp(Prefs.barNotchFlare);
const bite = 0.5;
const cap = (key, corner, v) => {
const c = plan.corners[key] || (plan.corners[key] = ({}));
c[corner] = Math.min(c[corner] === undefined ? 1e6 : c[corner], Math.max(0, v));
};
const fillet = (x, y, size, mirrored, flipped) => {
if (size >= 1)
plan.fillets.push({ "x": x, "y": y, "size": size, "mirrored": mirrored, "flipped": flipped });
};
// islands: runs of joined modules as they stand
const islands = [];
const shelves = [];
for (const zone of Prefs.barZones) {
const keys = bar.zoneKeys(zone);
let run = null;
for (let i = 0; i < keys.length; i++) {
const mod = bar.pillFor[keys[i]];
if (!mod || bar.widthOf(keys[i]) <= 0.5) {
run = null;
continue;
}
if (run && Prefs.barJoinedNow(keys[i - 1], keys[i])) {
run.keys.push(keys[i]);
run.r = bar.surfaceSpan(mod)[1];
} else {
run = ({ "keys": [keys[i]], "l": bar.surfaceSpan(mod)[0], "r": bar.surfaceSpan(mod)[1], "top": mod.y, "bottom": mod.y + mod.compactHeight });
islands.push(run);
}
}
}
// what hangs: a notched pop-up, or a joined module opened in place
for (const isle of islands) {
for (let j = 0; j < isle.keys.length; j++) {
const m = bar.pillFor[isle.keys[j]];
if (popups) {
const pop = m.notchedPopup === true && m.popupOpen ? m.popupItem : null;
if (pop && pop.height > 0.5)
shelves.push({ "key": isle.keys[j], "l": m.x + pop.x, "r": m.x + pop.x + pop.width, "y": m.y + pop.y, "depth": pop.height });
} else if (isle.keys.length > 1 && m !== workspacesMod && m.height - m.compactHeight > 0.5) {
const depth = m.height - m.compactHeight;
const side = ({});
if (j > 0) {
const run = m.x - isle.l;
const f = Math.floor(Math.min(most, run / 2, depth / 2));
fillet(m.x - f + bite, isle.bottom - bite, f, false, false);
cap(isle.keys[0], "bl", run - f);
side.bl = depth - f;
}
if (j < isle.keys.length - 1) {
const run = isle.r - (m.x + m.width);
const f = Math.floor(Math.min(most, run / 2, depth / 2));
fillet(m.x + m.width - bite, isle.bottom - bite, f, true, false);
cap(isle.keys[isle.keys.length - 1], "br", run - f);
side.br = depth - f;
}
plan.sides[isle.keys[j]] = side;
}
}
}
// a pop-up's top is the screen edge upside down: islands stand on it or it hangs from them
for (const shelf of shelves) {
const over = islands.filter((isle) => {
return isle.l < shelf.r - 0.5 && isle.r > shelf.l + 0.5 && Math.abs(isle.bottom - shelf.y) < 1;
});
const top = ({});
plan.tops[shelf.key] = top;
if (over.length === 0)
continue;
for (let k = 0; k < over.length; k++) {
const isle = over[k];
const first = isle.keys[0];
const last = isle.keys[isle.keys.length - 1];
const tall = isle.bottom - isle.top;
const dl = isle.l - shelf.l;
if (k > 0) {
const f = Math.floor(Math.min(most, (isle.l - over[k - 1].r) / 2, tall));
fillet(isle.l - f + bite, shelf.y - f + bite, f, false, true);
cap(first, "bl", 0);
} else if (dl > 0.5) {
const f = Math.floor(Math.min(most, dl / 2, tall));
fillet(isle.l - f + bite, shelf.y - f + bite, f, false, true);
cap(first, "bl", 0);
top.tl = dl - f;
} else if (dl < -0.5) {
const f = Math.floor(Math.min(most, -dl / 2, shelf.depth / 2));
fillet(shelf.l - f + bite, shelf.y - bite, f, false, false);
cap(first, "bl", -dl - f);
top.tl = 0;
} else {
cap(first, "bl", 0);
top.tl = 0;
}
const dr = shelf.r - isle.r;
if (k < over.length - 1) {
const f = Math.floor(Math.min(most, (over[k + 1].l - isle.r) / 2, tall));
fillet(isle.r - bite, shelf.y - f + bite, f, true, true);
cap(last, "br", 0);
} else if (dr > 0.5) {
const f = Math.floor(Math.min(most, dr / 2, tall));
fillet(isle.r - bite, shelf.y - f + bite, f, true, true);
cap(last, "br", 0);
top.tr = dr - f;
} else if (dr < -0.5) {
const f = Math.floor(Math.min(most, -dr / 2, shelf.depth / 2));
fillet(shelf.r - bite, shelf.y - bite, f, true, false);
cap(last, "br", -dr - f);
top.tr = 0;
} else {
cap(last, "br", 0);
top.tr = 0;
}
}
}
return plan;
}
property real wsCollapse: (workspacesMod.expanded && !Prefs.barPopupMode) ? 0 : 1
// the modules are addressed by their id in Prefs.barModules; the
// groups in Prefs.barLayout decide which slot each one lands in
readonly property var pillFor: ({
"workspaces": workspacesMod,
"media": mprisMod,
"tray": sysTrayMod,
"clock": clockMod,
"notifications": notifMod,
"system": systemMod,
"privacy": privacyMod,
"power": powerMod,
"window": windowMod,
"start": startMod
})
// what BarPill looks itself up in, for a right click to its card
readonly property var moduleById: bar.pillFor
readonly property var modules: [workspacesMod, mprisMod, sysTrayMod, clockMod, notifMod, systemMod, privacyMod, powerMod, windowMod, startMod]
// which modules switched on have left the bar for now, for the Bar
// page's arrangement
Binding {
target: Prefs
property: "barModulesAway"
when: bar.primary
value: Prefs.barModules.filter((m) => {
const mod = bar.pillFor[m.id];
return Prefs[m.key] === true && mod && !mod.shown;
}).map((m) => {
return m.id;
})
}
// a module on its way out of the bar keeps the slot it had until its
// pill has finished emptying, so its neighbours close the gap at the
// rate it shrinks rather than dropping into it
function zoneKeys(zone) {
const live = bar.dragGroups ? bar.dragGroups[zone === "centre" ? "center" : zone].filter((id) => {
return Prefs.barShown(id);
}) : Prefs.barKeysOf(zone);
const r = Prefs.barRetiring;
if (!r || r.zone !== zone || Prefs.barHas(r.key))
return live;
const out = live.slice();
out.splice(Math.min(r.at, out.length), 0, r.key);
return out;
}
readonly property var retiring: Prefs.barRetiring
onRetiringChanged: {
if (bar.retiring)
retireTimer.restart();
}
Timer {
id: retireTimer
interval: Theme.barMs(560)
onTriggered: Prefs.barClearRetiring()
}
// key -> which zone it sits in and how far along
readonly property var placeMap: {
const out = ({});
for (var z = 0; z < Prefs.barZones.length; z++) {
const zone = Prefs.barZones[z];
const keys = bar.zoneKeys(zone);
for (var i = 0; i < keys.length; i++) out[keys[i]] = ({ "zone": zone, "at": i })
}
return out;
}
// workspaces empties its slot as it flies to the screen centre, so the
// rest of its zone closes up behind it
function realWidthOf(key) {
const it = bar.pillFor[key];
if (!it)
return 0;
return it === workspacesMod ? it.width * bar.wsCollapse : it.width;
}
// in edit mode a module with nothing to show keeps a stand-in's room
function widthOf(key) {
return Math.max(bar.realWidthOf(key), barEdit.ghostWidthOf(key));
}
function widthsOf(zone) {
return bar.zoneKeys(zone).map(bar.widthOf);
}
function placesOf(zone) {
return zone === "left" ? bar.leftPlaces : (zone === "centre" ? bar.centrePlaces : bar.rightPlaces);
}
function alignFor(zone) {
return zone === "centre" ? "center" : (zone === "right" ? "right" : "left");
}
function placeFor(key) {
const at = bar.placeMap[key];
if (!at)
return 0;
const places = bar.placesOf(at.zone);
return at.at < places.length ? places[at.at] : 0;
}
function alignOf(key) {
const at = bar.placeMap[key];
return bar.alignFor(at ? at.zone : "left");
}
readonly property var leftWidths: bar.widthsOf("left")
readonly property var centreWidths: bar.widthsOf("centre")
readonly property var rightWidths: bar.widthsOf("right")
readonly property real leftGroupWidth: bar.placeGroup(bar.leftWidths, 0, bar.zoneKeys("left"))[bar.leftWidths.length]
readonly property real centreGroupWidth: bar.placeGroup(bar.centreWidths, 0, bar.zoneKeys("centre"))[bar.centreWidths.length]
readonly property real rightGroupWidth: bar.placeGroup(bar.rightWidths, 0, bar.zoneKeys("right"))[bar.rightWidths.length]
readonly property int zoneGap: Theme.dp(Prefs.barZoneGap)
// pills ease to their new slots only while the arrangement is changing.
// a Behavior left on all the time would sit on top of the width
// animations a panel opening or a tray icon arriving already drive, and
// one that restarts every frame never arrives
readonly property int reorderTick: Prefs.barReorderTick
property bool shuffling: false
onReorderTickChanged: {
bar.shuffling = true;
shuffleTimer.restart();
}
Timer {
id: shuffleTimer
interval: Theme.barMs(520)
onTriggered: bar.shuffling = false
}
readonly property real leftOriginX: bar.sideMargin
readonly property real rightOriginX: bar.width - bar.rightGroupWidth - bar.sideMargin
readonly property real centreOriginX: Math.min(Math.max((bar.width - bar.centreGroupWidth) / 2, bar.leftOriginX + bar.leftGroupWidth + bar.zoneGap), bar.rightOriginX - bar.centreGroupWidth - bar.zoneGap)
readonly property var leftPlaces: bar.placeGroup(bar.leftWidths, bar.leftOriginX, bar.zoneKeys("left"))
readonly property var centrePlaces: bar.placeGroup(bar.centreWidths, bar.centreOriginX, bar.zoneKeys("centre"))
readonly property var rightPlaces: bar.placeGroup(bar.rightWidths, bar.rightOriginX, bar.zoneKeys("right"))
Behavior on wsCollapse {
NumberAnimation {
duration: Theme.barMs(380)
easing.type: Easing.OutCubic
}
}
readonly property real sideMargin: Theme.dp(Prefs.barSideMargin)
// busy: a panel is open somewhere on the bar, so it stays out
// regardless of the pointer. held: the pointer is on the reveal
// strip or already on a module - either keeps it from hiding
readonly property bool barBusy: bar.modules.some((m) => {
return m.expanded === true || m.anyOpen === true;
})
property bool slidingAway: false
readonly property bool heldByPointer: revealArea.containsMouse || (!bar.slidingAway && bar.modules.some((m) => {
return m.visible && (m.surfaceHovered === true || m.compactHovered === true);
}))
readonly property bool barRevealed: !Prefs.barAutoHide || bar.barBusy || bar.heldByPointer || Edit.active
// how far the compact content sits above its resting y=0 while
// hidden - past the window's own top edge, so it clips away for free
property real hiddenOffset: bar.barRevealed ? 0 : -(Theme.dp(Prefs.barHeight) + Prefs.effectiveBarTopMargin + Theme.dp(4))
Behavior on hiddenOffset {
NumberAnimation {
duration: Theme.durLong
easing.type: Easing.Bezier
easing.bezierCurve: Theme.easeEmphasizedDecel
}
}
onBarRevealedChanged: {
if (bar.barRevealed) {
bar.slidingAway = false;
slideAwayTimer.stop();
} else {
bar.slidingAway = true;
slideAwayTimer.restart();
}
}
Timer {
id: slideAwayTimer
interval: Theme.durLong
onTriggered: bar.slidingAway = false
}
Component.onCompleted: laidOutTimer.start()
Timer {
id: laidOutTimer
interval: 120
onTriggered: bar.laidOut = true
}
color: "transparent"
implicitHeight: bar.screen ? bar.screen.height - Prefs.effectiveBarTopMargin : Theme.dp(800)
exclusiveZone: (Prefs.barEnabled && bar.anyModuleShown && !Prefs.barAutoHide) ? Theme.dp(Prefs.barHeight) + Prefs.shellGap : 0
anchors {
top: true
bottom: false
left: true
right: true
}
margins {
top: Prefs.effectiveBarTopMargin
}
// full bar: one strip across the whole top edge, under every module
Rectangle {
id: fullStrip
visible: Prefs.barFull && bar.anyModuleShown
x: 0
y: bar.hiddenOffset
z: -2
width: bar.width
height: Theme.dp(Prefs.barHeight)
color: Theme.bg
Behavior on color {
enabled: bar.laidOut
ColorAnimation {
duration: Theme.barMs(260)
easing.type: Easing.OutCubic
}
}
}
// rounded corners hanging off the strip where it meets the screen sides
BarFlare {
visible: fullStrip.visible && size > 0
mirrored: true
size: Theme.dp(Prefs.barFullCorner)
x: 0
y: Theme.dp(Prefs.barHeight) + bar.hiddenOffset
z: -2
}
BarFlare {
visible: fullStrip.visible && size > 0
size: Theme.dp(Prefs.barFullCorner)
x: bar.width - width
y: Theme.dp(Prefs.barHeight) + bar.hiddenOffset
z: -2
}
// full bar: an open panel hangs off the strip, so round the two concave
// corners where its sides meet the strip's lower edge. They grow with
// the panel's height, so a folding panel takes them with it
Repeater {
model: (Prefs.barFull && !Prefs.barPopupMode) ? bar.modules.filter((m) => {
return m !== workspacesMod;
}) : []
Item {
id: panelFlares
required property var modelData
// surfaceOpen, not the height alone: a pill that is only
// resizing its compact face is see-through on the full bar
readonly property real reach: (panelFlares.modelData.visible && panelFlares.modelData.surfaceOpen === true) ? panelFlares.modelData.height - Theme.dp(Prefs.barHeight) : 0
// never past the screen edge, and never into the screen corner
// that already rounds that side
function sizeFor(toTheLeft) {
var m = panelFlares.modelData;
var corner = Theme.dp(Prefs.barFullCorner);
var toEdge = toTheLeft ? m.x : bar.width - m.x - m.width;
var room = toEdge - (toEdge < corner * 2 ? corner : 0);
return Math.max(0, Math.floor(Math.min(corner, panelFlares.reach, room)));
}
anchors.fill: parent
z: -1
BarFlare {
size: panelFlares.sizeFor(true)
x: panelFlares.modelData.x - width + 0.5
y: Theme.dp(Prefs.barHeight)
}
BarFlare {
mirrored: true
size: panelFlares.sizeFor(false)
x: panelFlares.modelData.x + panelFlares.modelData.width - 0.5
y: Theme.dp(Prefs.barHeight)
}
}
}
Mpris {
id: mprisMod
popupAlign: bar.alignOf("media")
hostWindow: bar
x: bar.xOf("media")
editLifted: bar.dragKey === "media"
anchors.top: parent.top
anchors.topMargin: bar.hiddenOffset + bar.liftOf("media")
Behavior on x {
enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "media"
NumberAnimation {
duration: Theme.barDurEnter
easing.type: Easing.Bezier
easing.bezierCurve: Theme.easeEmphasizedDecel
}
}
}
SysTray {
id: sysTrayMod
popupAlign: bar.alignOf("tray")
hostWindow: bar
x: bar.xOf("tray")
editLifted: bar.dragKey === "tray"
anchors.top: parent.top
anchors.topMargin: bar.hiddenOffset + bar.liftOf("tray")
Behavior on x {
enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "tray"
NumberAnimation {
duration: Theme.barDurEnter
easing.type: Easing.Bezier
easing.bezierCurve: Theme.easeEmphasizedDecel
}
}
}
Clock {
id: clockMod
popupAlign: bar.alignOf("clock")
hostWindow: bar
anchors.top: parent.top
anchors.topMargin: bar.hiddenOffset + bar.liftOf("clock")
x: bar.xOf("clock")
editLifted: bar.dragKey === "clock"
Behavior on x {
enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "clock"
NumberAnimation {
duration: Theme.barDurEnter
easing.type: Easing.Bezier
easing.bezierCurve: Theme.easeEmphasizedDecel
}
}
}
Notifications {
id: notifMod
showsPopups: bar.primary
popupAlign: bar.alignOf("notifications")
hostWindow: bar
x: bar.xOf("notifications")
editLifted: bar.dragKey === "notifications"
anchors.top: parent.top
anchors.topMargin: bar.hiddenOffset + bar.liftOf("notifications")
Behavior on x {
enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "notifications"
NumberAnimation {
duration: Theme.barDurEnter
easing.type: Easing.Bezier
easing.bezierCurve: Theme.easeEmphasizedDecel
}
}
}
Privacy {
id: privacyMod
popupAlign: bar.alignOf("privacy")
toast: bar.primary ? toastMod : null
hostWindow: bar
x: bar.xOf("privacy")
editLifted: bar.dragKey === "privacy"
anchors.top: parent.top
anchors.topMargin: bar.hiddenOffset + bar.liftOf("privacy")
Behavior on x {
enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "privacy"
NumberAnimation {
duration: Theme.barDurEnter
easing.type: Easing.Bezier
easing.bezierCurve: Theme.easeEmphasizedDecel
}
}
}
SessionMenu {
id: powerMod
popupAlign: bar.alignOf("power")
hostWindow: bar
x: bar.xOf("power")
editLifted: bar.dragKey === "power"
anchors.top: parent.top
anchors.topMargin: bar.hiddenOffset + bar.liftOf("power")
Behavior on x {
enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "power"
NumberAnimation {
duration: Theme.barDurEnter
easing.type: Easing.Bezier
easing.bezierCurve: Theme.easeEmphasizedDecel
}
}
}
StartButton {
id: startMod
popupAlign: bar.alignOf("start")
hostWindow: bar
dockMod: dock
x: bar.xOf("start")
editLifted: bar.dragKey === "start"
anchors.top: parent.top
anchors.topMargin: bar.hiddenOffset + bar.liftOf("start")
Behavior on x {
enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "start"
NumberAnimation {
duration: Theme.barDurEnter
easing.type: Easing.Bezier
easing.bezierCurve: Theme.easeEmphasizedDecel
}
}
}
ActiveWindow {
id: windowMod
popupAlign: bar.alignOf("window")
hostWindow: bar
x: bar.xOf("window")
editLifted: bar.dragKey === "window"
anchors.top: parent.top
anchors.topMargin: bar.hiddenOffset + bar.liftOf("window")
Behavior on x {
enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "window"
NumberAnimation {
duration: Theme.barDurEnter
easing.type: Easing.Bezier
easing.bezierCurve: Theme.easeEmphasizedDecel
}
}
}
System {
id: systemMod
popupAlign: bar.alignOf("system")
hostWindow: bar
mprisMod: mprisMod
x: bar.xOf("system")
editLifted: bar.dragKey === "system"
anchors.top: parent.top
anchors.topMargin: bar.hiddenOffset + bar.liftOf("system")
Behavior on x {
enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "system"
NumberAnimation {
duration: Theme.barDurEnter
easing.type: Easing.Bezier
easing.bezierCurve: Theme.easeEmphasizedDecel
}
}
}
Repeater {
model: Prefs.barNotch ? bar.modules : []
Item {
id: flares
required property var modelData
readonly property bool present: flares.modelData && flares.modelData.width > 0.5 && flares.modelData.visible && !(bar.dragLift > 0.5 && bar.pillFor[bar.dragKey] === flares.modelData)
readonly property bool modHovered: flares.modelData ? (flares.modelData.compactHovered === true && flares.modelData !== workspacesMod) : false
function flareFor(toTheLeft) {
if (!flares.present)
return 0;
var mods = bar.modules;
var edge = toTheLeft ? flares.modelData.x : flares.modelData.x + flares.modelData.width;
var toEdge = toTheLeft ? edge : bar.width - edge;
var toNeighbour = 100000;
for (var i = 0; i < mods.length; i++) {
var o = mods[i];
if (!o || o === flares.modelData || o.width <= 0.5 || !o.visible)
continue;
var d = toTheLeft ? edge - (o.x + o.width) : o.x - edge;
if (d >= 0)
toNeighbour = Math.min(toNeighbour, d);
}
return Math.max(0, Math.min(Theme.dp(Prefs.barNotchFlare), Math.floor(toNeighbour / 2), Math.floor(toEdge)));
}
anchors.fill: parent
z: -1
visible: Prefs.barNotch && flares.present
readonly property real bite: 0.5
BarFlare {
hovered: flares.modHovered
size: flares.flareFor(true)
x: flares.modelData ? flares.modelData.x - width + flares.bite : 0
y: bar.hiddenOffset
}
BarFlare {
hovered: flares.modHovered
mirrored: true
size: flares.flareFor(false)
x: flares.modelData ? flares.modelData.x + flares.modelData.width - flares.bite : 0
y: bar.hiddenOffset
}
}
}
// fillets where a hanging panel meets the islands above it
Repeater {
model: 16
BarFlare {
required property int index
readonly property var spec: bar.hangPlan.fillets.length > index ? bar.hangPlan.fillets[index] : null
z: 0
size: spec ? spec.size : 0
mirrored: spec ? spec.mirrored : false
flipped: spec ? spec.flipped : false
x: spec ? spec.x : 0
y: spec ? spec.y : 0
}
}
Workspaces {
id: workspacesMod
hostWindow: bar
dockMod: dock
restX: bar.xOf("workspaces")
restY: bar.hiddenOffset + bar.liftOf("workspaces")
z: bar.dragKey === "workspaces" ? 120 : (workspacesMod.popupOpen ? 100 : 1)
Behavior on restX {
enabled: bar.laidOut && bar.shuffling && bar.dragKey !== "workspaces"
NumberAnimation {
duration: Theme.barDurEnter
easing.type: Easing.Bezier
easing.bezierCurve: Theme.easeEmphasizedDecel
}
}
}
mask: Region {
ModuleRegion {
mod: workspacesMod
}
ModuleRegion {
mod: mprisMod
}
ModuleRegion {
mod: sysTrayMod
}
ModuleRegion {
mod: clockMod
}
ModuleRegion {
mod: notifMod
}
ModuleRegion {
mod: systemMod
}
ModuleRegion {
mod: privacyMod
}
ModuleRegion {
mod: powerMod
}
ModuleRegion {
mod: windowMod
}
ModuleRegion {
mod: startMod
}
Region {
item: Prefs.barAutoHide ? revealArea : null
}
Region {
width: bar.dragHeld || Edit.active ? bar.width : 0
height: bar.dragHeld ? bar.height : (Edit.active ? Theme.dp(Prefs.barHeight) + Theme.dp(4) : 0)
}
}
BarEdit {
id: barEdit
host: bar
}
// touching this strip reveals the bar
MouseArea {
id: revealArea
x: 0
y: 0
width: bar.width
height: Theme.dp(4)
hoverEnabled: true
acceptedButtons: Qt.NoButton
enabled: Prefs.barAutoHide
visible: Prefs.barAutoHide
}
// the overview and the dock menu shut each other
Connections {
function onExpandedChanged() {
if (workspacesMod.expanded)
dock.menuOpen = false;
}
target: workspacesMod
}
Connections {
function onMenuOpenChanged() {
if (dock.menuOpen)
workspacesMod.expanded = false;
}
target: dock
}
BackgroundEffect.blurRegion: (Theme.blurAmount > 0 && bar.laidOut) ? barBlurRegion : null
// clipped to the surface: hyprland frosts the whole layer for a region
// wholly outside it, which is where a hidden bar's pills are
Region {
id: barBlurRegion
intersection: Intersection.Intersect
Region {
x: 0
y: fullStrip.visible ? bar.hiddenOffset : 0
width: fullStrip.visible ? Math.round(bar.width) : 0
height: fullStrip.visible ? Theme.dp(Prefs.barHeight) : 0
}
ModuleRegion {
blur: true
mod: workspacesMod
}
ModuleRegion {
blur: true
mod: mprisMod
}
ModuleRegion {
blur: true
mod: sysTrayMod
}
ModuleRegion {
blur: true
mod: clockMod
}
ModuleRegion {
blur: true
mod: notifMod
}
ModuleRegion {
blur: true
mod: systemMod
}
ModuleRegion {
blur: true
mod: privacyMod
}
ModuleRegion {
blur: true
mod: powerMod
}
ModuleRegion {
blur: true
mod: windowMod
}
ModuleRegion {
blur: true
mod: startMod
}
}
}
}
Dock {
id: dock
}
// edit mode's toolbar and its sheet of options, over everything
EditLayer {
}
// lucidshot: the engine that saves and copies captures, the overlay they
// are chosen on, the cards they land in, pins, and the recording controls
ShotEngine {
id: shotEngine
cards: shotCards
onReady: (entry) => {
// the overlay made its card already and only needed the file
if (entry.owner === "overlay") {
shotCards.update(entry);
return ;
}
if (entry.owner === "pin")
return ;
root.announceShot(entry);
}
onFailed: (id, why) => {
return toastMod.popup("warning", why, true);
}
onTextRead: (r) => {
// the overlay and the cards take their own; this is a read over ipc
if (!shotEngine.claim(r.id))
return ;
if (r.status === "ok") {
if (Prefs.shotPreview === "preview")
shotCards.show({
"id": r.id,
"kind": "text",
"text": r.text,
"codes": r.codes,
"screen": Monitors.focusedScreen ? Monitors.focusedScreen.name : ""
});
else
toastMod.popup("copy", "Text copied", false);
} else if (r.status === "notool") {
toastMod.popup("alert", "Install tesseract to copy text", true);
} else {
toastMod.popup("alert", "No text found", true);
}
}
}
ShotOverlay {
id: shotOverlay
engine: shotEngine
cards: shotCards
pins: shotPins
onNotify: (icon, text, warn) => {
return toastMod.popup(icon, text, warn);
}
onSwatch: (hex, text) => {
return toastMod.popupSwatch(hex, text);
}
onFreezeFailed: (why) => {
return toastMod.popup("alert", "Couldn't capture the screen", true);
}
}
ShotCards {
id: shotCards
engine: shotEngine
pins: shotPins
overlay: shotOverlay
}
PinBoard {
id: shotPins
engine: shotEngine
}
RecordHud {
}
Connections {
function onSaved(file, seconds) {
root.announceShot({
"id": "rec-" + Date.now(),
"kind": "video",
"file": file,
"duration": seconds,
"screen": Capture.output
});
}
function onFailed(why) {
toastMod.popup("warning", why, true);
}
target: Capture
}
WidgetLayer {
id: widgetLayer
}
WidgetIpc {
}
MonitorIpc {
}
Desktop {
}
// the desktop icons' overlays: the rename field, the undo snackbar and the
// confirm dialog, built only while the icons are on
LazyLoader {
active: DesktopIcons.live
Scope {
IconRename {
}
DeskSnackbar {
}
DeskDialog {
}
}
}
Osd {
id: osdMod
}
PinnedWindows {
}
UrgentWindows {
}
Toast {
id: toastMod
}
ToastEvents {
toast: toastMod
}
Lock {
id: lockMod
}
Moji {
id: mojiMod
}
Keyboard {
id: keyboardMod
}
KeybindSheet {
id: keybindSheetMod
}
WhatsNew {
id: whatsNewMod
}
// the update pop-up: warning, password, progress, restart
UpdateSheet {
id: updateSheetMod
}
SettingsHost {
id: settingsMod
}
// the numbers the Displays page puts on every screen
DisplayIdentify {
}
// "keep these display settings?" on every screen while a change is on trial
DisplayTrial {
}
// the map on the Displays page marks the displays these sit on
Binding {
target: Monitors
property: "barWindows"
value: bars.instances
}
Auth {
id: polkitMod
}
Session {
id: sessionMod
}
Switcher {
id: switcherMod
dockMod: dock
}
Connections {
function onSettingsRequested() {
settingsMod.show("notifications");
}
target: Notifs
}
PanelWindow {
id: clickCatcher
visible: dock.menuOpen && Monitors.surfacesUp
color: "transparent"
exclusiveZone: 0
WlrLayershell.layer: WlrLayer.Top
anchors {
top: true
bottom: true
left: true
right: true
}
MouseArea {
anchors.fill: parent
onClicked: dock.menuOpen = false
}
}
// every surface there is one of, on the display Settings > Displays picks —
// the dock can be sent to one of its own (the bar places itself), the rest
// follow the shell. gated, because unset must leave the choice to hyprland,
// which null would not. emoji and screenshot act on the window you are in,
// so they follow the focus, and so does the launcher while the dock is off
// (Dock.launcherPlacement)
Instantiator {
model: [dock, clickCatcher]
Binding {
required property var modelData
target: modelData
property: "screen"
value: dock.launcherPlacement || Monitors.dockPlacement
when: (dock.launcherPlacement || Monitors.dockPlacement) !== null
}
}
Instantiator {
model: [toastMod, keyboardMod, polkitMod, keybindSheetMod, whatsNewMod]
Binding {
required property var modelData
target: modelData
property: "screen"
value: Monitors.shellPlacement
when: Monitors.shellPlacement !== null
}
}
// the osd follows the shell too, unless no display is picked for it: then
// it shows on the one being worked on, as the launcher does (Osd.placement)
Binding {
target: osdMod
property: "screen"
value: osdMod.placement || Monitors.shellPlacement
when: (osdMod.placement || Monitors.shellPlacement) !== null
}
Connections {
function onKeyboardRequested() {
keyboardMod.show();
}
target: Prefs
}
Connections {
function onDesktopActionRequested(action) {
if (action === "wallpaper")
dock.openLauncher(">wallpaper");
else if (action === "theme")
dock.openLauncher(">theme");
else if (action === "screenshot")
shotOverlay.begin("shot", "");
}
target: Prefs
}
}