Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature request] Hideable widget bar #1466

Closed
gfwilliams opened this issue Feb 17, 2022 · 30 comments
Closed

[Feature request] Hideable widget bar #1466

gfwilliams opened this issue Feb 17, 2022 · 30 comments

Comments

@gfwilliams
Copy link
Member

Just a thought here, but I see a lot of clock apps now having an option to hide the widgets.

Maybe we could have a library (in modules) that adjusts the widget draw call such that it renders widgets to an offscreen buffer. Swipe-down could then make the widget bar slide out of the top of the screen, and after a few secs it could pop back up.

Having it as a library would mean we could avoid a bunch of code duplication too.

@andiohn
Copy link

andiohn commented Apr 11, 2022

This is an excellent idea, I used a watchface without a widget zone was disappointed that they weren't still running. Great idea!

@rigrig
Copy link
Contributor

rigrig commented May 23, 2022

I wonder if this needs to be a library, or an installable boot code app that "just works" for all clocks?

@gfwilliams
Copy link
Member Author

It's an idea - it's just getting clocks to work with it. Many of them will just assume there's always a widget bar and leave 24px spare all the time.

Also, I guess we want it to only work for clocks, not all apps?

@Rarder44
Copy link
Contributor

Rarder44 commented May 23, 2022

Also, I guess we want it to only work for clocks, not all apps?

I think it is also useful to extend it to apps.

the downside is that the "Swipe-down" command cannot be used (unless overwritten) and I don't know if it could interfere with scrolling through the menus ....

in any case, I think it is good to have a "standard" that manages the visibility of the widgetbar (personally I don't use it and the apps I use / have written don't display the widgetbar in order to have more screen space)

@rigrig
Copy link
Contributor

rigrig commented May 23, 2022

getting clocks to work with it. Many of them will just assume there's always a widget bar and leave 24px spare all the time.

Sure, but just having that empty space for a "cleaner" look would be a nice start, and then we can just convert clocks/apps to properly use appRect when it comes up.

I guess we want it to only work for clocks

the downside is that the "Swipe-down" command cannot be used

Maybe we could split it into two separate options?

  • Clock widgets: Show/Hide/Swipe
  • App widgets: Show/Hide/Swipe -> If you choose "Swipe" and it interferes with an app, that's your own fault... (But probably we can make it so that you just won't get the widget bar at all in 90% of problem cases.)

I don't use it and the apps I use / have written don't display the widgetbar

