|
9 | 9 |
|
10 | 10 | const Meta = imports.gi.Meta; |
11 | 11 | const Clutter = imports.gi.Clutter; |
| 12 | +const GLib = imports.gi.GLib; |
12 | 13 | const Settings = imports.ui.settings; |
13 | 14 |
|
| 15 | +const FRAME_MS = 16; // ~60 fps |
| 16 | + |
14 | 17 | const EFFECT_DESAT = "dim-inactive-desat"; |
15 | 18 | const EFFECT_DIM = "dim-inactive-dim"; |
16 | 19 | const EPS = 0.001; |
@@ -129,90 +132,104 @@ DimInactiveWindows.prototype = { |
129 | 132 | }, |
130 | 133 |
|
131 | 134 | // --- 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. |
138 | 142 |
|
139 | 143 | _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; |
182 | 158 | } |
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 | + }); |
184 | 181 | }, |
185 | 182 |
|
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) { |
189 | 186 | try { |
| 187 | + let desatOn = this.mode === "both" || this.mode === "desaturate"; |
| 188 | + let dimOn = this.mode === "both" || this.mode === "dim"; |
| 189 | + |
190 | 190 | let desat = actor.get_effect(EFFECT_DESAT); |
191 | 191 | 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); |
194 | 195 | } |
195 | 196 | let dim = actor.get_effect(EFFECT_DIM); |
196 | 197 | 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); |
199 | 201 | } |
200 | 202 | } catch (e) { |
201 | 203 | // Actor may have been destroyed mid-fade; nothing to do. |
202 | 204 | } |
203 | 205 | }, |
204 | 206 |
|
| 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 | + |
205 | 223 | _reset: function(actor) { |
| 224 | + this._stopAnim(actor); |
206 | 225 | try { |
207 | | - actor.remove_transition("@effects." + EFFECT_DESAT + ".factor"); |
208 | | - actor.remove_transition("@effects." + EFFECT_DIM + ".brightness"); |
209 | 226 | if (actor.get_effect(EFFECT_DESAT)) |
210 | 227 | actor.remove_effect_by_name(EFFECT_DESAT); |
211 | 228 | if (actor.get_effect(EFFECT_DIM)) |
212 | 229 | actor.remove_effect_by_name(EFFECT_DIM); |
213 | 230 | } catch (e) {} |
214 | | - delete actor._diwSeen; |
215 | | - delete actor._diwDimmed; |
| 231 | + delete actor._diwLevel; |
| 232 | + delete actor._diwTarget; |
216 | 233 | }, |
217 | 234 |
|
218 | 235 | _clearAll: function() { |
|
0 commit comments