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

[widclkinfo] Keeps focus when widget_utils.swipeOn() #3680

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

cbkerr
Copy link
Contributor

@cbkerr cbkerr commented Dec 1, 2024

I tried implementing a possible fix to understand this emergent bug when using widclkinfo and widget_utils. How can we make these components not interfere while keeping them fairly modular?

Symptoms

Load widget_utils and widclkinfo.

  1. The Clock Info of widclkinfo stays focused after the widgets autohide, so the selected clock info (options.menuA, options.menuB) changes when swiping the screen (like when using menus).
  2. When the widgets are hidden, tapping in the top left corner focuses the clock info inside widclkinfo.
  3. Swiping up to hide the widget bar also changes the selected Clock Info, and if the hidden widclkinfo is focused, swiping down to show widgets also changes the selected clock info.

Minimal reproduction of bug

With useful debug info printed

// development
WIDGETS = [];
widget_utils = require("widget_utils");

//(() => { // don't load if a clock_info was already loaded
// Load the clock infos
let clockInfoItems = require("clock_info").load();
// Add the
let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, {
  app: "widclkinfo",
  // Add the dimensions we're rendering to here - these are used to detect taps on the clock info area
  x: 0,
  y: 0,
  w: 72,
  h: 24,
  // You can add other information here you want to be passed into 'options' in 'draw'
  // This function draws the info
  draw: (itm, info, options) => {
    // itm: the item containing name/hasRange/etc
    // info: data returned from itm.get() containing text/img/etc
    // options: options passed into addInteractive
    clockInfoInfo = info;
    if (WIDGETS["clkinfo"])
      WIDGETS["clkinfo"].draw(WIDGETS["clkinfo"]);
  }
});
let clockInfoInfo; // when clockInfoMenu.draw is called we set this up

// The actual widget we're displaying
WIDGETS["clkinfo"] = {
  area: "tl",
  width: clockInfoMenu.w,
  draw: function(e) {
    clockInfoMenu.x = e.x;
    clockInfoMenu.y = e.y;
    var o = clockInfoMenu;
    // Clear the background
    g.reset();
    // indicate focus - make background reddish
    //if (clockInfoMenu.focus) g.setBgColor(g.blendColor(g.theme.bg, "#f00", 0.25));
    if (clockInfoMenu.focus) g.setColor("#f00");
    g.clearRect(o.x, o.y, o.x + o.w - 1, o.y + o.h - 1);
    if (clockInfoInfo) {
      var x = o.x;
      if (clockInfoInfo.img) {
        g.drawImage(clockInfoInfo.img, x, o.y); // draw the image
        x += 24;
      }
      var availableWidth = o.x + clockInfoMenu.w - (x + 2);
      g.setFont("6x8:2").setFontAlign(-1, 0);
      if (g.stringWidth(clockInfoInfo.text) > availableWidth)
        g.setFont("6x8:1x2");
      g.drawString(clockInfoInfo.text, x + 2, o.y + 12); // draw the text
    }
  }
};
//})();

Bangle.setUI('clock');
g.clear();
Bangle.loadWidgets();
require("widget_utils").swipeOn();

setInterval(() => {
  g.clear(Bangle.appRect);
  g.setFont("6x8");

  g.drawString("widget_utils.offset: " + widget_utils.offset, 4, 40);
  g.drawString("Bangle.CLKINFO_FOCUS: " + Bangle.CLKINFO_FOCUS, 4, 50);
  g.drawString("clockInfoMenu.y: " + clockInfoMenu.y, 4, 70);
  g.drawString("clockInfoMenu.focus: " + clockInfoMenu.focus, 4, 60);
  g.drawString("menuA: " + clockInfoMenu.menuA, 4, 80);
  g.drawString("menuB: " + clockInfoMenu.menuB, 4, 90);
  //g.drawString(clock_info.clockInfos, 4, 90);
  // g.drawString("cIM.x: " + clockInfoMenu.x, 4, 100);
  //g.drawString("items: " + clockInfoItems, 4, 90)
}, 500);