That should be possible by just not installing any widgets: if code is doing something it should go into a boot.js file.
(Not saying it works like that now, just how I'd like things to be)

For non-clock apps, I feel trading the widgetbar for screen space is fine, but for clocks it would be nice if they looked at appRect, and at least can work for users that do want widgets.

@rigrig
Copy link
Contributor

rigrig commented May 23, 2022

Whoops, I was thinking of the Bangle.js 1, totally forgot that setLCDOffset() won't work on the Bangle.js 2.

I've got some working code for Bangle.js 2, but it has no way to tell apps to repaint the top area after we show and hide the widget bar :-(

// widhide.boot.js

Bangle.loadWidgets = (o => () => {
  o();
  if (Bangle.CLOCK) Bangle.appRect = {
    // apps can use the complete screen now
    x: 0, y: 0,
    w: g.getWidth(), h: g.getHeight(),
    x2: g.getWidth()-1, y2: g.getHeight()-1,
  };
})(Bangle.loadWidgets);

Bangle.drawWidgets = (o => () => {
  if (!Bangle.CLOCK) return o();
  // draw widgets to buffer instead of screen
  if (!Bangle.widgetBuffer) Bangle.widgetBuffer = Graphics.createArrayBuffer(
    g.getWidth(),
    24,
    4,
    {msb: true}
  );
  let _g = g;
  g = Bangle.widgetBuffer;
  o();
  g = _g;
  // draw the buffer at the offset position (0 = completely offscreen, 24 = fully visible)
  g.drawImage(Bangle.widgetBuffer, 0, (Bangle.widgetBuffer.o|0)-24);
})(Bangle.drawWidgets);

Bangle.on("swipe", (_, d) => {
  if (!Bangle.CLOCK || !d) return; // not a clock, or horizontal swipe
  if (!Bangle.widgetBuffer) Bangle.drawWidgets();
  let w = Bangle.widgetBuffer; // we also save 'o'ffset, 'm'ovement and a 't'imeout in the buffer object
  if (w.m*d>0) return; // already moving/moved in this direction
  w.m = 2*d; // movement: same direction as swipe
  if (w.t) clearTimeout(w.t);
  delete w.t;
  function anim() {
    w.o = Math.min(24, w.m+(w.o|0)); // calculate new offset (0 = 24 pixels above screen = hidden)
    g.setClipRect(0, 0, g.getWidth(), 24) // make sure we can paint to the widget area
      .drawImage(w, 0, w.o-24);
    if (w.m>0) { // moving down: sliding into view
      if (w.o<24) {
        w.t=setTimeout(anim, 10);
      } else w.t=setTimeout(() => {
        // auto-hide after 10 seconds
        if (w.o>=24 && w.m>=0) {
          w.m = -2;
          anim();
        }
      }, 10e3);
    } else { // moving up: sliding out of view
      g.clearRect(0, w.o, g.getWidth()-1, w.o-w.m);
      if (w.o>0) w.t=setTimeout(anim, 10);
    }
    g.setClipRect(0, w.o, g.getWidth()-1, g.getHeight()-1); // prevent app from drawing over widgets
  }
  anim();
});

@gfwilliams
Copy link
Member Author

gfwilliams commented May 24, 2022

Yes, this is the problem... You can use g.asImage to get a copy of what's on the screen at that moment on Bangle.js 2 so it can be written back, but it does end up being pretty big (784 vars). Then there's what happens if an app tries to draw to the screen.

Something like:

// save
oldg = g;
g = Graphics.createArrayBuffer(176,176,4,{msb:true})
g.drawImage(oldg.asImage());
// restore
oldg.drawImage({width:176,height:176,bpp:4,buffer:g.buffer, palette:new Uint16Array([...])});
g = oldg;

would work (ish) but it's pretty inefficient with memory, and any updates to the clock/app in the meantime would end up undithered.

Also looks like your code doesn't override the widget draw, so widgets like battery that auto-update would still break things.

I think this is one reason to have a library rather than boot code - the app can give the widget hider a callback for redraw, and could also avoid redraws itself when widgets were showing, so things would go a lot more smoothly.

Otherwise I think to do this nicely there'd need to be some kind of firmware mod

@rigrig
Copy link
Contributor

rigrig commented May 24, 2022

there's what happens if an app tries to draw to the screen

your code doesn't override the widget draw, so widgets like battery that auto-update would still break things.

Yeah, I only thought about that later. Overriding draw seems doable, but its starting to get "a bit" hacky...

If we get the widget side of things to work, I figured for app support there are two options:

  • add a library, so apps can redraw themselves
  • don't adjust appRect: simply keep the top 24px empty while hiding widgets. You get a cleaner screen, but not more app-space

I'm thinking we might do both: add a library, but if apps don't use it just leave the widgetbar blank?

@gfwilliams
Copy link
Member Author

add a library, but if apps don't use it just leave the widgetbar blank?

Could do, but personally it seems like if you have the space you should use it.

I'm thinking maybe I should just add another 24px (or more) to the top of the offscreen buffer and allow setting the Bangle.js screen offset like we do on Bangle.js 1? In the long run that's probably easier.

@rigrig
Copy link
Contributor

rigrig commented May 24, 2022

add another 24px (or more) to the top of the offscreen buffer

That's probably the way to go, I'm pretty much convinced now this needs firmware support.
I've no idea how much work that is and how badly we'll miss that RAM though, so I guess it's up to you to decide if we should go ahead with it just for hiding widgets...

(To be honest I'm quite happy with always-visible widgets, this just looked like a interesting feature to code.)

@gfwilliams
Copy link
Member Author

I just took a look at adding fw support, but there's something a bit odd going on and I can't figure it out at the moment.

This is actually the code that I'd done before for this:

Bangle.loadWidgets();
Bangle.drawWidgets();

Bangle.widgetAreaGfx = undefined;

function hide() {
  if (Bangle.widgetAreaGfx) {
    g.drawImage(Bangle.widgetAreaGfx,0,0);
    Bangle.widgetAreaGfx = undefined;
  }
  Object.keys(WIDGETS).forEach(wi=>{
    var w = WIDGETS[wi];
    var hide = true;
    if (w.area=="tl") w.area="hl";
    else if (w.area=="tr") w.area="hr";
    else hide = false;
    if (hide) {
      w.olddraw = w.draw;
      w.draw = ()=>{};
    }
  });
}

function show() {
  Bangle.widgetAreaGfx = Graphics.createArrayBuffer(g.getWidth(),24,16);
  Bangle.widgetAreaGfx.drawImage(g.asImage()); // a bit inefficient!
  Object.keys(WIDGETS).forEach(wi=>{
    var w = WIDGETS[wi];
    var hide = true;
    if (w.area=="hl") w.area="tl";
    else if (w.area=="hr") w.area="tr";
    else hide = false;
    if (hide) {
      w.draw = w.olddraw;
      delete w.olddraw;
    }
  });
  Bangle.drawWidgets();
}

hide();
g.clear(1).drawLine(0,0,200,200).drawString("HELLO WORLD");

So you can hide and show widgets and the background is still kept around. BUT if the app tries to draw while the widgets are shown it'll overwrite them.

@gfwilliams
Copy link
Member Author

Just a note on firmware support - while we could do a simple scroll I'm still in two minds about whether there should be to ability to do something a bit more flexible (like allowing notifications over the middle of the screen) as folks have requested that for alarms/notifications/etc - but I think that could be a minefield

@sir-indy
Copy link
Contributor

I like the sound of adding the extra 24px to the offscreen buffer, it gives us other ways to show the widgets then too, like shake to show widgets.
It kind of reminds me of how the Pebble used to do some notifications, popping up on the bottom of the screen and pushing the rest of the display up, and dropping back down after a short while.
In fact, if we had an extra 24px at the top of the screen for widgets, and an extra space at the bottom for notifications, that could be quite a neat interface: drag up or down to show extra things in either direction.

@sir-indy
Copy link
Contributor

I've had a go at converting the code from @gfwilliams and @rigrig above to use the new Bangle.setLCDOverlay. It works quite well, but only for widgets which call Bangle.drawWidgets() to update their display. Any that directly call the widget .draw() function still draws on the main display. Any better ideas other than altering all widgets to alway use Bangle.drawWidgets()?

It also needs clocks that correctly use Bangle.appRect to set their view size.

// widhide.boot.js

Bangle.widgetsShown = false;

Bangle.loadWidgets = (o => () => {
//  console.log('loadWidgets');
  o();
  if (Bangle.CLOCK) Bangle.appRect = {
    // apps can use the complete screen now
    x: 0, y: 0,
    w: g.getWidth(), h: g.getHeight(),
    x2: g.getWidth()-1, y2: g.getHeight()-1,
  };
})(Bangle.loadWidgets);

Bangle.drawWidgets = (o => () => {
  if (!Bangle.CLOCK) return o();
//  console.log('drawWidgets');
  // draw widgets to buffer instead of screen
  if (!Bangle.widgetBuffer) Bangle.widgetBuffer = Graphics.createArrayBuffer(
    g.getWidth(), 24, 8, {msb: true}
  );
  let _g = g;
  g = Bangle.widgetBuffer;
  o();
  g = _g;
  if (Bangle.widgetsShown) Bangle.setLCDOverlay(Bangle.widgetBuffer, 0, 0);
})(Bangle.drawWidgets);

Bangle.on("swipe", (_, d) => {
  if (!Bangle.CLOCK || !d) return; // not a clock, or horizontal swipe
  if (!Bangle.widgetBuffer) Bangle.drawWidgets();

  if (Bangle.widgetBuffer.m*d>0) return; // already moving/moved in this direction
  Bangle.widgetBuffer.m = d; // movement: same direction as swipe

  if (d>0) {
//    console.log('Show widgets');
    Bangle.widgetsShown = true;
  } else {
//    console.log('Hide widgets');
    Bangle.widgetsShown = false;
    Bangle.setLCDOverlay();
  }
  Object.keys(WIDGETS).forEach(wi=>{
    var w = WIDGETS[wi];
    if (Bangle.widgetsShown) {
//      console.log('Enable widget draw', wi);
      if (w.olddraw) {
        w.draw = w.olddraw;
        delete w.olddraw;
      }
    } else {
//      console.log('Disable widget draw', wi);
      w.olddraw = w.draw;
      w.draw = ()=>{};
    }
  });
  if (Bangle.widgetsShown) Bangle.drawWidgets();
});

@gfwilliams
Copy link
Member Author

Any better ideas other than altering all widgets to alway use Bangle.drawWidgets()?

:o yes, don't do that!

Instead of overwriting g in drawWidgets, why not rewrite each draw handler to do it:

Object.keys(WIDGETS).forEach(wi=>{
    var w = WIDGETS[wi];
    w.olddraw = w.draw;
    w.draw = function(){
     let _g = g;
     g = Bangle.widgetBuffer;
     this.olddraw(this);
     g = _g;
     if (Bangle.widgetsShown) Bangle.setLCDOverlay(Bangle.widgetBuffer, 0, 0);
    };
  });

@sir-indy
Copy link
Contributor

sir-indy commented Sep 23, 2022

'Why not' is always a good question! I didn't know how is the answer, but now you've shown me, here's a new version that seems to work better (I didn't use this when I tried, I used w and it didn't work). Looks to always be drawing on the overlay, but I'll run it for a while and check. I've changed drawWidgets to always give us a new blank buffer, reusing the old one didn't clear out widgets that moved:

// widhide.boot.js

Bangle.widgetsShown = false;

Bangle.loadWidgets = (o => () => {
//  console.log('loadWidgets');
  o();
  if (Bangle.CLOCK) {
    Bangle.appRect = {
      // apps can use the complete screen now
      x: 0, y: 0,
      w: g.getWidth(), h: g.getHeight(),
      x2: g.getWidth()-1, y2: g.getHeight()-1,
    };
    Object.keys(WIDGETS).forEach(wi=>{
      var w = WIDGETS[wi];
      w.olddraw = w.draw;
      w.draw = function(){
        let _g = g;
        g = Bangle.widgetBuffer;
        this.olddraw(this);
        g = _g;
        if (Bangle.widgetsShown) Bangle.setLCDOverlay(Bangle.widgetBuffer, 0, 0);
      };
    });
  }
})(Bangle.loadWidgets);

Bangle.drawWidgets = (o => () => {
  if (!Bangle.CLOCK) return o();
//  console.log('drawWidgets');
  Bangle.widgetBuffer = Graphics.createArrayBuffer(g.getWidth(), 24, 8, {msb: true});
  o();
  if (Bangle.widgetsShown) Bangle.setLCDOverlay(Bangle.widgetBuffer, 0, 0);
})(Bangle.drawWidgets);

Bangle.on("swipe", (_, d) => {
  if (!Bangle.CLOCK || !d) return; // not a clock, or horizontal swipe
  if (!Bangle.widgetBuffer) Bangle.drawWidgets();

  if (d>0 & !Bangle.widgetsShown) {
//    console.log('Show widgets');
    Bangle.widgetsShown = true;
    Bangle.drawWidgets();
  } else if (d<0 & Bangle.widgetsShown){
//    console.log('Hide widgets');
    Bangle.widgetsShown = false;
    Bangle.setLCDOverlay();
  }
});

@gfwilliams
Copy link
Member Author

'Why not' is always a good question!

Just because you end up redrawing all widgets all the time, when maybe you only wanted to change one part of one widget (eg to make it flash on and off).

I've changed drawWidgets to always give us a new blank buffer,

Ahh - yes, the original drawWidgets just does a clearRect: https://github.com/espruino/Espruino/blob/master/libs/js/banglejs/Bangle_drawWidgets_Q3.js

Personally I'd allocate the Graphics at the start and just do widgetBuffer.clear(1) in drawWidgets... But this looks great.

Only thing it's missing is slowly moving setLCDOverlay downwards so it does a nice animation ;)

@sir-indy
Copy link
Contributor

the original drawWidgets just does a clearRect

I hadn't spotted that. Turns out it still does, so I'll need to stop that.

Only thing it's missing is slowly moving setLCDOverlay downwards so it does a nice animation ;)

Yeah, I've had a go, it's harder than it sounds! I had a look at it here: http://forum.espruino.com/conversations/379983
The gist of it is that you can set an image partially off the screen, with a negative position: g.drawImage(img, 0, -10); But that doesn't seem to work with Bangle.setLCDOverlay(img, 0, -10);, I just got nothing appearing on the screen until it hit 0 or positive.

@gfwilliams
Copy link
Member Author

I just got nothing appearing on the screen until it hit 0 or positive.

Ahh, that's a shame. That could well be a firmware issue then :)

@rigrig
Copy link
Contributor

rigrig commented Sep 23, 2022

As a workaround, you could add another buffer...
(Or if you want a challenge: re-use parts of the buffer using DataView...)

@sir-indy
Copy link
Contributor

I've tweaked the code, now here: https://github.com/sir-indy/BangleApps/tree/widget-hide/apps/widhide
It's not on my apploader at the moment, I've got that on a different branch to play with messages.

That could well be a firmware issue then :)

I think you're right, and I think it's here: https://github.com/espruino/Espruino/blob/5a99fbdafedf5b8a551ac53f2370c76f63be9523/libs/graphics/lcd_memlcd.c#L308-L309
I believe (though could be wrong) that's it's converting the int position that we give it to an unsigned number, so it's not drawing in the correct place. Does that sound likely?

@gfwilliams
Copy link
Member Author

I think you're right, and I think it's here

That sounds like it - thanks! I've just pushed some changes (and tested) and with the latest cutting edge builds you should now be able to display the overlay with negative offsets

@sir-indy
Copy link
Contributor

Ah ha! With firmware 2v15.29, drawing off screen works! Thank you Gordon!

Updated my repo above with animation code. It works! And looks good!

@TheGLander
Copy link
Contributor

TheGLander commented Oct 24, 2022

I've tweaked the code, now here: https://github.com/sir-indy/BangleApps/tree/widget-hide/apps/widhide It's not on my apploader at the moment, I've got that on a different branch to play with messages.

That could well be a firmware issue then :)

