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 (fadeExtent: 0.0–0.5, fraction of width that fades) 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: contentWidth > maxWidth 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 } }