Expected behavior

When widgets autohide, the Clock Info in widclkinfo blurs itself just like tapping outside of its region.
Tapping only selects the Clock Info in widclkinfo if it is visible. (when widget_utils.offset == 0)

To test these fixes

  1. Upload to storage the slightly tweaked clock_info to clock_info
  2. Run this from web IDE:
Code to upload to RAM from Web IDE that writes custom widget_utils to storage
const clock_info = require("clock_info"); // only slight modification to expose force_blur

require("Storage").write("mywidut", `
exports.offset = 0;
exports.hide = function() {
  exports.cleanup();
  if (!global.WIDGETS) return;
  g.reset(); // reset colors
  for (var w of global.WIDGETS) {
    if (w._draw) return; // already hidden
    w._draw = w.draw;
    w.draw = () => {};
    w._area = w.area;
    w.area = "";
    if (w.x!=undefined) g.clearRect(w.x,w.y,w.x+w.width-1,w.y+23);
  }
};

/// Show any hidden widgets
exports.show = function() {
  exports.cleanup();
  if (!global.WIDGETS) return;
  for (var w of global.WIDGETS) {
    if (!w._draw) return; // not hidden
    w.draw = w._draw;
    w.area = w._area;
    delete w._draw;
    delete w._area;
    w.draw(w);
  }
};

/// Remove anything not needed if the overlay was removed
exports.cleanupOverlay = function() {
  exports.offset = -24;
  Bangle.setLCDOverlay(undefined, {id: "widget_utils"});
  delete exports.autohide;
  delete Bangle.appRect;
  if (exports.animInterval) {
    clearInterval(exports.animInterval);
    delete exports.animInterval;
  }
  if (exports.hideTimeout) {
    clearTimeout(exports.hideTimeout);
    delete exports.hideTimeout;
  }
};

/// Remove any intervals/handlers/etc that we might have added. Does NOT re-show widgets that were hidden
exports.cleanup = function() {
  exports.cleanupOverlay();
  delete exports.offset;
  if (exports.swipeHandler) {
    Bangle.removeListener("swipe", exports.swipeHandler);
    delete exports.swipeHandler;
  }
  if (exports.origDraw) {
    Bangle.drawWidgets = exports.origDraw;
    delete exports.origDraw;
  }
};

/** Put widgets offscreen, and allow them to be swiped
back onscreen with a downwards swipe. Use .show to undo.
First parameter controls automatic hiding time, 0 equals not hiding at all.
Default value is 2000ms until hiding.
Bangle.js 2 only at the moment. On Bangle.js 1 widgets will be hidden permanently.

Note: On Bangle.js 1 is is possible to draw widgets in an offscreen area of the LCD
and use Bangle.setLCDOffset. However we can't detect a downward swipe so how to
actually make this work needs some thought.
*/
exports.swipeOn = function(autohide) {
  if (process.env.HWVERSION!==2) return exports.hide();
  exports.cleanup();
  if (!global.WIDGETS) return;
  exports.autohide=autohide===undefined?2000:autohide;
  /* TODO: maybe when widgets are offscreen we don't even
  store them in an offscreen buffer? */

  // force app rect to be fullscreen
  Bangle.appRect = { x: 0, y: 0, w: g.getWidth(), h: g.getHeight(), x2: g.getWidth()-1, y2: g.getHeight()-1 };
  // setup offscreen graphics for widgets
  let og = Graphics.createArrayBuffer(g.getWidth(),26,16,{msb:true});
  og.theme = g.theme;
  og._reset = og.reset;
  og.reset = function() {
    return this._reset().setColor(g.theme.fg).setBgColor(g.theme.bg);
  };
  og.reset().clearRect(0,0,og.getWidth(),23).fillRect(0,24,og.getWidth(),25);
  let _g = g;
  exports.offset = -24; // where on the screen are we? -24=hidden, 0=full visible

  function queueDraw() {
    const o = exports.offset;
    Bangle.appRect.y = o+24;
    Bangle.appRect.h = 1 + Bangle.appRect.y2 - Bangle.appRect.y;
    if (o>-24) {
      Bangle.setLCDOverlay(og, 0, o, {
        id:"widget_utils",
        remove:()=>{
          require("widget_utils").cleanupOverlay();
        }
      });
    } else {
      Bangle.setLCDOverlay(undefined, {id: "widget_utils"});
    }
  }

  for (var w of global.WIDGETS) if (!w._draw) { // already hidden
    w._draw = w.draw;
    w.draw = function() {
      g=og;
      this._draw(this);
      g=_g;
      if (exports.offset>-24) queueDraw();
    };
    w._area = w.area;
    if (w.area.startsWith("b"))
      w.area = "t"+w.area.substr(1);
  }

  exports.origDraw = Bangle.drawWidgets;
  Bangle.drawWidgets = ()=>{
    g=og;
    exports.origDraw();
    g=_g;
  };

  function anim(dir, callback) {
    if (exports.animInterval) clearInterval(exports.animInterval);
    exports.animInterval = setInterval(function() {
      exports.offset += dir;
      let stop = false;
      if (dir>0 && exports.offset>=0) { // fully down
        stop = true;
        exports.offset = 0;
        exports.emit("shown");
      } else if (dir<0 && exports.offset<-23) { // fully up
        stop = true;
        exports.offset = -24;
        exports.emit("hidden");
      }
      if (stop) {
        clearInterval(exports.animInterval);
        delete exports.animInterval;
        if (callback) callback();
      }
      queueDraw();
    }, 50);
  }
  // On swipe down, animate to show widgets
  exports.swipeHandler = function(lr,ud) {
    if (exports.hideTimeout) {
      clearTimeout(exports.hideTimeout);
      delete exports.hideTimeout;
    }
    let cb;
    if (exports.autohide > 0) cb = function() {
      exports.hideTimeout = setTimeout(function() {
        anim(-4);
      }, exports.autohide);
    };
    if (ud>0 && exports.offset<0) anim(4, cb);
    if (ud<0 && exports.offset>-24) anim(-4);
  };
  Bangle.on("swipe", exports.swipeHandler);
  Bangle.drawWidgets();
};
`);