I think you're right, and I think it's here: https://github.com/espruino/Espruino/blob/5a99fbdafedf5b8a551ac53f2370c76f63be9523/libs/graphics/lcd_memlcd.c#L308-L309 I believe (though could be wrong) that's it's converting the int position that we give it to an unsigned number, so it's not drawing in the correct place. Does that sound likely?

The animation is pretty cool, I think it would look better if it weren't linear, so here's a modification of this which uses a quadratic.

// widhide.boot.js

Bangle.widgetBuffer = Graphics.createArrayBuffer(g.getWidth(), 24, 8, {msb: true});
Bangle.widgetBuffer.shown = false;
Bangle.widgetBuffer.offset = -24;

Bangle.loadWidgets = (o => () => {
//  console.log('loadWidgets');
  o();
  if (Bangle.CLOCK) {
    Bangle.appRect = {
      // apps can use the complete screen now
      x: 0, y: 0,
      w: g.getWidth(), h: g.getHeight(),
      x2: g.getWidth()-1, y2: g.getHeight()-1,
    };
    Object.keys(WIDGETS).forEach(wi=>{
      var w = WIDGETS[wi];
      w.olddraw = w.draw;
      w.draw = function(){
        if (Bangle.widgetBuffer.shown) {
          let _g = g;
          g = Bangle.widgetBuffer;
          this.olddraw(this);
          g = _g;
          Bangle.setLCDOverlay(Bangle.widgetBuffer, 0, Bangle.widgetBuffer.offset);
        }
      };
    });
  }
})(Bangle.loadWidgets);

