Browse Source

build: convert touch-bar to typescript (#24511)

Samuel Attard 4 years ago
parent
commit
4c3da359fc

+ 4 - 0
docs/api/touch-bar-button.md

@@ -41,6 +41,10 @@ the button in the touch bar.
 A `NativeImage` representing the button's current icon. Changing this value immediately updates the button
 in the touch bar.
 
+#### `touchBarButton.iconPosition`
+
+A `String` - Can be `left`, `right` or `overlay`.  Defaults to `overlay`.
+
 #### `touchBarButton.enabled`
 
 A `Boolean` representing whether the button is in an enabled state.

+ 4 - 0
docs/api/touch-bar-segmented-control.md

@@ -49,3 +49,7 @@ updates the control in the touch bar. Updating deep properties inside this array
 
 An `Integer` representing the currently selected segment. Changing this value immediately updates the control
 in the touch bar. User interaction with the touch bar will update this value automatically.
+
+#### `touchBarSegmentedControl.mode`
+
+A `String` representing the current selection mode of the control.  Can be `single`, `multiple` or `buttons`.

+ 8 - 0
docs/api/touch-bar-spacer.md

@@ -11,3 +11,11 @@ Process: [Main](../tutorial/application-architecture.md#main-and-renderer-proces
     * `small` - Small space between items. Maps to `NSTouchBarItemIdentifierFixedSpaceSmall`. This is the default.
     * `large` - Large space between items. Maps to `NSTouchBarItemIdentifierFixedSpaceLarge`.
     * `flexible` - Take up all available space. Maps to `NSTouchBarItemIdentifierFlexibleSpace`.
+
+### Instance Properties
+
+The following properties are available on instances of `TouchBarSpacer`:
+
+#### `touchBarSpacer.size`
+
+A `String` representing the size of the spacer.  Can be `small`, `large` or `flexible`.

+ 1 - 1
filenames.auto.gni

@@ -217,7 +217,7 @@ auto_filenames = {
     "lib/browser/api/screen.ts",
     "lib/browser/api/session.ts",
     "lib/browser/api/system-preferences.ts",
-    "lib/browser/api/touch-bar.js",
+    "lib/browser/api/touch-bar.ts",
     "lib/browser/api/tray.ts",
     "lib/browser/api/view.ts",
     "lib/browser/api/views/image-view.ts",

+ 0 - 365
lib/browser/api/touch-bar.js

@@ -1,365 +0,0 @@
-'use strict';
-
-const { EventEmitter } = require('events');
-
-let nextItemID = 1;
-
-class TouchBar extends EventEmitter {
-  // Bind a touch bar to a window
-  static _setOnWindow (touchBar, window) {
-    if (window._touchBar != null) {
-      window._touchBar._removeFromWindow(window);
-    }
-
-    if (touchBar == null) {
-      window._setTouchBarItems([]);
-      return;
-    }
-
-    if (Array.isArray(touchBar)) {
-      touchBar = new TouchBar(touchBar);
-    }
-    touchBar._addToWindow(window);
-  }
-
-  constructor (options) {
-    super();
-
-    if (options == null) {
-      throw new Error('Must specify options object as first argument');
-    }
-
-    let { items, escapeItem } = options;
-
-    if (!Array.isArray(items)) {
-      items = [];
-    }
-
-    this.changeListener = (item) => {
-      this.emit('change', item.id, item.type);
-    };
-
-    this.windowListeners = {};
-    this.items = {};
-    this.ordereredItems = [];
-    this.escapeItem = escapeItem;
-
-    const registerItem = (item) => {
-      this.items[item.id] = item;
-      item.on('change', this.changeListener);
-      if (item.child instanceof TouchBar) {
-        item.child.ordereredItems.forEach(registerItem);
-      }
-    };
-
-    let hasOtherItemsProxy = false;
-    const idSet = new Set();
-    items.forEach((item) => {
-      if (!(item instanceof TouchBarItem)) {
-        throw new Error('Each item must be an instance of TouchBarItem');
-      }
-
-      if (item.type === 'other_items_proxy') {
-        if (!hasOtherItemsProxy) {
-          hasOtherItemsProxy = true;
-        } else {
-          throw new Error('Must only have one OtherItemsProxy per TouchBar');
-        }
-      }
-
-      if (!idSet.has(item.id)) {
-        idSet.add(item.id);
-      } else {
-        throw new Error('Cannot add a single instance of TouchBarItem multiple times in a TouchBar');
-      }
-    });
-
-    // register in separate loop after all items are validated
-    for (const item of items) {
-      this.ordereredItems.push(item);
-      registerItem(item);
-    }
-  }
-
-  set escapeItem (item) {
-    if (item != null && !(item instanceof TouchBarItem)) {
-      throw new Error('Escape item must be an instance of TouchBarItem');
-    }
-    if (this.escapeItem != null) {
-      this.escapeItem.removeListener('change', this.changeListener);
-    }
-    this._escapeItem = item;
-    if (this.escapeItem != null) {
-      this.escapeItem.on('change', this.changeListener);
-    }
-    this.emit('escape-item-change', item);
-  }
-
-  get escapeItem () {
-    return this._escapeItem;
-  }
-
-  _addToWindow (window) {
-    const { id } = window;
-
-    // Already added to window
-    if (Object.prototype.hasOwnProperty.call(this.windowListeners, id)) return;
-
-    window._touchBar = this;
-
-    const changeListener = (itemID) => {
-      window._refreshTouchBarItem(itemID);
-    };
-    this.on('change', changeListener);
-
-    const escapeItemListener = (item) => {
-      window._setEscapeTouchBarItem(item != null ? item : {});
-    };
-    this.on('escape-item-change', escapeItemListener);
-
-    const interactionListener = (event, itemID, details) => {
-      let item = this.items[itemID];
-      if (item == null && this.escapeItem != null && this.escapeItem.id === itemID) {
-        item = this.escapeItem;
-      }
-      if (item != null && item.onInteraction != null) {
-        item.onInteraction(details);
-      }
-    };
-    window.on('-touch-bar-interaction', interactionListener);
-
-    const removeListeners = () => {
-      this.removeListener('change', changeListener);
-      this.removeListener('escape-item-change', escapeItemListener);
-      window.removeListener('-touch-bar-interaction', interactionListener);
-      window.removeListener('closed', removeListeners);
-      window._touchBar = null;
-      delete this.windowListeners[id];
-      const unregisterItems = (items) => {
-        for (const item of items) {
-          item.removeListener('change', this.changeListener);
-          if (item.child instanceof TouchBar) {
-            unregisterItems(item.child.ordereredItems);
-          }
-        }
-      };
-      unregisterItems(this.ordereredItems);
-      if (this.escapeItem) {
-        this.escapeItem.removeListener('change', this.changeListener);
-      }
-    };
-    window.once('closed', removeListeners);
-    this.windowListeners[id] = removeListeners;
-
-    window._setTouchBarItems(this.ordereredItems);
-    escapeItemListener(this.escapeItem);
-  }
-
-  _removeFromWindow (window) {
-    const removeListeners = this.windowListeners[window.id];
-    if (removeListeners != null) removeListeners();
-  }
-}
-
-class TouchBarItem extends EventEmitter {
-  constructor () {
-    super();
-    this._addImmutableProperty('id', `${nextItemID++}`);
-    this._parents = [];
-  }
-
-  _addImmutableProperty (name, value) {
-    Object.defineProperty(this, name, {
-      get: function () {
-        return value;
-      },
-      set: function () {
-        throw new Error(`Cannot override property ${name}`);
-      },
-      enumerable: true,
-      configurable: false
-    });
-  }
-
-  _addLiveProperty (name, initialValue) {
-    const privateName = `_${name}`;
-    this[privateName] = initialValue;
-    Object.defineProperty(this, name, {
-      get: function () {
-        return this[privateName];
-      },
-      set: function (value) {
-        this[privateName] = value;
-        this.emit('change', this);
-      },
-      enumerable: true
-    });
-  }
-
-  _addParent (item) {
-    const existing = this._parents.some(test => test.id === item.id);
-    if (!existing) {
-      this._parents.push({
-        id: item.id,
-        type: item.type
-      });
-    }
-  }
-}
-
-TouchBar.TouchBarButton = class TouchBarButton extends TouchBarItem {
-  constructor (config) {
-    super();
-    if (config == null) config = {};
-    this._addImmutableProperty('type', 'button');
-    this._addLiveProperty('label', config.label);
-    this._addLiveProperty('accessibilityLabel', config.accessibilityLabel);
-    this._addLiveProperty('backgroundColor', config.backgroundColor);
-    this._addLiveProperty('icon', config.icon);
-    this._addLiveProperty('iconPosition', config.iconPosition);
-    this._addLiveProperty('enabled', typeof config.enabled !== 'boolean' ? true : config.enabled);
-    if (typeof config.click === 'function') {
-      this._addImmutableProperty('onInteraction', () => {
-        config.click();
-      });
-    }
-  }
-};
-
-TouchBar.TouchBarColorPicker = class TouchBarColorPicker extends TouchBarItem {
-  constructor (config) {
-    super();
-    if (config == null) config = {};
-    this._addImmutableProperty('type', 'colorpicker');
-    this._addLiveProperty('availableColors', config.availableColors);
-    this._addLiveProperty('selectedColor', config.selectedColor);
-
-    if (typeof config.change === 'function') {
-      this._addImmutableProperty('onInteraction', (details) => {
-        this._selectedColor = details.color;
-        config.change(details.color);
-      });
-    }
-  }
-};
-
-TouchBar.TouchBarGroup = class TouchBarGroup extends TouchBarItem {
-  constructor (config) {
-    super();
-    if (config == null) config = {};
-    this._addImmutableProperty('type', 'group');
-    const defaultChild = (config.items instanceof TouchBar) ? config.items : new TouchBar(config.items);
-    this._addLiveProperty('child', defaultChild);
-    this.child.ordereredItems.forEach((item) => item._addParent(this));
-  }
-};
-
-TouchBar.TouchBarLabel = class TouchBarLabel extends TouchBarItem {
-  constructor (config) {
-    super();
-    if (config == null) config = {};
-    this._addImmutableProperty('type', 'label');
-    this._addLiveProperty('label', config.label);
-    this._addLiveProperty('accessibilityLabel', config.accessibilityLabel);
-    this._addLiveProperty('textColor', config.textColor);
-  }
-};
-
-TouchBar.TouchBarPopover = class TouchBarPopover extends TouchBarItem {
-  constructor (config) {
-    super();
-    if (config == null) config = {};
-    this._addImmutableProperty('type', 'popover');
-    this._addLiveProperty('label', config.label);
-    this._addLiveProperty('icon', config.icon);
-    this._addLiveProperty('showCloseButton', config.showCloseButton);
-    const defaultChild = (config.items instanceof TouchBar) ? config.items : new TouchBar(config.items);
-    this._addLiveProperty('child', defaultChild);
-    this.child.ordereredItems.forEach((item) => item._addParent(this));
-  }
-};
-
-TouchBar.TouchBarSlider = class TouchBarSlider extends TouchBarItem {
-  constructor (config) {
-    super();
-    if (config == null) config = {};
-    this._addImmutableProperty('type', 'slider');
-    this._addLiveProperty('label', config.label);
-    this._addLiveProperty('minValue', config.minValue);
-    this._addLiveProperty('maxValue', config.maxValue);
-    this._addLiveProperty('value', config.value);
-
-    if (typeof config.change === 'function') {
-      this._addImmutableProperty('onInteraction', (details) => {
-        this._value = details.value;
-        config.change(details.value);
-      });
-    }
-  }
-};
-
-TouchBar.TouchBarSpacer = class TouchBarSpacer extends TouchBarItem {
-  constructor (config) {
-    super();
-    if (config == null) config = {};
-    this._addImmutableProperty('type', 'spacer');
-    this._addImmutableProperty('size', config.size);
-  }
-};
-
-TouchBar.TouchBarSegmentedControl = class TouchBarSegmentedControl extends TouchBarItem {
-  constructor (config) {
-    super();
-    if (config == null) config = {};
-    this._addImmutableProperty('type', 'segmented_control');
-    this._addLiveProperty('segmentStyle', config.segmentStyle);
-    this._addLiveProperty('segments', config.segments || []);
-    this._addLiveProperty('selectedIndex', config.selectedIndex);
-    this._addLiveProperty('mode', config.mode);
-
-    if (typeof config.change === 'function') {
-      this._addImmutableProperty('onInteraction', (details) => {
-        this._selectedIndex = details.selectedIndex;
-        config.change(details.selectedIndex, details.isSelected);
-      });
-    }
-  }
-};
-
-TouchBar.TouchBarScrubber = class TouchBarScrubber extends TouchBarItem {
-  constructor (config) {
-    super();
-    if (config == null) config = {};
-    let { select, highlight } = config;
-    this._addImmutableProperty('type', 'scrubber');
-    this._addLiveProperty('items', config.items);
-    this._addLiveProperty('selectedStyle', config.selectedStyle || null);
-    this._addLiveProperty('overlayStyle', config.overlayStyle || null);
-    this._addLiveProperty('showArrowButtons', config.showArrowButtons || false);
-    this._addLiveProperty('mode', config.mode || 'free');
-
-    const cont = typeof config.continuous === 'undefined' ? true : config.continuous;
-    this._addLiveProperty('continuous', cont);
-
-    if (typeof select === 'function' || typeof highlight === 'function') {
-      if (select == null) select = () => {};
-      if (highlight == null) highlight = () => {};
-      this._addImmutableProperty('onInteraction', (details) => {
-        if (details.type === 'select' && typeof select === 'function') {
-          select(details.selectedIndex);
-        } else if (details.type === 'highlight' && typeof highlight === 'function') {
-          highlight(details.highlightedIndex);
-        }
-      });
-    }
-  }
-};
-
-TouchBar.TouchBarOtherItemsProxy = class TouchBarOtherItemsProxy extends TouchBarItem {
-  constructor (config) {
-    super();
-    this._addImmutableProperty('type', 'other_items_proxy');
-  }
-};
-
-module.exports = TouchBar;

+ 459 - 0
lib/browser/api/touch-bar.ts

@@ -0,0 +1,459 @@
+import { EventEmitter } from 'events';
+
+let nextItemID = 1;
+
+const hiddenProperties = Symbol('hidden touch bar props');
+
+const extendConstructHook = (target: any, hook: Function) => {
+  const existingHook = target._hook;
+  target._hook = function () {
+    hook.call(this);
+    if (existingHook) existingHook.call(this);
+  };
+};
+
+const ImmutableProperty = <T extends TouchBarItem<any>>(def: (config: T extends TouchBarItem<infer C> ? C : never, setInternalProp: <K extends keyof T>(k: K, v: T[K]) => void) => any) => (target: T, propertyKey: keyof T) => {
+  extendConstructHook(target as any, function (this: T) {
+    (this as any)[hiddenProperties][propertyKey] = def((this as any)._config, (k, v) => {
+      (this as any)[hiddenProperties][k] = v;
+    });
+  });
+  Object.defineProperty(target, propertyKey, {
+    get: function () {
+      return (this as any)[hiddenProperties][propertyKey];
+    },
+    set: function () {
+      throw new Error(`Cannot override property ${name}`);
+    },
+    enumerable: true,
+    configurable: false
+  });
+};
+
+const LiveProperty = <T extends TouchBarItem<any>>(def: (config: T extends TouchBarItem<infer C> ? C : never) => any, onMutate?: (self: T, newValue: any) => void) => (target: T, propertyKey: keyof T) => {
+  extendConstructHook(target as any, function (this: T) {
+    (this as any)[hiddenProperties][propertyKey] = def((this as any)._config);
+    if (onMutate) onMutate((this as any), (this as any)[hiddenProperties][propertyKey]);
+  });
+  Object.defineProperty(target, propertyKey, {
+    get: function () {
+      return this[hiddenProperties][propertyKey];
+    },
+    set: function (value) {
+      if (onMutate) onMutate((this as any), value);
+      this[hiddenProperties][propertyKey] = value;
+      this.emit('change', this);
+    },
+    enumerable: true
+  });
+};
+
+abstract class TouchBarItem<ConfigType> extends EventEmitter {
+  @ImmutableProperty(() => `${nextItemID++}`) id!: string;
+  abstract type: string;
+  abstract onInteraction: Function | null;
+  child?: TouchBar;
+
+  private _parents: { id: string; type: string }[] = [];
+  private _config!: ConfigType;
+
+  constructor (config: ConfigType) {
+    super();
+    this._config = this._config || config || {} as any;
+    (this as any)[hiddenProperties] = {};
+    const hook = (this as any)._hook;
+    if (hook) hook.call(this);
+    delete (this as any)._hook;
+  }
+
+  public _addParent (item: TouchBarItem<any>) {
+    const existing = this._parents.some(test => test.id === item.id);
+    if (!existing) {
+      this._parents.push({
+        id: item.id,
+        type: item.type
+      });
+    }
+  }
+
+  public _removeParent (item: TouchBarItem<any>) {
+    this._parents = this._parents.filter(test => test.id !== item.id);
+  }
+}
+
+class TouchBarButton extends TouchBarItem<Electron.TouchBarButtonConstructorOptions> implements Electron.TouchBarButton {
+  @ImmutableProperty(() => 'button')
+  type!: string;
+
+  @LiveProperty<TouchBarButton>(config => config.label)
+  label!: string;
+
+  @LiveProperty<TouchBarButton>(config => config.accessibilityLabel)
+  accessibilityLabel!: string;
+
+  @LiveProperty<TouchBarButton>(config => config.backgroundColor)
+  backgroundColor!: string;
+
+  @LiveProperty<TouchBarButton>(config => config.icon)
+  icon!: Electron.NativeImage;
+
+  @LiveProperty<TouchBarButton>(config => config.iconPosition)
+  iconPosition!: Electron.TouchBarButton['iconPosition'];
+
+  @LiveProperty<TouchBarButton>(config => typeof config.enabled !== 'boolean' ? true : config.enabled)
+  enabled!: boolean;
+
+  @ImmutableProperty<TouchBarButton>(({ click: onClick }) => typeof onClick === 'function' ? () => onClick() : null)
+  onInteraction!: Function | null;
+}
+
+class TouchBarColorPicker extends TouchBarItem<Electron.TouchBarColorPickerConstructorOptions> implements Electron.TouchBarColorPicker {
+  @ImmutableProperty(() => 'colorpicker')
+  type!: string;
+
+  @LiveProperty<TouchBarColorPicker>(config => config.availableColors)
+  availableColors!: string[];
+
+  @LiveProperty<TouchBarColorPicker>(config => config.selectedColor)
+  selectedColor!: string;
+
+  @ImmutableProperty<TouchBarColorPicker>(({ change: onChange }, setInternalProp) => typeof onChange === 'function' ? (details: { color: string }) => {
+    setInternalProp('selectedColor', details.color);
+    onChange(details.color);
+  } : null)
+  onInteraction!: Function | null;
+}
+
+class TouchBarGroup extends TouchBarItem<Electron.TouchBarGroupConstructorOptions> implements Electron.TouchBarGroup {
+  @ImmutableProperty(() => 'group')
+  type!: string;
+
+  @LiveProperty<TouchBarGroup>(config => config.items instanceof TouchBar ? config.items : new TouchBar(config.items), (self, newChild: TouchBar) => {
+    if (self.child) {
+      for (const item of self.child.orderedItems) {
+        item._removeParent(self);
+      }
+    }
+    for (const item of newChild.orderedItems) {
+      item._addParent(item);
+    }
+  })
+  child!: TouchBar;
+
+  onInteraction = null;
+}
+
+class TouchBarLabel extends TouchBarItem<Electron.TouchBarLabelConstructorOptions> implements Electron.TouchBarLabel {
+  @ImmutableProperty(() => 'label')
+  type!: string;
+
+  @LiveProperty<TouchBarLabel>(config => config.label)
+  label!: string;
+
+  @LiveProperty<TouchBarLabel>(config => config.accessibilityLabel)
+  accessibilityLabel!: string;
+
+  @LiveProperty<TouchBarLabel>(config => config.textColor)
+  textColor!: string;
+
+  onInteraction = null;
+}
+
+class TouchBarPopover extends TouchBarItem<Electron.TouchBarPopoverConstructorOptions> implements Electron.TouchBarPopover {
+  @ImmutableProperty(() => 'popover')
+  type!: string;
+
+  @LiveProperty<TouchBarPopover>(config => config.label)
+  label!: string;
+
+  @LiveProperty<TouchBarPopover>(config => config.icon)
+  icon!: Electron.NativeImage;
+
+  @LiveProperty<TouchBarPopover>(config => config.showCloseButton)
+  showCloseButton!: boolean;
+
+  @LiveProperty<TouchBarPopover>(config => config.items instanceof TouchBar ? config.items : new TouchBar(config.items), (self, newChild: TouchBar) => {
+    if (self.child) {
+      for (const item of self.child.orderedItems) {
+        item._removeParent(self);
+      }
+    }
+    for (const item of newChild.orderedItems) {
+      item._addParent(item);
+    }
+  })
+  child!: TouchBar;
+
+  onInteraction = null;
+}
+
+class TouchBarSlider extends TouchBarItem<Electron.TouchBarSliderConstructorOptions> implements Electron.TouchBarSlider {
+  @ImmutableProperty(() => 'slider')
+  type!: string;
+
+  @LiveProperty<TouchBarSlider>(config => config.label)
+  label!: string;
+
+  @LiveProperty<TouchBarSlider>(config => config.minValue)
+  minValue!: number;
+
+  @LiveProperty<TouchBarSlider>(config => config.maxValue)
+  maxValue!: number;
+
+  @LiveProperty<TouchBarSlider>(config => config.value)
+  value!: number;
+
+  @ImmutableProperty<TouchBarSlider>(({ change: onChange }, setInternalProp) => typeof onChange === 'function' ? (details: { value: number }) => {
+    setInternalProp('value', details.value);
+    onChange(details.value);
+  } : null)
+  onInteraction!: Function | null;
+}
+
+class TouchBarSpacer extends TouchBarItem<Electron.TouchBarSpacerConstructorOptions> implements Electron.TouchBarSpacer {
+  @ImmutableProperty(() => 'spacer')
+  type!: string;
+
+  @ImmutableProperty<TouchBarSpacer>(config => config.size)
+  size!: Electron.TouchBarSpacer['size'];
+
+  onInteraction = null;
+}
+
+class TouchBarSegmentedControl extends TouchBarItem<Electron.TouchBarSegmentedControlConstructorOptions> implements Electron.TouchBarSegmentedControl {
+  @ImmutableProperty(() => 'segmented_control')
+  type!: string;
+
+  @LiveProperty<TouchBarSegmentedControl>(config => config.segmentStyle)
+  segmentStyle!: Electron.TouchBarSegmentedControl['segmentStyle'];
+
+  @LiveProperty<TouchBarSegmentedControl>(config => config.segments || [])
+  segments!: Electron.SegmentedControlSegment[];
+
+  @LiveProperty<TouchBarSegmentedControl>(config => config.selectedIndex)
+  selectedIndex!: number;
+
+  @LiveProperty<TouchBarSegmentedControl>(config => config.mode)
+  mode!: Electron.TouchBarSegmentedControl['mode'];
+
+  @ImmutableProperty<TouchBarSegmentedControl>(({ change: onChange }, setInternalProp) => typeof onChange === 'function' ? (details: { selectedIndex: number, isSelected: boolean }) => {
+    setInternalProp('selectedIndex', details.selectedIndex);
+    onChange(details.selectedIndex, details.isSelected);
+  } : null)
+  onInteraction!: Function | null;
+}
+
+class TouchBarScrubber extends TouchBarItem<Electron.TouchBarScrubberConstructorOptions> implements Electron.TouchBarScrubber {
+  @ImmutableProperty(() => 'scrubber')
+  type!: string;
+
+  @LiveProperty<TouchBarScrubber>(config => config.items)
+  items!: Electron.ScrubberItem[];
+
+  @LiveProperty<TouchBarScrubber>(config => config.selectedStyle || null)
+  selectedStyle!: Electron.TouchBarScrubber['selectedStyle'];
+
+  @LiveProperty<TouchBarScrubber>(config => config.overlayStyle || null)
+  overlayStyle!: Electron.TouchBarScrubber['overlayStyle'];
+
+  @LiveProperty<TouchBarScrubber>(config => config.showArrowButtons || false)
+  showArrowButtons!: boolean;
+
+  @LiveProperty<TouchBarScrubber>(config => config.mode || 'free')
+  mode!: Electron.TouchBarScrubber['mode'];
+
+  @LiveProperty<TouchBarScrubber>(config => typeof config.continuous === 'undefined' ? true : config.continuous)
+  continuous!: boolean;
+
+  @ImmutableProperty<TouchBarScrubber>(({ select: onSelect, highlight: onHighlight }) => typeof onSelect === 'function' || typeof onHighlight === 'function' ? (details: { type: 'select'; selectedIndex: number } | { type: 'highlight'; highlightedIndex: number }) => {
+    if (details.type === 'select') {
+      if (onSelect) onSelect(details.selectedIndex);
+    } else {
+      if (onHighlight) onHighlight(details.highlightedIndex);
+    }
+  } : null)
+  onInteraction!: Function | null;
+}
+
+class TouchBarOtherItemsProxy extends TouchBarItem<null> implements Electron.TouchBarOtherItemsProxy {
+  @ImmutableProperty(() => 'other_items_proxy') type!: string;
+  onInteraction = null;
+}
+
+const escapeItemSymbol = Symbol('escape item');
+
+class TouchBar extends EventEmitter implements Electron.TouchBar {
+  // Bind a touch bar to a window
+  static _setOnWindow (touchBar: TouchBar | Electron.TouchBarConstructorOptions['items'], window: Electron.BrowserWindow) {
+    if (window._touchBar != null) {
+      window._touchBar._removeFromWindow(window);
+    }
+
+    if (!touchBar) {
+      window._setTouchBarItems([]);
+      return;
+    }
+
+    if (Array.isArray(touchBar)) {
+      touchBar = new TouchBar({ items: touchBar });
+    }
+    touchBar._addToWindow(window);
+  }
+
+  private windowListeners: Record<number, Function> = {};
+  private items: Record<string, TouchBarItem<any>> = {};
+  orderedItems: TouchBarItem<any>[] = [];
+
+  constructor (options: Electron.TouchBarConstructorOptions) {
+    super();
+
+    if (options == null) {
+      throw new Error('Must specify options object as first argument');
+    }
+
+    let { items, escapeItem } = options;
+
+    if (!Array.isArray(items)) {
+      items = [];
+    }
+
+    this.windowListeners = {};
+    this.items = {};
+    this.escapeItem = (escapeItem as any) || null;
+
+    const registerItem = (item: TouchBarItem<any>) => {
+      this.items[item.id] = item;
+      item.on('change', this.changeListener);
+      if (item.child instanceof TouchBar) {
+        item.child.orderedItems.forEach(registerItem);
+      }
+    };
+
+    let hasOtherItemsProxy = false;
+    const idSet = new Set();
+    items.forEach((item) => {
+      if (!(item instanceof TouchBarItem)) {
+        throw new Error('Each item must be an instance of TouchBarItem');
+      }
+
+      if (item.type === 'other_items_proxy') {
+        if (!hasOtherItemsProxy) {
+          hasOtherItemsProxy = true;
+        } else {
+          throw new Error('Must only have one OtherItemsProxy per TouchBar');
+        }
+      }
+
+      if (!idSet.has(item.id)) {
+        idSet.add(item.id);
+      } else {
+        throw new Error('Cannot add a single instance of TouchBarItem multiple times in a TouchBar');
+      }
+    });
+
+    // register in separate loop after all items are validated
+    for (const item of (items as TouchBarItem<any>[])) {
+      this.orderedItems.push(item);
+      registerItem(item);
+    }
+  }
+
+  private changeListener = (item: TouchBarItem<any>) => {
+    this.emit('change', item.id, item.type);
+  };
+
+  private [escapeItemSymbol]: TouchBarItem<unknown> | null = null;
+
+  set escapeItem (item: TouchBarItem<unknown> | null) {
+    if (item != null && !(item instanceof TouchBarItem)) {
+      throw new Error('Escape item must be an instance of TouchBarItem');
+    }
+    const escapeItem = this.escapeItem;
+    if (escapeItem) {
+      escapeItem.removeListener('change', this.changeListener);
+    }
+    this[escapeItemSymbol] = item;
+    if (this.escapeItem != null) {
+      this.escapeItem.on('change', this.changeListener);
+    }
+    this.emit('escape-item-change', item);
+  }
+
+  get escapeItem (): TouchBarItem<unknown> | null {
+    return this[escapeItemSymbol];
+  }
+
+  _addToWindow (window: Electron.BrowserWindow) {
+    const { id } = window;
+
+    // Already added to window
+    if (Object.prototype.hasOwnProperty.call(this.windowListeners, id)) return;
+
+    window._touchBar = this;
+
+    const changeListener = (itemID: string) => {
+      window._refreshTouchBarItem(itemID);
+    };
+    this.on('change', changeListener);
+
+    const escapeItemListener = (item: Electron.TouchBarItemType | null) => {
+      window._setEscapeTouchBarItem(item != null ? item : {});
+    };
+    this.on('escape-item-change', escapeItemListener);
+
+    const interactionListener = (_: any, itemID: string, details: any) => {
+      let item = this.items[itemID];
+      if (item == null && this.escapeItem != null && this.escapeItem.id === itemID) {
+        item = this.escapeItem;
+      }
+      if (item != null && item.onInteraction != null) {
+        item.onInteraction(details);
+      }
+    };
+    window.on('-touch-bar-interaction', interactionListener);
+
+    const removeListeners = () => {
+      this.removeListener('change', changeListener);
+      this.removeListener('escape-item-change', escapeItemListener);
+      window.removeListener('-touch-bar-interaction', interactionListener);
+      window.removeListener('closed', removeListeners);
+      window._touchBar = null;
+      delete this.windowListeners[id];
+      const unregisterItems = (items: TouchBarItem<any>[]) => {
+        for (const item of items) {
+          item.removeListener('change', this.changeListener);
+          if (item.child instanceof TouchBar) {
+            unregisterItems(item.child.orderedItems);
+          }
+        }
+      };
+      unregisterItems(this.orderedItems);
+      if (this.escapeItem) {
+        this.escapeItem.removeListener('change', this.changeListener);
+      }
+    };
+    window.once('closed', removeListeners);
+    this.windowListeners[id] = removeListeners;
+
+    window._setTouchBarItems(this.orderedItems);
+    escapeItemListener(this.escapeItem);
+  }
+
+  _removeFromWindow (window: Electron.BrowserWindow) {
+    const removeListeners = this.windowListeners[window.id];
+    if (removeListeners != null) removeListeners();
+  }
+
+  static TouchBarButton = TouchBarButton;
+  static TouchBarColorPicker = TouchBarColorPicker;
+  static TouchBarGroup = TouchBarGroup;
+  static TouchBarLabel = TouchBarLabel;
+  static TouchBarPopover = TouchBarPopover;
+  static TouchBarSlider = TouchBarSlider;
+  static TouchBarSpacer = TouchBarSpacer;
+  static TouchBarSegmentedControl = TouchBarSegmentedControl;
+  static TouchBarScrubber = TouchBarScrubber;
+  static TouchBarOtherItemsProxy = TouchBarOtherItemsProxy;
+}
+
+export default TouchBar;

+ 15 - 0
typings/internal-electron.d.ts

@@ -19,6 +19,17 @@ declare namespace Electron {
     setAppPath(path: string | null): void;
   }
 
+  type TouchBarItemType = NonNullable<Electron.TouchBarConstructorOptions['items']>[0];
+
+  interface BrowserWindow {
+    _touchBar: Electron.TouchBar | null;
+    _setTouchBarItems: (items: TouchBarItemType[]) => void;
+    _setEscapeTouchBarItem: (item: TouchBarItemType | {}) => void;
+    _refreshTouchBarItem: (itemID: string) => void;
+    on(event: '-touch-bar-interaction', listener: (event: Event, itemID: string, details: any) => void): this;
+    removeListener(event: '-touch-bar-interaction', listener: (event: Event, itemID: string, details: any) => void): this;
+  }
+
   interface ContextBridge {
     internalContextBridge: {
       contextIsolationEnabled: boolean;
@@ -29,6 +40,10 @@ declare namespace Electron {
     }
   }
 
+  interface TouchBar {
+    _removeFromWindow: (win: BrowserWindow) => void;
+  }
+
   interface WebContents {
     _getURL(): string;
     _loadURL(url: string, options: Electron.LoadURLOptions): void;