import QtQuick 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 // gradient controls property bool showGradients: true property real gradientWidth: Math.round(12 * Style.uiScaleRatio) property color gradientColor: Color.mSurfaceVariant property real cornerRadius: 0 readonly property bool gradientsEnabled: root.showGradients && Settings.data.bar.capsuleOpacity >= 1.0 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 clip: true implicitWidth: alwaysMaxWidth ? maxWidth : Math.min(maxWidth, contentWidth) implicitHeight: titleText.height 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 } } // Fade Gradients Rectangle { id: leftGradient anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom width: root.gradientWidth z: 2 visible: root.gradientsEnabled && root.contentWidth > root.maxWidth radius: root.cornerRadius opacity: scrollContainer.x < -1 ? 1 : 0 gradient: Gradient { orientation: Gradient.Horizontal GradientStop { position: 0.0 color: root.gradientColor } GradientStop { position: 1.0 color: "transparent" } } Behavior on opacity { NumberAnimation { duration: Style.animationFast easing.type: Easing.InOutQuad } } } Rectangle { id: rightGradient anchors.right: parent.right anchors.top: parent.top anchors.bottom: parent.bottom width: root.gradientWidth z: 2 visible: root.gradientsEnabled && root.contentWidth > root.maxWidth radius: root.cornerRadius opacity: 1 // Always show if overflowing as it loops gradient: Gradient { orientation: Gradient.Horizontal GradientStop { position: 0.0 color: "transparent" } GradientStop { position: 1.0 color: root.gradientColor } } Behavior on opacity { NumberAnimation { duration: Style.animationFast easing.type: Easing.InOutQuad } } } }