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