Skip to content

Instantly share code, notes, and snippets.

@gfwilliams
Last active February 21, 2022 20:57
Show Gist options
  • Save gfwilliams/0aac0c7a020913a0f162571d9d3b1ee6 to your computer and use it in GitHub Desktop.
Save gfwilliams/0aac0c7a020913a0f162571d9d3b1ee6 to your computer and use it in GitHub Desktop.
test code for new Bangle.js menu style
/*
TODO
Better rounded rect
setUI could be better - maybe include 'back' widget/handling code?
submenus could allow 'back' without modifying the menu item
*/
g.fillRRect = (x,y,w,h) => g.fillPoly([
x+4,y,
x+w-4,y,
x+w,y+4,
x+w,y+h-4,
x+w-4,y+h,
x+4,y+h,
x,y+h-4,
x,y+4
]);
Bangle.setUI = (function(mode, cb) {
if (Bangle.btnWatches) {
Bangle.btnWatches.forEach(clearWatch);
delete Bangle.btnWatches;
}
if (Bangle.dragHandler) {
Bangle.removeListener("drag", Bangle.dragHandler);
delete Bangle.dragHandler;
}
if (Bangle.touchHandler) {
Bangle.removeListener("touch", Bangle.touchHandler);
delete Bangle.touchHandler;
}
if (Bangle.uiRemove) {
Bangle.uiRemove();
delete Bangle.uiRemove;
}
function b() {
try{Bangle.buzz(30);}catch(e){}
}
if (!mode) return;
else if (mode=="updown") {
var dy = 0;
Bangle.dragHandler = e=>{
dy += e.dy;
if (!e.b) dy=0;
while (Math.abs(dy)>32) {
if (dy>0) { dy-=32; cb(1) }
else { dy+=32; cb(-1) }
Bangle.buzz(20);
}
};
Bangle.on('drag',Bangle.dragHandler);
Bangle.touchHandler = d => {b();cb();};
Bangle.on("touch", Bangle.touchHandler);
Bangle.btnWatches = [
setWatch(function() { b();cb(); }, BTN1, {repeat:1}),
];
} else if (mode=="leftright") {
var dx = 0;
Bangle.dragHandler = e=>{
dx += e.dx;
if (!e.b) dx=0;
while (Math.abs(dx)>32) {
if (dx>0) { dx-=32; cb(1) }
else { dx+=32; cb(-1) }
Bangle.buzz(20);
}
};
Bangle.on('drag',Bangle.dragHandler);
Bangle.touchHandler = d => {b();cb();};
Bangle.on("touch", Bangle.touchHandler);
Bangle.btnWatches = [
setWatch(function() { b();cb(); }, BTN1, {repeat:1}),
];
} else if (mode=="clock") {
Bangle.CLOCK=1;
Bangle.btnWatches = [
setWatch(Bangle.showLauncher, BTN1, {repeat:1,edge:"falling"})
];
} else if (mode=="clockupdown") {
Bangle.CLOCK=1;
Bangle.touchHandler = (d,e) => {
if (e.x < 120) return;
b();cb((e.y > 88) ? 1 : -1);
};
Bangle.on("touch", Bangle.touchHandler);
Bangle.btnWatches = [
setWatch(Bangle.showLauncher, BTN1, {repeat:1,edge:"falling"})
];
} else if (mode=="touch") {
Bangle.touchHandler = (_,e) => {b();cb(e);};
Bangle.on("touch", Bangle.touchHandler);
} else
throw new Error("Unknown UI mode");
});
E.showScroller = (function(options) {
/* options = {
h = height
c = # of items
scroll = initial scroll position
scrollMin = minimum scroll amount (can be negative)
draw = function(idx, rect)
select = function(idx)
}
returns {
draw = draw all
drawItem(idx) = draw specific item
}
*/
Bangle.setUI(); // remove existing handlers
if (!options) return;
var menuShowing = false;
var R = Bangle.appRect;
var Y = Bangle.appRect.y;
var n = Math.ceil(R.h/options.h);
var menuScrollMin = 0|options.scrollMin;
var menuScrollMax = options.h*options.c - R.h;
if (menuScrollMax<menuScrollMin) menuScrollMax=menuScrollMin;
function idxToY(i) {
return i*options.h + R.y - rScroll;
}
function YtoIdx(y) {
return Math.floor((y + rScroll - R.y)/options.h);
}
var s = {
scroll : E.clip(0|options.scroll,menuScrollMin,menuScrollMax),
draw : () => {
g.reset().clearRect(R.x,R.y,R.x2,R.y2);
g.setClipRect(R.x,R.y,R.x2,R.y2);
var a = YtoIdx(R.y);
var b = Math.min(YtoIdx(R.y2),options.c-1);
for (var i=a;i<=b;i++)
options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h});
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
}, drawItem : i => {
var y = idxToY(i);
g.reset().setClipRect(R.x,y,R.x2,y+options.h);
options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
}};
var rScroll = s.scroll&~1; // rendered menu scroll (we only shift by 2 because of dither)
s.draw(); // draw the full scroller
g.flip(); // force an update now to make this snappier
Bangle.dragHandler = e=>{
var dy = e.dy;
if (s.scroll - dy > menuScrollMax)
dy = s.scroll - menuScrollMax;
if (s.scroll - dy < menuScrollMin)
dy = s.scroll - menuScrollMin;
s.scroll -= dy;
var oldScroll = rScroll;
rScroll = s.scroll &~1;
dy = oldScroll-rScroll;
if (!dy) return;
g.reset().setClipRect(R.x,R.y,R.x2,R.y2);
g.scroll(0,dy);
var d = e.dy;
if (d < 0) {
g.setClipRect(R.x,R.y2-(1-d),R.x2,R.y2);
let i = YtoIdx(R.y2-(1-d));
let y = idxToY(i);
while (y < R.y2) {
options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
i++;
y += options.h;
}
} else { // d>0
g.setClipRect(R.x,R.y,R.x2,R.y+d);
let i = YtoIdx(R.y+d);
let y = idxToY(i);
while (y > R.y-options.h) {
options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
y -= options.h;
i--;
}
}
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
};
Bangle.on('drag',Bangle.dragHandler);
Bangle.touchHandler = (_,e)=>{
if (e.y<R.y-4) return;
var i = YtoIdx(e.y);
if ((menuScrollMin<0 || i>=0) && i<options.c)
options.select(i);
};
Bangle.on("touch", Bangle.touchHandler);
return s;
});
E.showMenu = function(menu) {
const H = 40;
if (menu===undefined) {
g.clearRect(Bangle.appRect);
return Bangle.setUI();
}
var menuIcon = "\0\f\f\x81\0\xFF\xFF\xFF\0\0\0\0\x0F\xFF\xFF\xF0\0\0\0\0\xFF\xFF\xFF";
var options = menu[""]||{};
if (!options.title) options.title="Menu";
var back = menu["< Back"];
var keys = Object.keys(menu).filter(k=>k!="" && k!="< Back");
keys.forEach(k => {
var item = menu[k];
if ("object" != typeof item) return;
if ("boolean" == typeof item.value &&
!item.format)
item.format = v=>"\0"+atob(v?"EhKBAH//v/////////////5//x//j//H+eP+Mf/A//h//z//////////3//g":"EhKBAH//v//8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA///3//g");
});
// Submenu for editing menu options...
function showSubMenu(item, title) {
if ("number"!=typeof item.value)
return console.log("Unhandled item type");
if (item.min!==undefined && item.max!==undefined &&
(item.max-item.min)<20) {
// show scrolling menu of options
E.showScroller({
h : H, c : item.max+1-item.min,
scrollMin : -24, scroll : -24, // title is 24px, rendered at -1
draw : (idx, r) => {
if (idx<0) // TITLE
return g.setFont("12x20").setFontAlign(-1,0).drawString(
menuIcon+" "+title, r.x+12, r.y+H-12);
g.setColor(g.theme.bg2).fillRRect(r.x+4,r.y+2,r.w-8, r.h-4);
var v = idx + item.min;
if (item.format) v=item.format(v);
g.setColor(g.theme.fg).setFont("12x20").setFontAlign(-1,0).drawString(v, r.x+12, r.y+H/2);
g.drawImage(/* 20x20 */atob(idx==item.value?"FBSBAAH4AH/gHgeDgBww8MY/xmf+bH/jz/88//PP/zz/88f+Nn/mY/xjDww4AcHgeAf+AB+A":"FBSBAAH4AH/gHgeDgBwwAMYABmAAbAADwAA8AAPAADwAA8AANgAGYABjAAw4AcHgeAf+AB+A"), r.x+r.w-32, r.y+H/2-10);
},
select : function(idx) {
if (idx<0) return; // TITLE
Bangle.buzz(20);
item.value = item.min + idx;
if (item.onchange) item.onchange(item.value);
scr.scroll = s.scroll; // set scroll to prev position
show(); // redraw original menu
}
});
} else {
// show simple box for scroll up/down
var R = Bangle.appRect;
var v = item.value;
g.reset().clearRect(Bangle.appRect);
g.setFont("12x20").setFontAlign(0,0).drawString(
menuIcon+" "+title, R.x+R.w/2,R.y+12);
function draw() {
var mx = R.x+R.w/2, my = 12+R.y+R.h/2;
g.reset().setColor(g.theme.bg2).fillRRect(R.x+24, R.y+36, R.w-48, R.h-48);
g.setColor(g.theme.fg).setFontVector(30).setFontAlign(0,0).drawString(item.format?item.format(v):v, mx, my);
g.fillPoly([mx,my-45, mx+15,my-30, mx-15,my-30]).fillPoly([mx,my+45, mx+15,my+30, mx-15,my+30]);
}
draw();
Bangle.setUI("updown", dir => {
if (dir) {
v -= (dir||1)*(item.step||1);
if (item.min!==undefined && item.value<item.min) item.value = item.wrap ? item.max : item.min;
if (item.max!==undefined && item.value>item.max) item.value = item.wrap ? item.min : item.max;
draw();
} else {
item.value = v;
if (item.onchange) item.onchange(item.value);
scr.scroll = s.scroll; // set scroll to prev position
show(); // redraw original menu
}
});
}
}
var l = {
draw : ()=>s.draw()
};
var scr = {
h : H, c : keys.length+1/*title*/,
scrollMin : -24, scroll : -24, // title is 24px, rendered at -1
draw : (idx, r) => {
if (idx<0) // TITLE
return g.setFont("12x20").setFontAlign(-1,0).drawString(
menuIcon+" "+options.title, r.x+12, r.y+H-12);
g.setColor(g.theme.bg2).fillRRect(r.x+4,r.y+2,r.w-8, r.h-4);
g.setColor(g.theme.fg).setFont("12x20");
var pad = 24;
var item = menu[keys[idx]];
if ("object" == typeof item) {
var v = item.value;
if (item.format) v=item.format(v);
if (g.stringMetrics(v).width > r.w/2) // bodge for broken wrapString with image
v = g.wrapString(v,r.w/2).join("\n");
print(keys[idx],E.toJS(v));
g.setFontAlign(1,0).drawString(v,r.x+r.w-8,r.y+H/2);
pad += g.stringWidth(v);
} else if ("function" == typeof item) {
g.drawImage(/* 9x18 */atob("CRKBAGA4Hg8DwPB4HgcDg8PB4eHg8HAwAA=="), r.x+r.w-21, r.y+H/2-9);
pad += 16;
}
g.setFontAlign(-1,0).drawString(g.wrapString(keys[idx],r.w-pad).join("\n"), r.x+12, r.y+H/2);
},
select : function(idx) {
if (idx<0) return back&&back(); // title
var item = menu[keys[idx]];
Bangle.buzz(20);
if ("function" == typeof item) item(l);
else if ("object" == typeof item) {
// if a bool, just toggle it
if ("boolean" == typeof item.value) {
item.value=!item.value;
if (item.onchange) item.onchange(item.value);
s.drawItem(idx);
} else showSubMenu(item, keys[idx]);
}
}
};
function show() {
s = E.showScroller(scr);
if (back) {
var touchHandler = (_,e) => {
if (e.y<24 && e.x<48) back();
};
Bangle.on("touch", touchHandler);
WIDGETS = Object.assign({back:{ area:"tl", width:24, draw:e=>g.reset().setColor("#f00").drawImage(atob("GBiBAAAYAAH/gAf/4A//8B//+D///D///H/P/n+H/n8P/n4f/vwAP/wAP34f/n8P/n+H/n/P/j///D///B//+A//8Af/4AH/gAAYAA=="),e.x,e.y)}},WIDGETS);
Bangle.drawWidgets();
Bangle.btnWatches = [
setWatch(function() {
back();
}, BTN1, {edge:"falling"}),
];
Bangle.uiRemove = function() {
Bangle.removeListener("touch", touchHandler);
delete WIDGETS.back;
Bangle.drawWidgets();
};
}
}
show();
return l;
};
var boolean = true;
var number = 50;
var hidV = [false, "kbmedia", "kb", "com", "joy"];
var hidN = ["Off", "Kbrd & Media", "Kbrd", "Kbrd & Mouse" ,"Joystick"];
// First menu
var mainmenu = {
"" : { "title" : "Main Menu" },
"< Back" : function() { print("Back") },
"A menu item" : function() { LED1.toggle(); },
"Submenu" : function() { E.showMenu(submenu); },
"A Boolean" : {
value : boolean,
//format : v => v?"On":"Off",
onchange : v => { boolean=v; }
},
"A Number" : {
value : number,
min:0,max:100,step:10,
onchange : v => { number=v; }
},
"A Big Number" : {
value : 9000,
min:0,max:10000,step:1000,
onchange : v => { }
},
'HID': {
value: 0,
min: 0, max: hidN.length-1,
format: v => hidN[v],
onchange: v => {
}
},
"A very long menu item" : function() { },
"Exit" : function() { E.showMenu(); }, // remove the menu
};
var submenu = {
"" : { "title" : "SubMenu" },
"< Back" : function() { E.showMenu(mainmenu); },
"One" : undefined, // do nothing
"Two" : undefined, // do nothing
};
Bangle.loadWidgets();
Bangle.drawWidgets();
var m = E.showMenu(mainmenu);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment