Files
bbedward ccbf1d38ad widgets: bump qml-common, rename Dank* prefixed widgets to D, add
deprecation notice to plugin compatibility shims

- When qml-common became a thing, we left qs.Widgets shims in place for
  plugin compatibility
- It's annoying to need to maintain them when qml-common has any change,
  add a warning so we can safely deprecate them in the future
- One shot rename the common stuff to D to make it shorter and nicer to
  write
2026-10-05 16:47:19 -04:00

223 lines
7.0 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.DCommon.Widgets
Item {
id: root
property alias text: label.text
property alias font: label.font
property alias fontToken: label.fontToken
property alias color: label.color
property bool active: true
property real holdStartMs: 2000
property real holdEndMs: 2000
property real pxPerMs: 1 / 60
property int stepInterval: 60
property real overscroll: 5
property bool animateTextChange: false
property bool loop: false
property bool fadeEdges: false
property real loopGap: Theme.spacingXL * 2
property int horizontalAlignment: Text.AlignLeft
readonly property alias contentWidth: label.contentWidth
readonly property alias implicitTextWidth: label.implicitWidth
readonly property alias implicitTextHeight: label.implicitHeight
readonly property bool needsScrolling: label.implicitWidth > width && SettingsData.scrollTitleEnabled
readonly property bool rightToLeft: label.effectiveHorizontalAlignment === Text.AlignRight
readonly property bool scrollActive: needsScrolling && visible && (Window.window?.visible ?? false) && active && !SettingsData.reduceMotion
readonly property real maxScrollOffset: loop ? label.implicitWidth + loopGap : Math.max(0, label.implicitWidth - width + overscroll)
property real scrollOffset: 0
property int scrollDirection: 1
property real scrollHoldMs: holdStartMs
property real textShift: 0
property real lastStepMs: 0
function resetScroll() {
scrollOffset = 0;
scrollDirection = 1;
scrollHoldMs = holdStartMs;
}
function stepScroll(deltaMs) {
if (scrollHoldMs > 0) {
scrollHoldMs -= deltaMs;
return;
}
const next = scrollOffset + scrollDirection * deltaMs * pxPerMs;
if (loop) {
scrollOffset = next % maxScrollOffset;
return;
}
if (next >= maxScrollOffset) {
scrollOffset = maxScrollOffset;
scrollDirection = -1;
scrollHoldMs = holdEndMs;
return;
}
if (next <= 0) {
scrollOffset = 0;
scrollDirection = 1;
scrollHoldMs = holdStartMs;
return;
}
scrollOffset = next;
}
onScrollActiveChanged: {
if (!scrollActive) {
resetScroll();
return;
}
lastStepMs = Date.now();
}
clip: true
implicitHeight: label.implicitHeight
layer.enabled: fadeEdges && needsScrolling
layer.effect: MultiEffect {
maskEnabled: true
maskSource: edgeMask.item
}
StyledText {
id: label
anchors.verticalCenter: parent.verticalCenter
wrapMode: Text.NoWrap
width: root.needsScrolling ? implicitWidth : Math.min(implicitWidth, root.width)
elide: SettingsData.scrollTitleEnabled ? Text.ElideNone : Text.ElideRight
// Whole-pixel x plus NativeRendering's glyph snapping quantises the offset twice, which
// turns even motion into a 1px/2px stutter. Both go while the text is actually scrolling.
renderType: root.needsScrolling ? Text.QtRendering : resolvedRenderType
x: {
if (root.needsScrolling) {
const offset = Math.min(root.scrollOffset, root.maxScrollOffset);
return (root.rightToLeft ? root.width - width + offset : -offset) + root.textShift;
}
switch (root.horizontalAlignment) {
case Text.AlignHCenter:
return (root.width - width) / 2;
case Text.AlignRight:
return root.width - width;
}
return root.textShift;
}
opacity: 1
onTextChanged: {
root.resetScroll();
root.textShift = 0;
if (!root.animateTextChange)
return;
textChangeAnimation.restart();
}
}
Loader {
anchors.verticalCenter: parent.verticalCenter
x: label.x + (root.rightToLeft ? -1 : 1) * root.maxScrollOffset
active: root.loop && root.needsScrolling
sourceComponent: StyledText {
text: label.text
font: label.font
color: label.color
wrapMode: Text.NoWrap
renderType: Text.QtRendering
Accessible.ignored: true
}
}
Loader {
id: edgeMask
anchors.fill: parent
active: root.fadeEdges && root.needsScrolling
sourceComponent: Rectangle {
visible: false
layer.enabled: true
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
position: 0
color: "transparent"
}
GradientStop {
position: Math.min(0.5, Theme.spacingL / Math.max(1, root.width))
color: "white"
}
GradientStop {
position: 1 - Math.min(0.5, Theme.spacingL / Math.max(1, root.width))
color: "white"
}
GradientStop {
position: 1
color: "transparent"
}
}
}
}
// Timer stepping, not NumberAnimation: a running animation commits frames every vsync (#2863).
// When cava frames are already driving renders, scroll steps ride those ticks instead (#2863).
Timer {
interval: root.stepInterval
repeat: true
running: root.scrollActive
onTriggered: {
const now = Date.now();
const elapsed = Math.min(now - root.lastStepMs, root.stepInterval * 4);
root.lastStepMs = now;
if (cavaTickWatch.running)
return;
root.stepScroll(elapsed);
}
}
Timer {
id: cavaTickWatch
interval: 150
}
Connections {
target: CavaService
enabled: root.scrollActive && SettingsData.audioVisualizerEnabled && CavaService.cavaAvailable
function onValuesChanged() {
cavaTickWatch.restart();
root.stepScroll(40);
}
}
SequentialAnimation {
id: textChangeAnimation
ParallelAnimation {
NumberAnimation {
target: label
property: "opacity"
from: 0.7
to: 1
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
NumberAnimation {
target: root
property: "textShift"
from: 4
to: 0
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
}
}