DEV Community

David Rickard
David Rickard

Posted on

Dynamic generation of task bar overlay icons in Electron

Electron exposes the ability to show an overlay icon on the taskbar. It's incredibly flexible: allowing you to show any image you want there. But in my case, I just wanted a circle with a number in it to represent the number of unread items.

Task bar overlay example

Pre-generating a bunch of images to bundle with the app didn't seem very appealing, so I settled on a dynamic generation approach:

1) Create an in-memory <canvas> element in the renderer code.
2) Draw the circle and the number in the center of it.
3) Call canvas.toDataURL() and pass this string to the main process via IPC.
4) Create a NativeImage from the data URI.
5) Call BrowserWindow.setOverlayIcon and pass in the NativeImage.

We do the generation in the render process because it has access to Canvas APIs for free.

Here's the code to generate the Data URI:

export function renderTaskBarOverlay(count: number): string {
  if (count > 99) {
    count = 99;
  }

  const canvas: HTMLCanvasElement = document.createElement('canvas');
  canvas.width = 32;
  canvas.height = 32;
  const ctx = canvas.getContext('2d');
  if (!ctx) {
    throw new Error('Could not get context');
  }

  const textY = count > 9 ? 24 : 25;
  const fontSize = count > 9 ? 20 : 24;

  ctx.beginPath();
  ctx.arc(16, 16, 16, 0, 2 * Math.PI, false);
  ctx.fillStyle = '#007A5D';
  ctx.fill();

  ctx.font = `${fontSize}px sans-serif`;
  ctx.textAlign = 'center';
  ctx.fillStyle = 'white';
  ctx.fillText(count.toString(), 16, textY);

  return canvas.toDataURL(); // Defaults to PNG
}
Enter fullscreen mode Exit fullscreen mode

The Electron docs say that this is a 16x16 icon, but I think that must be device-independent pixels they are using because 32x32 looked a lot better on my 4k monitor.

Once it gets to the main process, you can handle it:

export interface TaskBarOverlay {
  imageDataUri: string;
  description: string;
}

ipcMain.on('setOverlay', (event: Event, overlay: TaskBarOverlay | null) => {
  if (overlay) {
    const image = nativeImage.createFromDataURL(overlay.imageDataUri);
    mainWindow.setOverlayIcon(image, overlay.description);
  } else {
    mainWindow.setOverlayIcon(null, '');
  }
});

mainWindow.on('closed', () => {
  ipcMain.removeHandler('setOverlay');
});
Enter fullscreen mode Exit fullscreen mode

And that's it. I was a bit surprised how little code was needed for this.

Top comments (0)