Bangle.drawWidgets = (o => () => {
  if (!Bangle.CLOCK) return o();
//  console.log('drawWidgets');
  if (Bangle.widgetBuffer.shown) {
    Bangle.widgetBuffer.clear(1);
    let _g = g;
    g = Bangle.widgetBuffer;
    o();
    g = _g;
    Bangle.setLCDOverlay(Bangle.widgetBuffer, 0, Bangle.widgetBuffer.offset);
  }
})(Bangle.drawWidgets);

Bangle.on("swipe", (_, d) => {
  if (!Bangle.CLOCK || !d) return; // not a clock, or horizontal swipe
  if (!Bangle.widgetBuffer) Bangle.drawWidgets();

  if (d>0 & !Bangle.widgetBuffer.shown) { // show widgets
    if (typeof WIDGETS != 'object') Bangle.loadWidgets();
    Bangle.widgetBuffer.shown = true;
    Bangle.widgetBuffer.offset = -24;
    Bangle.drawWidgets();
  } else if (d<0 & Bangle.widgetBuffer.shown){ // hide widgets
    Bangle.widgetBuffer.shown = false;
    Bangle.widgetBuffer.offset = 0;
  } else {
    return;
  }
  function anim() {
    Bangle.widgetBuffer.offset += d;
    Bangle.widgetBuffer.rOffset = Math.pow(Bangle.widgetBuffer.offset / Math.sqrt(24) + Math.sqrt(24), 2) - 24;
    Bangle.setLCDOverlay(Bangle.widgetBuffer, 0, Bangle.widgetBuffer.rOffset);
    if (Bangle.widgetBuffer.offset >= 0 || Bangle.widgetBuffer.offset <= -24) {
      clearTimeout(animTimeout);
      return;
    }
    animTimeout = setTimeout(anim, 1);
  }
  var animTimeout = setTimeout(anim, 1);
});