const widget_utils = require("mywidut"); // only 8 chars allowed in name

// development
WIDGETS = [];


// code from widclkinfo for debugging

//(() => { // don't load if a clock_info was already loaded
  // Load the clock infos
  let clockInfoItems = clock_info.load();

  let wuo = widget_utils.offset;

  let clockInfoMenu = clock_info.addInteractive(clockInfoItems, {
    app: "widclkinfo",
    // Add the dimensions we're rendering to here - these are used to detect taps on the clock info area
    x: 0,
    y: 0, // set offset to initial offset
    w: 72,
    h: 24,
    // You can add other information here you want to be passed into 'options' in 'draw'
    // This function draws the info
    draw: (itm, info, options) => {
      // itm: the item containing name/hasRange/etc
      // info: data returned from itm.get() containing text/img/etc
      // options: options passed into addInteractive
      clockInfoInfo = info;
      //wuo = 0 | widget_utils.offset;
      clockInfoMenu.y = options.y + wuo;
      if (WIDGETS["clkinfo"]) {
        WIDGETS["clkinfo"].draw(WIDGETS["clkinfo"]);
        console.log("Clock Info was updated, thus drawing widget.");
      }
    }
  });

  let clockInfoInfo; // when clockInfoMenu.draw is called we set this up

  // The actual widget we're displaying
  WIDGETS["clkinfo"] = {
    area: "tl",
    width: clockInfoMenu.w,
    draw: function(e) {
      clockInfoMenu.x = e.x;
      wuo = 0 | widget_utils.offset;
      clockInfoMenu.y = e.y + wuo;
      var o = clockInfoMenu;
      // Clear the background
      g.reset();
      // indicate focus
      if (clockInfoMenu.focus) g.setColor("#f00");
      g.clearRect(o.x, o.y, o.x + o.w - 1, o.y + o.h - 1);

      if (clockInfoInfo) {
        var x = o.x;
        if (clockInfoInfo.img) {
          g.drawImage(clockInfoInfo.img, x, o.y); // draw the image
          x += 24;
        }
        var availableWidth = o.x + clockInfoMenu.w - (x + 2);
        g.setFont("6x8:2").setFontAlign(-1, 0);
        if (g.stringWidth(clockInfoInfo.text) > availableWidth)
          g.setFont("6x8:1x2");
        g.drawString(clockInfoInfo.text, x + 2, o.y + 12); // draw the text
      }
    }
  };

  widget_utils.on("hidden", () => {
    console.log("hidden");
    clockInfoMenu.y = -24;
    clockInfoMenu.force_blur(); // needs to be here so it doesn't stay the focused color
    if (clockInfoMenu.focus) {
      //clockInfoMenu.force_blur();
      console.log("Forced blur bc hidden");
    }
  });

  widget_utils.on("shown", () => {
    clockInfoMenu.y = 0;
    console.log("shown");
    if (WIDGETS["clkinfo"]) {
      WIDGETS["clkinfo"].draw(WIDGETS["clkinfo"]);
    }
  });

  // for debug:
