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

187 lines
5.6 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, and the trend shows
* on a small LED. Changed characters flip with a cascading animation
* spread over the configured number of seconds.
* 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: "--"
property color trendColor: "#9e9e9e"
// seconds over which the cascade to a new price is spread
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 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);
// spread the changed cells evenly across flapSeconds
var changed = [];
for (var i = 0; i < n; i++) {
var from = i < chars.length ? chars[i] : "";
var to = i < nc.length ? nc[i] : "";
if (from !== to) {
changed.push([i, to]);
}
}
var span = Math.max(1, flapSeconds) * 1000 - flapDuration;
var step = changed.length > 1 ? Math.max(0, Math.floor(span / (changed.length - 1))) : 0;
for (var j = 0; j < changed.length; j++) {
var cell = rep.itemAt(changed[j][0]);
if (cell) {
cell.flipTo(changed[j][1], j * step);
}
}
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 property (not a binding) so the flip animation
// controls when the character actually changes
property string ch: ""
property string pendingCh: ""
Component.onCompleted: {
ch = root.chars[index] !== undefined ? root.chars[index] : "";
}
function flipTo(newCh, delay) {
if (newCh === ch) {
return;
}
pendingCh = newCh;
delayTimer.interval = Math.max(0, delay);
delayTimer.restart();
}
Timer {
id: delayTimer
interval: 0
onTriggered: flipAnim.start()
}
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.pendingCh
}
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
}
}
}
// 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.66
}
// trend LED (green/red/grey)
Rectangle {
width: root.height * 0.14
height: width
radius: width / 2
anchors.verticalCenter: parent.verticalCenter
color: root.trendColor
border.color: "#26262b"
border.width: 1
}
}