quickshell: 2x rounding again, card padding, session menu as a dropdown
Radii doubled a second time (32/24/16). Dropdown panels were +16 on the cross axis while the shader insets 8 top and bottom, leaving the panel edge flush with the content (0 padding) against a 4px overhang on the growth axis. Now +24 with the content offset 12, giving 4px above and below to match the right. Session menu becomes a real BarDropdown instead of its own surface: it shares the morphing chrome with the other bar menus, so switching between it and e.g. volume glides rather than cross-fading. Drops its dedicated SDF surface, shader uniform and mask region, and the power button gains the same hover-to-switch the other widgets have. Moved beside volDropdown so it sits after the component declaration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8c549e1422
commit
a36a3f35b7
1 changed files with 120 additions and 160 deletions
|
|
@ -68,7 +68,6 @@ in
|
||||||
vec4 cutout; // cx, cy, hw, hh — rounded inner screen cutout
|
vec4 cutout; // cx, cy, hw, hh — rounded inner screen cutout
|
||||||
vec4 panel; // cx, cy, hw, hh — dropdown panel (hw <= 0: none)
|
vec4 panel; // cx, cy, hw, hh — dropdown panel (hw <= 0: none)
|
||||||
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 launcher; // cx, cy, hw, hh — bottom launcher (hw <= 0: none)
|
vec4 launcher; // cx, cy, hw, hh — bottom launcher (hw <= 0: none)
|
||||||
vec4 fillColor; // straight (non-premultiplied) rgba
|
vec4 fillColor; // straight (non-premultiplied) rgba
|
||||||
vec4 borderColor;
|
vec4 borderColor;
|
||||||
|
|
@ -100,8 +99,6 @@ in
|
||||||
d = smin(d, sdRoundedBox(p, panel.xy, panel.zw, panelR), meltK);
|
d = smin(d, sdRoundedBox(p, panel.xy, panel.zw, panelR), meltK);
|
||||||
if (toast.z > 0.5)
|
if (toast.z > 0.5)
|
||||||
d = smin(d, sdRoundedBox(p, toast.xy, toast.zw, panelR), meltK);
|
d = smin(d, sdRoundedBox(p, toast.xy, toast.zw, panelR), meltK);
|
||||||
if (session.z > 0.5)
|
|
||||||
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);
|
||||||
|
|
||||||
|
|
@ -183,9 +180,9 @@ in
|
||||||
// here, not in ~15 places. Wide enough for the stacked HH/mm
|
// here, not in ~15 places. Wide enough for the stacked HH/mm
|
||||||
// clock, which is what sets the floor.
|
// clock, which is what sets the floor.
|
||||||
readonly property int barWidth: 46
|
readonly property int barWidth: 46
|
||||||
readonly property int radius: 16 // cards, panels, dropdowns
|
readonly property int radius: 32 // cards, panels, dropdowns
|
||||||
readonly property int radiusSmall: 12 // workspace dots, pill buttons
|
readonly property int radiusSmall: 24 // workspace dots, pill buttons
|
||||||
readonly property int radiusTiny: 8 // hover rows, action chips
|
readonly property int radiusTiny: 16 // 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)
|
||||||
|
|
||||||
|
|
@ -632,7 +629,7 @@ in
|
||||||
// (caelestia's): the grab redirects focus to this window and
|
// (caelestia's): the grab redirects focus to this window and
|
||||||
// OnDemand lets the layer surface accept it. Exclusive fights
|
// OnDemand lets the layer surface accept it. Exclusive fights
|
||||||
// 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: (launcherPanel.open || bar.activeDropdown !== null) && !bar.screenshotPinned ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||||
|
|
||||||
// Vertical bar on the left edge. The surface still covers the
|
// Vertical bar on the left edge. The surface still covers the
|
||||||
// whole screen (the frame band and every panel are drawn into
|
// whole screen (the frame band and every panel are drawn into
|
||||||
|
|
@ -661,12 +658,6 @@ in
|
||||||
width: toastItem.visible ? toastItem.width : 0
|
width: toastItem.visible ? toastItem.width : 0
|
||||||
height: toastItem.visible ? toastItem.height : 0
|
height: toastItem.visible ? toastItem.height : 0
|
||||||
}
|
}
|
||||||
Region {
|
|
||||||
x: sessionMenu.visible ? sessionMenu.x : 0
|
|
||||||
y: sessionMenu.visible ? sessionMenu.y : 0
|
|
||||||
width: sessionMenu.visible ? sessionMenu.width : 0
|
|
||||||
height: sessionMenu.visible ? sessionMenu.height : 0
|
|
||||||
}
|
|
||||||
Region {
|
Region {
|
||||||
x: launcherPanel.visible ? launcherPanel.x : 0
|
x: launcherPanel.visible ? launcherPanel.x : 0
|
||||||
y: launcherPanel.visible ? launcherPanel.y : 0
|
y: launcherPanel.visible ? launcherPanel.y : 0
|
||||||
|
|
@ -691,7 +682,10 @@ in
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSession() {
|
function toggleSession() {
|
||||||
sessionMenu.toggle();
|
bar.toggleDropdown(sessionMenu, function() {
|
||||||
|
let pos = powerWidget.mapToItem(bar.contentItem, 0, powerWidget.height / 2);
|
||||||
|
sessionMenu.dropdownY = pos.y;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLauncher() {
|
function toggleLauncher() {
|
||||||
|
|
@ -1000,12 +994,6 @@ in
|
||||||
? Qt.vector4d(surfLeftX + _toastRect.width / 2, (meltTop + toastBot) / 2,
|
? Qt.vector4d(surfLeftX + _toastRect.width / 2, (meltTop + toastBot) / 2,
|
||||||
4 + _toastRect.width / 2, (toastBot - meltTop) / 2)
|
4 + _toastRect.width / 2, (toastBot - meltTop) / 2)
|
||||||
: Qt.vector4d(0, 0, 0, 0)
|
: Qt.vector4d(0, 0, 0, 0)
|
||||||
// Session menu sits in the bottom-left corner and melts
|
|
||||||
// down into the bottom frame band.
|
|
||||||
property vector4d session: sessionMenu.visible
|
|
||||||
? Qt.vector4d(surfLeftX + sessionMenu.width / 2, (sessionMenu.y + meltBot) / 2,
|
|
||||||
4 + sessionMenu.width / 2, (meltBot - sessionMenu.y) / 2)
|
|
||||||
: Qt.vector4d(0, 0, 0, 0)
|
|
||||||
// The launcher is the one surface that stays put: it still
|
// The launcher is the one surface that stays put: it still
|
||||||
// grows from the bottom frame band at screen centre.
|
// 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
|
||||||
|
|
@ -1023,142 +1011,6 @@ in
|
||||||
fragmentShader: "file://${chromeShader}"
|
fragmentShader: "file://${chromeShader}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Session menu: icon-only power controls morphing out of
|
|
||||||
// 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
|
|
||||||
property int selIdx: 0
|
|
||||||
// Both axes animate so the panel expands from a small
|
|
||||||
// point on the column (like the top dropdowns' stub seed)
|
|
||||||
// instead of rolling out at full height.
|
|
||||||
property real openW: open ? sessionCard.width + 2 * Theme.panelGap : 0
|
|
||||||
property real openH: open ? sessionCard.height + 2 * Theme.panelGap : 36
|
|
||||||
Behavior on openW {
|
|
||||||
NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo }
|
|
||||||
}
|
|
||||||
Behavior on openH {
|
|
||||||
NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo }
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property var actions: [
|
|
||||||
{ icon: "lock", danger: false, act: "lock" },
|
|
||||||
{ icon: "logout", danger: false, act: "logout" },
|
|
||||||
{ icon: "restart_alt", danger: true, act: "reboot" },
|
|
||||||
{ icon: "power_settings_new", danger: true, act: "poweroff" }
|
|
||||||
]
|
|
||||||
|
|
||||||
function activate(act) {
|
|
||||||
open = false;
|
|
||||||
if (act === "lock") bar.shellRoot.lock();
|
|
||||||
else if (act === "logout") Hyprland.dispatch("hl.dsp.exit()");
|
|
||||||
else if (act === "reboot") Quickshell.execDetached([Commands.systemctl, "reboot"]);
|
|
||||||
else Quickshell.execDetached([Commands.systemctl, "poweroff"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
open = !open;
|
|
||||||
if (open) {
|
|
||||||
selIdx = 0;
|
|
||||||
forceActiveFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unfolds rightward out of the bar at the bottom-left,
|
|
||||||
// level with the power button that opens it. Bottom edge
|
|
||||||
// sits on the frame's inner edge so the shader can melt
|
|
||||||
// the two together (meltBot) instead of leaving a seam.
|
|
||||||
x: Theme.barWidth
|
|
||||||
y: Math.round(bar.height - Theme.frameWidth - height)
|
|
||||||
width: openW
|
|
||||||
height: openH
|
|
||||||
visible: openW > 0.5
|
|
||||||
|
|
||||||
focus: open
|
|
||||||
Keys.onEscapePressed: open = false
|
|
||||||
Keys.onUpPressed: selIdx = (selIdx + actions.length - 1) % actions.length
|
|
||||||
Keys.onDownPressed: selIdx = (selIdx + 1) % actions.length
|
|
||||||
Keys.onTabPressed: selIdx = (selIdx + 1) % actions.length
|
|
||||||
Keys.onReturnPressed: activate(actions[selIdx].act)
|
|
||||||
Keys.onEnterPressed: activate(actions[selIdx].act)
|
|
||||||
|
|
||||||
// Content pinned to the column edge, revealed by the grow
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
clip: true
|
|
||||||
opacity: sessionMenu.open ? 1 : 0
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation { duration: Theme.animContent; easing.type: Easing.OutCubic }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Card backing, matching the other dropdowns
|
|
||||||
Rectangle {
|
|
||||||
id: sessionCard
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.panelGap
|
|
||||||
width: 48
|
|
||||||
height: sessionCol.height + 8
|
|
||||||
radius: Theme.radius
|
|
||||||
color: Theme.cardBg
|
|
||||||
|
|
||||||
// Sliding selection pill — same tech as the power
|
|
||||||
// profile selector; glides between the buttons.
|
|
||||||
Rectangle {
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
radius: Theme.radius
|
|
||||||
color: Theme.base02
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.base03
|
|
||||||
x: sessionCol.x
|
|
||||||
y: sessionCol.y + sessionMenu.selIdx * 44
|
|
||||||
Behavior on y {
|
|
||||||
NumberAnimation { duration: 250; easing.type: Easing.OutExpo }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: sessionCol
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: sessionMenu.actions
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: sessBtn
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
SIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: sessBtn.modelData.icon
|
|
||||||
// All four buttons share the logout
|
|
||||||
// setup: base05, FILL on selection.
|
|
||||||
color: Theme.base05
|
|
||||||
font.pixelSize: 20
|
|
||||||
font.weight: 600
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onEntered: sessionMenu.selIdx = sessBtn.index
|
|
||||||
onClicked: sessionMenu.activate(sessBtn.modelData.act)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Launcher: rises out of the bottom frame edge (Super+R).
|
// ── Launcher: rises out of the bottom frame edge (Super+R).
|
||||||
// Lives in the SDF field, so it melts into the frame and its
|
// Lives in the SDF field, so it melts into the frame and its
|
||||||
// height morphs live as results filter. Results sit above the
|
// height morphs live as results filter. Results sit above the
|
||||||
|
|
@ -1375,11 +1227,10 @@ in
|
||||||
// closes whatever is open. Suspended while screenshotPinned so
|
// closes whatever is open. Suspended while screenshotPinned so
|
||||||
// slurp can grab input without dismissing the menu.
|
// slurp can grab input without dismissing the menu.
|
||||||
HyprlandFocusGrab {
|
HyprlandFocusGrab {
|
||||||
active: (sessionMenu.open || launcherPanel.open || bar.activeDropdown !== null) && !bar.screenshotPinned
|
active: (launcherPanel.open || bar.activeDropdown !== null) && !bar.screenshotPinned
|
||||||
windows: [bar]
|
windows: [bar]
|
||||||
onCleared: {
|
onCleared: {
|
||||||
if (bar.screenshotPinned) return;
|
if (bar.screenshotPinned) return;
|
||||||
sessionMenu.open = false;
|
|
||||||
launcherPanel.open = false;
|
launcherPanel.open = false;
|
||||||
bar.closeAllDropdowns();
|
bar.closeAllDropdowns();
|
||||||
}
|
}
|
||||||
|
|
@ -2044,6 +1895,11 @@ in
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: bar.toggleSession()
|
onClicked: bar.toggleSession()
|
||||||
|
onEntered: {
|
||||||
|
if (bar.activeDropdown) {
|
||||||
|
if (bar.activeDropdown !== sessionMenu) bar.toggleSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2099,7 +1955,11 @@ in
|
||||||
x: Theme.barWidth
|
x: Theme.barWidth
|
||||||
y: Math.round(Math.max(yMin, Math.min(yMax, dropdownY - height / 2)))
|
y: Math.round(Math.max(yMin, Math.min(yMax, dropdownY - height / 2)))
|
||||||
width: fullWidth + 4
|
width: fullWidth + 4
|
||||||
height: fullHeight + 16
|
// +24, not +16: the shader insets the panel 8px top and
|
||||||
|
// bottom, so +16 left the panel edge flush with the content
|
||||||
|
// (0 padding) while the growth axis overhung by 4. The
|
||||||
|
// extra 8 gives 4px above and below, matching the right.
|
||||||
|
height: fullHeight + 24
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
|
|
@ -2138,7 +1998,9 @@ in
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: dropdownContent
|
id: dropdownContent
|
||||||
y: 8 - _dropdownRect.y
|
// 12 = panel inset (8) + the 4px breathing room,
|
||||||
|
// keeping the content centred in the taller panel.
|
||||||
|
y: 12 - _dropdownRect.y
|
||||||
width: dropdown.fullWidth
|
width: dropdown.fullWidth
|
||||||
height: dropdown.fullHeight
|
height: dropdown.fullHeight
|
||||||
}
|
}
|
||||||
|
|
@ -2298,6 +2160,104 @@ in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Session menu: icon-only power controls morphing out of
|
||||||
|
// the bar at the bottom-left (Super+L, or the power button).
|
||||||
|
// Keyboard: arrows/Tab move the selection, Enter activates,
|
||||||
|
// Esc closes.
|
||||||
|
BarDropdown {
|
||||||
|
id: sessionMenu
|
||||||
|
fullWidth: 48 + 2 * Theme.panelGap
|
||||||
|
fullHeight: sessionCard.height + 2 * Theme.panelGap
|
||||||
|
property int selIdx: 0
|
||||||
|
|
||||||
|
readonly property var actions: [
|
||||||
|
{ icon: "lock", danger: false, act: "lock" },
|
||||||
|
{ icon: "logout", danger: false, act: "logout" },
|
||||||
|
{ icon: "restart_alt", danger: true, act: "reboot" },
|
||||||
|
{ icon: "power_settings_new", danger: true, act: "poweroff" }
|
||||||
|
]
|
||||||
|
|
||||||
|
function activate(act) {
|
||||||
|
animateClose();
|
||||||
|
if (act === "lock") bar.shellRoot.lock();
|
||||||
|
else if (act === "logout") Hyprland.dispatch("hl.dsp.exit()");
|
||||||
|
else if (act === "reboot") Quickshell.execDetached([Commands.systemctl, "reboot"]);
|
||||||
|
else Quickshell.execDetached([Commands.systemctl, "poweroff"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
focus: open
|
||||||
|
onOpenChanged: if (open) { selIdx = 0; forceActiveFocus(); }
|
||||||
|
Keys.onEscapePressed: bar.closeAllDropdowns()
|
||||||
|
Keys.onUpPressed: selIdx = (selIdx + actions.length - 1) % actions.length
|
||||||
|
Keys.onDownPressed: selIdx = (selIdx + 1) % actions.length
|
||||||
|
Keys.onTabPressed: selIdx = (selIdx + 1) % actions.length
|
||||||
|
Keys.onReturnPressed: activate(actions[selIdx].act)
|
||||||
|
Keys.onEnterPressed: activate(actions[selIdx].act)
|
||||||
|
|
||||||
|
// Card backing, matching the other dropdowns. The grow /
|
||||||
|
// clip reveal is BarDropdown's job now.
|
||||||
|
Rectangle {
|
||||||
|
id: sessionCard
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 48
|
||||||
|
height: sessionCol.height + 8
|
||||||
|
radius: Theme.radius
|
||||||
|
color: Theme.cardBg
|
||||||
|
|
||||||
|
// Sliding selection pill — same tech as the power
|
||||||
|
// profile selector; glides between the buttons.
|
||||||
|
Rectangle {
|
||||||
|
width: 40
|
||||||
|
height: 40
|
||||||
|
radius: Theme.radius
|
||||||
|
color: Theme.base02
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.base03
|
||||||
|
x: sessionCol.x
|
||||||
|
y: sessionCol.y + sessionMenu.selIdx * 44
|
||||||
|
Behavior on y {
|
||||||
|
NumberAnimation { duration: 250; easing.type: Easing.OutExpo }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: sessionCol
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: sessionMenu.actions
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: sessBtn
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
width: 40
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
SIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: sessBtn.modelData.icon
|
||||||
|
// All four buttons share the logout
|
||||||
|
// setup: base05, FILL on selection.
|
||||||
|
color: Theme.base05
|
||||||
|
font.pixelSize: 20
|
||||||
|
font.weight: 600
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onEntered: sessionMenu.selIdx = sessBtn.index
|
||||||
|
onClicked: sessionMenu.activate(sessBtn.modelData.act)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Volume dropdown
|
// Volume dropdown
|
||||||
BarDropdown {
|
BarDropdown {
|
||||||
id: volDropdown
|
id: volDropdown
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue