text: //////////////////////////////////////////////////////////////////////////////////////////
  //        ___            _     ___                                                      //
  //        |   |   \/    | ) |  |           This software may be modified and distri-    //
  //    O-  |-  |   |  -  |   |  |-  -O      buted under the terms of the MIT license.    //
  //        |   |_  |     |   |  |_          See the LICENSE file for details.            //
  //                                                                                      //
  //////////////////////////////////////////////////////////////////////////////////////////
  
  'use strict';
  
  const Cairo                                    = imports.cairo;
  const {Clutter, Cogl, Gio, GObject, GLib, Gtk} = imports.gi;
  
  const Me    = imports.misc.extensionUtils.getCurrentExtension();
  const utils = Me.imports.src.common.utils;
  
  //////////////////////////////////////////////////////////////////////////////////////////
  // This class is instantiated once by the Menu and is responsible for drawing the       //
  // wedges behind the currently active menu. The wedge beneath the mouse pointer is      //
  // highlighted. Signals such as "child-hovered-event" or "parent-selected-event" are    //
  // fired once the user clicks inside one of the wedges.                                 //
  // This class also tracks the mouse movement and registers stroke direction changes for //
  // the marking mode.                                                                    //
  //                                                                                      //
  // Originally I tried to implement the wedge-drawing with a huge Clutter.Canvas, but    //
  // this turned out to be much to slow. Then I tried to override vfunc_paint() of the    //
  // actor, but I failed to create a shader program in GJS.                               //
  //  * There are some deprecated Cogl.create_*() methods but they return undefined.      //
  //  * The new CoglSnippet API of Cogl 2.0+ is not yet available as it seems.            //
  // I still believe that this approach would be better than the Clutter.ShaderEffect I   //
  // am using now. As a Clutter.OffscreenEffect, the Clutter.ShaderEffect allocates an    //
  // offscreen framebuffer for rendering which is not required in our case. So if there   //
  // are performance issues, this is definitely something to look at!                     //
  //                                                                                      //
  // In the current implementation one large square-shaped actor with a                   //
  // Clutter.ShaderEffect is responsible for drawing the wedge-fill gradients, including  //
  // the currently highlighted one. Then there are multiple smaller elongated actors,     //
  // one for each separator line. They all share one Clutter.Canvas which contains a      //
  // simple gradient line drawn with Cairo.                                               //
  //////////////////////////////////////////////////////////////////////////////////////////
  
  // clang-format off
  var SelectionWedges = GObject.registerClass({
    Properties: {},
    Signals: {
      // This is fired when the mouse pointer enters one of the wedges. The index of the
      // corresponding child is passed in as parameter. If no wedge is hovered anymore
      // (e.g. the center element is hovered) -1 will be passed in.
      "child-hovered-event":    { param_types: [GObject.TYPE_INT] },
  
      // This is fired when the primary mouse button is pressed inside a wedge. It will also
      // be fired if a gesture was detected (either a corner or a timeout). In the latter
      // case, the passed boolean will be true. The first integer is the child's index, the
      // last two integers are the pixel coordinates of the selection event.
      "child-selected-event":   { param_types: [GObject.TYPE_INT, GObject.TYPE_BOOLEAN,
                                                GObject.TYPE_INT, GObject.TYPE_INT] },
  
      // Same as "child-hovered-event", but for the parent wedge.
      "parent-hovered-event":   {},
  
      // Same as "child-selected-event", but for the parent wedge.
      "parent-selected-event":  { param_types: [GObject.TYPE_BOOLEAN,
                                                GObject.TYPE_INT, GObject.TYPE_INT] },
  
      // This is fired if secondary mouse button is pressed. In future there might be ofter
      // reasons for this to get fired.
      "cancel-selection-event": {},
    }
  },
  class SelectionWedges extends Clutter.Actor {
    // clang-format on
  
    // ------------------------------------------------------------ constructor / destructor
  
    _init(params = {}) {
      super._init(params);
  
      // This will contain the angles at which separator lines should be drawn. In degree,
      // clockwise, O° at the top.
      this._separatorAngles = [];
  
      // This is the index of the currently hovered wedge.
      this._hoveredWedge = -1;
  
      // This is the index of the parent wedge.
      this._parentIndex = -1;
  
      // This stores information required for gesture detection.
      this._stroke = {
        start: null,         // The point where the mouse stroke began.
        end: null,           // The current end point of the stroke.
        pauseTimeout: null,  // A timeout ID which is used for stroke pause detection.
      };
  
      // This is attached as a child to *this* and is responsible for drawing the
      // wedge radial gradient. This is done with a Clutter.ShaderEffect as this is much
      // faster than a Clutter.Canvas.
      this._wedgeActor  = new Clutter.Actor();
      this._wedgeShader = new Clutter.ShaderEffect({
        shader_type: Clutter.ShaderType.FRAGMENT_SHADER,
      });
  
      // This shader could be simplified by using vec4's for the colors, but I did not
      // manage to call Clutter.ShaderEffect.set_uniform_value() accordingly from GJS. If
      // anybody knows how this is accomplished, feel free to send a pull request :-)
      this._wedgeShader.set_shader_source(`
        uniform float r;
        uniform float g;
        uniform float b;
        uniform float a;
  
        uniform float rHover;
        uniform float gHover;
        uniform float bHover;
        uniform float aHover;
  
        uniform float innerRadius;
        uniform float startAngle;
        uniform float endAngle;
  
        void main(void) {
  
          // We first assume the normal color for all fragments.
          cogl_color_out = vec4(r, g, b, a);
          
          // Then we use the hover color for fragments outside the innerRadius and
          // between startAngle and endAngle.
          vec2  pos      = 2.0 * (cogl_tex_coord_in[0].xy - vec2(0.5));
          float distance = sqrt(pos.x * pos.x + pos.y * pos.y);
          float angle    = 0;
          if (distance > innerRadius) {
            angle = acos(pos.x / distance) * 180 / 3.141592653589793;
            if (pos.y < 0) {
              angle = 360 - angle;
            }
            
            angle = mod((angle + 90), 360);
            if (angle > startAngle && angle <= endAngle ||
                angle + 360 > startAngle && angle + 360 <= endAngle) {
  
              cogl_color_out = vec4(rHover, gHover, bHover, aHover);
            }
          }
  
          // Fade-out the color towards the outer radius.
          float distFac     = 1.0 - clamp((length(pos) - innerRadius) / (1.0-innerRadius), 0.0, 1.0);
          cogl_color_out.a *= distFac;
  
          // Inherit the opacity of parent actors.
          cogl_color_out.a *= cogl_color_in.a;
          
          // We use premultiplied alpha.
          cogl_color_out.rgb *= cogl_color_out.a;
        }
      `);
  
      this._wedgeActor.add_effect(this._wedgeShader);
      this.add_child(this._wedgeActor);
  
      // Now we create the Clutter.Canvas which will be used by the separator actors. It
      // just contains a simple line with a gradient.
      this._separatorCanvas = new Clutter.Canvas();
      this._separatorCanvas.connect('draw', (canvas, ctx, width, height) => {
        ctx.setOperator(Cairo.Operator.CLEAR);
        ctx.paint();
        ctx.setOperator(Cairo.Operator.OVER);
  
        const gradient =
            new Cairo.LinearGradient(this._settings.wedgeInnerRadius, 0, width, 0);
        const color = this._settings.wedgeSeparatorColor;
        gradient.addColorStopRGBA(0, color.red, color.green, color.blue, color.alpha);
        gradient.addColorStopRGBA(1, color.red, color.green, color.blue, 0);
  
        ctx.setLineWidth(this._settings.wedgeSeparatorWidth);
        ctx.setLineCap(Cairo.LineCap.ROUND);
        ctx.moveTo(this._settings.wedgeInnerRadius, height / 2);
        ctx.lineTo(width, height / 2);
        ctx.setSource(gradient);
        ctx.stroke();
  
        // Explicitly tell Cairo to free the context memory. Is this really necessary?
        ctx.$dispose();
      });
  
      // This actor acts as a group for all separators so that we can easily iterate over
      // them and destroy them once we're done.
      this._separators = new Clutter.Actor();
      this.add_child(this._separators);
    }
  
    // -------------------------------------------------------------------- public interface
  
    // This is called once in the beginning and then whenever the user changes something in
    // the settings.
    onSettingsChange(settings) {
  
      // Parse all settings required for wedge rendering.
      const globalScale = settings.get_double('global-scale');
  
      // clang-format off
      this._settings = {
        wedgeWidth:              settings.get_double('wedge-width')          * globalScale,
        wedgeInnerRadius:        settings.get_double('wedge-inner-radius')   * globalScale,
        wedgeColor:              utils.stringToRGBA(settings.get_string('wedge-color')),
        wedgeColorHover:         utils.stringToRGBA(settings.get_string('wedge-color-hover')),
        wedgeSeparatorColor:     utils.stringToRGBA(settings.get_string('wedge-separator-color')),
        wedgeSeparatorWidth:     settings.get_double('wedge-separator-width') * globalScale,
        gestureSelectionTimeout: settings.get_double('gesture-selection-timeout'),
        gestureJitterThreshold:  settings.get_double('gesture-jitter-threshold')  * globalScale,
        gestureMinStrokeLength:  settings.get_double('gesture-min-stroke-length') * globalScale,
        gestureMinStrokeAngle:   settings.get_double('gesture-min-stroke-angle'),
        hoverMode:               settings.get_boolean('hover-mode'),
      };
      // clang-format on
  
      // Update the size and position of the wedge actor.
      const outerRadius = this._settings.wedgeInnerRadius + this._settings.wedgeWidth;
      this._wedgeActor.set_size(outerRadius * 2, outerRadius * 2);
      this._wedgeActor.set_translation(-outerRadius, -outerRadius, 0);
  
      // Update and re-draw the separator canvas.
      this._separatorCanvas.set_size(
          this._settings.wedgeInnerRadius + this._settings.wedgeWidth,
          this._settings.wedgeSeparatorWidth + 5);
      this._separatorCanvas.invalidate();
  
      // Update the size and position of all separator actors.
      this._separators.get_children().forEach(child => {
        child.set_size(
            this._settings.wedgeInnerRadius + this._settings.wedgeWidth,
            this._settings.wedgeSeparatorWidth + 5);
        child.translation_y = -child.height / 2;
      });
  
      // This could be shortened if we could set vec4's as uniforms...
      this._setFloatUniform('r', this._settings.wedgeColor.red);
      this._setFloatUniform('g', this._settings.wedgeColor.green);
      this._setFloatUniform('b', this._settings.wedgeColor.blue);
      this._setFloatUniform('a', this._settings.wedgeColor.alpha);
  
      this._setFloatUniform('rHover', this._settings.wedgeColorHover.red);
      this._setFloatUniform('gHover', this._settings.wedgeColorHover.green);
      this._setFloatUniform('bHover', this._settings.wedgeColorHover.blue);
      this._setFloatUniform('aHover', this._settings.wedgeColorHover.alpha);
  
      this._setFloatUniform('innerRadius', this._settings.wedgeInnerRadius / outerRadius);
    }
  
    // Returns the currently hovered child's index. Returns -1 if either no wedge or the
    // parent wedge is currently hovered.
    getHoveredChild() {
      if (this._hoveredWedge < 0 || this._parentIndex < 0) {
        return this._hoveredWedge;
      }
  
      if (this._parentIndex == this._hoveredWedge) {
        return -1;
      }
  
      // There is a parent wedge - we have to decrease all indices after the parent wedge by
      // one.
      return (this._hoveredWedge > this._parentIndex) ? this._hoveredWedge - 1 :
                                                        this._hoveredWedge;
    }
  
    // Given the angular positions of all child items, this calculates the separator angles.
    // It deletes all previous separator actors and creates new ones accordingly. The
    // itemAngles array should be sorted, but the smallest value has not to be at the start.
    // That means, the smallest value can be somewhere in the list, all following angles
    // should be monotonically increasing continuing at the start of the list (in a
    // ring-like fashion).
    setItemAngles(itemAngles, parentAngle) {
  
      this._itemAngles   = itemAngles;
      this._hoveredWedge = -1;
      this._parentIndex  = -1;
  
      this._setFloatUniform('startAngle', 0);
      this._setFloatUniform('endAngle', 0);
  
      // If a parentAngle is given, we have to insert it into the list. Due to the ring-like
      // sorting of the itemAngles, this is surprisingly involved.
      if (parentAngle != undefined) {
        for (let i = 0; i <= itemAngles.length; i++) {
          let doInsertion = false;
  
          // Insert it definitely if we reached the end of the list.
          if (i == itemAngles.length) {
            doInsertion = true;
  
          } else if (i > 0) {
            if (itemAngles[i - 1] < parentAngle && parentAngle < itemAngles[i]) {
              doInsertion = true;
            }
  
            if (itemAngles[i - 1] > itemAngles[i]) {
              if (parentAngle < itemAngles[i]) doInsertion = true;
              if (parentAngle > itemAngles[i - 1]) doInsertion = true;
            }
          }
  
          // We found the index to insert it.
          if (doInsertion) {
            this._parentIndex = i;
            itemAngles.splice(i, 0, parentAngle);
            break;
          }
        }
      }
  
      // Destroy obsolete actors. We could re-use existing ones, but they seem to be quite
      // cheap to construct.
      this._separatorAngles = [];
      this._separators.destroy_all_children();
  
      // There are no separators if we have only one or no child items.
      if (itemAngles.length >= 2) {
        for (let i = 0; i < itemAngles.length; i++) {
          const startAngle = itemAngles[(i + itemAngles.length - 1) % itemAngles.length];
          let endAngle     = itemAngles[i];
  
          // Make sure we wrap around.
          if (endAngle < startAngle) {
            endAngle += 360;
          }
  
          // The separator is exactly in the middle of two items.
          const separatorAngle = (startAngle + endAngle) / 2;
          this._separatorAngles.push(separatorAngle);
  
          // Now create the separator actor and rotate it according to the angle.
          const separator = new Clutter.Actor({
            width: this._settings.wedgeInnerRadius + this._settings.wedgeWidth,
            height: this._settings.wedgeSeparatorWidth + 5
          });
  
          // Turn by 90° as 0° is up in our case.
          separator.rotation_angle_z = separatorAngle - 90;
          separator.translation_y    = -separator.height / 2;
          separator.set_pivot_point(0, 0.5);
          separator.set_content(this._separatorCanvas);
          this._separators.add_child(separator);
        }
      }
    }
  
    // This emits 'parent-selected-event' or 'child-selected-event' depending on the
    // currently hovered wedge. It also resets the current gesture detection.
    emitSelection(coords, fromGesture = false) {
      this._resetStroke();
  
      if (this._hoveredWedge >= 0) {
        if (this._hoveredWedge == this._parentIndex) {
          this.emit('parent-selected-event', fromGesture, coords[0], coords[1]);
        } else {
          this.emit(
              'child-selected-event', this.getHoveredChild(), fromGesture, coords[0],
              coords[1]);
        }
      }
    }
  
    // Given the relative pointer position, this calculates the currently active child
    // wedge. For a wedge to become active, the pointer position must be farther away from
    // the center than defined by the wedge-inner-radius settings key.
    // It emits the 'parent-hovered-event' and 'child-hovered-event' signals when the
    // hovered wedge changes.
    // It also tracks the motion of the mouse while the left mouse button is pressed and
    // emits selection events when a stroke corner or a pause in the motion is detected.
    onMotionEvent(coords, state) {
      const [ok, x, y] = this.transform_stage_point(coords[0], coords[1]);
  
      const distance             = Math.sqrt(x * x + y * y);
      let hoveredWedge           = -1;
      let hoveredWedgeStartAngle = 0;
      let hoveredWedgeEndAngle   = 0;
  
      // There is only something hovered if the pointer is outside the inner circle.
      if (distance > this._settings.wedgeInnerRadius) {
        let angle = Math.acos(x / distance) * 180 / Math.PI;
        if (y < 0) {
          angle = 360 - angle;
        }
  
        // Turn 0° up.
        angle = (angle + 90) % 360;
  
        // If there is only one full 360°-wedge, it is hovered.
        if (this._itemAngles.length == 1) {
          hoveredWedge           = 0;
          hoveredWedgeStartAngle = 0;
          hoveredWedgeEndAngle   = 360;
        }
  
        // Now search the wedge the pointer resides in currently.
        for (let i = 0; i < this._separatorAngles.length; i++) {
          const startAngle = this._separatorAngles[i];
          let endAngle     = this._separatorAngles[(i + 1) % this._separatorAngles.length];
  
          // Make sure we wrap around.
          if (endAngle < startAngle) {
            endAngle += 360;
          }
  
          if ((angle > startAngle && angle <= endAngle) ||
              (angle + 360 > startAngle && angle + 360 <= endAngle)) {
            hoveredWedge           = i;
            hoveredWedgeStartAngle = startAngle;
            hoveredWedgeEndAngle   = endAngle;
            break
          }
        }
      }
  
      // Update uniforms only if hovered wedge changed.
      if (hoveredWedge != this._hoveredWedge) {
        this._setFloatUniform('startAngle', hoveredWedgeStartAngle);
        this._setFloatUniform('endAngle', hoveredWedgeEndAngle);
        this._hoveredWedge = hoveredWedge;
  
        if (hoveredWedge >= 0 && hoveredWedge == this._parentIndex) {
          this.emit('parent-hovered-event');
        } else {
          this.emit('child-hovered-event', this.getHoveredChild());
        }
      }
  
      // Now we try to detect gestures. This is done only if either the left mouse button is
      // pressed or a modifier key is held down. Consider the diagram below:
      //
      //                                  M
      //                                .
      //                              .
      //     S -------------------- E
      //
      // The mouse button was pressed at S (_stroke.start) and the moved to E (_stroke.end).
      // When the next motion event comes in (M) and the left button is still pressed, we
      // compare the directions of S->E with E->M. If they differ significantly, this is
      // considered a corner. There are some minimum lengths for both vectors - if they are
      // not long enough, nothing is done. If E->M is long enough, but there is no corner, E
      // is set to M and we wait for the next motion event.
      if (this.isGestureModifier(state)) {
        if (this._stroke.start == null) {
  
          // It's the first event of this gesture, so we store the current mouse position as
          // start and end. There is nothing more to be done.
          this._stroke.start = coords;
          this._stroke.end   = coords;
  
        } else {
  
          // Calculate the vector S->E in the diagram above.
          const strokeDir = [
            this._stroke.end[0] - this._stroke.start[0],
            this._stroke.end[1] - this._stroke.start[1]
          ];
  
          const strokeLength =
              Math.sqrt(strokeDir[0] * strokeDir[0] + strokeDir[1] * strokeDir[1]);
  
          if (strokeLength > this._settings.gestureMinStrokeLength) {
  
            // Calculate the vector E->M in the diagram above.
            const tipDir =
                [coords[0] - this._stroke.end[0], coords[1] - this._stroke.end[1]];
  
            const tipLength = Math.sqrt(tipDir[0] * tipDir[0] + tipDir[1] * tipDir[1]);
  
            if (tipLength > this._settings.gestureJitterThreshold) {
  
              // If the tip vector is long enough, the pointer was not stationary. Remove
              // the timer again.
              if (this._stroke.pauseTimeout != null) {
                GLib.source_remove(this._stroke.pauseTimeout);
                this._stroke.pauseTimeout = null;
              }
  
              // Now compute the angle between S->E and E->M.
              const angle = Math.acos(
                  tipDir[0] / tipLength * strokeDir[0] / strokeLength +
                  tipDir[1] / tipLength * strokeDir[1] / strokeLength);
  
              //  Emit the selection events if it exceeds the configured threshold. We pass
              //  the coordinates of E for the selection event.
              if (angle * 180 / Math.PI > this._settings.gestureMinStrokeAngle) {
                this.emitSelection(this._stroke.end, true);
              }
  
              // Update the point M in the diagram above to be the new E for the next motion
              // event.
              this._stroke.end = coords;
            }
  
            // The stroke is long enough to become a gesture. We register a timer to detect
            // pause-events where the pointer was stationary for some time. These events
            // also lead to selections. If the selection timeout is set to zero, we emit the
            // selection instantaneously.
            if (this._settings.gestureSelectionTimeout == 0) {
  
              // If the selection timeout is set to zero, we want to select an item as soon
              // as the pointer exceeds the minimum stroke length. As the pointer may have
              // moved really quickly, we reconstruct where it left this radius.
              const scale           = this._settings.gestureMinStrokeLength / strokeLength;
              const selectionCoords = [
                this._stroke.start[0] + strokeDir[0] * scale,
                this._stroke.start[1] + strokeDir[1] * scale
              ];
  
              this.emitSelection(selectionCoords, true);
  
            } else if (this._stroke.pauseTimeout == null) {
              this._stroke.pauseTimeout = GLib.timeout_add(
                  GLib.PRIORITY_DEFAULT, this._settings.gestureSelectionTimeout, () => {
                    this.emitSelection(coords, true);
                    return false;
                  });
            }
  
          } else {
  
            // The vector S->E is not long enough to be a gesture, so we only update the end
            // point.
            this._stroke.end = coords;
          }
        }
      } else {
        // The mouse button is not pressed anymore, so we can abort gesture detection.
        this._resetStroke();
      }
    }
  
  
    // Returns true if the left or right button is pressed, or a modifier is held down (for
    // the "Turbo-Mode"). Thanks to the Super+RMB mode, we can actually select items with
    // the right mouse button...
    isGestureModifier(mods) {
      const hoverMode = this._settings.hoverMode;
      const buttonPressed =
          (mods &
           (Clutter.ModifierType.BUTTON1_MASK | Clutter.ModifierType.BUTTON2_MASK |
            Clutter.ModifierType.BUTTON3_MASK)) > 0;
  
      const shortcutPressed =
          (mods &
           (Gtk.accelerator_get_default_mod_mask() | Clutter.ModifierType.MOD4_MASK)) > 0;
  
      return hoverMode || buttonPressed || shortcutPressed;
    }
  
    // ----------------------------------------------------------------------- private stuff
  
    // Clutter.ShaderEffect.set_uniform_value() works well if floating point Numbers are
    // passed to the method. However, when you pass an integer Number, an OpenGL error
    // (1282, Invalid operation) is thrown. It seems that the implementation assumes in this
    // case that the uniform is actually of type int. Here is a crude workaround - I just
    // make sure that no integers are passed to the method!
    _setFloatUniform(name, value) {
      if (Number.isInteger(value)) {
        value += 0.0000001;
      }
      this._wedgeShader.set_uniform_value(name, value);
    }
  
    // Resets the current gesture detection. This is done when an item was selected or the
    // mouse pointer released.
    _resetStroke() {
      if (this._stroke.pauseTimeout != null) {
        GLib.source_remove(this._stroke.pauseTimeout);
        this._stroke.pauseTimeout = null;
      }
  
      this._stroke.start = null;
      this._stroke.end   = null;
    }
  });