This runs a formula on the offset, as if offset is x, but I think maybe a formula thing could be added to offset to maybe make it faster and neater?

@jt-nti
Copy link
Contributor

jt-nti commented Dec 30, 2022

I've just been experimenting with making use of require("widget_utils").swipeOn(); but I'm hitting problems and wondered if they were just due to this feature being a work in progress, or if I should open an issue.

I initially thought it was my code but it looks like the MacWatch2 app does the same thing: when I first try to swipe down the widgets I get a blank bar (no widgets displayed at all), but now (not sure if it's just a timing thing, or because the screen locked/unlocked, or something else) I do see widgets when swiping but both the top and bottom widgets appear at the top with the top widgets drawn on top of the bottom widgets! I'm going to give up on the swipe feature for now but it seems like it would be really useful when it's working.

@gfwilliams
Copy link
Member Author

Interesting - thanks! I was pretty sure this feature works ok - maybe you could file an issue...

But please ensure you list what's installed... It works here with the default widgets so maybe it's an issue with the bottom widget bar?

It could even be because of a broken widget....

@jt-nti
Copy link
Contributor

jt-nti commented Jan 8, 2023

Ah, thanks, good point, it was with my widget so that could definitely be broken. I'll investigate more...

@jt-nti
Copy link
Contributor

jt-nti commented Jan 8, 2023

I tried with a different bottom widget (the bottom digital clock widget) and didn't see the top and bottom widgets shown on top of each other, which is good, so I guess I'm doing something wrong in the medical alert widget
https://github.com/espruino/BangleApps/tree/master/apps/widmeda

The bottom clock widget didn't show up at all though- is that expected? I like the hideable widget bar idea a lot but I'm also keen to keep the medical alert widget visible on the clock screen. I was wondering if it would be possible to use a system setting to control widget hiding (top/bottom widgets or both on clock/app screens or both) rather than leaving it to apps to code explicitly. Is that technically possible?

I've opened #2474 for the odd blank bar issue - it's just a bit unexpected rather than a big problem.

@gfwilliams
Copy link
Member Author

Ok, thanks - lets move discussion to #2474 then...

@bobrippling
Copy link
Collaborator

Closing as completed - see #2474 / widget_utils

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants