Renamed and moved NPill to BarPill.

Pill should not be used outside of the Bar as they rely on bar settings.
This commit is contained in:
LemmyCook 2025-09-16 22:26:56 -04:00
parent 3a6bf8d299
commit a92b4b311a
11 changed files with 26 additions and 16 deletions

View file

@ -0,0 +1,111 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
import qs.Services
import qs.Widgets
Item {
id: root
property string icon: ""
property string text: ""
property string suffix: ""
property string tooltipText: ""
property bool autoHide: false
property bool forceOpen: false
property bool forceClose: false
property bool disableOpen: false
property bool rightOpen: false
property bool hovered: false
property bool compact: false
readonly property string barPosition: Settings.data.bar.position
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
signal shown
signal hidden
signal entered
signal exited
signal clicked
signal rightClicked
signal middleClicked
signal wheel(int delta)
// Dynamic sizing based on loaded component
width: pillLoader.item ? pillLoader.item.width : 0
height: pillLoader.item ? pillLoader.item.height : 0
// Loader to switch between vertical and horizontal pill implementations
Loader {
id: pillLoader
sourceComponent: isVerticalBar ? verticalPillComponent : horizontalPillComponent
Component {
id: verticalPillComponent
BarPillVertical {
icon: root.icon
text: root.text
suffix: root.suffix
tooltipText: root.tooltipText
autoHide: root.autoHide
forceOpen: root.forceOpen
forceClose: root.forceClose
disableOpen: root.disableOpen
rightOpen: root.rightOpen
hovered: root.hovered
compact: root.compact
onShown: root.shown()
onHidden: root.hidden()
onEntered: root.entered()
onExited: root.exited()
onClicked: root.clicked()
onRightClicked: root.rightClicked()
onMiddleClicked: root.middleClicked()
onWheel: delta => root.wheel(delta)
}
}
Component {
id: horizontalPillComponent
BarPillHorizontal {
icon: root.icon
text: root.text
suffix: root.suffix
tooltipText: root.tooltipText
autoHide: root.autoHide
forceOpen: root.forceOpen
forceClose: root.forceClose
disableOpen: root.disableOpen
rightOpen: root.rightOpen
hovered: root.hovered
compact: root.compact
onShown: root.shown()
onHidden: root.hidden()
onEntered: root.entered()
onExited: root.exited()
onClicked: root.clicked()
onRightClicked: root.rightClicked()
onMiddleClicked: root.middleClicked()
onWheel: delta => root.wheel(delta)
}
}
}
function show() {
if (pillLoader.item && pillLoader.item.show) {
pillLoader.item.show()
}
}
function hide() {
if (pillLoader.item && pillLoader.item.hide) {
pillLoader.item.hide()
}
}
function showDelayed() {
if (pillLoader.item && pillLoader.item.showDelayed) {
pillLoader.item.showDelayed()
}
}
}

View file

