BangleApps/apps/ctrlpad/main.ts

375 lines
8.5 KiB
TypeScript
Raw Normal View History

2024-04-24 17:18:00 +00:00
(() => {
2023-08-28 11:17:07 +00:00
if(!Bangle.prependListener){
type Event<T> = T extends `#on${infer Evt}` ? Evt : never;
Bangle.prependListener = function(
evt: Event<keyof BangleEvents>,
listener: () => void
){
// move our drag to the start of the event listener array
const handlers = (Bangle as BangleEvents)[`#on${evt}`]
if(!handlers){
Bangle.on(evt as any, listener);
}else{
if(typeof handlers === "function"){
// get Bangle to convert to array
Bangle.on(evt as any, listener);
}
// shuffle array
(Bangle as BangleEvents)[`#on${evt}`] = [listener as any].concat(
(handlers as Array<any>).filter((f: unknown) => f !== listener)
);
}
};
}
class Overlay {
g2: Graphics;
width: number;
height: number;
constructor() {
// x padding: 10 each side
// y top: 24, y bottom: 10
this.width = g.getWidth() - 10 * 2;
this.height = g.getHeight() - 24 - 10;
this.g2 = Graphics.createArrayBuffer(
this.width,
this.height,
2024-04-24 17:08:20 +00:00
/*bpp*/4,
2023-08-28 11:17:07 +00:00
{ msb: true }
);
this.renderG2();
}
setBottom(bottom: number): void {
const { g2 } = this;
const y = bottom - this.height;
Bangle.setLCDOverlay(g2, 10, y - 10);
}
hide(): void {
Bangle.setLCDOverlay();
}
renderG2(): void {
2023-08-28 21:30:38 +00:00
this.g2
2023-08-28 11:19:46 +00:00
.reset()
2024-04-25 11:46:52 +00:00
.setColor(g.theme.bg)
.fillRect(0, 0, this.width, this.height)
.setColor(colour.on.bg)
2023-08-28 11:19:46 +00:00
.drawRect(0, 0, this.width - 1, this.height - 1)
.drawRect(1, 1, this.width - 2, this.height - 2);
2023-08-28 21:30:38 +00:00
}
}
type Control = {
x: number,
y: number,
2024-04-25 11:46:19 +00:00
fg: ColorResolvable,
bg: ColorResolvable,
2023-08-28 21:30:38 +00:00
text: string,
};
const colour = {
on: {
2024-04-24 17:08:20 +00:00
fg: "#fff",
bg: "#00a",
},
off: {
2024-04-24 17:08:20 +00:00
fg: "#000",
bg: "#bbb",
},
2024-04-25 11:46:19 +00:00
} as const;
2023-08-28 21:30:38 +00:00
class Controls {
controls: [Control, Control, Control, Control, Control];
constructor(g: Graphics) {
// const connected = NRF.getSecurityStatus().connected;
// if (0&&connected) {
// // TODO
// return [
// { text: "<", cb: hid.next },
// { text: "@", cb: hid.toggle },
// { text: ">", cb: hid.prev },
// { text: "-", cb: hid.down },
// { text: "+", cb: hid.up },
// ];
// }
const height = g.getHeight();
const centreY = height / 2;
const circleGapY = 30;
const width = g.getWidth();
this.controls = [
{ x: width / 4 - 10, y: centreY - circleGapY, text: "BLE", fg: colour.on.fg, bg: colour.on.bg }, // FIXME: init
{ x: width / 2, y: centreY - circleGapY, text: "DnD", fg: colour.off.fg, bg: colour.off.bg },
{ x: width * 3/4 + 10, y: centreY - circleGapY, text: "HRM", fg: colour.off.fg, bg: colour.off.bg }, // FIXME: init
{ x: width / 3, y: centreY + circleGapY, text: "B-", fg: colour.on.fg, bg: colour.on.bg },
{ x: width * 2/3, y: centreY + circleGapY, text: "B+", fg: colour.on.fg, bg: colour.on.bg },
2023-08-28 21:30:38 +00:00
];
}
2023-08-28 11:17:07 +00:00
draw(g: Graphics, single?: Control): void {
2023-08-28 11:17:07 +00:00
g
2023-08-28 11:19:46 +00:00
.setFontAlign(0, 0)
2024-04-24 16:26:28 +00:00
.setFont("4x6:3" as any /* FIXME */);
2023-08-28 11:17:07 +00:00
for(const ctrl of single ? [single] : this.controls){
2023-08-28 21:30:38 +00:00
g
2024-04-25 11:46:19 +00:00
.setColor(ctrl.bg)
2023-08-28 21:30:38 +00:00
.fillCircle(ctrl.x, ctrl.y, 23)
2024-04-25 11:46:19 +00:00
.setColor(ctrl.fg)
2023-08-28 21:30:38 +00:00
.drawString(ctrl.text, ctrl.x, ctrl.y);
}
2023-08-28 11:21:58 +00:00
}
2023-08-28 21:30:38 +00:00
hitTest(x: number, y: number): Control | undefined {
2024-04-24 16:21:53 +00:00
let dist = Infinity;
let closest;
2023-08-28 11:17:07 +00:00
2024-04-24 16:21:53 +00:00
for(const ctrl of this.controls){
const dx = x-ctrl.x;
const dy = y-ctrl.y;
const d = Math.sqrt(dx*dx + dy*dy);
if(d < dist){
dist = d;
closest = ctrl;
}
}
2023-08-28 11:17:07 +00:00
2024-04-24 16:21:53 +00:00
return dist < 30 ? closest : undefined;
2023-08-28 11:17:07 +00:00
}
}
2023-08-04 21:48:40 +00:00
2023-07-27 16:21:36 +00:00
const enum State {
Idle,
TopDrag,
IgnoreCurrent,
Active,
}
type UI = { overlay: Overlay, ctrls: Controls };
2023-08-28 11:18:31 +00:00
let state = State.Idle;
2023-07-27 16:21:36 +00:00
let startY = 0;
2023-08-28 11:17:07 +00:00
let startedUpDrag = false;
let upDragAnim: IntervalId | undefined;
let ui: undefined | UI;
2023-08-28 11:17:07 +00:00
let touchDown = false;
2024-04-24 17:18:00 +00:00
const initUI = () => {
if (ui) return;
const overlay = new Overlay();
ui = {
overlay,
ctrls: new Controls(overlay.g2, controls),
};
ui.ctrls.draw(ui.overlay.g2);
};
2024-04-24 17:18:00 +00:00
const onDrag = (e => {
2023-08-28 11:17:07 +00:00
const dragDistance = 30;
2023-08-28 11:17:07 +00:00
if (e.b === 0) touchDown = startedUpDrag = false;
2023-07-27 16:21:36 +00:00
switch (state) {
case State.IgnoreCurrent:
2023-08-28 11:17:07 +00:00
if(e.b === 0){
2023-08-28 11:19:46 +00:00
state = State.Idle;
2023-08-28 21:30:38 +00:00
ui = undefined;
2023-08-28 11:19:46 +00:00
}
break;
2023-07-27 16:21:36 +00:00
case State.Idle:
if(e.b && !touchDown){ // no need to check Bangle.CLKINFO_FOCUS
2023-08-28 11:19:46 +00:00
if(e.y <= 40){
state = State.TopDrag
startY = e.y;
2024-04-24 16:25:48 +00:00
//console.log(" topdrag detected, starting @ " + startY);
2023-08-28 11:19:46 +00:00
}else{
2024-04-24 16:25:48 +00:00
//console.log(" ignoring this drag (too low @ " + e.y + ")");
2023-08-28 11:19:46 +00:00
state = State.IgnoreCurrent;
2023-08-28 21:30:38 +00:00
ui = undefined
2023-08-28 11:19:46 +00:00
}
2023-08-28 11:17:07 +00:00
}
2023-08-28 11:19:46 +00:00
break;
2023-07-27 16:21:36 +00:00
case State.TopDrag:
if(e.b === 0){
2024-04-24 16:25:48 +00:00
//console.log("topdrag stopped, distance: " + (e.y - startY));
2023-08-28 11:19:46 +00:00
if(e.y > startY + dragDistance){
2024-04-24 16:25:48 +00:00
//console.log("activating");
initUI();
2023-08-28 11:19:46 +00:00
state = State.Active;
startY = 0;
Bangle.prependListener("touch", onTouch);
Bangle.buzz(20);
2023-08-28 21:30:38 +00:00
ui!.overlay.setBottom(g.getHeight());
2023-08-28 11:19:46 +00:00
break;
}
2024-04-24 16:25:48 +00:00
//console.log("returning to idle");
2023-08-28 11:19:46 +00:00
state = State.Idle;
2023-08-28 21:30:38 +00:00
ui?.overlay.hide();
ui = undefined;
2023-08-28 11:19:46 +00:00
}else{
// partial drag, show UI feedback:
const dragOffset = 32;
2023-08-28 11:17:07 +00:00
initUI();
ui!.overlay.setBottom(e.y - dragOffset);
2023-08-28 11:19:46 +00:00
}
2024-04-24 16:25:16 +00:00
E.stopEventPropagation?.();
2023-08-28 11:19:46 +00:00
break;
2023-07-27 16:21:36 +00:00
case State.Active:
2023-08-28 21:30:38 +00:00
//console.log("stolen drag handling, do whatever here");
2023-08-28 11:19:46 +00:00
E.stopEventPropagation?.();
if(e.b){
if(!touchDown){
startY = e.y;
}else if(startY){
const dist = Math.max(0, startY - e.y);
if (startedUpDrag || (startedUpDrag = dist > 10)) // ignore small drags
2023-08-28 21:30:38 +00:00
ui!.overlay.setBottom(g.getHeight() - dist);
}
}else if(e.b === 0){
if((startY - e.y) > dragDistance){
let bottom = g.getHeight() - Math.max(0, startY - e.y);
if (upDragAnim) clearInterval(upDragAnim);
upDragAnim = setInterval(() => {
if (!ui || bottom <= 0) {
clearInterval(upDragAnim!);
upDragAnim = undefined;
ui?.overlay.hide();
ui = undefined;
return;
}
ui.overlay.setBottom(bottom);
2023-08-28 21:30:38 +00:00
bottom -= 30;
}, 50)
Bangle.removeListener("touch", onTouch);
state = State.Idle;
}else{
ui!.overlay.setBottom(g.getHeight());
2023-08-28 11:17:07 +00:00
}
2023-08-28 11:19:46 +00:00
}
break;
2024-04-24 17:18:00 +00:00
}
2023-08-28 11:17:07 +00:00
if(e.b) touchDown = true;
2024-04-24 17:18:00 +00:00
}) satisfies DragCallback;
2023-08-28 21:30:38 +00:00
const onTouch = ((_btn, xy) => {
if(!ui || !xy) return;
const top = g.getHeight() - ui.overlay.height; // assumed anchored to bottom
const left = (g.getWidth() - ui.overlay.width) / 2; // more assumptions
const ctrl = ui.ctrls.hitTest(xy.x - left, xy.y - top);
if(ctrl){
onCtrlTap(ctrl, ui);
2024-04-24 16:25:16 +00:00
E.stopEventPropagation?.();
}
2023-08-28 11:17:07 +00:00
}) satisfies TouchCallback;
let origBuzz: undefined | (() => Promise<void>);
const onCtrlTap = (ctrl: Control, ui: UI) => {
2024-04-25 11:52:41 +00:00
Bangle.buzz(20);
2024-04-24 18:38:44 +00:00
let on = true;
switch(ctrl.text){
case "BLE":
if(NRF.getSecurityStatus().advertising){
NRF.sleep();
on = false;
}else{
NRF.wake();
}
break;
case "DnD":
if(origBuzz){
Bangle.buzz = origBuzz;
origBuzz = undefined;
on = false;
}else{
origBuzz = Bangle.buzz;
Bangle.buzz = () => (Promise as any).resolve(); // FIXME
setTimeout(() => {
if(!origBuzz) return;
Bangle.buzz = origBuzz;
origBuzz = undefined;
}, 1000 * 60 * 10);
}
break;
case "HRM": {
const id = "widhid";
const hrm: undefined | Array<string> = (Bangle as any)._PWR?.HRM;
if(!hrm || hrm.indexOf(id) === -1){
Bangle.setHRMPower(1, id);
}else{
Bangle.setHRMPower(0, id);
on = false;
}
break;
}
default:
console.log(`widhid: couldn't handle "${ctrl.text}" tap`);
on = ctrl.fg !== colour.on.fg;
}
const col = on ? colour.on : colour.off;
ctrl.fg = col.fg;
ctrl.bg = col.bg;
//console.log("hit on " + ctrl.text + ", col: " + ctrl.fg);
ui.ctrls.draw(ui.overlay.g2, ctrl);
};
2023-08-28 11:17:07 +00:00
Bangle.prependListener("drag", onDrag);
2024-04-24 17:44:51 +00:00
Bangle.on("lock", () => {
2024-04-25 11:48:55 +00:00
state = State.Idle;
2024-04-24 17:44:51 +00:00
ui?.overlay.hide();
ui = undefined;
});
2023-07-27 16:21:36 +00:00
2024-04-24 17:18:00 +00:00
2024-04-24 16:07:07 +00:00
/*
const settings = require("Storage").readJSON("setting.json", true) as Settings || ({ HID: false } as Settings);
const haveMedia = settings.HID === "kbmedia";
// @ts-ignore
delete settings;
const sendHid = (code: number) => {
try{
NRF.sendHIDReport(
[1, code],
() => NRF.sendHIDReport([1, 0]),
);
}catch(e){
console.log("sendHIDReport:", e);
}
};
const hid = haveMedia ? {
next: () => sendHid(0x01),
prev: () => sendHid(0x02),
toggle: () => sendHid(0x10),
up: () => sendHid(0x40),
down: () => sendHid(0x80),
} : null;
*/
2024-04-24 17:18:00 +00:00
})()