AndyObtiva/glimmer-dsl-swt

View on GitHub
lib/glimmer/swt/custom/animation.rb

Summary

Maintainability
C
1 day
Test Coverage
# Copyright (c) 2007-2024 Andy Maleh
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

require 'glimmer/data_binding/observable_model'
require 'glimmer/swt/properties'
require 'glimmer/swt/custom/shape'
require 'bigdecimal'

module Glimmer
  module SWT
    module Custom
      # Represents an animation declaratively
      class Animation
        include Properties
        include Glimmer::DataBinding::ObservableModel
        
        class << self
          def schedule_frame_animation(animation, &frame_animation_block)
            frame_animation_queue(animation).prepend(frame_animation_block)
            swt_display.async_exec do
              frame_animation_queue(next_animation)&.pop&.call
            end
          end
          
          def next_animation
            animation = nil
            while frame_animation_queues.values.reduce(:+)&.any? && (animation.nil? || frame_animation_queue(animation).last.nil?)
              animation = frame_animation_queues.keys[next_animation_index]
              frame_animation_queues.delete(animation) if frame_animation_queues.values.reduce(:+)&.any? && !animation.nil? && frame_animation_queue(animation).empty?
            end
            animation
          end
          
          def next_animation_index
            next_schedule_index % frame_animation_queues.keys.size
          end
          
          def next_schedule_index
            unless defined? @@next_schedule_index
              @@next_schedule_index = 0
            else
              @@next_schedule_index += 1
            end
          end
          
          def frame_animation_queues
            unless defined? @@frame_animation_queues
              @@frame_animation_queues = {}
            end
            @@frame_animation_queues
          end
          
          def frame_animation_queue(animation)
            frame_animation_queues[animation] ||= []
          end
          
          def swt_display
            unless defined? @@swt_display
              @@swt_display = DisplayProxy.instance.swt_display
            end
            @@swt_display
          end
        end
        
        attr_reader :parent, :options
        attr_accessor :frame_index, :cycle, :frame_block, :every, :fps, :cycle_count, :frame_count, :started, :duration_limit, :duration, :finished, :cycle_count_index
        alias current_frame_index frame_index
        alias started? started
        alias finished? finished
        alias frame_rate fps
        alias frame_rate= fps=
        # TODO consider supporting an async: false option
        
        def initialize(parent)
          @parent = parent
          @parent.requires_shape_disposal = true
          self.started = true
          self.frame_index = 0
          self.cycle_count_index = 0
          @start_number = 0 # denotes the number of starts (increments on every start)
          self.class.swt_display # ensures initializing variable to set from GUI thread
        end
        
        def post_add_content
          if @dispose_listener_registration.nil?
            @dispose_listener_registration = @parent.on_widget_disposed { stop }
            start if started?
          end
        end
        
        def content(&block)
          Glimmer::SWT::DisplayProxy.instance.auto_exec do
            Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::SWT::AnimationExpression.new, 'animation', &block)
          end
        end
                
        # Starts an animation that is indefinite or has never been started before (i.e. having `started: false` option).
        # Otherwise, resumes a stopped animation that has not been completed.
        def start
          return if @start_number > 0 && started?
          @start_number += 1
          @start_time = Time.now
          @duration = 0
          @original_start_time = @start_time if @duration.nil?
          self.finished = false if finished?
          self.started = true
          # TODO track when finished in a variable for finite animations (whether by frame count, cycle count, or duration limit)
          Thread.new do
            start_number = @start_number
            if cycle_count.is_a?(Integer) && cycle.is_a?(Array)
              (cycle_count * cycle.length).times do
                break unless draw_frame(start_number)
              end
            else
              loop do
                # this code has to be duplicated to break from a loop (break keyword only works when literally in a loop block)
                break unless draw_frame(start_number)
              end
            end
          end
        end
        
        def stop
          return if stopped?
          self.started = false
          self.duration = (Time.now - @start_time) + @duration.to_f if duration_limited? && !@start_time.nil?
        end
        
        # Restarts an animation (whether indefinite or not and whether stopped or not)
        def restart
          @original_start_time = @start_time = Time.now
          self.duration = 0
          self.frame_index = 0
          self.cycle_count_index = 0
          stop
          start
        end
        
        def stopped?
          !started?
        end
        
        def finite?
          frame_count_limited? || cycle_limited? || duration_limited?
        end
        
        def infinite?
          !finite?
        end
        alias indefinite? infinite?
        
        def has_attribute?(attribute_name, *args)
          respond_to?(ruby_attribute_setter(attribute_name)) && respond_to?(ruby_attribute_getter(attribute_name))
        end
  
        def set_attribute(attribute_name, *args)
          send(ruby_attribute_setter(attribute_name), *args)
        end
  
        def get_attribute(attribute_name)
          send(ruby_attribute_getter(attribute_name))
        end
        
        def cycle=(*args)
          if args.size == 1
            if args.first.is_a?(Array)
              @cycle = args.first
            else
              @cycle = [args.first]
            end
          elsif args.size > 1
            @cycle = args
          end
        end
        
        def cycle_count_index=(value)
          @cycle_count_index = value
          self.finished = true if cycle_limited? && @cycle_count_index == @cycle_count
          @cycle_count_index
        end
        
        def frame_index=(value)
          @frame_index = value
          self.finished = true if frame_count_limited? && @frame_index == @frame_count
          @frame_index
        end
        
        def duration=(value)
          @duration = value
          self.finished = true if surpassed_duration_limit?
          @duration
        end
        
        def cycle_enabled?
          @cycle.is_a?(Array)
        end
        
        def cycle_limited?
          cycle_enabled? && @cycle_count.is_a?(Integer)
        end
        
        def duration_limited?
          @duration_limit.is_a?(Numeric) && @duration_limit > 0
        end
        
        def frame_count_limited?
          @frame_count.is_a?(Integer) && @frame_count > 0
        end
        
        def surpassed_duration_limit?
          duration_limited? && ((Time.now - @start_time) > @duration_limit)
        end
        
        def within_duration_limit?
          !surpassed_duration_limit?
        end
        
        private
        
        # Returns true on success of painting a frame and false otherwise
        def draw_frame(start_number)
          if stopped? or
              (start_number != @start_number) or
              (frame_count_limited? && @frame_index == @frame_count) or
              (cycle_limited? && @cycle_count_index == @cycle_count) or
              surpassed_duration_limit?
            self.duration = Time.now - @start_time
            return false
          end
          block_args = [@frame_index]
          block_args << @cycle[@frame_index % @cycle.length] if cycle_enabled?
          current_frame_index = @frame_index
          current_cycle_count_index = @cycle_count_index
          self.class.schedule_frame_animation(self) do
            self.duration = Time.now - @start_time # TODO should this be set here, after the if statement, in the else too, or outside both?
            if started? && start_number == @start_number && within_duration_limit?
              unless @parent.isDisposed
                @shapes.to_a.each(&:dispose)
                parent_shapes_before = @parent.shapes.clone
                @parent.content {
                  frame_block.call(*block_args)
                }
                @shapes = @parent.shapes - parent_shapes_before
                self.duration = Time.now - @start_time # TODO consider if this is needed
              end
            else
              if stopped? && @frame_index > current_frame_index
                self.frame_index = current_frame_index
                self.cycle_count_index = current_cycle_count_index
              end
            end
          end
          self.frame_index += 1
          self.cycle_count_index += 1 if cycle_limited? && (@frame_index % @cycle&.length&.to_i) == 0
          # TODO consider using timer_exec as a perhaps more reliable alternative
          if every.is_a?(Numeric) && every > 0
            sleep(every)
          elsif fps.is_a?(Numeric) && fps > 0
            sleep((BigDecimal(1.0.to_s) / BigDecimal(fps.to_f.to_s)).to_f)
          end
          true
        rescue => e
          Glimmer::Config.logger.error {e}
          false
        end
        
      end
      
    end
    
  end
  
end