------------------------------------------- -- @author https://github.com/Kasper24 -- @copyright 2021-2025 Kasper24 ------------------------------------------- local GLib = require("lgi").GLib local gobject = require "gears.object" local gpcall = require "gears.protected_call" local gtable = require "gears.table" local gtimer = require "gears.timer" local subscribable = require "quarrel.animation.subscribable" -- local qconsts = require "quarrel.consts" local qtween = require "quarrel.animation.tween" -- local qbezier = require "quarrel.animation.bezier" local ipairs = ipairs local table = table local pairs = pairs local animation_manager = {} -- local easing_bezier = qbezier(0.2, 0, 0, 1) -- animation_manager.consts = qconsts.protect(function() -- local duration = 0.3 -- return { -- DURATION = duration, -- INTRO = duration / 4, -- EASING = function(t, b, c, d) -- local epsilon = 1000 / d -- return c * easing_bezier:solve(t/d, epsilon) + b -- end -- } -- end) local animation = {} local function second_to_micro(sec) return sec * 1000000 end local function framerate_tomilli(framerate) return 1000 / framerate end local function on_no_running_animations(self, callback) gtimer.start_new(0.1, function() if #self._private.animations <= 0 then callback() return false else local has_non_looped_anim = false for _, animation in ipairs(self._private.animations) do if animation.loop == false then has_non_looped_anim = true end end if has_non_looped_anim == false then callback() return false end end return true end) end local function animation_loop(self) self._private.source_id = GLib.timeout_add( GLib.PRIORITY_DEFAULT, framerate_tomilli(self._private.framerate), function() for index, animation in ipairs(self._private.animations) do if animation.state == true then -- compute delta time local time = GLib.get_monotonic_time() local delta = time - animation.last_elapsed animation.last_elapsed = time -- If pos is true, the animation has ended local pos = gpcall(animation.tween.update, animation.tween, delta) if pos == true then -- Loop the animation, don't end it. -- Useful for widgets like the spinning cicle if animation.loop == true then animation.tween:reset() else animation.state = false -- Snap to end animation.pos = animation.tween.target gpcall(animation.emit_signal, animation, "update", animation.pos) gpcall(animation.fire, animation, animation.pos) gpcall(animation.emit_signal, animation, "ended", animation.pos) gpcall(animation.ended.fire, animation, animation.pos) table.remove(self._private.animations, index) end -- Animation in process, keep updating else animation.pos = pos gpcall(animation.emit_signal, animation, "update", animation.pos) gpcall(animation.fire, animation, animation.pos) end else table.remove(self._private.animations, index) end end -- call again the function after cooldown return true end ) end function animation:set(args) args = args or {} -- Awestoer/Rubbto compatibility -- I'd rather this always be a table, but Awestore/Rubbto -- except the :set() method to have 1 number value parameter -- used to set the target local is_table = type(args) == "table" local initial = is_table and (args.pos or self.pos) or self.pos local subject = is_table and (args.subject or self.subject) or self.subject local target = is_table and (args.target or self.target) or args local duration = is_table and (args.duration or self.duration) or self.duration local easing = is_table and (args.easing or self.easing) or self.easing if self.tween == nil or self.reset_on_stop == true then self.tween = qtween.new { initial = initial, subject = subject, target = target, duration = second_to_micro(duration), easing = easing, } end if self._private.anim_manager._private.instant and self.override_instant ~= true then self.pos = self.tween.target self:fire(self.pos) self:emit_signal("update", self.pos) self.state = false self.ended:fire(self.pos) self:emit_signal("ended", self.pos) return end if self._private.anim_manager._private.animations[self.index] == nil then table.insert(self._private.anim_manager._private.animations, self) end self.state = true self.last_elapsed = GLib.get_monotonic_time() self.started:fire() self:emit_signal "started" end -- Rubato compatibility function animation:abort() self.state = false end function animation:stop() self.state = false end function animation:initial() return self._private.initial end function animation_manager:set_instant(value) if value == true and self._private.instant == false then on_no_running_animations(self, function() -- GLib.source_remove(self._private.source_id) self._private.instant = true end) elseif self._private.instant == true then self._private.instant = false -- animation_loop(self) end end function animation_manager:set_framerate(value) self._private.framerate = value -- if self._private.instant == false then on_no_running_animations(self, function() GLib.source_remove(self._private.source_id) animation_loop(self) end) -- end end function animation_manager:new(args) args = args or {} args.pos = args.pos or 0 args.subject = args.subject or nil args.target = args.target or nil args.duration = args.duration or 0 args.easing = args.easing or nil args.loop = args.loop or false args.signals = args.signals or {} args.update = args.update or nil args.reset_on_stop = args.reset_on_stop == nil and true or args.reset_on_stop -- Awestoer/Rubbto compatibility args.subscribed = args.subscribed or nil local ret = subscribable() ret.started = subscribable() ret.ended = subscribable() if args.subscribed ~= nil then ret:subscribe(args.subscribed) end for sig, sigfun in pairs(args.signals) do ret:connect_signal(sig, sigfun) end if args.update ~= nil then ret:connect_signal("update", args.update) end gtable.crush(ret, args, true) gtable.crush(ret, animation, true) ret._private = {} ret._private.anim_manager = self ret._private.initial = args.pos -- Can't have it private for rubato compatibility ret.state = false return ret end local instance = gobject {} gtable.crush(instance, animation_manager, true) instance._private = {} instance._private.animations = {} instance._private.instant = false instance._private.framerate = 60 animation_loop(instance) return instance