improvements

This commit is contained in:
2026-08-11 17:35:26 +02:00
parent 39f925cb58
commit a868f50ab7
24 changed files with 924 additions and 239 deletions
+27
View File
@@ -0,0 +1,27 @@
import QtQuick
import "root:/config"
import "root:/components"
// Calm, permanent-chrome preset. It is deliberately flat and opaque: one
// neutral keyline, no texture, no offset copy, and no accent unless active.
P5Panel {
property bool active: false
property color tone: Theme.treatmentActiveMark
fill: Theme.treatmentRestFill
fillOpacity: 1.0
border: Theme.treatmentRestMark
borderWidth: 1
// Keep the hard ink contour. Removing both texture and keyline made the
// permanent slabs read as generic broadcast HUD chrome.
keyline: true
drop: false
halftone: false
sheen: false
slash: active
slashColor: tone
slashWidth: 4
lean: 0.06
chop: Theme.cut
padding: Theme.padS
}
+58 -20
View File
@@ -1,15 +1,13 @@
import QtQuick
import "root:/config"
// Chopped, leaning button. On hover it lunges toward the pointer and flips to
// crimson-on-white; on press it snaps flat.
Item {
// Chopped, leaning button with one activation path for pointer and keyboard.
FocusScope {
id: root
property string text: ""
property string icon: ""
property color accent: Theme.primary
property color hoverFill: Theme.accent
property bool destructive: false
property bool selected: false
property real lean: Theme.skew
@@ -18,8 +16,25 @@ Item {
signal clicked()
readonly property bool hovered: hover.hovered || root.selected
readonly property bool hovered: root.enabled && hover.hovered
readonly property bool pressed: tap.pressed
readonly property bool focusedOrSelected: root.activeFocus || root.selected
readonly property bool highlighted: root.hovered || root.focusedOrSelected
// Pointer activation must not turn a transient action into the black/white
// keyboard-focus treatment. Tab still reaches it, and programmatic focus
// (power-menu roving selection / pinned popouts) still works.
focusPolicy: root.enabled ? Qt.TabFocus : Qt.NoFocus
function activate(): void {
if (root.enabled)
root.clicked();
}
function activateFromKey(event): void {
if (!event.isAutoRepeat)
root.activate();
}
implicitWidth: wide ? 260 : (row.implicitWidth + panel.contentPad * 2)
implicitHeight: 44
@@ -29,13 +44,24 @@ Item {
anchors.fill: parent
lean: root.lean
padding: Theme.padS
fill: root.hovered ? (root.destructive ? root.hoverFill : Theme.text) : Theme.surface
fillOpacity: root.hovered ? 1.0 : 0.9
border: root.hovered ? "transparent" : Theme.alpha(root.accent, 0.55)
slash: !root.hovered
slashColor: root.destructive ? Theme.accent : root.accent
halftone: root.hovered
halftoneColor: root.destructive ? Theme.text : Theme.base
fill: {
if (!root.enabled) return Theme.treatmentDisabledFill;
if (root.destructive) return Theme.treatmentDestructiveFill;
if (root.highlighted) return Theme.treatmentHoverFill;
return Theme.treatmentRestFill;
}
fillOpacity: root.enabled ? 1.0 : 0.72
border: {
if (!root.enabled) return Theme.treatmentDisabledMark;
if (root.destructive) return Theme.treatmentFocusMark;
if (root.highlighted) return Theme.treatmentHoverMark;
return Theme.alpha(root.accent, 0.55);
}
borderWidth: Theme.stroke
slash: !root.highlighted
slashColor: root.destructive ? Theme.treatmentDestructiveMark : root.accent
halftone: root.enabled && root.highlighted
halftoneColor: Theme.ink
halftoneOpacity: 0.14
Behavior on fillOpacity {
@@ -53,17 +79,23 @@ Item {
visible: root.icon !== ""
text: root.icon
font.pixelSize: Theme.fsTitle
color: root.hovered
? (root.destructive ? Theme.text : Theme.base)
: root.accent
color: {
if (!root.enabled) return Theme.treatmentDisabledText;
if (root.destructive) return Theme.treatmentDestructiveText;
if (root.highlighted) return Theme.treatmentHoverText;
return root.accent;
}
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.text
color: root.hovered
? (root.destructive ? Theme.text : Theme.base)
: Theme.text
color: {
if (!root.enabled) return Theme.treatmentDisabledText;
if (root.destructive) return Theme.treatmentDestructiveText;
if (root.highlighted) return Theme.treatmentHoverText;
return Theme.treatmentRestText;
}
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay
@@ -80,7 +112,7 @@ Item {
}
transform: Translate {
x: root.pressed ? 0 : (root.hovered ? root.lunge : 0)
x: root.pressed ? 0 : (root.highlighted ? root.lunge : 0)
Behavior on x {
NumberAnimation {
@@ -97,11 +129,17 @@ Item {
HoverHandler {
id: hover
enabled: root.enabled
cursorShape: Qt.PointingHandCursor
}
TapHandler {
id: tap
onTapped: root.clicked()
enabled: root.enabled
onTapped: root.activate()
}
Keys.onReturnPressed: event => root.activateFromKey(event)
Keys.onEnterPressed: event => root.activateFromKey(event)
Keys.onSpacePressed: event => root.activateFromKey(event)
}
+48 -8
View File
@@ -8,23 +8,38 @@ Item {
property real value: 0 // 0..1
property color accent: Theme.primary
property bool enabledControl: true
property real stepSize: 0.05
signal moved(real value)
implicitHeight: 18
implicitWidth: 200
focusPolicy: root.enabled ? Qt.StrongFocus : Qt.NoFocus
readonly property real _lean: Theme.skew * height
function commit(nextValue: real): void {
if (!root.enabled)
return;
root.moved(Math.max(0, Math.min(1, nextValue)));
}
// Rail
Skew {
id: rail
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
height: 6
color: Theme.alpha(Theme.outline, 0.7)
height: root.activeFocus ? 10 : 6
color: root.activeFocus
? Theme.treatmentFocusFill
: Theme.alpha(root.enabled ? Theme.outline : Theme.treatmentDisabledMark, 0.7)
borderColor: root.activeFocus ? Theme.treatmentFocusMark : "transparent"
borderWidth: root.activeFocus ? Theme.stroke : 0
Behavior on height {
NumberAnimation { duration: Theme.durFast }
}
}
// Fill
@@ -33,7 +48,7 @@ Item {
anchors.left: parent.left
width: Math.max(0, Math.min(1, root.value)) * root.width
height: 6
color: root.enabledControl ? root.accent : Theme.muted
color: root.enabled ? root.accent : Theme.treatmentDisabledText
Behavior on width {
enabled: !drag.active
@@ -49,8 +64,8 @@ Item {
Skew {
id: handle
width: 6
height: hover.hovered || drag.active ? 18 : 14
color: root.enabledControl ? Theme.text : Theme.muted
height: root.activeFocus || hover.hovered || drag.active ? 18 : 14
color: root.enabled ? Theme.text : Theme.treatmentDisabledText
anchors.verticalCenter: parent.verticalCenter
x: Math.max(0, Math.min(1, root.value)) * root.width - width / 2
@@ -65,21 +80,46 @@ Item {
HoverHandler {
id: hover
enabled: root.enabled
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: event => root.moved(Math.max(0, Math.min(1, event.position.x / root.width)))
enabled: root.enabled
onPressedChanged: if (pressed) root.forceActiveFocus(Qt.MouseFocusReason)
onTapped: event => root.commit(event.position.x / root.width)
}
DragHandler {
id: drag
enabled: root.enabled
target: null
xAxis.enabled: true
yAxis.enabled: false
onCentroidChanged: {
if (!active) return;
root.moved(Math.max(0, Math.min(1, centroid.position.x / root.width)));
root.forceActiveFocus(Qt.MouseFocusReason);
root.commit(centroid.position.x / root.width);
}
}
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Left:
root.commit(root.value - root.stepSize);
break;
case Qt.Key_Right:
root.commit(root.value + root.stepSize);
break;
case Qt.Key_Home:
root.commit(0);
break;
case Qt.Key_End:
root.commit(1);
break;
default:
return;
}
event.accepted = true;
}
}
+114
View File
@@ -0,0 +1,114 @@
import QtQuick
import Quickshell
import "root:/config"
// Calm, informational hover label for permanent chrome. It never takes focus
// or participates in popup coordination; a delayed label should not dismiss or
// displace the detail panel attached to the same rail control.
PopupWindow {
id: root
required property Item anchorItem
property bool triggerHovered: false
property string title: ""
property string status: ""
property string hint: ""
property int delay: 400
property bool suppressed: false
readonly property int pad: Theme.padS
readonly property real textWidth: Math.max(titleText.implicitWidth,
statusText.implicitWidth, hintText.implicitWidth)
readonly property real contentWidth: Math.max(112, textWidth + pad * 4)
readonly property real contentHeight: textColumn.implicitHeight + pad * 2
anchor.item: anchorItem
anchor.edges: Edges.Left
anchor.gravity: Edges.Left
anchor.adjustment: PopupAdjustment.Flip | PopupAdjustment.SlideY
anchor.margins.left: Theme.popoutGap
implicitWidth: contentWidth
implicitHeight: contentHeight
color: "transparent"
visible: shown && title !== "" && !suppressed
property bool shown: false
onTriggerHoveredChanged: {
if (triggerHovered && title !== "" && !suppressed) {
hideTimer.stop();
showTimer.restart();
} else {
showTimer.stop();
hideTimer.restart();
}
}
onSuppressedChanged: {
if (suppressed) {
showTimer.stop();
shown = false;
} else if (triggerHovered) {
showTimer.restart();
}
}
Timer {
id: showTimer
interval: root.delay
onTriggered: root.shown = root.triggerHovered && !root.suppressed
}
Timer {
id: hideTimer
interval: 70
onTriggered: root.shown = false
}
ChromeSurface {
anchors.fill: parent
padding: root.pad
lean: 0.06
Column {
id: textColumn
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
id: titleText
text: root.title.toUpperCase()
color: Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.weight: Theme.weightDisplay
font.italic: true
renderType: Text.NativeRendering
}
Text {
id: statusText
visible: text !== ""
text: root.status
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
Text {
id: hintText
visible: text !== ""
text: root.hint.toUpperCase()
color: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
font.letterSpacing: 0.8
renderType: Text.NativeRendering
}
}
}
}
+96 -25
View File
@@ -1,6 +1,5 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import "root:/config"
// Hover-driven panel that hangs off a bar item. Opens on a short dwell, stays
@@ -17,6 +16,8 @@ PopupWindow {
property int fromEdge: Edges.Bottom
property real contentWidth: Theme.popoutWidth
property real contentHeight: 200
property Item initialFocusItem: null
property bool remappingPinned: false
// Extra surface around the content so the entry overshoot and glow are not
// clipped by the window edge.
@@ -25,6 +26,8 @@ PopupWindow {
default property alias body: contentHolder.data
readonly property bool wantsOpen: triggerHovered || panelHover.hovered || pinned
readonly property bool pending: showTimer.running || remapTimer.running || remappingPinned
readonly property bool detailActive: shown || pending
signal dismissedFully()
@@ -52,13 +55,15 @@ PopupWindow {
implicitHeight: contentHeight + bleed * 2
color: "transparent"
visible: shown
grabFocus: pinned && shown
property bool shown: false
onWantsOpenChanged: {
if (wantsOpen) {
hideTimer.stop();
showTimer.restart();
if (!pinned && !remappingPinned)
showTimer.restart();
} else {
showTimer.stop();
hideTimer.restart();
@@ -71,29 +76,17 @@ PopupWindow {
onTriggered: root.open()
}
// A pinned popout ignores hover by design, which left it with exactly one
// way out: another click on the same bar item that opened it. Moving the
// pointer away did nothing, so a panel opened by a click — the notification
// history, most often — simply stayed on screen. Click anywhere else and
// the compositor drops the grab, which unpins it and hands it back to the
// hover rules.
//
// The grab can only take hold once the surface is mapped; asked for in the
// same tick as `shown`, Hyprland clears it immediately.
Timer {
id: grabDelay
interval: 60
running: root.pinned && root.shown
onVisibleChanged: {
if (!visible && shown)
dismissImmediately();
}
HyprlandFocusGrab {
active: root.pinned && root.shown && !grabDelay.running
windows: [root]
// A click on the trigger itself also clears the grab, but that click is
// the pill's own toggle — unpinning here as well would cancel it out and
// the panel would never close from its own button.
onCleared: if (!root.triggerHovered) root.pinned = false
onClosed: {
if (remappingPinned) {
remappingPinned = false;
remapTimer.restart();
} else if (shown || pinned) {
dismissImmediately();
}
}
Timer {
@@ -109,14 +102,85 @@ PopupWindow {
enterAnim.restart();
}
// Focus policy is fixed at map time. A hover-visible passive surface must
// therefore unmap before returning as a click-owned keyboard popup.
function openPinned() {
showTimer.stop();
hideTimer.stop();
PopupCoordinator.claim(root);
if (shown) {
remappingPinned = true;
shown = false;
enterAnim.stop();
exitAnim.stop();
pinned = true;
} else {
pinned = true;
open();
focusTimer.restart();
}
}
function togglePinned() {
if (pinned) {
pinned = false;
dismiss();
} else {
openPinned();
}
}
function dismiss() {
if (!shown) return;
if (!shown) {
if (pinned || pending)
dismissImmediately();
return;
}
pinned = false;
PopupCoordinator.release(root);
enterAnim.stop();
exitAnim.restart();
}
function dismissImmediately() {
showTimer.stop();
hideTimer.stop();
remapTimer.stop();
focusTimer.stop();
enterAnim.stop();
exitAnim.stop();
remappingPinned = false;
pinned = false;
shown = false;
contentHolder.opacity = 0;
contentHolder.scale = 0.94;
contentHolder.x = root.bleed;
contentHolder.y = root.bleed;
PopupCoordinator.release(root);
root.dismissedFully();
}
Timer {
id: remapTimer
interval: 0
onTriggered: {
root.open();
focusTimer.restart();
}
}
Timer {
id: focusTimer
interval: 0
onTriggered: {
const target = root.initialFocusItem || contentHolder;
target.forceActiveFocus(Qt.TabFocusReason);
}
}
Item {
id: contentHolder
focus: root.pinned
x: root.bleed
y: root.bleed
width: root.contentWidth
@@ -145,6 +209,13 @@ PopupWindow {
id: panelHover
enabled: root.shown
}
Keys.onEscapePressed: event => {
if (root.pinned) {
event.accepted = true;
root.dismissImmediately();
}
}
}
ParallelAnimation {