MarqueeLabel: Smooth Scrolling

This merge request reworks the MarqueeLabel to add smooth scrolling, giving it a more clean appearance while also improving scrolling when filled when characters of different lengths.

Before:

![margueeLabel_before__トリミング_](/uploads/b162949db828a0e243b0ef109456bfdd/margueeLabel_before__トリミング_.webm)

After:

![margueeLabel__トリミング_](/uploads/7ec2a2b72799a56412b08d04b2f6eb00/margueeLabel__トリミング_.webm)
This commit is contained in:
Micah Stanley 2025-06-12 12:25:42 +00:00 committed by Devin Lin
parent 5511cd73a5
commit a9ddcf726d

View file

@ -1,64 +1,119 @@
// SPDX-FileCopyrightText: 2022 Yari Polla <skilvingr@gmail.com>
// SPDX-FileCopyrightText: 2025 Micah Stanley <stanleymicah@proton.me>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.components 3.0 as PlasmaComponents
import Qt5Compat.GraphicalEffects
/**
* This is a simple marquee (flowing) label based on PlasmaComponents Label.
*/
PlasmaComponents.Label {
OpacityMask {
id: root
height: row.height
// label values
required property string inputText
property font font
property var textFormat: Text.RichText
// properties for the marquee label scroll speed and wait duration
readonly property real scrollSpeed: 0.025
readonly property int waitDuration: 2000
readonly property string filteredText: inputText.replace(/\n/g, ' ') // remove new line characters
readonly property bool charactersOverflowing: txtMeter.advanceWidth > root.width // true when text is overflowing
readonly property int interval: 200 // update position every 200 ms
readonly property int longDuration: 300
readonly property int waitDuration: 900
// update animation values and text positions whenever the label overflows or changes
onFilteredTextChanged: if (root.charactersOverflowing) { textAnimationLoop.restart() }
onCharactersOverflowingChanged: if (charactersOverflowing) { row.scrollPosition = 0 }
readonly property int charactersOverflow: Math.ceil((txtMeter.advanceWidth - root.width) / (txtMeter.advanceWidth / filteredText.length))
property int step: 0
Item {
id: rowContaner
anchors.fill: parent
height: row.height
opacity: 0 // we display with the opacity gradient below
TextMetrics {
id: txtMeter
font: root.font
text: filteredText
}
// use two identical labels for scrolling so we can give the illusion of infinite scrolling
RowLayout {
id: row
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
Timer {
property bool paused: false
property real scrollPosition: 0
interval: root.interval
running: visible && charactersOverflow > 0
repeat: true
onTriggered: {
if (paused) {
if (step != 0) {
interval = root.longDuration;
step = 0;
} else {
interval = root.interval;
paused = false;
transform: [
Translate {
x: row.scrollPosition
}
} else {
step = (step + 1) % filteredText.length;
if (step === charactersOverflow) {
interval = root.waitDuration;
paused = true;
]
spacing: 32
PlasmaComponents.Label {
id: label
font: root.font
textFormat: root.textFormat
text: filteredText
Layout.alignment: Qt.AlignLeft
TextMetrics {
id: txtMeter
font: root.font
text: filteredText
}
}
}
onRunningChanged: {
if (!running) {
step = 0;
PlasmaComponents.Label {
// hide this label when the text is not overflowing so the user never sees both labels
visible: textAnimationLoop.running
font: root.font
textFormat: root.textFormat
text: filteredText
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
}
}
}
text: filteredText.substring(step, step + filteredText.length - charactersOverflow)
// setting the gradient mask source
source: rowContaner
// if the label is overflowing, this animation in a loop smoothly scrolling thought the text
SequentialAnimation {
id: textAnimationLoop
running: root.charactersOverflowing && root.visible
onRunningChanged: row.scrollPosition = 0
loops: Animation.Infinite
PauseAnimation { duration: root.waitDuration }
NumberAnimation { target: row; property: "scrollPosition"; from: 0; to: -txtMeter.advanceWidth - row.spacing; duration: (txtMeter.advanceWidth + row.spacing) / root.scrollSpeed }
}
// gradient mask to smoothly fade the ends of the label when it is scrolling
maskSource: Rectangle {
id: mask
width: root.width
height: root.height
property real gradientPct: (Kirigami.Units.gridUnit * 0.35) / root.width
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0; color: row.scrollPosition == 0 || row.scrollPosition < -txtMeter.advanceWidth ? 'white' : 'transparent' } // remove the beginning of the gradient when at the start of the label so the front text is fully visible
GradientStop { position: 0 + mask.gradientPct; color: 'white' }
GradientStop { position: 1.0 - mask.gradientPct; color: 'white' }
GradientStop { position: 1.0; color: 'transparent' }
}
}
}