diff --git a/components/mobileshell/qml/components/MarqueeLabel.qml b/components/mobileshell/qml/components/MarqueeLabel.qml index 41a80c44..0dede97b 100644 --- a/components/mobileshell/qml/components/MarqueeLabel.qml +++ b/components/mobileshell/qml/components/MarqueeLabel.qml @@ -1,64 +1,119 @@ // SPDX-FileCopyrightText: 2022 Yari Polla +// SPDX-FileCopyrightText: 2025 Micah Stanley // 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' } + } + } } +