Files
noctalia-shell/Widgets/NScrollText.qml
T
BUSTheid b18d33742a refactor(NScrollText.qml): MultiEffect fade mask
Replace the leftGradient and rightGradient Rectangles in NScrollText
with a MultiEffect mask approach. Old solution caused visual bugs.
2026-03-28 17:59:11 +01:00

219 lines
5.1 KiB
QML

import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import qs.Commons
/*
NScrollText {
NText {
pointSize: Style.fontSizeS
// here any NText properties can be used
}
maxWidth: 200
text: "Some long long long text"
scrollMode: NScrollText.ScrollMode.Always
}
*/
Item {
id: root
required property string text
default property Component delegate: NText {
pointSize: Style.fontSizeS
}
property real maxWidth: Infinity
enum ScrollMode {
Never = 0,
Always = 1,
Hover = 2
}
property int scrollMode: NScrollText.ScrollMode.Never
property bool alwaysMaxWidth: false
property bool forcedHover: false
property int cursorShape: Qt.ArrowCursor
property real waitBeforeScrolling: 1000
property real scrollCycleDuration: Math.max(4000, root.text.length * 120)
property real resettingDuration: 300
// Fade controls
property real fadeExtent: 0.1
property real fadeCornerRadius: 0
property bool fadeRoundLeftCorners: true
readonly property real contentWidth: {
if (!titleText.item)
return 0;
const implicit = titleText.item.implicitWidth;
return implicit > 0 ? implicit : titleText.item.width;
}
readonly property real measuredWidth: scrollContainer.width
implicitWidth: alwaysMaxWidth ? maxWidth : Math.min(maxWidth, contentWidth)
implicitHeight: titleText.height
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
maskSource: fadeMask
}
enum ScrollState {
None = 0,
Scrolling = 1,
Resetting = 2
}
property int state: NScrollText.ScrollState.None
onTextChanged: {
if (titleText.item)
titleText.item.text = text;
if (loopingText.item)
loopingText.item.text = text;
// reset state
resetState();
}
onMaxWidthChanged: resetState()
onContentWidthChanged: root.updateState()
onForcedHoverChanged: updateState()
function resetState() {
root.state = NScrollText.ScrollState.None;
scrollContainer.x = 0;
scrollTimer.restart();
root.updateState();
}
Timer {
id: scrollTimer
interval: root.waitBeforeScrolling
onTriggered: {
root.state = NScrollText.ScrollState.Scrolling;
root.updateState();
}
}
MouseArea {
id: hoverArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
onEntered: root.updateState()
onExited: root.updateState()
cursorShape: root.cursorShape
}
function ensureReset() {
if (state === NScrollText.ScrollState.Scrolling)
state = NScrollText.ScrollState.Resetting;
}
function updateState() {
if (contentWidth <= root.maxWidth || scrollMode === NScrollText.ScrollMode.Never) {
state = NScrollText.ScrollState.None;
return;
}
if (scrollMode === NScrollText.ScrollMode.Always) {
if (hoverArea.containsMouse) {
ensureReset();
} else {
scrollTimer.restart();
}
} else if (scrollMode === NScrollText.ScrollMode.Hover) {
if (hoverArea.containsMouse || forcedHover)
state = NScrollText.ScrollState.Scrolling;
else
ensureReset();
}
}
RowLayout {
id: scrollContainer
height: parent.height
x: 0
spacing: 50
Loader {
id: titleText
sourceComponent: root.delegate
Layout.fillHeight: true
onLoaded: {
this.item.text = root.text;
// Bind height to container to enable vertical centering of overly high text
this.item.height = Qt.binding(() => titleText.height);
}
}
Loader {
id: loopingText
sourceComponent: root.delegate
Layout.fillHeight: true
visible: root.state !== NScrollText.ScrollState.None
onLoaded: {
this.item.text = root.text;
this.item.height = Qt.binding(() => loopingText.height);
}
}
NumberAnimation on x {
running: root.state === NScrollText.ScrollState.Resetting
to: 0
duration: root.resettingDuration
easing.type: Easing.OutQuad
onFinished: {
root.state = NScrollText.ScrollState.None;
root.updateState();
}
}
NumberAnimation on x {
running: root.state === NScrollText.ScrollState.Scrolling
to: -(titleText.width + scrollContainer.spacing)
duration: root.scrollCycleDuration
loops: Animation.Infinite
easing.type: Easing.Linear
}
}
// Transparency Fade Rectangle
Rectangle {
id: fadeMask
width: root.width
height: root.height
topLeftRadius: fadeRoundLeftCorners ? fadeCornerRadius : 0
bottomLeftRadius: fadeRoundLeftCorners ? fadeCornerRadius : 0
topRightRadius: fadeCornerRadius
bottomRightRadius: fadeCornerRadius
gradient: Gradient {
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: fadeExtent
color: "white"
}
GradientStop {
position: 1 - fadeExtent
color: "white"
}
GradientStop {
position: 1.0
color: "transparent"
}
orientation: Gradient.Horizontal
}
layer.enabled: true
layer.smooth: true
opacity: 0 // Great for debugging! Will show the white masks
}
}