BangleApps/apps/ctrlpad/main.ts

419 lines
9.2 KiB
TypeScript
Raw Permalink 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 ControlCallback = (tap: boolean) => boolean | number;
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,
cb: ControlCallback,
};
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
type FiveOf<X> = [X, X, X, X, X];
type ControlTemplate = { text: string, cb: ControlCallback };
2023-08-28 21:30:38 +00:00
class Controls {
controls: FiveOf<Control>;
2023-08-28 21:30:38 +00:00
constructor(g: Graphics, controls: FiveOf<ControlTemplate>) {
2023-08-28 21:30:38 +00:00
// 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 },
{ x: width / 2, y: centreY - circleGapY },
{ x: width * 3/4 + 10, y: centreY - circleGapY },
{ x: width / 3, y: centreY + circleGapY },
{ x: width * 2/3, y: centreY + circleGapY },
].map((xy, i) => {
const ctrl = xy as Control;
const from = controls[i]!;
ctrl.text = from.text;
ctrl.cb = from.cb;
Object.assign(ctrl, from.cb(false) ? colour.on : colour.off);
return ctrl;
}) as FiveOf<Control>;
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)
.setFont("4x6:3" as any);
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 controls: FiveOf<ControlTemplate> = [
{
text: "BLE",
cb: tap => {
const on = NRF.getSecurityStatus().advertising;
if(tap){
if(on) NRF.sleep();
else NRF.wake();
}
return on !== tap; // on ^ tap
}
},
{
text: "DnD",
cb: tap => {
let on;
2024-05-04 21:21:43 +00:00
if((on = !!origBuzz)){
if(tap){
Bangle.buzz = origBuzz;
origBuzz = undefined;
}
}else{
if(tap){
origBuzz = Bangle.buzz;
2024-04-25 17:29:23 +00:00
Bangle.buzz = () => Promise.resolve();
setTimeout(() => {
if(!origBuzz) return;
Bangle.buzz = origBuzz;
origBuzz = undefined;
}, 1000 * 60 * 10);
}
}
return on !== tap; // on ^ tap
}
},
{
text: "HRM",
cb: tap => {
const id = "widhid";
const hrm = (Bangle as any)._PWR?.HRM as undefined | Array<string> ;
const off = !hrm || hrm.indexOf(id) === -1;
if(off){
if(tap)
Bangle.setHRMPower(1, id);
}else if(tap){
Bangle.setHRMPower(0, id);
}
return !off !== tap; // on ^ tap
}
},
{
text: "clk",
cb: tap => {
if (tap) Bangle.showClock(), terminateUI();
return true;
},
},
{
text: "lch",
cb: tap => {
if (tap) Bangle.showLauncher(), terminateUI();
return true;
},
},
];
const overlay = new Overlay();
ui = {
overlay,
ctrls: new Controls(overlay.g2, controls),
};
ui.ctrls.draw(ui.overlay.g2);
};
2024-04-26 17:42:23 +00:00
const terminateUI = () => {
state = State.Idle;
ui?.overlay.hide();
ui = undefined;
};
2024-04-27 09:11:41 +00:00
const onSwipe = () => {
switch (state) {
case State.Idle:
case State.IgnoreCurrent:
return;
case State.TopDrag:
case State.Active:
E.stopEventPropagation?.();
}
};
Bangle.prependListener('swipe', onSwipe);
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:
2024-04-25 11:53:27 +00:00
if(e.b === 0)
2023-08-28 11:19:46 +00:00
state = State.Idle;
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;
E.stopEventPropagation?.();
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 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());
2024-04-25 11:53:27 +00:00
}else{
//console.log("returning to idle");
2024-04-26 17:42:23 +00:00
terminateUI();
break; // skip stopEventPropagation
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;
2024-04-26 17:42:23 +00:00
terminateUI();
2023-08-28 21:30:38 +00:00
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
const col = ctrl.cb(true) ? 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-26 17:42:23 +00:00
Bangle.on("lock", terminateUI);
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
})()