@ -0,0 +1,292 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
import qs.Services
import qs.Widgets
Item {
id: root
property string icon: ""
property string text: ""
property string suffix: ""
property string tooltipText: ""
property bool autoHide: false
property bool forceOpen: false
property bool forceClose: false
property bool disableOpen: false
property bool rightOpen: false
property bool hovered: false
property bool compact: false
// Effective shown state (true if hovered/animated open or forced)
readonly property bool revealed: forceOpen || showPill
signal shown
signal hidden
signal entered
signal exited
signal clicked
signal rightClicked
signal middleClicked
signal wheel(int delta)
// Internal state
property bool showPill: false
property bool shouldAnimateHide: false
readonly property int pillHeight: Math.round(Style.capsuleHeight * scaling)
readonly property int pillPaddingHorizontal: Math.round(Style.capsuleHeight * 0.2 * scaling)
readonly property int pillOverlap: Math.round(Style.capsuleHeight * 0.5 * scaling)
readonly property int pillMaxWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap)
readonly property real iconSize: Math.max(1, compact ? pillHeight * 0.65 : pillHeight * 0.48)
readonly property real textSize: Math.max(1, compact ? pillHeight * 0.45 : pillHeight * 0.33)
width: pillHeight + Math.max(0, pill.width - pillOverlap)
height: pillHeight
Rectangle {
id: pill
width: revealed ? pillMaxWidth : 1
height: pillHeight
x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right
(iconCircle.x + iconCircle.width / 2) - width // Opens left
opacity: revealed ? Style.opacityFull : Style.opacityNone
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
topLeftRadius: rightOpen ? 0 : pillHeight * 0.5
bottomLeftRadius: rightOpen ? 0 : pillHeight * 0.5
topRightRadius: rightOpen ? pillHeight * 0.5 : 0
bottomRightRadius: rightOpen ? pillHeight * 0.5 : 0
anchors.verticalCenter: parent.verticalCenter
NText {
id: textItem
anchors.verticalCenter: parent.verticalCenter
x: {
// Better text horizontal centering
var centerX = (parent.width - width) / 2
var offset = rightOpen ? Style.marginXS * scaling : -Style.marginXS * scaling
if (forceOpen) {
// If its force open, the icon disc background is the same color as the bg pill move text slightly
offset += rightOpen ? -Style.marginXXS * scaling : Style.marginXXS * scaling
}
return centerX + offset
}
text: root.text + root.suffix
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
font.weight: Style.fontWeightBold
color: forceOpen ? Color.mOnSurface : Color.mPrimary
visible: revealed
}
Behavior on width {
enabled: showAnim.running || hideAnim.running
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
Behavior on opacity {
enabled: showAnim.running || hideAnim.running
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
}
Rectangle {
id: iconCircle
width: pillHeight
height: pillHeight
radius: width * 0.5
color: hovered ? Color.mTertiary : Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
anchors.verticalCenter: parent.verticalCenter
x: rightOpen ? 0 : (parent.width - width)
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
NIcon {
icon: root.icon
font.pointSize: iconSize
color: hovered ? Color.mOnTertiary : Color.mOnSurface
// Center horizontally
x: (iconCircle.width - width) / 2
// Center vertically accounting for font metrics
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
}
}
ParallelAnimation {
id: showAnim
running: false
NumberAnimation {
target: pill
property: "width"
from: 1
to: pillMaxWidth
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
NumberAnimation {
target: pill
property: "opacity"
from: 0
to: 1
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
onStarted: {
showPill = true
}
onStopped: {
delayedHideAnim.start()
root.shown()
}
}
SequentialAnimation {
id: delayedHideAnim
running: false
PauseAnimation {
duration: 2500
}
ScriptAction {
script: if (shouldAnimateHide) {
hideAnim.start()
}
}
}
ParallelAnimation {
id: hideAnim
running: false
NumberAnimation {
target: pill
property: "width"
from: pillMaxWidth
to: 1
duration: Style.animationNormal
easing.type: Easing.InCubic
}
NumberAnimation {
target: pill
property: "opacity"
from: 1
to: 0
duration: Style.animationNormal
easing.type: Easing.InCubic
}
onStopped: {
showPill = false
shouldAnimateHide = false
root.hidden()
}
}
NTooltip {
id: tooltip
positionAbove: Settings.data.bar.position === "bottom"
target: pill
delay: Style.tooltipDelayLong
text: root.tooltipText
}
Timer {
id: showTimer
interval: Style.pillDelay
onTriggered: {
if (!showPill) {
showAnim.start()
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onEntered: {
hovered = true
root.entered()
tooltip.show()
if (disableOpen) {
return
}
if (!forceOpen) {
showDelayed()
}
}
onExited: {
hovered = false
root.exited()
if (!forceOpen) {
hide()
}
tooltip.hide()
}
onClicked: function (mouse) {
if (mouse.button === Qt.LeftButton) {
root.clicked()
} else if (mouse.button === Qt.RightButton) {
root.rightClicked()
} else if (mouse.button === Qt.MiddleButton) {
root.middleClicked()
}
}
onWheel: wheel => root.wheel(wheel.angleDelta.y)
}
function show() {
if (!showPill) {
shouldAnimateHide = autoHide
showAnim.start()
} else {
hideAnim.stop()
delayedHideAnim.restart()
}
}
function hide() {
if (forceOpen) {
return
}
if (showPill) {
hideAnim.start()
}
showTimer.stop()
}
function showDelayed() {
if (!showPill) {
shouldAnimateHide = autoHide
showTimer.start()
} else {
hideAnim.stop()
delayedHideAnim.restart()
}
}
onForceOpenChanged: {
if (forceOpen) {
// Immediately lock open without animations
showAnim.stop()
hideAnim.stop()
delayedHideAnim.stop()
showPill = true
} else {
hide()
}
}
}

View file

@ -0,0 +1,336 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
import qs.Services
import qs.Widgets
Item {
id: root
property string icon: ""
property string text: ""
property string suffix: ""
property string tooltipText: ""
property bool autoHide: false
property bool forceOpen: false
property bool forceClose: false
property bool disableOpen: false
property bool rightOpen: false
property bool hovered: false
property bool compact: false
// Bar position detection for pill direction
readonly property string barPosition: Settings.data.bar.position
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
// Determine pill direction based on section position
readonly property bool openDownward: rightOpen
readonly property bool openUpward: !rightOpen
// Effective shown state (true if animated open or forced, but not if force closed)
readonly property bool revealed: !forceClose && (forceOpen || showPill)
signal shown
signal hidden
signal entered
signal exited
signal clicked
signal rightClicked
signal middleClicked
signal wheel(int delta)
// Internal state
property bool showPill: false
property bool shouldAnimateHide: false
// Sizing logic for vertical bars
readonly property int buttonSize: Math.round(Style.capsuleHeight * scaling)
readonly property int pillHeight: buttonSize
readonly property int pillPaddingVertical: 3 * 2 * scaling // Very precise adjustment don't replace by Style.margin
readonly property int pillOverlap: buttonSize * 0.5
readonly property int maxPillWidth: buttonSize
readonly property int maxPillHeight: Math.max(1, textItem.implicitHeight + pillPaddingVertical * 4)
readonly property real iconSize: Math.max(1, compact ? pillHeight * 0.65 : pillHeight * 0.48)
readonly property real textSize: Math.max(1, compact ? pillHeight * 0.38 : pillHeight * 0.33)
// For vertical bars: width is just icon size, height includes pill space
width: buttonSize
height: revealed ? (buttonSize + maxPillHeight - pillOverlap) : buttonSize
Rectangle {
id: pill
width: revealed ? maxPillWidth : 1
height: revealed ? maxPillHeight : 1
// Position based on direction - center the pill relative to the icon
x: 0
y: openUpward ? (iconCircle.y + iconCircle.height / 2 - height) : (iconCircle.y + iconCircle.height / 2)
opacity: revealed ? Style.opacityFull : Style.opacityNone
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
// Radius logic for vertical expansion - rounded on the side that connects to icon
topLeftRadius: openUpward ? buttonSize * 0.5 : 0
bottomLeftRadius: openDownward ? buttonSize * 0.5 : 0
topRightRadius: openUpward ? buttonSize * 0.5 : 0
bottomRightRadius: openDownward ? buttonSize * 0.5 : 0
anchors.horizontalCenter: parent.horizontalCenter
NText {
id: textItem
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: {
var offset = openDownward ? pillPaddingVertical * 0.75 : -pillPaddingVertical * 0.75
if (forceOpen) {
// If its force open, the icon disc background is the same color as the bg pill move text slightly
offset += rightOpen ? -Style.marginXXS * scaling : Style.marginXXS * scaling
}
return offset
}
text: root.text + root.suffix
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
font.weight: Style.fontWeightMedium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: forceOpen ? Color.mOnSurface : Color.mPrimary
visible: revealed
}
Behavior on width {
enabled: showAnim.running || hideAnim.running
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
Behavior on height {
enabled: showAnim.running || hideAnim.running
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
Behavior on opacity {
enabled: showAnim.running || hideAnim.running
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
}
Rectangle {
id: iconCircle
width: buttonSize
height: buttonSize
radius: width * 0.5
color: hovered ? Color.mTertiary : Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
// Icon positioning based on direction
x: 0
y: openUpward ? (parent.height - height) : 0
anchors.horizontalCenter: parent.horizontalCenter
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
NIcon {
icon: root.icon
font.pointSize: iconSize
color: hovered ? Color.mOnTertiary : Color.mOnSurface
// Center horizontally
x: (iconCircle.width - width) / 2
// Center vertically accounting for font metrics
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
}
}
ParallelAnimation {
id: showAnim
running: false
NumberAnimation {
target: pill
property: "width"
from: 1
to: maxPillWidth
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
NumberAnimation {
target: pill
property: "height"
from: 1
to: maxPillHeight
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
NumberAnimation {
target: pill
property: "opacity"
from: 0
to: 1
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
onStarted: {
showPill = true
}
onStopped: {
delayedHideAnim.start()
root.shown()
}
}
SequentialAnimation {
id: delayedHideAnim
running: false
PauseAnimation {
duration: 2500
}
ScriptAction {
script: if (shouldAnimateHide) {
hideAnim.start()
}
}
}
ParallelAnimation {
id: hideAnim
running: false
NumberAnimation {
target: pill
property: "width"
from: maxPillWidth
to: 1
duration: Style.animationNormal
easing.type: Easing.InCubic
}
NumberAnimation {
target: pill
property: "height"
from: maxPillHeight
to: 1
duration: Style.animationNormal
easing.type: Easing.InCubic
}
NumberAnimation {
target: pill
property: "opacity"
from: 1
to: 0
duration: Style.animationNormal
easing.type: Easing.InCubic
}
onStopped: {
showPill = false
shouldAnimateHide = false
root.hidden()
}
}
NTooltip {
id: tooltip
target: pill
text: root.tooltipText
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
positionAbove: Settings.data.bar.position === "bottom"
delay: Style.tooltipDelayLong
}
Timer {
id: showTimer
interval: Style.pillDelay
onTriggered: {
if (!showPill) {
showAnim.start()
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onEntered: {
hovered = true
root.entered()
tooltip.show()
if (disableOpen || forceClose) {
return
}
if (!forceOpen) {
showDelayed()
}
}
onExited: {
hovered = false
root.exited()
if (!forceOpen && !forceClose) {
hide()
}
tooltip.hide()
}
onClicked: function (mouse) {
if (mouse.button === Qt.LeftButton) {
root.clicked()
} else if (mouse.button === Qt.RightButton) {
root.rightClicked()
} else if (mouse.button === Qt.MiddleButton) {
root.middleClicked()
}
}
onWheel: wheel => root.wheel(wheel.angleDelta.y)
}
function show() {
if (!showPill) {
shouldAnimateHide = autoHide
showAnim.start()
} else {
hideAnim.stop()
delayedHideAnim.restart()
}
}
function hide() {
if (forceOpen) {
return
}
if (showPill) {
hideAnim.start()
}
showTimer.stop()
}
function showDelayed() {
if (!showPill) {
shouldAnimateHide = autoHide
showTimer.start()
} else {
hideAnim.stop()
delayedHideAnim.restart()
}
}
onForceOpenChanged: {
if (forceOpen) {
// Immediately lock open without animations
showAnim.stop()
hideAnim.stop()
delayedHideAnim.stop()
showPill = true
} else {
hide()
}
}
}

View file

@ -5,6 +5,7 @@ import QtQuick.Layouts
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
Item {
id: root
@ -81,11 +82,11 @@ Item {
}
}
NPill {
BarPill {
id: pill
compact: (Settings.data.bar.density === "compact")
rightOpen: BarWidgetRegistry.getNPillDirection(root)
rightOpen: BarWidgetRegistry.getPillDirection(root)
icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, charging, isReady)
text: (isReady || testMode) ? Math.round(percent) : "-"
suffix: "%"

View file

@ -4,6 +4,7 @@ import qs.Commons
import qs.Modules.SettingsPanel
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
Item {
id: root
@ -73,11 +74,11 @@ Item {
onTriggered: pill.hide()
}
NPill {
BarPill {
id: pill
compact: (Settings.data.bar.density === "compact")
rightOpen: BarWidgetRegistry.getNPillDirection(root)
rightOpen: BarWidgetRegistry.getPillDirection(root)
icon: getIcon()
autoHide: false // Important to be false so we can hover as long as we want
text: {

View file

@ -40,7 +40,7 @@ Rectangle {
readonly property bool verticalMode: barPosition === "left" || barPosition === "right"
implicitWidth: verticalMode ? Math.round(Style.capsuleHeight * scaling) : Math.round(layout.implicitWidth + Style.marginM * 2 * scaling)
implicitHeight: verticalMode ? Math.round(Style.capsuleHeight * 2.5 * scaling) : Math.round(Style.capsuleHeight * scaling) // Match NPill
implicitHeight: verticalMode ? Math.round(Style.capsuleHeight * 2.5 * scaling) : Math.round(Style.capsuleHeight * scaling) // Match BarPill
radius: Math.round(Style.radiusS * scaling)
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent

View file

@ -6,6 +6,7 @@ import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.SettingsPanel
import qs.Modules.Bar.Extras
Item {
id: root
@ -43,10 +44,10 @@ Item {
implicitWidth: pill.width
implicitHeight: pill.height
NPill {
BarPill {
id: pill
rightOpen: BarWidgetRegistry.getNPillDirection(root)
rightOpen: BarWidgetRegistry.getPillDirection(root)
icon: customIcon
text: _dynamicText
compact: (Settings.data.bar.density === "compact")

View file

@ -6,6 +6,7 @@ import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
Item {
id: root
@ -38,12 +39,12 @@ Item {
implicitWidth: pill.width
implicitHeight: pill.height
NPill {
BarPill {
id: pill
anchors.verticalCenter: parent.verticalCenter
compact: (Settings.data.bar.density === "compact")
rightOpen: BarWidgetRegistry.getNPillDirection(root)
rightOpen: BarWidgetRegistry.getPillDirection(root)
icon: "keyboard"
autoHide: false // Important to be false so we can hover as long as we want
text: currentLayout.toUpperCase()

View file

@ -6,6 +6,7 @@ import qs.Commons
import qs.Modules.SettingsPanel
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
Item {
id: root
@ -86,9 +87,9 @@ Item {
}
}
NPill {
BarPill {
id: pill
rightOpen: BarWidgetRegistry.getNPillDirection(root)
rightOpen: BarWidgetRegistry.getPillDirection(root)
icon: getIcon()
compact: (Settings.data.bar.density === "compact")
autoHide: false // Important to be false so we can hover as long as we want

View file

@ -6,6 +6,7 @@ import qs.Commons
import qs.Modules.SettingsPanel
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
Item {
id: root
@ -71,11 +72,11 @@ Item {
}
}
NPill {
BarPill {
id: pill
compact: (Settings.data.bar.density === "compact")
rightOpen: BarWidgetRegistry.getNPillDirection(root)
rightOpen: BarWidgetRegistry.getPillDirection(root)
icon: getIcon()
autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.volume * 100)