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: 2022 Yari Polla <skilvingr@gmail.com>
// SPDX-FileCopyrightText: 2025 Micah Stanley <stanleymicah@proton.me>
// SPDX-License-Identifier: LGPL-2.0-or-later // SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import org.kde.kirigami 2.20 as Kirigami import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.components 3.0 as PlasmaComponents
import Qt5Compat.GraphicalEffects
/** /**
* This is a simple marquee (flowing) label based on PlasmaComponents Label. * This is a simple marquee (flowing) label based on PlasmaComponents Label.
*/ */
PlasmaComponents.Label { OpacityMask {
id: root id: root
height: row.height
// label values
required property string inputText 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 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 // update animation values and text positions whenever the label overflows or changes
readonly property int longDuration: 300 onFilteredTextChanged: if (root.charactersOverflowing) { textAnimationLoop.restart() }
readonly property int waitDuration: 900 onCharactersOverflowingChanged: if (charactersOverflowing) { row.scrollPosition = 0 }
readonly property int charactersOverflow: Math.ceil((txtMeter.advanceWidth - root.width) / (txtMeter.advanceWidth / filteredText.length)) Item {
property int step: 0 id: rowContaner
anchors.fill: parent
height: row.height
opacity: 0 // we display with the opacity gradient below
// 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
property real scrollPosition: 0
transform: [
Translate {
x: row.scrollPosition
}
]
spacing: 32
PlasmaComponents.Label {
id: label
font: root.font
textFormat: root.textFormat
text: filteredText
Layout.alignment: Qt.AlignLeft
TextMetrics { TextMetrics {
id: txtMeter id: txtMeter
font: root.font font: root.font
text: filteredText text: filteredText
} }
Timer {
property bool paused: false
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;
} }
} else {
step = (step + 1) % filteredText.length; PlasmaComponents.Label {
if (step === charactersOverflow) { // hide this label when the text is not overflowing so the user never sees both labels
interval = root.waitDuration; visible: textAnimationLoop.running
paused = true; font: root.font
textFormat: root.textFormat
text: filteredText
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
} }
} }
} }
onRunningChanged: { // setting the gradient mask source
if (!running) { source: rowContaner
step = 0;
} // 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 }
} }
text: filteredText.substring(step, step + filteredText.length - charactersOverflow) // 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' }
}
}
} }