Refine shell panel transitions and layout

This commit is contained in:
David Heinemeier Hansson
2026-05-22 11:22:42 +02:00
parent 4055fa7cae
commit 7b6a03fa08
9 changed files with 180 additions and 163 deletions
+4 -1
View File
@@ -95,7 +95,10 @@ Item {
function requestPopout(owner) {
if (activePopout === owner) return
if (activePopout && "close" in activePopout) activePopout.close()
if (activePopout) {
if ("closeForPopoutSwitch" in activePopout) activePopout.closeForPopoutSwitch()
else if ("close" in activePopout) activePopout.close()
}
activePopout = owner
}
+1 -1
View File
@@ -27,7 +27,7 @@ Item {
if (n === "microphone-muted" || n === "microphone-off" || n === "mic-muted" || n === "mic-off") return "󰍭"
if (n === "microphone" || n === "mic") return "󰍬"
if (n === "keyboard") return "󰌌"
if (n === "brightness" || n === "display") return "󰃠"
if (n === "brightness" || n === "display") return "󰍹"
if (n === "touchpad") return "󰟸"
if (n === "touch" || n === "touchscreen") return "󰜉"
if (n === "media" || n === "player") return "󰝚"
+66 -97
View File
@@ -573,7 +573,7 @@ Panel {
bar: root.bar
open: root.opened
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(370))
contentWidth: panel.fittedContentWidth(Style.space(380))
contentHeight: panel.fittedContentHeight(panelColumn.implicitHeight, Style.space(560))
PanelKeyCatcher {
@@ -616,11 +616,11 @@ Panel {
width: scrollArea.availableWidth
spacing: Style.space(14)
// ---------- Hero: speaker icon · title/status · % ----------
// ---------- Hero: speaker icon · title/status ----------
Item {
id: heroItem
width: parent.width
implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight, heroPercent.implicitHeight)
implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight)
Text {
id: heroIcon
@@ -643,8 +643,7 @@ Panel {
id: heroLabels
anchors.left: heroIcon.right
anchors.leftMargin: Style.space(14)
anchors.right: heroPercent.left
anchors.rightMargin: Style.space(10)
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Style.space(2)
@@ -673,37 +672,40 @@ Panel {
width: parent.width
}
}
Text {
id: heroPercent
text: Math.round((outputSlider.dragging ? outputSlider.liveValue : root.outputVolume) * 100) + "%"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.displayLarge
font.bold: true
opacity: root.outputMuted ? 0.5 : 1.0
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
// ---- Output ----
// ---- Output devices ----
Column {
width: parent.width
spacing: Style.space(6)
Text {
text: root.sink ? "Output · " + root.nodeLabel(root.sink) : "Output"
color: Qt.darker(root.bar.foreground, 1.6)
font.family: root.bar.fontFamily
font.pixelSize: Style.font.bodySmall
elide: Text.ElideRight
Item {
width: parent.width
implicitHeight: Math.max(outputHeader.implicitHeight, outputPercent.implicitHeight)
PanelSectionHeader {
id: outputHeader
text: "OUTPUT"
foreground: root.bar.foreground
fontFamily: root.bar.fontFamily
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
Text {
id: outputPercent
text: Math.round((outputSlider.dragging ? outputSlider.liveValue : root.outputVolume) * 100) + "%"
color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
font.pixelSize: Style.font.caption
font.bold: true
anchors.right: parent.right
anchors.rightMargin: Style.space(6)
anchors.verticalCenter: parent.verticalCenter
opacity: root.outputMuted ? 0.5 : 1.0
}
}
// Output slider row — keyboard cursor target (selectedIndex === -1
// when focusSection === "output"). Icon/percent live in the hero
// above, so the slider takes the full row width.
CursorSurface {
id: outputSliderRow
width: parent.width
@@ -751,95 +753,67 @@ Panel {
}
}
PanelSeparator {
visible: root.audioSources.length > 0 || !!root.source
foreground: root.bar.foreground
}
// ---- Input ----
Column {
width: parent.width
spacing: Style.space(6)
visible: root.audioSources.length > 0 || !!root.source
Text {
text: root.source ? "Input · " + root.nodeLabel(root.source) : "Input"
color: Qt.darker(root.bar.foreground, 1.6)
font.family: root.bar.fontFamily
font.pixelSize: Style.font.bodySmall
elide: Text.ElideRight
Item {
width: parent.width
implicitHeight: Math.max(microphoneHeader.implicitHeight, microphonePercent.implicitHeight)
PanelSectionHeader {
id: microphoneHeader
text: "INPUT"
foreground: root.bar.foreground
fontFamily: root.bar.fontFamily
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
Text {
id: microphonePercent
text: Math.round((inputSlider.dragging ? inputSlider.liveValue : root.inputVolume) * 100) + "%"
color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
font.pixelSize: Style.font.caption
font.bold: true
anchors.right: parent.right
anchors.rightMargin: Style.space(6)
anchors.verticalCenter: parent.verticalCenter
opacity: root.inputMuted ? 0.5 : 1.0
}
}
CursorSurface {
id: inputSliderRow
visible: !!root.source
width: parent.width
height: inputSliderInner.implicitHeight + Style.spacing.controlGap
height: inputSlider.implicitHeight + Style.spacing.controlGap
hasCursor: root.cursorActive && root.focusSection === "input" && root.selectedIndex === -1
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(inputSliderRow)
foreground: root.bar.foreground
outline: true
Row {
id: inputSliderInner
PanelSlider {
id: inputSlider
bar: root.bar
anchors.fill: parent
anchors.leftMargin: Style.space(6)
anchors.rightMargin: Style.space(6)
spacing: Style.space(8)
minimum: 0
maximum: 1
step: 0.05
value: root.inputVolume
opacity: root.inputMuted ? 0.5 : 1.0
enabled: !!root.source
Text {
id: inputIconText
text: root.inputIcon()
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.heading
width: Style.space(22)
horizontalAlignment: Text.AlignHCenter
anchors.verticalCenter: parent.verticalCenter
opacity: root.inputMuted ? 0.5 : 1.0
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.toggleInputMute()
}
}
PanelSlider {
id: inputSlider
bar: root.bar
width: parent.width - inputIconText.width - inputPercent.width - Style.space(16)
anchors.verticalCenter: parent.verticalCenter
minimum: 0
maximum: 1
step: 0.05
value: root.inputVolume
opacity: root.inputMuted ? 0.5 : 1.0
enabled: !!root.source
onMoved: function(v) { root.setInputVolume(v) }
}
Text {
id: inputPercent
text: Math.round((inputSlider.dragging ? inputSlider.liveValue : root.inputVolume) * 100) + "%"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.bodySmall
width: Style.space(36)
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
opacity: root.inputMuted ? 0.5 : 1.0
}
onMoved: function(v) { root.setInputVolume(v) }
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
onContainsMouseChanged: if (containsMouse) {
HoverHandler {
onHoveredChanged: if (hovered) {
root.cursorActive = true
root.focusSection = "input"
root.selectedIndex = -1
@@ -860,11 +834,6 @@ Panel {
}
}
PanelSeparator {
visible: root.audioStreams.length > 0
foreground: root.bar.foreground
}
// ---- Per-app streams ----
Column {
width: parent.width
+1 -1
View File
@@ -455,7 +455,7 @@ Panel {
bar: root.bar
open: root.opened
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(320))
contentWidth: panel.fittedContentWidth(Style.space(380))
contentHeight: panel.fittedContentHeight(column.implicitHeight)
PanelKeyCatcher {
+62 -53
View File
@@ -399,11 +399,11 @@ Panel {
width: scrollArea.availableWidth
spacing: Style.space(14)
// ---------- Hero: display icon · title/status · percentage ----------
// ---------- Hero: display icon · title/status ----------
Item {
width: parent.width
visible: root.brightnessAvailable
implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight, heroPercent.implicitHeight)
implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight)
Text {
id: heroIcon
@@ -419,8 +419,7 @@ Panel {
id: heroLabels
anchors.left: heroIcon.right
anchors.leftMargin: Style.space(14)
anchors.right: heroPercent.left
anchors.rightMargin: Style.space(10)
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Style.space(2)
@@ -446,53 +445,73 @@ Panel {
width: parent.width
}
}
Text {
id: heroPercent
text: Math.round(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent) + "%"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.displayLarge
font.bold: true
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
// ---------- Brightness slider (full width below hero) ----------
CursorSurface {
id: brightnessRow
// ---------- Brightness ----------
Column {
visible: root.brightnessAvailable
width: parent.width
height: brightnessSlider.implicitHeight + Style.spacing.controlGap
hasCursor: root.cursorActive && root.focusSection === "brightness" && root.selectedIndex === -1
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(brightnessRow)
foreground: root.bar.foreground
outline: true
spacing: Style.space(6)
PanelSlider {
id: brightnessSlider
bar: root.bar
anchors.fill: parent
anchors.leftMargin: Style.space(6)
anchors.rightMargin: Style.space(6)
minimum: 1
maximum: 100
step: 1
value: root.brightnessPercent
integer: true
onMoved: function(v) { root.previewBrightness(v) }
onReleased: function(v) {
brightnessDebounce.stop()
root.setBrightness(v)
Item {
width: parent.width
implicitHeight: Math.max(brightnessHeader.implicitHeight, brightnessPercent.implicitHeight)
PanelSectionHeader {
id: brightnessHeader
text: "BRIGHTNESS"
foreground: root.bar.foreground
fontFamily: root.bar.fontFamily
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
Text {
id: brightnessPercent
text: Math.round(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent) + "%"
color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
font.pixelSize: Style.font.caption
font.bold: true
anchors.right: parent.right
anchors.rightMargin: Style.space(6)
anchors.verticalCenter: parent.verticalCenter
}
}
HoverHandler {
onHoveredChanged: if (hovered) {
root.cursorActive = true
root.focusSection = "brightness"
root.selectedIndex = -1
CursorSurface {
id: brightnessRow
width: parent.width
height: brightnessSlider.implicitHeight + Style.spacing.controlGap
hasCursor: root.cursorActive && root.focusSection === "brightness" && root.selectedIndex === -1
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(brightnessRow)
foreground: root.bar.foreground
outline: true
PanelSlider {
id: brightnessSlider
bar: root.bar
anchors.fill: parent
anchors.leftMargin: Style.space(6)
anchors.rightMargin: Style.space(6)
minimum: 1
maximum: 100
step: 1
value: root.brightnessPercent
integer: true
onMoved: function(v) { root.previewBrightness(v) }
onReleased: function(v) {
brightnessDebounce.stop()
root.setBrightness(v)
}
}
HoverHandler {
onHoveredChanged: if (hovered) {
root.cursorActive = true
root.focusSection = "brightness"
root.selectedIndex = -1
}
}
}
}
@@ -505,11 +524,6 @@ Panel {
font.pixelSize: Style.font.bodySmall
}
PanelSeparator {
visible: root.brightnessAvailable
foreground: root.bar.foreground
}
// ---------- Scale ----------
Column {
width: parent.width
@@ -545,11 +559,6 @@ Panel {
}
}
PanelSeparator {
visible: root.displays.length > 1
foreground: root.bar.foreground
}
// ---------- Monitors ----------
Column {
width: parent.width
-4
View File
@@ -418,10 +418,6 @@ Panel {
}
}
PanelSeparator {
foreground: root.bar.foreground
}
// ---------- Power profile picker ----------
Column {
width: parent.width