diff --git a/Overlay.qml b/Overlay.qml index c212d11..daa7bb4 100644 --- a/Overlay.qml +++ b/Overlay.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Window +import QtQuick.Controls as Controls import Quickshell import Quickshell.Io import qs.Commons @@ -767,6 +768,19 @@ Item { return "start typing / tab changes mode / h shows stats" } + function trendAverage(runs) { + if (!runs || !runs.length) return 0 + var total = 0 + for (var i = 0; i < runs.length; i++) total += Number(runs[i].wpm) || 0 + return total / runs.length + } + + function trendHeader(runs) { + if (!runs || !runs.length) return "RECENT WPM" + return "RECENT WPM / LAST " + runs.length + " / PEAK " + Math.round(root.trendMaximum(runs)) + + " / AVG " + Math.round(root.trendAverage(runs)) + " / LATEST " + Math.round(runs[runs.length - 1].wpm) + } + function trendMaximum(runs) { var maximum = 1 for (var i = 0; i < runs.length; i++) maximum = Math.max(maximum, Number(runs[i].wpm) || 0) @@ -1636,7 +1650,11 @@ Item { } Rectangle { + id: statsPanel visible: root.statsOpen + // Short tiled windows drop the trend chart and shrink the list so the profile + // row and results stay reachable. Anything left over scrolls with a visible bar. + readonly property bool compact: parent.height < Style.space(560) width: Math.min(parent.width, Style.space(920)) height: Math.min(parent.height, statsContent.implicitHeight + Style.space(56)) anchors.centerIn: parent @@ -1656,11 +1674,12 @@ Item { clip: true interactive: contentHeight > height boundsBehavior: Flickable.StopAtBounds + Controls.ScrollBar.vertical: Controls.ScrollBar { policy: statsFlick.contentHeight > statsFlick.height ? Controls.ScrollBar.AlwaysOn : Controls.ScrollBar.AlwaysOff } Column { id: statsContent width: statsFlick.width - spacing: Style.space(10) + spacing: Style.space(statsPanel.compact ? 8 : 14) Row { width: parent.width @@ -1694,13 +1713,14 @@ Item { } Item { + visible: !statsPanel.compact width: parent.width - height: Style.space(58) + height: Style.space(76) Text { anchors.left: parent.left anchors.top: parent.top - text: "RECENT WPM" + text: root.trendHeader(root.trendRuns) color: root.muted font.family: root.fontFamily font.pixelSize: Style.font.caption @@ -1708,11 +1728,23 @@ Item { font.letterSpacing: 1 } + // Average line so the bars read as numbers, not just a shape. + Rectangle { + visible: root.trendRuns.length > 1 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.space(52) * root.trendAverage(root.trendRuns) / root.trendMaximum(root.trendRuns) + height: 1 + color: root.foreground + opacity: 0.35 + } + Row { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - height: Style.space(48) + height: Style.space(52) spacing: Style.space(3) Repeater { @@ -1834,7 +1866,7 @@ Item { Flickable { id: historyFlick width: parent.width - height: Math.min(Style.space(178), Math.max(Style.space(34), historyList.implicitHeight)) + height: Math.min(Style.space(statsPanel.compact ? 110 : 178), Math.max(Style.space(34), historyList.implicitHeight)) contentWidth: width contentHeight: historyList.implicitHeight clip: true @@ -2075,8 +2107,7 @@ Item { Text { anchors.left: parent.left anchors.top: parent.top - text: "RECENT WPM / LAST " + root.trendRuns.length - + (root.trendRuns.length ? " / PEAK " + Math.round(root.trendMaximum(root.trendRuns)) : "") + text: root.trendHeader(root.trendRuns) color: root.muted font.family: root.fontFamily font.pixelSize: Style.font.caption diff --git a/tests/native-challenge-smoke.mjs b/tests/native-challenge-smoke.mjs index cf5ff9d..37d5b18 100644 --- a/tests/native-challenge-smoke.mjs +++ b/tests/native-challenge-smoke.mjs @@ -36,7 +36,7 @@ Window { } Timer { id: finished; interval: 300; onTriggered: Qt.quit() } }`); - const result = spawnSync('quickshell',['--no-color','--path',join(directory,'shell.qml')],{env:{...process.env,QT_QPA_PLATFORM:'offscreen',QSG_RHI_BACKEND:'software'},encoding:'utf8',timeout:15000}); + const result = spawnSync('quickshell',['--no-color','--path',join(directory,'shell.qml')],{env:{...process.env,TYPEARCHY_STATE_DIR:join(directory,'state'),XDG_STATE_HOME:join(directory,'state-home'),XDG_DATA_HOME:join(directory,'data'),QT_QPA_PLATFORM:'offscreen',QSG_RHI_BACKEND:'software'},encoding:'utf8',timeout:15000}); const output = result.stdout + result.stderr; assert.equal(result.status,0,output); assert.match(output,/NATIVE_PARITY_PASSED/); diff --git a/tests/native-practice-smoke.mjs b/tests/native-practice-smoke.mjs index a67238a..b94dba2 100644 --- a/tests/native-practice-smoke.mjs +++ b/tests/native-practice-smoke.mjs @@ -37,7 +37,7 @@ ShellRoot { } } Timer { id: finish; interval: 200; onTriggered: Qt.quit() } }`); - const result=spawnSync('quickshell',['--no-color','--path',join(directory,'shell.qml')],{env:{...process.env,QT_QPA_PLATFORM:'offscreen',QSG_RHI_BACKEND:'software'},encoding:'utf8',timeout:15000}); + const result=spawnSync('quickshell',['--no-color','--path',join(directory,'shell.qml')],{env:{...process.env,TYPEARCHY_STATE_DIR:join(directory,'state'),XDG_STATE_HOME:join(directory,'state-home'),XDG_DATA_HOME:join(directory,'data'),QT_QPA_PLATFORM:'offscreen',QSG_RHI_BACKEND:'software'},encoding:'utf8',timeout:15000}); const output=result.stdout+result.stderr; assert.equal(result.status,0,output);assert.match(output,/NATIVE_PRACTICE_PASSED/); assert.doesNotMatch(output,/ReferenceError|TypeError|Unable to load|Cannot assign|not a type|Binding loop/i);