Files
plasma-bitcoin-price/contents/ui/DisplayFlip.qml
T

216 lines
6.8 KiB
QML

/*
* 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
}
// trend arrow: green up / red down; unchanged keeps the last arrow
Text {
anchors.verticalCenter: parent.verticalCenter
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 * 0.5
}
}