Files
chatter/qml/Main.qml
Andy Kopra 5f21d9099c Initial commit: Chatter — assistive-writing app for reMarkable Paper Pro Move
Direct-framebuffer ink pipeline (stock-quality strokes), finger-wipe erase,
growable scrolling canvas with color-ghost cleanup, bidirectional toggle with a
persistent 4-finger return launcher, instant button feedback. Includes prebuilt
aarch64 binaries (dist/), build/deploy/install scripts, a user guide, and a
complete technical reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 18:53:31 +02:00

107 lines
3.8 KiB
QML

// Chatter — UI only. The white "page" plus the top control bar. Ink is drawn
// directly into the framebuffer by the C++ InkEngine, not by the scene graph,
// so there is no canvas item here. Keep the scene STATIC (only the buttons ever
// repaint, on tap) so it never recomposites over the direct-drawn ink.
import QtQuick
import QtQuick.Window
Window {
id: root
width: Screen.width
height: Screen.height
visible: true
color: "white"
// Finger gestures (the stylus is handled in C++ and is NOT delivered here):
// 1 finger = erase wipe (a tap erases nothing — only movement does)
// 2 fingers = vertical scroll (content follows the fingers)
// The gesture type is latched until all fingers lift, so a 2-finger scroll
// never turns into an erase when one finger is raised. Buttons (z:10) are above.
MultiPointTouchArea {
id: touch
anchors.fill: parent
z: 0
minimumTouchPoints: 1
maximumTouchPoints: 2
touchPoints: [
TouchPoint { id: tpA },
TouchPoint { id: tpB }
]
property int mode: 0 // 0 idle, 1 erase, 2 scroll
property real lastPanY: 0
function activeCount() { return (tpA.pressed ? 1 : 0) + (tpB.pressed ? 1 : 0); }
function soloPoint() { return tpA.pressed ? tpA : tpB; }
function handle() {
var n = activeCount();
if (n >= 2) {
var cy = (tpA.y + tpB.y) / 2;
if (mode !== 2) {
if (mode === 1) ink.eraseEnd();
mode = 2;
lastPanY = cy;
} else {
ink.panBy(cy - lastPanY);
lastPanY = cy;
}
} else if (n === 1) {
if (mode === 0) { mode = 1; ink.eraseStart(soloPoint().x, soloPoint().y); }
else if (mode === 1) { ink.eraseMove(soloPoint().x, soloPoint().y); }
// mode === 2 (one finger left after a scroll): stay idle until all up
} else {
if (mode === 1) ink.eraseEnd();
else if (mode === 2) ink.panEnd(); // full refresh to clear scroll ghosts
mode = 0;
}
}
onPressed: handle()
onUpdated: handle()
onReleased: handle()
}
// Buttons keep a FIXED appearance in the Qt scene (rendered once); their
// pressed/normal feedback and actions are handled by the ink engine
// (ink.flashButton / ink.activateButton) so both finger AND stylus work and
// updates are instant single-pass (no scene flashing).
component TopButton: Rectangle {
id: btn
height: 90
// Size to the label so horizontal padding ≈ vertical padding.
width: label.implicitWidth + (height - label.implicitHeight)
color: "white"
border.color: "black"
border.width: 3
radius: 8
z: 10
property alias text: label.text
Text { id: label; anchors.centerIn: parent; anchors.verticalCenterOffset: -2; font.pixelSize: 38 }
// Register geometry once laid out (ink-exclusion, clear-redraw, stylus hit-test).
Component.onCompleted: Qt.callLater(function() {
ink.registerButton(btn.x, btn.y, btn.width, btn.height, btn.text);
})
MouseArea {
anchors.fill: parent
onPressed: ink.flashButton(btn.x, btn.y, btn.width, btn.height, btn.text, true)
onClicked: ink.activateButton(btn.text)
}
}
TopButton {
id: backBtn
text: "Back"
anchors.left: parent.left
anchors.top: parent.top
anchors.margins: 24
}
TopButton {
id: clearBtn
text: "Clear"
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 24
}
}