From 0035fbcc4e14107ebfa89e6a727146bdcc527788 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 13 Sep 2025 20:52:20 +0200 Subject: [PATCH] NPill: act as loder for NVerticalPill and NHorizontalPill NHorizontalPill: should be used for anything that expands horizontal NVerticalPill: should be used for anything that expands vertical --- Modules/Bar/Widgets/Volume.qml | 9 - Widgets/NHorizontalPill.qml | 324 +++++++++++++++++++++++++++++++++ Widgets/NPill.qml | 291 ++++++----------------------- Widgets/NVerticalPill.qml | 324 +++++++++++++++++++++++++++++++++ Widgets/NVerticalText.qml | 26 +++ 5 files changed, 731 insertions(+), 243 deletions(-) create mode 100644 Widgets/NHorizontalPill.qml create mode 100644 Widgets/NVerticalPill.qml create mode 100644 Widgets/NVerticalText.qml diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index 2dfa42f..fe795a0 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -61,15 +61,6 @@ Item { } } - Timer { - id: externalHideTimer - running: false - interval: 1500 - onTriggered: { - pill.hide() - } - } - NPill { id: pill diff --git a/Widgets/NHorizontalPill.qml b/Widgets/NHorizontalPill.qml new file mode 100644 index 0000000..68963bd --- /dev/null +++ b/Widgets/NHorizontalPill.qml @@ -0,0 +1,324 @@ +import QtQuick +import QtQuick.Controls +import qs.Commons +import qs.Services + +Item { + id: root + + property string icon: "" + property string text: "" + property string tooltipText: "" + property real sizeRatio: 0.8 + property bool autoHide: false + property bool forceOpen: false + property bool disableOpen: false + property bool rightOpen: false + property bool hovered: false + property real fontSize: Style.fontSizeXS + + // Bar position detection for pill direction + readonly property string barPosition: Settings.data.bar.position + readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" + + // Determine pill direction based on section position + readonly property bool openRightward: rightOpen + readonly property bool openLeftward: !rightOpen + + // Effective shown state (true if animated open or forced) + readonly property bool revealed: forceOpen || showPill + + signal shown + signal hidden + signal entered + signal exited + signal clicked + signal rightClicked + signal middleClicked + signal wheel(int delta) + + // Internal state + property bool showPill: false + property bool shouldAnimateHide: false + + // Sizing logic for horizontal bars + readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling) + readonly property int pillWidth: iconSize + readonly property int pillPaddingHorizontal: Style.marginS * scaling + readonly property int pillPaddingVertical: Style.marginS * scaling + readonly property int pillOverlap: iconSize * 0.5 + readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 4) + readonly property int maxPillHeight: iconSize + + // For horizontal bars: height is just icon size, width includes pill space + width: revealed ? (iconSize + maxPillWidth - pillOverlap) : iconSize + height: iconSize + + Rectangle { + id: pill + width: revealed ? maxPillWidth : 1 + height: revealed ? maxPillHeight : 1 + + // Position based on direction - center the pill relative to the icon + x: openLeftward ? (iconCircle.x + iconCircle.width / 2 - width) : (iconCircle.x + iconCircle.width / 2) + y: 0 + + opacity: revealed ? Style.opacityFull : Style.opacityNone + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + // Radius logic for horizontal expansion - rounded on the side that connects to icon + topLeftRadius: openLeftward ? iconSize * 0.5 : 0 + bottomLeftRadius: openLeftward ? iconSize * 0.5 : 0 + topRightRadius: openRightward ? iconSize * 0.5 : 0 + bottomRightRadius: openRightward ? iconSize * 0.5 : 0 + + anchors.verticalCenter: parent.verticalCenter + + NText { + id: textItem + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + text: root.text + font.pointSize: Style.fontSizeXXS * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + visible: revealed + } + + Behavior on width { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on height { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on opacity { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + } + + Rectangle { + id: iconCircle + width: iconSize + height: iconSize + radius: width * 0.5 + color: hovered && !forceOpen ? Color.mTertiary : Color.mSurfaceVariant + + // Icon positioning based on direction + x: openLeftward ? (parent.width - width) : 0 + y: 0 + anchors.verticalCenter: parent.verticalCenter + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + + NIcon { + icon: root.icon + font.pointSize: Style.fontSizeM * scaling + color: hovered && !forceOpen ? Color.mOnTertiary : Color.mOnSurfaceVariant + // Center horizontally + x: (iconCircle.width - width) / 2 + // Center vertically accounting for font metrics + y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2 + } + } + + ParallelAnimation { + id: showAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: 1 + to: maxPillWidth + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "height" + from: 1 + to: maxPillHeight + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 0 + to: 1 + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + onStarted: { + showPill = true + } + onStopped: { + delayedHideAnim.start() + root.shown() + } + } + + SequentialAnimation { + id: delayedHideAnim + running: false + PauseAnimation { + duration: 2500 + } + ScriptAction { + script: if (shouldAnimateHide) { + hideAnim.start() + } + } + } + + ParallelAnimation { + id: hideAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: maxPillWidth + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + NumberAnimation { + target: pill + property: "height" + from: maxPillHeight + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 1 + to: 0 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + onStopped: { + showPill = false + shouldAnimateHide = false + root.hidden() + } + } + + NTooltip { + id: tooltip + target: pill + text: root.tooltipText + positionLeft: barPosition === "right" + positionRight: barPosition === "left" + positionAbove: Settings.data.bar.position === "bottom" + delay: Style.tooltipDelayLong + } + + Timer { + id: showTimer + interval: Style.pillDelay + onTriggered: { + if (!showPill) { + showAnim.start() + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onEntered: { + hovered = true + root.entered() + tooltip.show() + if (disableOpen) { + return + } + if (!forceOpen) { + showDelayed() + } + } + onExited: { + hovered = false + root.exited() + if (!forceOpen) { + hide() + } + tooltip.hide() + } + onClicked: function (mouse) { + if (mouse.button === Qt.LeftButton) { + root.clicked() + } else if (mouse.button === Qt.RightButton) { + root.rightClicked() + } else if (mouse.button === Qt.MiddleButton) { + root.middleClicked() + } + } + onWheel: wheel => { + root.wheel(wheel.angleDelta.y) + } + } + + function show() { + if (!showPill) { + shouldAnimateHide = autoHide + showAnim.start() + } else { + hideAnim.stop() + delayedHideAnim.restart() + } + } + + function hide() { + if (forceOpen) { + return + } + if (showPill) { + hideAnim.start() + } + showTimer.stop() + } + + function showDelayed() { + if (!showPill) { + shouldAnimateHide = autoHide + showTimer.start() + } else { + hideAnim.stop() + delayedHideAnim.restart() + } + } + + onForceOpenChanged: { + if (forceOpen) { + // Immediately lock open without animations + showAnim.stop() + hideAnim.stop() + delayedHideAnim.stop() + showPill = true + } else { + hide() + } + } +} diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index e3faeaf..50bf6ad 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -17,8 +17,8 @@ Item { property bool hovered: false property real fontSize: Style.fontSizeXS - // Effective shown state (true if hovered/animated open or forced) - readonly property bool revealed: forceOpen || showPill + readonly property string barPosition: Settings.data.bar.position + readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" signal shown signal hidden @@ -29,258 +29,81 @@ Item { signal middleClicked signal wheel(int delta) - // Internal state - property bool showPill: false - property bool shouldAnimateHide: false + // Dynamic sizing based on loaded component + width: pillLoader.item ? pillLoader.item.width : 0 + height: pillLoader.item ? pillLoader.item.height : 0 - // Exposed width logic - readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling) - readonly property int pillHeight: iconSize - readonly property int pillPaddingHorizontal: Style.marginS * scaling - readonly property int pillOverlap: iconSize * 0.5 - readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap) + // Loader to switch between vertical and horizontal pill implementations + Loader { + id: pillLoader + sourceComponent: isVerticalBar ? verticalPillComponent : horizontalPillComponent - width: iconSize + Math.max(0, pill.width - pillOverlap) - height: pillHeight + Component { + id: verticalPillComponent + NVerticalPill { + icon: root.icon + text: root.text + tooltipText: root.tooltipText + sizeRatio: root.sizeRatio + autoHide: root.autoHide + forceOpen: root.forceOpen + disableOpen: root.disableOpen + rightOpen: root.rightOpen + hovered: root.hovered + fontSize: root.fontSize - Rectangle { - id: pill - width: revealed ? maxPillWidth : 1 - height: pillHeight - - x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right - (iconCircle.x + iconCircle.width / 2) - width // Opens left - - opacity: revealed ? Style.opacityFull : Style.opacityNone - color: Color.mSurfaceVariant - - topLeftRadius: rightOpen ? 0 : pillHeight * 0.5 - bottomLeftRadius: rightOpen ? 0 : pillHeight * 0.5 - topRightRadius: rightOpen ? pillHeight * 0.5 : 0 - bottomRightRadius: rightOpen ? pillHeight * 0.5 : 0 - anchors.verticalCenter: parent.verticalCenter - - NText { - id: textItem - anchors.verticalCenter: parent.verticalCenter - x: { - // Little tweak to have a better text horizontal centering - var centerX = (parent.width - width) / 2 - var offset = rightOpen ? Style.marginXS * scaling : -Style.marginXS * scaling - return centerX + offset - } - text: root.text - font.pointSize: root.fontSize * scaling - font.weight: Style.fontWeightBold - color: Color.mPrimary - visible: revealed - } - - Behavior on width { - enabled: showAnim.running || hideAnim.running - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - } - Behavior on opacity { - enabled: showAnim.running || hideAnim.running - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - } - } - - Rectangle { - id: iconCircle - width: iconSize - height: iconSize - radius: width * 0.5 - color: hovered && !forceOpen ? Color.mTertiary : Color.mSurfaceVariant - anchors.verticalCenter: parent.verticalCenter - - x: rightOpen ? 0 : (parent.width - width) - - Behavior on color { - ColorAnimation { - duration: Style.animationNormal - easing.type: Easing.InOutQuad + onShown: root.shown() + onHidden: root.hidden() + onEntered: root.entered() + onExited: root.exited() + onClicked: root.clicked() + onRightClicked: root.rightClicked() + onMiddleClicked: root.middleClicked() + onWheel: root.wheel } } - NIcon { - icon: root.icon - font.pointSize: Style.fontSizeM * scaling - color: hovered && !forceOpen ? Color.mOnTertiary : Color.mOnSurface - // Center horizontally - x: (iconCircle.width - width) / 2 - // Center vertically accounting for font metrics - y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2 - } - } + Component { + id: horizontalPillComponent + NHorizontalPill { + icon: root.icon + text: root.text + tooltipText: root.tooltipText + sizeRatio: root.sizeRatio + autoHide: root.autoHide + forceOpen: root.forceOpen + disableOpen: root.disableOpen + rightOpen: root.rightOpen + hovered: root.hovered + fontSize: root.fontSize - ParallelAnimation { - id: showAnim - running: false - NumberAnimation { - target: pill - property: "width" - from: 1 - to: maxPillWidth - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - NumberAnimation { - target: pill - property: "opacity" - from: 0 - to: 1 - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - onStarted: { - showPill = true - } - onStopped: { - delayedHideAnim.start() - root.shown() - } - } - - SequentialAnimation { - id: delayedHideAnim - running: false - PauseAnimation { - duration: 2500 - } - ScriptAction { - script: if (shouldAnimateHide) { - hideAnim.start() - } - } - } - - ParallelAnimation { - id: hideAnim - running: false - NumberAnimation { - target: pill - property: "width" - from: maxPillWidth - to: 1 - duration: Style.animationNormal - easing.type: Easing.InCubic - } - NumberAnimation { - target: pill - property: "opacity" - from: 1 - to: 0 - duration: Style.animationNormal - easing.type: Easing.InCubic - } - onStopped: { - showPill = false - shouldAnimateHide = false - root.hidden() - } - } - - NTooltip { - id: tooltip - positionAbove: Settings.data.bar.position === "bottom" - target: pill - delay: Style.tooltipDelayLong - text: root.tooltipText - } - - Timer { - id: showTimer - interval: Style.pillDelay - onTriggered: { - if (!showPill) { - showAnim.start() + onShown: root.shown() + onHidden: root.hidden() + onEntered: root.entered() + onExited: root.exited() + onClicked: root.clicked() + onRightClicked: root.rightClicked() + onMiddleClicked: root.middleClicked() + onWheel: root.wheel } } } - MouseArea { - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onEntered: { - hovered = true - root.entered() - tooltip.show() - if (disableOpen) { - return - } - if (!forceOpen) { - showDelayed() - } - } - onExited: { - hovered = false - root.exited() - if (!forceOpen) { - hide() - } - tooltip.hide() - } - onClicked: function (mouse) { - if (mouse.button === Qt.LeftButton) { - root.clicked() - } else if (mouse.button === Qt.RightButton) { - root.rightClicked() - } else if (mouse.button === Qt.MiddleButton) { - root.middleClicked() - } - } - onWheel: wheel => { - root.wheel(wheel.angleDelta.y) - } - } - function show() { - if (!showPill) { - shouldAnimateHide = autoHide - showAnim.start() - } else { - hideAnim.stop() - delayedHideAnim.restart() + if (pillLoader.item && pillLoader.item.show) { + pillLoader.item.show() } } function hide() { - if (forceOpen) { - return + if (pillLoader.item && pillLoader.item.hide) { + pillLoader.item.hide() } - if (showPill) { - hideAnim.start() - } - showTimer.stop() } function showDelayed() { - if (!showPill) { - shouldAnimateHide = autoHide - showTimer.start() - } else { - hideAnim.stop() - delayedHideAnim.restart() - } - } - - onForceOpenChanged: { - if (forceOpen) { - // Immediately lock open without animations - showAnim.stop() - hideAnim.stop() - delayedHideAnim.stop() - showPill = true - } else { - hide() + if (pillLoader.item && pillLoader.item.showDelayed) { + pillLoader.item.showDelayed() } } } diff --git a/Widgets/NVerticalPill.qml b/Widgets/NVerticalPill.qml new file mode 100644 index 0000000..e955c95 --- /dev/null +++ b/Widgets/NVerticalPill.qml @@ -0,0 +1,324 @@ +import QtQuick +import QtQuick.Controls +import qs.Commons +import qs.Services + +Item { + id: root + + property string icon: "" + property string text: "" + property string tooltipText: "" + property real sizeRatio: 0.8 + property bool autoHide: false + property bool forceOpen: false + property bool disableOpen: false + property bool rightOpen: false + property bool hovered: false + property real fontSize: Style.fontSizeXS + + // Bar position detection for pill direction + readonly property string barPosition: Settings.data.bar.position + readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" + + // Determine pill direction based on section position + readonly property bool openDownward: rightOpen + readonly property bool openUpward: !rightOpen + + // Effective shown state (true if animated open or forced) + readonly property bool revealed: forceOpen || showPill + + signal shown + signal hidden + signal entered + signal exited + signal clicked + signal rightClicked + signal middleClicked + signal wheel(int delta) + + // Internal state + property bool showPill: false + property bool shouldAnimateHide: false + + // Sizing logic for vertical bars + readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling) + readonly property int pillHeight: iconSize + readonly property int pillPaddingHorizontal: Style.marginS * scaling + readonly property int pillPaddingVertical: Style.marginS * scaling + readonly property int pillOverlap: iconSize * 0.5 + readonly property int maxPillWidth: iconSize + readonly property int maxPillHeight: Math.max(1, textItem.implicitHeight + pillPaddingVertical * 3) + + // For vertical bars: width is just icon size, height includes pill space + width: iconSize + height: revealed ? (iconSize + maxPillHeight - pillOverlap) : iconSize + + Rectangle { + id: pill + width: revealed ? maxPillWidth : 1 + height: revealed ? maxPillHeight : 1 + + // Position based on direction - center the pill relative to the icon + x: 0 + y: openUpward ? (iconCircle.y + iconCircle.height / 2 - height) : (iconCircle.y + iconCircle.height / 2) + + opacity: revealed ? Style.opacityFull : Style.opacityNone + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + // Radius logic for vertical expansion - rounded on the side that connects to icon + topLeftRadius: openUpward ? iconSize * 0.5 : 0 + bottomLeftRadius: openDownward ? iconSize * 0.5 : 0 + topRightRadius: openUpward ? iconSize * 0.5 : 0 + bottomRightRadius: openDownward ? iconSize * 0.5 : 0 + + anchors.horizontalCenter: parent.horizontalCenter + + NVerticalText { + id: textItem + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + text: root.text + fontSize: Style.fontSizeXXS * scaling + fontWeight: Style.fontWeightBold + color: Color.mOnSurface + visible: revealed + } + + Behavior on width { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on height { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on opacity { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + } + + Rectangle { + id: iconCircle + width: iconSize + height: iconSize + radius: width * 0.5 + color: hovered && !forceOpen ? Color.mTertiary : Color.mSurfaceVariant + + // Icon positioning based on direction + x: 0 + y: openUpward ? (parent.height - height) : 0 + anchors.horizontalCenter: parent.horizontalCenter + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + + NIcon { + icon: root.icon + font.pointSize: Style.fontSizeM * scaling + color: hovered && !forceOpen ? Color.mOnTertiary : Color.mOnSurfaceVariant + // Center horizontally + x: (iconCircle.width - width) / 2 + // Center vertically accounting for font metrics + y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2 + } + } + + ParallelAnimation { + id: showAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: 1 + to: maxPillWidth + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "height" + from: 1 + to: maxPillHeight + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 0 + to: 1 + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + onStarted: { + showPill = true + } + onStopped: { + delayedHideAnim.start() + root.shown() + } + } + + SequentialAnimation { + id: delayedHideAnim + running: false + PauseAnimation { + duration: 2500 + } + ScriptAction { + script: if (shouldAnimateHide) { + hideAnim.start() + } + } + } + + ParallelAnimation { + id: hideAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: maxPillWidth + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + NumberAnimation { + target: pill + property: "height" + from: maxPillHeight + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 1 + to: 0 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + onStopped: { + showPill = false + shouldAnimateHide = false + root.hidden() + } + } + + NTooltip { + id: tooltip + target: pill + text: root.tooltipText + positionLeft: barPosition === "right" + positionRight: barPosition === "left" + positionAbove: Settings.data.bar.position === "bottom" + delay: Style.tooltipDelayLong + } + + Timer { + id: showTimer + interval: Style.pillDelay + onTriggered: { + if (!showPill) { + showAnim.start() + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onEntered: { + hovered = true + root.entered() + tooltip.show() + if (disableOpen) { + return + } + if (!forceOpen) { + showDelayed() + } + } + onExited: { + hovered = false + root.exited() + if (!forceOpen) { + hide() + } + tooltip.hide() + } + onClicked: function (mouse) { + if (mouse.button === Qt.LeftButton) { + root.clicked() + } else if (mouse.button === Qt.RightButton) { + root.rightClicked() + } else if (mouse.button === Qt.MiddleButton) { + root.middleClicked() + } + } + onWheel: wheel => { + root.wheel(wheel.angleDelta.y) + } + } + + function show() { + if (!showPill) { + shouldAnimateHide = autoHide + showAnim.start() + } else { + hideAnim.stop() + delayedHideAnim.restart() + } + } + + function hide() { + if (forceOpen) { + return + } + if (showPill) { + hideAnim.start() + } + showTimer.stop() + } + + function showDelayed() { + if (!showPill) { + shouldAnimateHide = autoHide + showTimer.start() + } else { + hideAnim.stop() + delayedHideAnim.restart() + } + } + + onForceOpenChanged: { + if (forceOpen) { + // Immediately lock open without animations + showAnim.stop() + hideAnim.stop() + delayedHideAnim.stop() + showPill = true + } else { + hide() + } + } +} diff --git a/Widgets/NVerticalText.qml b/Widgets/NVerticalText.qml new file mode 100644 index 0000000..969ebf8 --- /dev/null +++ b/Widgets/NVerticalText.qml @@ -0,0 +1,26 @@ +import QtQuick +import qs.Commons +import qs.Services + +Column { + id: root + + property string text: "" + property real fontSize: Style.fontSizeXS + property color color: Color.mOnSurface + property int fontWeight: Style.fontWeightBold + + spacing: -2 * scaling + + Repeater { + model: root.text.split("") + NText { + text: modelData + font.pointSize: root.fontSize + font.weight: root.fontWeight + font.family: Settings.data.ui.fontDefault + color: root.color + horizontalAlignment: Text.AlignHCenter + } + } +}