//})();



Bangle.setUI('clock');
g.clear();
Bangle.loadWidgets();
widget_utils.swipeOn();


// debug info:
setInterval(() => {
  g.clear(Bangle.appRect);
  g.setFont("6x8");

  g.drawString("widget_utils.offset: " + widget_utils.offset, 4, 40);
  g.drawString("Bangle.CLKINFO_FOCUS: " + Bangle.CLKINFO_FOCUS, 4, 50);
  g.drawString("clockInfoMenu.y: " + clockInfoMenu.y, 4, 70);
  g.drawString("clockInfoMenu.focus: " + clockInfoMenu.focus, 4, 60);
  g.drawString("menuA: " + clockInfoMenu.menuA, 4, 80);
  g.drawString("menuB: " + clockInfoMenu.menuB, 4, 90);
  //g.drawString(clock_info.clockInfos, 4, 90);
  // g.drawString("cIM.x: " + clockInfoMenu.x, 4, 100);
  //g.drawString("items: " + clockInfoItems, 4, 90)
}, 500);

// these work:
//global.clockInfoMenu
//clockInfoMenu.focus

// get list of all added infos:
//clock_info.clockInfos

// Added a function clockInfoMenu.force_blur()

Corwin Kerr added 3 commits December 1, 2024 15:13
...so an external action like hiding widgets with widclkinfo can
unfocus the clock info.
This always decrements Bangle.CLKINFO_FOCUS,
so in the future we should only call force_blur if we know the Clock
Info is focused. We could provide a different function ensure_blur.
This can help widgets, such as widclkinfo, know when they are on
screen.
TODO: disable these changes when widget_utils are not being used.

Use new (prototype) widget_utils events to blur and set offscreen the
clock info when the widgets are hidden. This prevents activating or
further interacting with the widclkinfo.
@bobrippling
Copy link
Collaborator

Nice find, and I like your fix too.

How can we make these components not interfere while keeping them fairly modular?

I like your event approach - one way to avoid having a dependency between the modules is to copy what's done elsewhere and emit (and listen to) the events on Bangle, so:

-exports.emit("shown");
+Bangle.emit("widgets-shown");

For the issue with "refcounting" blur, were there some problems with this initial approach you had?

