/* * Split-flap display mode — airport departure board style. * Only digits are shown as flaps; the euro sign is rendered as plain * text (default font, white) next to the board, followed by a trend * arrow (green up / red down; unchanged keeps the last arrow). * On a price change the board first resets every flap to "0" (swept * over ~2 s), then takes the new values (swept over the remainder of * the cascade setting). * Pure QtQuick so it can be tested outside the plasmoid shell. */ import QtQuick 2.15 Row { id: root // wired by the parent loader property string display: "--" // 1 = up, -1 = down, 0 = unchanged -> keep showing the last arrow property int trendDir: 0 property int lastDir: 0 onTrendDirChanged: { if (trendDir !== 0) { lastDir = trendDir; } } // total seconds over which the reset-to-zero + new value cycle spreads property int flapSeconds: 3 spacing: height * 0.06 function digitsOf(s) { var out = ""; for (var i = 0; i < s.length; i++) { var ch = s.charAt(i); if ((ch >= "0" && ch <= "9") || ch === "-") { out += ch; } } return out; } // UI-only derived values (euro/arrow visibility); logic must not rely // on these — QML gives no ordering guarantee between a change handler // and the re-evaluation of dependent bindings. readonly property string digitsOnly: digitsOf(display) readonly property string boardText: digitsOnly.length > 0 ? digitsOnly : "--" property var chars: [] property bool ready: false readonly property real cellFont: height * 0.72 readonly property real cellWidth: cellFont * 0.80 // one flap open+close takes 260 ms; keep delays above that readonly property int flapDuration: 260 Component.onCompleted: { chars = boardText.split(""); ready = true; } onDisplayChanged: { if (!ready) { return; } // compute from display directly: derived bindings may still be // stale at this point (QML makes no ordering guarantee) var digits = digitsOf(display); var nc = (digits.length > 0 ? digits : "--").split(""); var n = Math.max(chars.length, nc.length); // Phase 1: every flap resets to "0", swept over ~2 s. // Phase 2: every flap takes its new value, swept over the // remainder of the total cascade time. var total = Math.max(1, flapSeconds) * 1000; var resetSpan = Math.min(2000, Math.floor(total * 2 / 3)); var valueStart = resetSpan; var resetStep = n > 1 ? Math.floor(Math.max(0, resetSpan - flapDuration) / (n - 1)) : 0; var valueStep = n > 1 ? Math.floor(Math.max(0, total - valueStart - flapDuration) / (n - 1)) : 0; for (var i = 0; i < n; i++) { var cell = rep.itemAt(i); var to = i < nc.length ? nc[i] : ""; if (cell) { cell.schedule(to, i * resetStep, valueStart + i * valueStep); } } chars = nc; } Repeater { id: rep model: root.chars.length delegate: Rectangle { id: cell width: root.cellWidth height: root.height radius: 2 color: "#0b0b0d" border.color: "#26262b" border.width: 1 // plain properties (not bindings) so the animations control // when the character actually changes property string ch: "" property string targetCh: "" property string animCh: "" Component.onCompleted: { ch = root.chars[index] !== undefined ? root.chars[index] : ""; } // schedule the two-phase cycle: reset to "0", then target function schedule(t, resetDelay, valueDelay) { targetCh = t; resetTimer.interval = Math.max(0, resetDelay); resetTimer.restart(); valueTimer.interval = Math.max(0, valueDelay); valueTimer.restart(); } function startFlip(newCh) { if (newCh === ch) { return; } animCh = newCh; flipAnim.restart(); } Timer { id: resetTimer interval: 0 onTriggered: cell.startFlip("0") } Timer { id: valueTimer interval: 0 onTriggered: cell.startFlip(cell.targetCh) } transform: Scale { id: scaleTransform origin.y: cell.height / 2 yScale: 1 } SequentialAnimation { id: flipAnim NumberAnimation { target: scaleTransform property: "yScale" to: 0.06 duration: 130 easing.type: Easing.InQuad } ScriptAction { script: cell.ch = cell.animCh } NumberAnimation { target: scaleTransform property: "yScale" to: 1 duration: 130 easing.type: Easing.OutQuad } } Text { anchors.centerIn: parent text: cell.ch color: "#f2f2f2" font.family: "DejaVu Sans Mono" font.pixelSize: root.cellFont } // the flap split line across the middle Rectangle { anchors.horizontalCenter: parent.horizontalCenter y: parent.height / 2 - 1 width: parent.width height: 2 color: "#000000" opacity: 0.55 } } } // breathing room between the board and the currency sign Item { width: root.height * 0.22 height: 1 } // euro sign in the plain-text font, white Text { anchors.verticalCenter: parent.verticalCenter text: "\u20AC" visible: root.digitsOnly.length > 0 color: "#ffffff" font.family: Qt.application.font.family font.pixelSize: root.height * 0.85 } // same breathing room between the euro sign and the arrow Item { width: root.height * 0.22 height: 1 } // trend arrow: green up / red down; unchanged keeps the last arrow. // The triangle glyph sits low on the baseline; shift it up so its // visual center matches the euro sign. Text { anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: -root.height * 0.125 text: root.lastDir > 0 ? "\u25B2" : "\u25BC" visible: root.digitsOnly.length > 0 && root.lastDir !== 0 color: root.lastDir > 0 ? "#2ecc71" : "#e74c3c" font.family: Qt.application.font.family font.pixelSize: root.height * 1.0 } }