Skip to content

Commit 7a09f46

Browse files
jarktclaude
andauthored
dim-inactive-windows@jark: smooth fade via GLib frame timer (#1091)
Drive the dim/desaturate level with a ~60 fps GLib timeout and call set_brightness()/set_factor() with a float each frame. A stand-alone Clutter.Timeline has no frame clock in this Muffin version, and easing the brightness property quantises it to an integer Clutter.Color, which looks stepped; applying a float per frame keeps the fade perfectly smooth. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 64767dd commit 7a09f46

1 file changed

Lines changed: 77 additions & 60 deletions

File tree

  • dim-inactive-windows@jark/files/dim-inactive-windows@jark

dim-inactive-windows@jark/files/dim-inactive-windows@jark/extension.js

Lines changed: 77 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99

1010
const Meta = imports.gi.Meta;
1111
const Clutter = imports.gi.Clutter;
12+
const GLib = imports.gi.GLib;
1213
const Settings = imports.ui.settings;
1314

15+
const FRAME_MS = 16; // ~60 fps
16+
1417
const EFFECT_DESAT = "dim-inactive-desat";
1518
const EFFECT_DIM = "dim-inactive-dim";
1619
const EPS = 0.001;
@@ -129,90 +132,104 @@ DimInactiveWindows.prototype = {
129132
},
130133

131134
// --- Effect handling with fade --------------------------------------
132-
// Each managed actor carries two named Clutter effects that are eased
133-
// toward their target on every focus change (via the actor easing patched
134-
// in by Cinnamon's environment.js, mirroring core's WindowDimmer). A dimmed
135-
// window has its brightness pushed below neutral and its colour desaturated;
136-
// the focused window eases back to neutral, where the effects are disabled
137-
// again so idle windows pay no GPU cost.
135+
// Each managed actor carries a float "level" in [0, 1] (0 = active/neutral,
136+
// 1 = fully dimmed) that is animated by a ~60 fps GLib timeout. Every frame
137+
// we set both effect values from that float with full precision, so the fade
138+
// stays perfectly smooth: set_brightness() takes a float, whereas easing the
139+
// BrightnessContrastEffect.brightness *property* would quantise it to an
140+
// integer Clutter.Color and look stepped. A plain GLib source is used because
141+
// a stand-alone Clutter.Timeline has no frame clock in this Muffin version.
138142

139143
_setDim: function(actor, dimmed, instant) {
140-
// Snap (no fade) the first time we see an actor, or when asked to.
141-
let firstSight = actor._diwSeen !== true;
142-
let duration = (instant || firstSight) ? 0 : this.animationTime;
143-
actor._diwSeen = true;
144-
actor._diwDimmed = dimmed;
145-
146-
let desat = this._effect(actor, EFFECT_DESAT,
147-
() => new Clutter.DesaturateEffect({ factor: 0 }));
148-
let dim = this._effect(actor, EFFECT_DIM,
149-
() => new Clutter.BrightnessContrastEffect());
150-
151-
let desatOn = this.mode === "both" || this.mode === "desaturate";
152-
let dimOn = this.mode === "both" || this.mode === "dim";
153-
let factor = (dimmed && desatOn) ? this.desaturateAmount : 0;
154-
// BrightnessContrastEffect.brightness is a Clutter.Color where 127 is
155-
// neutral; brightness b in [-1, 0] maps to a neutral grey 127*(1 + b).
156-
let b = (dimmed && dimOn) ? -this.dimAmount : 0;
157-
let val = Math.round(127 * (1 + b));
158-
let color = Clutter.Color.new(val, val, val, 255);
159-
160-
// Keep effects live while (re)animating; _syncEnabled turns off the
161-
// ones that settle back at neutral.
162-
desat.enabled = true;
163-
dim.enabled = true;
164-
165-
actor.ease_property("@effects." + EFFECT_DESAT + ".factor", factor, {
166-
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
167-
duration: duration,
168-
onComplete: () => this._syncEnabled(actor)
169-
});
170-
actor.ease_property("@effects." + EFFECT_DIM + ".brightness", color, {
171-
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
172-
duration: duration,
173-
onComplete: () => this._syncEnabled(actor)
174-
});
175-
},
176-
177-
_effect: function(actor, name, create) {
178-
let e = actor.get_effect(name);
179-
if (!e) {
180-
e = create();
181-
actor.add_effect_with_name(name, e);
144+
let target = dimmed ? 1 : 0;
145+
let firstSight = actor._diwLevel === undefined;
146+
let duration = this.animationTime;
147+
148+
this._effect(actor, EFFECT_DESAT, () => new Clutter.DesaturateEffect({ factor: 0 }));
149+
this._effect(actor, EFFECT_DIM, () => new Clutter.BrightnessContrastEffect());
150+
151+
// Snap (no fade) on first sighting or when an instant update is asked for.
152+
if (firstSight || instant || duration <= 0) {
153+
this._stopAnim(actor);
154+
actor._diwLevel = target;
155+
actor._diwTarget = target;
156+
this._apply(actor, target);
157+
return;
182158
}
183-
return e;
159+
160+
if (actor._diwTarget === target)
161+
return; // already at or heading toward this target
162+
163+
this._stopAnim(actor);
164+
let from = actor._diwLevel;
165+
actor._diwTarget = target;
166+
let start = GLib.get_monotonic_time(); // microseconds
167+
168+
actor._diwTimer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, FRAME_MS, () => {
169+
let t = (GLib.get_monotonic_time() - start) / 1000 / duration;
170+
if (t >= 1) {
171+
actor._diwLevel = target;
172+
this._apply(actor, target);
173+
actor._diwTimer = 0;
174+
return GLib.SOURCE_REMOVE;
175+
}
176+
let p = 1 - (1 - t) * (1 - t); // easeOutQuad
177+
actor._diwLevel = from + (target - from) * p;
178+
this._apply(actor, actor._diwLevel);
179+
return GLib.SOURCE_CONTINUE;
180+
});
184181
},
185182

186-
// Disable an effect once it has settled at neutral (no transition running),
187-
// mirroring Cinnamon's own WindowDimmer so idle windows cost nothing.
188-
_syncEnabled: function(actor) {
183+
// Apply a float level to both effects; disable an effect at neutral so idle
184+
// (active) windows pay no GPU cost.
185+
_apply: function(actor, level) {
189186
try {
187+
let desatOn = this.mode === "both" || this.mode === "desaturate";
188+
let dimOn = this.mode === "both" || this.mode === "dim";
189+
190190
let desat = actor.get_effect(EFFECT_DESAT);
191191
if (desat) {
192-
let animating = actor.get_transition("@effects." + EFFECT_DESAT + ".factor") != null;
193-
desat.enabled = animating || desat.factor > EPS;
192+
let on = level > EPS && desatOn;
193+
desat.enabled = on;
194+
desat.set_factor(on ? level * this.desaturateAmount : 0);
194195
}
195196
let dim = actor.get_effect(EFFECT_DIM);
196197
if (dim) {
197-
let animating = actor.get_transition("@effects." + EFFECT_DIM + ".brightness") != null;
198-
dim.enabled = animating || dim.brightness.red != 127;
198+
let on = level > EPS && dimOn;
199+
dim.enabled = on;
200+
dim.set_brightness(on ? -(level * this.dimAmount) : 0);
199201
}
200202
} catch (e) {
201203
// Actor may have been destroyed mid-fade; nothing to do.
202204
}
203205
},
204206

207+
_effect: function(actor, name, create) {
208+
let e = actor.get_effect(name);
209+
if (!e) {
210+
e = create();
211+
actor.add_effect_with_name(name, e);
212+
}
213+
return e;
214+
},
215+
216+
_stopAnim: function(actor) {
217+
if (actor._diwTimer) {
218+
try { GLib.source_remove(actor._diwTimer); } catch (e) {}
219+
actor._diwTimer = 0;
220+
}
221+
},
222+
205223
_reset: function(actor) {
224+
this._stopAnim(actor);
206225
try {
207-
actor.remove_transition("@effects." + EFFECT_DESAT + ".factor");
208-
actor.remove_transition("@effects." + EFFECT_DIM + ".brightness");
209226
if (actor.get_effect(EFFECT_DESAT))
210227
actor.remove_effect_by_name(EFFECT_DESAT);
211228
if (actor.get_effect(EFFECT_DIM))
212229
actor.remove_effect_by_name(EFFECT_DIM);
213230
} catch (e) {}
214-
delete actor._diwSeen;
215-
delete actor._diwDimmed;
231+
delete actor._diwLevel;
232+
delete actor._diwTarget;
216233
},
217234

218235
_clearAll: function() {

0 commit comments

Comments
 (0)