-clockInfoMenu.force_blur(); // needs to be here so it doesn't stay the focused color
 if (clockInfoMenu.focus) {
-  //clockInfoMenu.force_blur();
+  clockInfoMenu.blur();
   console.log("Forced blur bc hidden");
 }

@cbkerr
Copy link
Contributor Author

cbkerr commented Dec 1, 2024

For the issue with "refcounting" blur, were there some problems with this initial approach you had?

I was having a problem with the widclkinfo sliding back in still in the redish "focused" color. Actually, in the current state with always calling clockInfoMenu.force_blur(), it still slides back in redish. Even when I add extra draws on widgets-hidden, it still comes down red. Even with a frequently updating clock_info like HRM.

Not sure if we should make events for the animation steps.

Comment on lines 61 to 62
console.log("hidden");
Copy link
Contributor Author

@cbkerr cbkerr Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't figure out how to get the offscreen buffer to re-draw in the normal color before showing, but blurring the clock info before the widgets leave the screen works and also makes sense for the UI.

Suggested change
widget_utils.on("hidden", () => {
console.log("hidden");
Bangle.on("widgets-start-hide", () => {
console.log("starting to hide");
if (WIDGETS["clkinfo"]) {
WIDGETS["clkinfo"].draw(WIDGETS["clkinfo"]);
}

I was trying things like this in the web IDE:

/*
  Bangle.on("widgets-start-hide", () => {
    console.log("starting to hide");
    if (WIDGETS["clkinfo"]) {
      WIDGETS["clkinfo"].draw({x:0,y:0});
    }
    clockInfoMenu.y = -24;
    if (clockInfoMenu.focus) {
      clockInfoMenu.force_blur();
      console.log("Forced blur bc hidden");
      console.log("focus is now: " + clockInfoMenu.focus);
    }
  });
  */

  Bangle.on("widgets-hidden", () => {
    console.log("hidden");
    if (WIDGETS["clkinfo"]) {
      WIDGETS["clkinfo"].draw({x:0,y:-24});
    }
    clockInfoMenu.y = -24;
    //clockInfoMenu.force_blur(); // needs to be here so it doesn't stay the focused color
    if (clockInfoMenu.focus) {
      clockInfoMenu.force_blur();
      console.log("Forced blur bc hidden");
      console.log("focus is now: " + clockInfoMenu.focus);
    }
  });

  Bangle.on("widgets-shown", () => {
    clockInfoMenu.y = 0;
    console.log("shown");
    if (WIDGETS["clkinfo"]) {
      WIDGETS["clkinfo"].draw(WIDGETS["clkinfo"]);
    }
  });

  Bangle.on("widgets-start-show", () => {
    console.log("showing");
    if (WIDGETS["clkinfo"]) {
      WIDGETS["clkinfo"].draw({x:0, y:0});
    }
  });

Copy link
Collaborator

@bobrippling bobrippling left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, yes this looks good!

@@ -153,12 +156,19 @@ exports.swipeOn = function(autohide) {
let cb;
if (exports.autohide > 0) cb = function() {
exports.hideTimeout = setTimeout(function() {
Bangle.emit("widgets-start-hide");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it work if we moved the widgets-start-show/hide into the start of the anim() function?

modules/widget_utils.js Outdated Show resolved Hide resolved
apps/widclkinfo/widget.js Outdated Show resolved Hide resolved
Comment on lines 64 to 65
clockInfoMenu.force_blur();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we've checked focus above, I think it's safe to just have the blur method and avoid the cost of an extra jsvar, wdyt?

Copy link
Contributor Author

@cbkerr cbkerr Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think clockInfoMenu.blur() is exposed. I chose the name force_blur to avoid internal name confusion inside clock_info. Also, clock_info exposes focus as a boolean but internally uses focus as a function, so I wasn't sure how to keep names consistent

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see your problem - I think that's ok to have the internal focus and the exposed one, we could rename the internal one so we present an API that doesn't give away internal details if you like?

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

Successfully merging this pull request may close these issues.

2 participants