summaryrefslogtreecommitdiffstats
path: root/js/ui/lightbox.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/lightbox.js')
-rw-r--r--js/ui/lightbox.js293
1 files changed, 293 insertions, 0 deletions
diff --git a/js/ui/lightbox.js b/js/ui/lightbox.js
new file mode 100644
index 0000000..9aaef1a
--- /dev/null
+++ b/js/ui/lightbox.js
@@ -0,0 +1,293 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported Lightbox */
+
+const { Clutter, GObject, Shell, St } = imports.gi;
+
+const Params = imports.misc.params;
+
+var DEFAULT_FADE_FACTOR = 0.4;
+var VIGNETTE_BRIGHTNESS = 0.5;
+var VIGNETTE_SHARPNESS = 0.7;
+
+const VIGNETTE_DECLARATIONS = '\
+uniform float brightness;\n\
+uniform float vignette_sharpness;\n';
+
+const VIGNETTE_CODE = '\
+cogl_color_out.a = cogl_color_in.a;\n\
+cogl_color_out.rgb = vec3(0.0, 0.0, 0.0);\n\
+vec2 position = cogl_tex_coord_in[0].xy - 0.5;\n\
+float t = length(2.0 * position);\n\
+t = clamp(t, 0.0, 1.0);\n\
+float pixel_brightness = mix(1.0, 1.0 - vignette_sharpness, t);\n\
+cogl_color_out.a = cogl_color_out.a * (1 - pixel_brightness * brightness);';
+
+var RadialShaderEffect = GObject.registerClass({
+ Properties: {
+ 'brightness': GObject.ParamSpec.float(
+ 'brightness', 'brightness', 'brightness',
+ GObject.ParamFlags.READWRITE,
+ 0, 1, 1),
+ 'sharpness': GObject.ParamSpec.float(
+ 'sharpness', 'sharpness', 'sharpness',
+ GObject.ParamFlags.READWRITE,
+ 0, 1, 0),
+ },
+}, class RadialShaderEffect extends Shell.GLSLEffect {
+ _init(params) {
+ this._brightness = undefined;
+ this._sharpness = undefined;
+
+ super._init(params);
+
+ this._brightnessLocation = this.get_uniform_location('brightness');
+ this._sharpnessLocation = this.get_uniform_location('vignette_sharpness');
+
+ this.brightness = 1.0;
+ this.sharpness = 0.0;
+ }
+
+ vfunc_build_pipeline() {
+ this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT,
+ VIGNETTE_DECLARATIONS, VIGNETTE_CODE, true);
+ }
+
+ get brightness() {
+ return this._brightness;
+ }
+
+ set brightness(v) {
+ if (this._brightness == v)
+ return;
+ this._brightness = v;
+ this.set_uniform_float(this._brightnessLocation,
+ 1, [this._brightness]);
+ this.notify('brightness');
+ }
+
+ get sharpness() {
+ return this._sharpness;
+ }
+
+ set sharpness(v) {
+ if (this._sharpness == v)
+ return;
+ this._sharpness = v;
+ this.set_uniform_float(this._sharpnessLocation,
+ 1, [this._sharpness]);
+ this.notify('sharpness');
+ }
+});
+
+/**
+ * Lightbox:
+ * @container: parent Clutter.Container
+ * @params: (optional) additional parameters:
+ * - inhibitEvents: whether to inhibit events for @container
+ * - width: shade actor width
+ * - height: shade actor height
+ * - fadeFactor: fading opacity factor
+ * - radialEffect: whether to enable the GLSL radial effect
+ *
+ * Lightbox creates a dark translucent "shade" actor to hide the
+ * contents of @container, and allows you to specify particular actors
+ * in @container to highlight by bringing them above the shade. It
+ * tracks added and removed actors in @container while the lightboxing
+ * is active, and ensures that all actors are returned to their
+ * original stacking order when the lightboxing is removed. (However,
+ * if actors are restacked by outside code while the lightboxing is
+ * active, the lightbox may later revert them back to their original
+ * order.)
+ *
+ * By default, the shade window will have the height and width of
+ * @container and will track any changes in its size. You can override
+ * this by passing an explicit width and height in @params.
+ */
+var Lightbox = GObject.registerClass({
+ Properties: {
+ 'active': GObject.ParamSpec.boolean(
+ 'active', 'active', 'active', GObject.ParamFlags.READABLE, false),
+ },
+}, class Lightbox extends St.Bin {
+ _init(container, params) {
+ params = Params.parse(params, {
+ inhibitEvents: false,
+ width: null,
+ height: null,
+ fadeFactor: DEFAULT_FADE_FACTOR,
+ radialEffect: false,
+ });
+
+ super._init({
+ reactive: params.inhibitEvents,
+ width: params.width,
+ height: params.height,
+ visible: false,
+ });
+
+ this._active = false;
+ this._container = container;
+ this._children = container.get_children();
+ this._fadeFactor = params.fadeFactor;
+ this._radialEffect = Clutter.feature_available(Clutter.FeatureFlags.SHADERS_GLSL) && params.radialEffect;
+
+ if (this._radialEffect)
+ this.add_effect(new RadialShaderEffect({ name: 'radial' }));
+ else
+ this.set({ opacity: 0, style_class: 'lightbox' });
+
+ container.add_actor(this);
+ container.set_child_above_sibling(this, null);
+
+ this.connect('destroy', this._onDestroy.bind(this));
+
+ if (!params.width || !params.height) {
+ this.add_constraint(new Clutter.BindConstraint({
+ source: container,
+ coordinate: Clutter.BindCoordinate.ALL,
+ }));
+ }
+
+ this._actorAddedSignalId = container.connect('actor-added', this._actorAdded.bind(this));
+ this._actorRemovedSignalId = container.connect('actor-removed', this._actorRemoved.bind(this));
+
+ this._highlighted = null;
+ }
+
+ get active() {
+ return this._active;
+ }
+
+ _actorAdded(container, newChild) {
+ let children = this._container.get_children();
+ let myIndex = children.indexOf(this);
+ let newChildIndex = children.indexOf(newChild);
+
+ if (newChildIndex > myIndex) {
+ // The child was added above the shade (presumably it was
+ // made the new top-most child). Move it below the shade,
+ // and add it to this._children as the new topmost actor.
+ this._container.set_child_above_sibling(this, newChild);
+ this._children.push(newChild);
+ } else if (newChildIndex == 0) {
+ // Bottom of stack
+ this._children.unshift(newChild);
+ } else {
+ // Somewhere else; insert it into the correct spot
+ let prevChild = this._children.indexOf(children[newChildIndex - 1]);
+ if (prevChild != -1) // paranoia
+ this._children.splice(prevChild + 1, 0, newChild);
+ }
+ }
+
+ lightOn(fadeInTime) {
+ this.remove_all_transitions();
+
+ let easeProps = {
+ duration: fadeInTime || 0,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ };
+
+ let onComplete = () => {
+ this._active = true;
+ this.notify('active');
+ };
+
+ this.show();
+
+ if (this._radialEffect) {
+ this.ease_property(
+ '@effects.radial.brightness', VIGNETTE_BRIGHTNESS, easeProps);
+ this.ease_property(
+ '@effects.radial.sharpness', VIGNETTE_SHARPNESS,
+ Object.assign({ onComplete }, easeProps));
+ } else {
+ this.ease(Object.assign(easeProps, {
+ opacity: 255 * this._fadeFactor,
+ onComplete,
+ }));
+ }
+ }
+
+ lightOff(fadeOutTime) {
+ this.remove_all_transitions();
+
+ this._active = false;
+ this.notify('active');
+
+ let easeProps = {
+ duration: fadeOutTime || 0,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ };
+
+ let onComplete = () => this.hide();
+
+ if (this._radialEffect) {
+ this.ease_property(
+ '@effects.radial.brightness', 1.0, easeProps);
+ this.ease_property(
+ '@effects.radial.sharpness', 0.0, Object.assign({ onComplete }, easeProps));
+ } else {
+ this.ease(Object.assign(easeProps, { opacity: 0, onComplete }));
+ }
+ }
+
+ _actorRemoved(container, child) {
+ let index = this._children.indexOf(child);
+ if (index != -1) // paranoia
+ this._children.splice(index, 1);
+
+ if (child == this._highlighted)
+ this._highlighted = null;
+ }
+
+ /**
+ * highlight:
+ * @param {Clutter.Actor=} window: actor to highlight
+ *
+ * Highlights the indicated actor and unhighlights any other
+ * currently-highlighted actor. With no arguments or a false/null
+ * argument, all actors will be unhighlighted.
+ */
+ highlight(window) {
+ if (this._highlighted == window)
+ return;
+
+ // Walk this._children raising and lowering actors as needed.
+ // Things get a little tricky if the to-be-raised and
+ // to-be-lowered actors were originally adjacent, in which
+ // case we may need to indicate some *other* actor as the new
+ // sibling of the to-be-lowered one.
+
+ let below = this;
+ for (let i = this._children.length - 1; i >= 0; i--) {
+ if (this._children[i] == window)
+ this._container.set_child_above_sibling(this._children[i], null);
+ else if (this._children[i] == this._highlighted)
+ this._container.set_child_below_sibling(this._children[i], below);
+ else
+ below = this._children[i];
+ }
+
+ this._highlighted = window;
+ }
+
+ /**
+ * _onDestroy:
+ *
+ * This is called when the lightbox' actor is destroyed, either
+ * by destroying its container or by explicitly calling this.destroy().
+ */
+ _onDestroy() {
+ if (this._actorAddedSignalId) {
+ this._container.disconnect(this._actorAddedSignalId);
+ this._actorAddedSignalId = 0;
+ }
+ if (this._actorRemovedSignalId) {
+ this._container.disconnect(this._actorRemovedSignalId);
+ this._actorRemovedSignalId = 0;
+ }
+
+ this.highlight(null);
+ }
+});