quickshell: vertical left bar, 2x rounding, left-aligned surfaces
Bar moves from a horizontal top band to a vertical left column (Theme.barHeight -> barWidth, 46px for the stacked HH/mm clock). Every panel now grows rightward out of the bar instead of down: the dropdown chrome's morph axis, the SDF surface geometry and the session menu all swap x/y. The launcher is the one surface left at bottom-centre, as asked. New: NixOS snowflake at the top (recoloured to the accent with the tray's hidden-Image + ColorOverlay trick), vertical workspace dots whose focused pill grows on height, an inline OSD slot in the gap between the two clusters, and a power button in the bottom-left that opens the session menu. Dropped the flush-to-far-edge dropdown special case — clamped tracking plus the SDF melt already covers it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
efd1331823
commit
33b729c560
2 changed files with 354 additions and 294 deletions
|
|
@ -104,8 +104,8 @@ in
|
|||
# 6px of the outer gap is occupied by the quickshell screen
|
||||
# frame (Theme.frameWidth) — 18 on framed sides keeps the
|
||||
# visible frame↔window gap equal to the inter-window gap (12).
|
||||
# Top stays 12: the bar reserves its own space.
|
||||
gaps_out = { top = 12; right = 18; bottom = 18; left = 18; };
|
||||
# Left stays 12: the bar lives there and reserves its own space.
|
||||
gaps_out = { top = 18; right = 18; bottom = 18; left = 12; };
|
||||
border_size = 2;
|
||||
layout = "dwindle";
|
||||
resize_on_border = true;
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ in
|
|||
vec4 toast; // cx, cy, hw, hh — toast (hw <= 0: none)
|
||||
vec4 session; // cx, cy, hw, hh — session menu (hw <= 0: none)
|
||||
vec4 launcher; // cx, cy, hw, hh — bottom launcher (hw <= 0: none)
|
||||
vec4 osd; // cx, cy, hw, hh — volume/brightness OSD (hw <= 0: none)
|
||||
vec4 fillColor; // straight (non-premultiplied) rgba
|
||||
vec4 borderColor;
|
||||
vec2 res;
|
||||
|
|
@ -105,8 +104,6 @@ in
|
|||
d = smin(d, sdRoundedBox(p, session.xy, session.zw, panelR), meltK);
|
||||
if (launcher.z > 0.5)
|
||||
d = smin(d, sdRoundedBox(p, launcher.xy, launcher.zw, panelR), meltK);
|
||||
if (osd.z > 0.5)
|
||||
d = smin(d, sdRoundedBox(p, osd.xy, osd.zw, panelR), meltK);
|
||||
|
||||
float fw = fwidth(d);
|
||||
// 1 inside the union, 0 outside (antialiased)
|
||||
|
|
@ -180,12 +177,15 @@ in
|
|||
readonly property int frameWidth: 6
|
||||
|
||||
// ── Layout / metric tokens (referenced, not hardcoded) ──
|
||||
// Bar band height. Drives widget heights, dropdown y-origin and
|
||||
// the shader cutout geometry — change here, not in ~10 places.
|
||||
readonly property int barHeight: 30
|
||||
readonly property int radius: 8 // cards, panels, dropdowns
|
||||
readonly property int radiusSmall: 6 // workspace dots, pill buttons
|
||||
readonly property int radiusTiny: 4 // hover rows, action chips
|
||||
// Bar band thickness. The bar is a VERTICAL column on the left
|
||||
// edge, so this is its width: it drives widget cell widths, the
|
||||
// dropdown x-origin and the shader cutout geometry — change
|
||||
// here, not in ~15 places. Wide enough for the stacked HH/mm
|
||||
// clock, which is what sets the floor.
|
||||
readonly property int barWidth: 46
|
||||
readonly property int radius: 16 // cards, panels, dropdowns
|
||||
readonly property int radiusSmall: 12 // workspace dots, pill buttons
|
||||
readonly property int radiusTiny: 8 // hover rows, action chips
|
||||
readonly property int cardPad: 8 // card inner padding (inset = 2×)
|
||||
readonly property int panelGap: 8 // gap between panel border and cards (all dropdowns/menus/toast)
|
||||
|
||||
|
|
@ -409,6 +409,9 @@ in
|
|||
readonly property string notifSound: "${pkgs.libcanberra-gtk3}/bin/canberra-gtk-play"
|
||||
readonly property string systemctl: "${pkgs.systemd}/bin/systemctl"
|
||||
readonly property string weatherFetch: "${weatherFetchScript}"
|
||||
// Recoloured to the theme accent at draw time (ColorOverlay),
|
||||
// so the stock blue/grey snowflake artwork is fine here.
|
||||
readonly property string nixLogo: "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg"
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
|
@ -631,14 +634,17 @@ in
|
|||
// the grab — it self-clears and instantly closes the panel.
|
||||
WlrLayershell.keyboardFocus: (sessionMenu.open || launcherPanel.open || bar.activeDropdown !== null) && !bar.screenshotPinned ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||
|
||||
// Vertical bar on the left edge. The surface still covers the
|
||||
// whole screen (the frame band and every panel are drawn into
|
||||
// it); only the reserved strip is the bar itself.
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
implicitHeight: bar.screen.height
|
||||
exclusiveZone: Theme.barHeight
|
||||
implicitWidth: bar.screen.width
|
||||
exclusiveZone: Theme.barWidth
|
||||
color: "transparent"
|
||||
|
||||
mask: Region {
|
||||
|
|
@ -673,8 +679,8 @@ in
|
|||
id: barBgRect
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: Theme.barHeight
|
||||
anchors.bottom: parent.bottom
|
||||
width: Theme.barWidth
|
||||
}
|
||||
|
||||
// Register the primary bar so shell.qml's IPC handler can
|
||||
|
|
@ -959,39 +965,38 @@ in
|
|||
// ears / border-gaps / melt geometry lives in the math now.
|
||||
ShaderEffect {
|
||||
anchors.fill: parent
|
||||
readonly property real panelLeft: chrome.x + 8
|
||||
readonly property real panelRight: chrome.x + chrome.width + (chrome.flushRight ? 4 : -8)
|
||||
// The panel/toast centre-y uses (barHeight − 4): the
|
||||
// surface overlaps 4px up into the bar so they melt.
|
||||
readonly property real surfTopY: Theme.barHeight - 4
|
||||
// Panels grow rightward out of the bar column, so the
|
||||
// cross-axis is now vertical: the 8px inset that used to
|
||||
// be left/right is top/bottom.
|
||||
readonly property real panelTop: chrome.y + 8
|
||||
readonly property real panelBottom: chrome.y + chrome.height - 8
|
||||
// The panel/toast/session centre-x uses (barWidth − 4): the
|
||||
// surface overlaps 4px into the bar so the two melt.
|
||||
readonly property real surfLeftX: Theme.barWidth - 4
|
||||
property vector4d cutout: Qt.vector4d(
|
||||
bar.width / 2,
|
||||
(Theme.barHeight + bar.height - Theme.frameWidth) / 2,
|
||||
bar.width / 2 - Theme.frameWidth,
|
||||
(bar.height - Theme.frameWidth - Theme.barHeight) / 2)
|
||||
(Theme.barWidth + bar.width - Theme.frameWidth) / 2,
|
||||
bar.height / 2,
|
||||
(bar.width - Theme.frameWidth - Theme.barWidth) / 2,
|
||||
bar.height / 2 - Theme.frameWidth)
|
||||
property vector4d panel: chrome.visible
|
||||
? Qt.vector4d((panelLeft + panelRight) / 2, surfTopY + chrome.height / 2,
|
||||
(panelRight - panelLeft) / 2, 4 + chrome.height / 2)
|
||||
? Qt.vector4d(surfLeftX + chrome.width / 2, (panelTop + panelBottom) / 2,
|
||||
4 + chrome.width / 2, (panelBottom - panelTop) / 2)
|
||||
: Qt.vector4d(0, 0, 0, 0)
|
||||
property vector4d toast: toastItem.visible && _toastRect.height > 0.5
|
||||
? Qt.vector4d(toastItem.x + 8 + _toastRect.width / 2, surfTopY + _toastRect.height / 2,
|
||||
_toastRect.width / 2, 4 + _toastRect.height / 2)
|
||||
property vector4d toast: toastItem.visible && _toastRect.width > 0.5
|
||||
? Qt.vector4d(surfLeftX + _toastRect.width / 2, toastItem.y + 8 + _toastRect.height / 2,
|
||||
4 + _toastRect.width / 2, _toastRect.height / 2)
|
||||
: Qt.vector4d(0, 0, 0, 0)
|
||||
readonly property real sessRight: bar.width - Theme.frameWidth + 4
|
||||
property vector4d session: sessionMenu.visible
|
||||
? Qt.vector4d((sessionMenu.x + sessRight) / 2, sessionMenu.y + sessionMenu.height / 2,
|
||||
(sessRight - sessionMenu.x) / 2, sessionMenu.height / 2)
|
||||
? Qt.vector4d(surfLeftX + sessionMenu.width / 2, sessionMenu.y + sessionMenu.height / 2,
|
||||
4 + sessionMenu.width / 2, sessionMenu.height / 2)
|
||||
: Qt.vector4d(0, 0, 0, 0)
|
||||
// The launcher is the one surface that stays put: it still
|
||||
// grows from the bottom frame band at screen centre.
|
||||
readonly property real launchBot: bar.height - Theme.frameWidth + 4
|
||||
property vector4d launcher: launcherPanel.visible
|
||||
? Qt.vector4d(launcherPanel.x + launcherPanel.width / 2, (launcherPanel.y + launchBot) / 2,
|
||||
launcherPanel.width / 2, (launchBot - launcherPanel.y) / 2)
|
||||
: Qt.vector4d(0, 0, 0, 0)
|
||||
// OSD shares the launcher's bottom melt line.
|
||||
property vector4d osd: osdItem.visible
|
||||
? Qt.vector4d(osdItem.x + osdItem.width / 2, (osdItem.y + launchBot) / 2,
|
||||
osdItem.width / 2, (launchBot - osdItem.y) / 2)
|
||||
: Qt.vector4d(0, 0, 0, 0)
|
||||
property vector4d fillColor: Qt.vector4d(Theme.barBg.r, Theme.barBg.g, Theme.barBg.b, Theme.barBg.a)
|
||||
property vector4d borderColor: Qt.vector4d(Theme.base03.r, Theme.base03.g, Theme.base03.b, 1)
|
||||
property vector2d res: Qt.vector2d(width, height)
|
||||
|
|
@ -1003,8 +1008,9 @@ in
|
|||
}
|
||||
|
||||
// ── Session menu: icon-only power controls morphing out of
|
||||
// the right frame column at screen centre (Super+L). Keyboard:
|
||||
// arrows/Tab move the selection, Enter activates, Esc closes.
|
||||
// the bar at the bottom-left (Super+L, or the power button).
|
||||
// Keyboard: arrows/Tab move the selection, Enter activates,
|
||||
// Esc closes.
|
||||
Item {
|
||||
id: sessionMenu
|
||||
property bool open: false
|
||||
|
|
@ -1044,8 +1050,10 @@ in
|
|||
}
|
||||
}
|
||||
|
||||
x: bar.width - Theme.frameWidth - openW
|
||||
y: Math.round((bar.height - height) / 2)
|
||||
// Unfolds rightward out of the bar at the bottom-left,
|
||||
// level with the power button that opens it.
|
||||
x: Theme.barWidth
|
||||
y: Math.round(bar.height - Theme.frameWidth - height - 8)
|
||||
width: openW
|
||||
height: openH
|
||||
visible: openW > 0.5
|
||||
|
|
@ -1071,8 +1079,8 @@ in
|
|||
Rectangle {
|
||||
id: sessionCard
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.panelGap
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.panelGap
|
||||
width: 48
|
||||
height: sessionCol.height + 8
|
||||
radius: Theme.radius
|
||||
|
|
@ -1417,17 +1425,98 @@ in
|
|||
}
|
||||
}
|
||||
|
||||
// Left — workspace dots: accent pill for the focused
|
||||
// workspace, dim dots otherwise. All colours from Theme
|
||||
// (stylix); the pill matches hyprland's active border accent.
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
// Corner symmetry: the dots sit 12px from the screen's
|
||||
// top edge (centered in the 30px bar), so the first dot's
|
||||
// VISIBLE edge sits 12px from the left edge too. Cells
|
||||
// pad their dots by 3px, hence the -3.
|
||||
anchors.leftMargin: 12 - 3
|
||||
anchors.verticalCenter: barBgRect.verticalCenter
|
||||
// SystemClock ticks on the minute boundary instead of a 1 Hz
|
||||
// Timer; the calendar reads clockText.now too.
|
||||
SystemClock {
|
||||
id: sysClock
|
||||
precision: SystemClock.Minutes
|
||||
}
|
||||
|
||||
// ── Top cluster: logo, clock, workspaces. The bar is a column
|
||||
// now, so the two clusters anchor to opposite ends and the gap
|
||||
// between them is the OSD slot.
|
||||
Column {
|
||||
id: barTop
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 12
|
||||
anchors.horizontalCenter: barBgRect.horizontalCenter
|
||||
spacing: 12
|
||||
|
||||
// NixOS snowflake, flattened to the theme accent with the
|
||||
// same hidden-Image + ColorOverlay trick the tray uses
|
||||
// (there is no Material Symbols glyph for it).
|
||||
Item {
|
||||
width: Theme.barWidth
|
||||
height: 24
|
||||
|
||||
Image {
|
||||
id: nixLogoImg
|
||||
anchors.centerIn: parent
|
||||
width: 22
|
||||
height: 22
|
||||
source: "file://" + Commands.nixLogo
|
||||
sourceSize.width: 22
|
||||
sourceSize.height: 22
|
||||
smooth: true
|
||||
visible: false
|
||||
}
|
||||
|
||||
ColorOverlay {
|
||||
anchors.fill: nixLogoImg
|
||||
source: nixLogoImg
|
||||
color: Theme.base0D
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: bar.toggleLauncher()
|
||||
}
|
||||
}
|
||||
|
||||
// Clock — HH over mm; "HH:mm" does not fit a 46px column.
|
||||
Item {
|
||||
id: clockText
|
||||
width: Theme.barWidth
|
||||
height: clockCol.height
|
||||
property date now: sysClock.date
|
||||
|
||||
Column {
|
||||
id: clockCol
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 0
|
||||
|
||||
SText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: clockText.now.toLocaleTimeString(Qt.locale(), "HH")
|
||||
font.pixelSize: 15
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
SText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: clockText.now.toLocaleTimeString(Qt.locale(), "mm")
|
||||
font.pixelSize: 15
|
||||
font.weight: Font.Medium
|
||||
color: Theme.base04
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: bar.toggleDropdown(calPopup, function() { calPopup.resetView(); })
|
||||
onEntered: {
|
||||
if (bar.activeDropdown) {
|
||||
if (bar.activeDropdown !== calPopup) bar.toggleDropdown(calPopup, function() { calPopup.resetView(); });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workspace dots: accent pill for the focused workspace,
|
||||
// dim dots otherwise. Stacked vertically now, so the
|
||||
// focused pill grows along HEIGHT rather than width.
|
||||
Column {
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
|
|
@ -1437,18 +1526,18 @@ in
|
|||
id: wsItem
|
||||
required property var modelData
|
||||
visible: modelData.id > 0
|
||||
width: visible ? dot.width + 6 : 0
|
||||
height: Theme.barHeight
|
||||
width: Theme.barWidth
|
||||
height: visible ? dot.height + 6 : 0
|
||||
|
||||
Rectangle {
|
||||
id: dot
|
||||
anchors.centerIn: parent
|
||||
width: wsItem.modelData.focused ? 18 : 6
|
||||
height: 6
|
||||
width: 6
|
||||
height: wsItem.modelData.focused ? 18 : 6
|
||||
radius: 3
|
||||
color: wsItem.modelData.focused ? Theme.base0D
|
||||
: wsMa.containsMouse ? Theme.base04 : Theme.base03
|
||||
Behavior on width {
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.OutExpo }
|
||||
}
|
||||
Behavior on color { ColorAnimation { duration: Theme.animFade } }
|
||||
|
|
@ -1463,53 +1552,23 @@ in
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Center — clock. SystemClock ticks on the minute boundary
|
||||
// instead of a 1 Hz Timer; the calendar reads clockText.now too.
|
||||
SystemClock {
|
||||
id: sysClock
|
||||
precision: SystemClock.Minutes
|
||||
}
|
||||
|
||||
SText {
|
||||
id: clockText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: barBgRect.verticalCenter
|
||||
property date now: sysClock.date
|
||||
text: now.toLocaleTimeString(Qt.locale(), "HH:mm")
|
||||
color: Theme.base05
|
||||
font.pixelSize: 13
|
||||
font.weight: Font.Medium
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: bar.toggleDropdown(calPopup, function() { calPopup.resetView(); })
|
||||
onEntered: {
|
||||
if (bar.activeDropdown) {
|
||||
if (bar.activeDropdown !== calPopup) bar.toggleDropdown(calPopup, function() { calPopup.resetView(); });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Right — network, battery, tray
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
// Corner symmetry like the dots: last tray icon's VISIBLE
|
||||
// edge 12px from the right screen edge; tray cells pad
|
||||
// their 16px icons by 4px, hence the -4. The extra -2
|
||||
// optically compensates for transparent padding baked
|
||||
// into typical tray icon artwork.
|
||||
anchors.rightMargin: 12 - 4 - 2
|
||||
anchors.verticalCenter: barBgRect.verticalCenter
|
||||
// ── Bottom cluster: status icons, tray, power button. Anchored
|
||||
// to the bottom edge so the gap above it — between here and the
|
||||
// workspace dots — stays free as the OSD slot.
|
||||
Column {
|
||||
id: barBottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 12
|
||||
anchors.horizontalCenter: barBgRect.horizontalCenter
|
||||
spacing: 10
|
||||
|
||||
// Volume
|
||||
Item {
|
||||
id: volWidget
|
||||
width: volRow.width
|
||||
height: Theme.barHeight
|
||||
width: Theme.barWidth
|
||||
height: 26
|
||||
|
||||
property PwNode sink: Pipewire.defaultAudioSink
|
||||
|
||||
|
|
@ -1527,29 +1586,18 @@ in
|
|||
|
||||
function openVolDropdown() {
|
||||
bar.toggleDropdown(volDropdown, function() {
|
||||
let pos = volWidget.mapToItem(bar.contentItem, volWidget.width / 2, 0);
|
||||
volDropdown.dropdownX = pos.x;
|
||||
let pos = volWidget.mapToItem(bar.contentItem, 0, volWidget.height / 2);
|
||||
volDropdown.dropdownY = pos.y;
|
||||
});
|
||||
}
|
||||
|
||||
Row {
|
||||
id: volRow
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 3
|
||||
|
||||
// Icon only — the level lives in the OSD slot above and
|
||||
// in the dropdown; "100%" does not fit the column.
|
||||
SIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.centerIn: parent
|
||||
text: volWidget.volIcon
|
||||
color: volWidget.muted ? Theme.base03 : Theme.base05
|
||||
font.pixelSize: 16
|
||||
}
|
||||
|
||||
SText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: volWidget.vol + "%"
|
||||
color: volWidget.muted ? Theme.base03 : Theme.base05
|
||||
font.pixelSize: 13
|
||||
}
|
||||
font.pixelSize: 18
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
|
@ -1575,8 +1623,8 @@ in
|
|||
// Network status
|
||||
Item {
|
||||
id: netWidget
|
||||
width: 16
|
||||
height: Theme.barHeight
|
||||
width: Theme.barWidth
|
||||
height: 26
|
||||
|
||||
property string netState: "disconnected"
|
||||
property string netConn: ""
|
||||
|
|
@ -1721,8 +1769,8 @@ in
|
|||
function openNetDropdown() {
|
||||
bar.toggleDropdown(netDropdown, function() {
|
||||
wifiScanProc.running = true;
|
||||
let pos = netWidget.mapToItem(bar.contentItem, netWidget.width / 2, 0);
|
||||
netDropdown.dropdownX = pos.x;
|
||||
let pos = netWidget.mapToItem(bar.contentItem, 0, netWidget.height / 2);
|
||||
netDropdown.dropdownY = pos.y;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1742,8 +1790,8 @@ in
|
|||
// Battery
|
||||
Item {
|
||||
id: batteryWidget
|
||||
width: batteryText.width + 4 + batteryIconText.width
|
||||
height: Theme.barHeight
|
||||
width: Theme.barWidth
|
||||
height: batteryCol.height
|
||||
|
||||
// Live DBus-driven properties from the UPower service —
|
||||
// no polling, no /sys parsing, no subprocess spawns.
|
||||
|
|
@ -1771,38 +1819,38 @@ in
|
|||
: batteryLevel >= 15 ? "battery_2_bar"
|
||||
: "battery_alert"
|
||||
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
|
||||
// Explicit vertical centering: Rows top-align by
|
||||
// default, and the icon font's taller line metrics
|
||||
// would push the text off the shared baseline.
|
||||
SText {
|
||||
id: batteryText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: batteryWidget.batteryLevel + "%"
|
||||
color: batteryWidget.batteryLevel <= 15 ? Theme.base08
|
||||
: batteryWidget.batteryLevel <= 30 ? Theme.base0A
|
||||
: Theme.base05
|
||||
font.pixelSize: 13
|
||||
}
|
||||
// Stacked: icon over percentage. Side by side no longer
|
||||
// fits, but the level is worth keeping on a laptop.
|
||||
Column {
|
||||
id: batteryCol
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 0
|
||||
|
||||
SIcon {
|
||||
id: batteryIconText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: batteryWidget.batteryIcon
|
||||
color: batteryWidget.batteryLevel <= 15 ? Theme.base08
|
||||
: batteryWidget.batteryLevel <= 30 ? Theme.base0A
|
||||
: Theme.base05
|
||||
font.pixelSize: 16
|
||||
font.pixelSize: 18
|
||||
}
|
||||
|
||||
SText {
|
||||
id: batteryText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: batteryWidget.batteryLevel
|
||||
color: batteryWidget.batteryLevel <= 15 ? Theme.base08
|
||||
: batteryWidget.batteryLevel <= 30 ? Theme.base0A
|
||||
: Theme.base05
|
||||
font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
|
||||
function openBatteryDropdown() {
|
||||
bar.toggleDropdown(batteryDropdown, function() {
|
||||
let pos = batteryWidget.mapToItem(bar.contentItem, batteryWidget.width / 2, 0);
|
||||
batteryDropdown.dropdownX = pos.x;
|
||||
let pos = batteryWidget.mapToItem(bar.contentItem, 0, batteryWidget.height / 2);
|
||||
batteryDropdown.dropdownY = pos.y;
|
||||
brightRead.running = true; // refresh sliders on open
|
||||
});
|
||||
}
|
||||
|
|
@ -1825,8 +1873,8 @@ in
|
|||
// desktop, so the card gets its own bar icon.
|
||||
Item {
|
||||
id: qsWidget
|
||||
width: qsIcon.width
|
||||
height: Theme.barHeight
|
||||
width: Theme.barWidth
|
||||
height: 26
|
||||
|
||||
SIcon {
|
||||
id: qsIcon
|
||||
|
|
@ -1837,8 +1885,8 @@ in
|
|||
|
||||
function openQsDropdown() {
|
||||
bar.toggleDropdown(qsDropdown, function() {
|
||||
let pos = qsWidget.mapToItem(bar.contentItem, qsWidget.width / 2, 0);
|
||||
qsDropdown.dropdownX = pos.x;
|
||||
let pos = qsWidget.mapToItem(bar.contentItem, 0, qsWidget.height / 2);
|
||||
qsDropdown.dropdownY = pos.y;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1855,11 +1903,11 @@ in
|
|||
}
|
||||
''}
|
||||
|
||||
// Tray icons
|
||||
Row {
|
||||
// Tray icons — stacked, like everything else in the column
|
||||
Column {
|
||||
id: trayArea
|
||||
spacing: 8
|
||||
height: Theme.barHeight
|
||||
width: Theme.barWidth
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Repeater {
|
||||
|
|
@ -1867,8 +1915,8 @@ in
|
|||
|
||||
Item {
|
||||
required property var modelData
|
||||
width: 24
|
||||
height: Theme.barHeight
|
||||
width: Theme.barWidth
|
||||
height: 24
|
||||
|
||||
Image {
|
||||
id: trayIcon
|
||||
|
|
@ -1898,14 +1946,14 @@ in
|
|||
if (modelData.hasMenu && !(bar.activeDropdown === contextMenu && contextMenu.trayItem === modelData)) {
|
||||
if (bar.activeDropdown === contextMenu) {
|
||||
// Same dropdown, just switch content
|
||||
let pos = parent.mapToItem(bar.contentItem, parent.width / 2, 0);
|
||||
contextMenu.dropdownX = pos.x;
|
||||
let pos = parent.mapToItem(bar.contentItem, 0, parent.height / 2);
|
||||
contextMenu.dropdownY = pos.y;
|
||||
contextMenu.trayItem = modelData;
|
||||
menuOpener.menu = modelData.menu;
|
||||
} else {
|
||||
bar.toggleDropdown(contextMenu, function() {
|
||||
let pos = parent.mapToItem(bar.contentItem, parent.width / 2, 0);
|
||||
contextMenu.dropdownX = pos.x;
|
||||
let pos = parent.mapToItem(bar.contentItem, 0, parent.height / 2);
|
||||
contextMenu.dropdownY = pos.y;
|
||||
contextMenu.trayItem = modelData;
|
||||
menuOpener.menu = modelData.menu;
|
||||
});
|
||||
|
|
@ -1920,8 +1968,8 @@ in
|
|||
onClicked: (event) => {
|
||||
if (modelData.hasMenu) {
|
||||
bar.toggleDropdown(contextMenu, function() {
|
||||
let pos = parent.mapToItem(bar.contentItem, parent.width / 2, 0);
|
||||
contextMenu.dropdownX = pos.x;
|
||||
let pos = parent.mapToItem(bar.contentItem, 0, parent.height / 2);
|
||||
contextMenu.dropdownY = pos.y;
|
||||
contextMenu.trayItem = modelData;
|
||||
menuOpener.menu = modelData.menu;
|
||||
});
|
||||
|
|
@ -1933,6 +1981,30 @@ in
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Power menu opener — bottom-left corner. Opens the same
|
||||
// session menu as Super+L, which now unfolds from here.
|
||||
Item {
|
||||
id: powerWidget
|
||||
width: Theme.barWidth
|
||||
height: 26
|
||||
|
||||
SIcon {
|
||||
anchors.centerIn: parent
|
||||
text: "power_settings_new"
|
||||
color: powerMa.containsMouse || sessionMenu.open ? Theme.base08 : Theme.base05
|
||||
font.pixelSize: 18
|
||||
Behavior on color { ColorAnimation { duration: Theme.animFade } }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: powerMa
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: bar.toggleSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown container — content, sizing and autoclose only.
|
||||
|
|
@ -1942,13 +2014,14 @@ in
|
|||
id: dropdown
|
||||
property bool open: false
|
||||
property bool closing: false
|
||||
property real dropdownX: 0
|
||||
// Vertical centre of the widget that owns this dropdown;
|
||||
// the panel tracks it, clamped to the screen. The old
|
||||
// flush-to-far-edge special case is gone — the SDF melt
|
||||
// already makes a clamped panel meet the frame cleanly.
|
||||
property real dropdownY: 0
|
||||
property real fullWidth: 200
|
||||
property real fullHeight: 200
|
||||
// Flush-right dropdowns merge into the screen frame's
|
||||
// right column instead of centering on their widget.
|
||||
property bool alignRight: false
|
||||
property real dropdownHeight: open ? fullHeight : 0
|
||||
property real dropdownWidth: open ? fullWidth : 0
|
||||
default property alias content: dropdownContent.data
|
||||
|
||||
function animateClose() {
|
||||
|
|
@ -1976,12 +2049,11 @@ in
|
|||
open = true;
|
||||
}
|
||||
|
||||
x: alignRight
|
||||
? bar.width - Theme.frameWidth - width
|
||||
: Math.round(Math.min(bar.width - Theme.frameWidth - width, Math.max(Theme.frameWidth, dropdownX - width / 2)))
|
||||
y: Theme.barHeight
|
||||
width: fullWidth + (alignRight ? 8 : 16)
|
||||
height: fullHeight + 4
|
||||
x: Theme.barWidth
|
||||
y: Math.round(Math.min(bar.height - Theme.frameWidth - height,
|
||||
Math.max(Theme.frameWidth, dropdownY - height / 2)))
|
||||
width: fullWidth + 4
|
||||
height: fullHeight + 16
|
||||
visible: false
|
||||
|
||||
onVisibleChanged: {
|
||||
|
|
@ -2004,13 +2076,14 @@ in
|
|||
// revealed as the panel slides/grows over it and wiped as
|
||||
// the panel leaves, instead of popping in place. The inner
|
||||
// item counter-offsets so content stays put while the clip
|
||||
// window moves across it.
|
||||
// window moves across it. Growth axis is now x (rightward);
|
||||
// the clip's height is the chrome's inner cross-axis span.
|
||||
Item {
|
||||
id: _dropdownRect
|
||||
x: (chrome.x + 8) - dropdown.x
|
||||
y: 0
|
||||
width: Math.max(0, chrome.width - (chrome.flushRight ? 8 : 16))
|
||||
height: Math.min(dropdown.fullHeight, chrome.height)
|
||||
x: 0
|
||||
y: (chrome.y + 8) - dropdown.y
|
||||
width: Math.min(dropdown.fullWidth, chrome.width)
|
||||
height: Math.max(0, chrome.height - 16)
|
||||
clip: true
|
||||
opacity: dropdown.open ? 1 : 0
|
||||
Behavior on opacity {
|
||||
|
|
@ -2019,7 +2092,7 @@ in
|
|||
|
||||
Item {
|
||||
id: dropdownContent
|
||||
x: 8 - _dropdownRect.x
|
||||
y: 8 - _dropdownRect.y
|
||||
width: dropdown.fullWidth
|
||||
height: dropdown.fullHeight
|
||||
}
|
||||
|
|
@ -2031,76 +2104,65 @@ in
|
|||
// instantly when opening from closed.
|
||||
Item {
|
||||
id: chrome
|
||||
property real tX: 0
|
||||
property real tW: 200
|
||||
property real tH: 0
|
||||
property bool flushRight: false
|
||||
property real openH: bar.activeDropdown ? tH : 0
|
||||
property real tY: 0
|
||||
property real tH: 200
|
||||
property real tW: 0
|
||||
property real openW: bar.activeDropdown ? tW : 0
|
||||
property bool snap: false
|
||||
readonly property real stubW: 32
|
||||
readonly property real stubH: 32
|
||||
|
||||
// Grow-from / shrink-to the widget that owns the dropdown:
|
||||
// the panel opens as a small stub on the button and
|
||||
// expands; closing retargets back to the stub while the
|
||||
// height collapses.
|
||||
function stubX(dd) {
|
||||
return Math.round(Math.min(bar.width - Theme.frameWidth - stubW, Math.max(Theme.frameWidth, dd.dropdownX - stubW / 2)));
|
||||
// the panel opens as a small stub beside the button and
|
||||
// expands rightward; closing retargets back to the stub
|
||||
// while the width collapses.
|
||||
function stubY(dd) {
|
||||
return Math.round(Math.min(bar.height - Theme.frameWidth - stubH, Math.max(Theme.frameWidth, dd.dropdownY - stubH / 2)));
|
||||
}
|
||||
// All dropdowns grow from / shrink to their own widget —
|
||||
// flush ones melt onto the frame column as they expand
|
||||
// (the SDF chrome makes that junction liquid, so the old
|
||||
// corner-parked seed workaround is unnecessary).
|
||||
function seedFromButton(dd) {
|
||||
snap = true;
|
||||
tX = stubX(dd);
|
||||
tW = stubW;
|
||||
tY = stubY(dd);
|
||||
tH = stubH;
|
||||
snap = false;
|
||||
}
|
||||
function shrinkToButton(dd) {
|
||||
tX = stubX(dd);
|
||||
tW = stubW;
|
||||
tY = stubY(dd);
|
||||
tH = stubH;
|
||||
}
|
||||
|
||||
x: tX
|
||||
y: Theme.barHeight
|
||||
width: tW
|
||||
height: openH
|
||||
visible: height > 0.5
|
||||
x: Theme.barWidth
|
||||
y: tY
|
||||
width: openW
|
||||
height: tH
|
||||
visible: width > 0.5
|
||||
|
||||
Binding {
|
||||
target: chrome; property: "tX"
|
||||
value: bar.activeDropdown ? bar.activeDropdown.x : 0
|
||||
when: bar.activeDropdown !== null
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
Binding {
|
||||
target: chrome; property: "tW"
|
||||
value: bar.activeDropdown ? bar.activeDropdown.width : 0
|
||||
target: chrome; property: "tY"
|
||||
value: bar.activeDropdown ? bar.activeDropdown.y : 0
|
||||
when: bar.activeDropdown !== null
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
Binding {
|
||||
target: chrome; property: "tH"
|
||||
value: bar.activeDropdown ? bar.activeDropdown.fullHeight + 4 : 0
|
||||
value: bar.activeDropdown ? bar.activeDropdown.height : 0
|
||||
when: bar.activeDropdown !== null
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
Binding {
|
||||
target: chrome; property: "flushRight"
|
||||
value: bar.activeDropdown ? bar.activeDropdown.alignRight : false
|
||||
target: chrome; property: "tW"
|
||||
value: bar.activeDropdown ? bar.activeDropdown.fullWidth + 4 : 0
|
||||
when: bar.activeDropdown !== null
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
Behavior on tX {
|
||||
Behavior on tY {
|
||||
enabled: !chrome.snap
|
||||
NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo }
|
||||
}
|
||||
Behavior on tW {
|
||||
Behavior on tH {
|
||||
enabled: !chrome.snap
|
||||
NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo }
|
||||
}
|
||||
Behavior on openH {
|
||||
Behavior on openW {
|
||||
NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo }
|
||||
}
|
||||
|
||||
|
|
@ -2109,7 +2171,6 @@ in
|
|||
// Context menu
|
||||
BarDropdown {
|
||||
id: contextMenu
|
||||
alignRight: true
|
||||
property var trayItem: null
|
||||
fullWidth: menuItems.width + 2 * Theme.panelGap
|
||||
fullHeight: menuItems.height + 2 * Theme.panelGap
|
||||
|
|
@ -2194,7 +2255,6 @@ in
|
|||
// Volume dropdown
|
||||
BarDropdown {
|
||||
id: volDropdown
|
||||
alignRight: true
|
||||
fullWidth: volDropdownCol.width + 2 * Theme.panelGap
|
||||
fullHeight: volDropdownCol.height + 2 * Theme.panelGap
|
||||
|
||||
|
|
@ -2351,7 +2411,6 @@ in
|
|||
// Network dropdown
|
||||
BarDropdown {
|
||||
id: netDropdown
|
||||
alignRight: true
|
||||
fullWidth: netDropdownCol.width + 2 * Theme.panelGap
|
||||
fullHeight: netDropdownCol.height + 2 * Theme.panelGap
|
||||
|
||||
|
|
@ -2486,7 +2545,6 @@ in
|
|||
// Battery dropdown
|
||||
BarDropdown {
|
||||
id: batteryDropdown
|
||||
alignRight: true
|
||||
fullWidth: batteryDropdownCol.width + 2 * Theme.panelGap
|
||||
fullHeight: batteryDropdownCol.height + 2 * Theme.panelGap
|
||||
|
||||
|
|
@ -2686,7 +2744,6 @@ in
|
|||
// Quick settings dropdown (desktop) — just the night light.
|
||||
BarDropdown {
|
||||
id: qsDropdown
|
||||
alignRight: true
|
||||
fullWidth: qsCol.width + 2 * Theme.panelGap
|
||||
fullHeight: qsCol.height + 2 * Theme.panelGap
|
||||
|
||||
|
|
@ -2705,7 +2762,7 @@ in
|
|||
// Right: MPRIS media controls + notification list.
|
||||
BarDropdown {
|
||||
id: calPopup
|
||||
dropdownX: bar.width / 2
|
||||
dropdownY: bar.height / 2
|
||||
// ceil: Text metrics give fractional sizes; fractional rect
|
||||
// edges render as soft 2px lines
|
||||
fullWidth: Math.ceil(calRow.width) + 2 * Theme.panelGap
|
||||
|
|
@ -3233,36 +3290,34 @@ in
|
|||
}
|
||||
}
|
||||
|
||||
// ── OSD: volume / brightness overlay melting out of the
|
||||
// bottom frame, on the focused monitor only. Volume is
|
||||
// reactive — Pipewire reports changes from any source (media
|
||||
// keys, pavucontrol, a hardware mixer), so no IPC is needed.
|
||||
// Brightness arrives via shellRoot's signal instead: sysfs
|
||||
// backlight notifies with POLLPRI, which inotify never sees.
|
||||
// Deliberately absent from the window `mask` above, unlike
|
||||
// every other surface — a transient overlay must not eat
|
||||
// clicks on whatever it covers.
|
||||
// ── OSD: volume / brightness level, shown inline in the bar's
|
||||
// empty middle — the gap between the two clusters exists for
|
||||
// this. Volume is reactive: Pipewire reports changes from any
|
||||
// source (media keys, pavucontrol, a hardware mixer), so no IPC
|
||||
// is needed. Brightness arrives via shellRoot's signal instead,
|
||||
// because sysfs backlight notifies with POLLPRI, which inotify
|
||||
// never sees. Focused monitor only.
|
||||
Item {
|
||||
id: osdItem
|
||||
readonly property bool isFocused: Hyprland.focusedMonitor
|
||||
&& Hyprland.focusedMonitor.name === bar.screen.name
|
||||
// Every binding evaluates once at load; arm on a delay so
|
||||
// a shell restart doesn't flash an OSD on screen.
|
||||
// a shell restart doesn't flash the slider on screen.
|
||||
property bool armed: false
|
||||
property bool shown: false
|
||||
property string icon: "volume_up"
|
||||
property real value: 0
|
||||
property color fill: Theme.base0D
|
||||
|
||||
width: 260
|
||||
height: shown ? 52 : 0
|
||||
x: Math.round((bar.width - width) / 2)
|
||||
y: bar.height - Theme.frameWidth - height
|
||||
visible: height > 0.5
|
||||
clip: true
|
||||
width: Theme.barWidth
|
||||
anchors.horizontalCenter: barBgRect.horizontalCenter
|
||||
anchors.top: barTop.bottom
|
||||
anchors.bottom: barBottom.top
|
||||
opacity: shown ? 1 : 0
|
||||
visible: opacity > 0.01
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo }
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Theme.animContent; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
function show(ic, v, f) {
|
||||
|
|
@ -3300,35 +3355,36 @@ in
|
|||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 4 * Theme.panelGap
|
||||
spacing: 10
|
||||
spacing: 8
|
||||
|
||||
// Vertical track — fill grows upward from the bottom.
|
||||
// PillSlider is horizontal-only, so this is its own
|
||||
// rather than a rotated instance.
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 6
|
||||
height: 96
|
||||
radius: 3
|
||||
color: Theme.base02
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width
|
||||
height: Math.max(parent.width,
|
||||
parent.height * Math.max(0, Math.min(1, osdItem.value)))
|
||||
radius: parent.radius
|
||||
color: osdItem.fill
|
||||
Behavior on height { NumberAnimation { duration: 80 } }
|
||||
}
|
||||
}
|
||||
|
||||
SIcon {
|
||||
id: osdIcon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 22
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: osdItem.icon
|
||||
color: Theme.base05
|
||||
font.pixelSize: 20
|
||||
}
|
||||
|
||||
PillSlider {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - osdIcon.width - osdPct.width - 2 * parent.spacing
|
||||
value: osdItem.value
|
||||
fillColor: osdItem.fill
|
||||
}
|
||||
|
||||
SText {
|
||||
id: osdPct
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 34
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: Math.round(osdItem.value * 100) + "%"
|
||||
color: Theme.base04
|
||||
font.pixelSize: 12
|
||||
font.pixelSize: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3342,10 +3398,10 @@ in
|
|||
readonly property var mutedApps: ["discord", "Discord", "Vesktop", "vesktop", "Spotify", "spotify", "vlc", "mpv"]
|
||||
readonly property bool isPrimary: bar.screen === Quickshell.screens[0]
|
||||
|
||||
x: Math.round(bar.width / 2 - width / 2)
|
||||
y: Theme.barHeight
|
||||
width: _toastRect.width + 16
|
||||
height: _toastRect.height + 4
|
||||
x: Theme.barWidth
|
||||
y: Theme.frameWidth + 8
|
||||
width: _toastRect.width + 4
|
||||
height: _toastRect.height + 16
|
||||
|
||||
Process {
|
||||
id: notifSoundProc
|
||||
|
|
@ -3413,13 +3469,17 @@ in
|
|||
|
||||
Item {
|
||||
id: _toastRect
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
width: 320
|
||||
height: toastItem.toastOpen ? toastCard.height + 2 * Theme.panelGap : 0
|
||||
anchors.topMargin: 8
|
||||
// Reveals rightward now, so width is the animated axis
|
||||
// and the card inside keeps a fixed width (otherwise it
|
||||
// would squash instead of being wiped in).
|
||||
width: toastItem.toastOpen ? 320 : 0
|
||||
height: toastCard.height + 2 * Theme.panelGap
|
||||
clip: true
|
||||
|
||||
Behavior on height {
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo }
|
||||
}
|
||||
|
||||
|
|
@ -3429,8 +3489,8 @@ in
|
|||
id: toastCard
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.panelGap
|
||||
width: 320 - 2 * Theme.panelGap
|
||||
height: Math.max(toastPreview.visible ? 48 : 0, toastCol.height) + 16
|
||||
radius: Theme.radius
|
||||
color: Theme.cardBg
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue