AndyObtiva/glimmer-dsl-libui

View on GitHub
lib/glimmer/libui/shape.rb

Summary

Maintainability
C
1 day
Test Coverage
# Copyright (c) 2021-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/libui/parent'
require 'glimmer/libui/control_proxy/area_proxy'
require 'glimmer/libui/control_proxy/path_proxy'
require 'glimmer/libui/data_bindable'
require 'glimmer/libui/perfect_shaped'

module Glimmer
  module LibUI
    # Represents LibUI lightweight shape objects nested under path (e.g. line, rectangle, arc, bezier)
    class Shape
      class << self
        def exists?(keyword)
          keyword = KEYWORD_ALIASES[keyword] || keyword
          Shape.constants.include?(constant_symbol(keyword)) and
            shape_class(keyword).respond_to?(:ancestors) and
            shape_class(keyword).ancestors.include?(Shape)
        end
        
        def create(keyword, parent, args, &block)
          keyword = KEYWORD_ALIASES[keyword] || keyword
          shape_class(keyword).new(keyword, parent, args, &block)
        end
        
        def shape_class(keyword)
          keyword = KEYWORD_ALIASES[keyword] || keyword
          Shape.const_get(constant_symbol(keyword))
        end
        
        def parameters(*params)
          if params.empty?
            @parameters
          else
            @parameters = params
          end
        end
        
        def parameter_defaults(*defaults)
          if defaults.empty?
            @parameter_defaults
          else
            @parameter_defaults = defaults
          end
        end
        
        def constant_symbol(keyword)
          keyword = KEYWORD_ALIASES[keyword] || keyword
          "#{keyword.camelcase(:upper)}".to_sym
        end
      end
      
      include Parent
      include PerfectShaped
      include DataBindable
      
      KEYWORD_ALIASES = {
        'shape' => 'composite_shape',
      }
      
      attr_reader :parent, :args, :keyword, :block, :content_added
      alias content_added? content_added
      
      def initialize(keyword, parent, args, &block)
        keyword = KEYWORD_ALIASES[keyword] || keyword
        @keyword = keyword
        @parent = parent
        @args = args
        @block = block
        set_parameter_defaults
        build_control if implicit_path?
        post_add_content if @block.nil?
      end
      
      # Subclasses may override to perform post add_content work (normally must call super)
      def post_add_content
        unless @content_added
          @parent&.post_initialize_child(self)
          @parent.post_add_content if implicit_path? && dynamic?
          @content_added = true
        end
      end
      
      def post_initialize_child(child, add_child: true)
        if child.is_a?(ControlProxy::MatrixProxy)
          path_proxy.post_initialize_child(child, add_child: add_child)
        else
          super(child, add_child: add_child)
        end
      end
      
      def content(&block)
        Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::Libui::ShapeExpression.new, @keyword, {post_add_content: @content_added}, &block)
        request_auto_redraw
      end
      
      # Subclasses must override to perform draw work and call super afterwards to ensure calling destroy when semi-declarative in an on_draw method
      def draw(area_draw_params)
        destroy if area_proxy.nil?
      end
      
      def redraw
        area_proxy&.redraw
      end
      
      def request_auto_redraw
        area_proxy&.request_auto_redraw
      end
      
      def destroy
        return if ControlProxy.main_window_proxy&.destroying?
        @parent.children.delete(self)
      end
      
      def area_proxy
        find_parent_in_ancestors { |parent| parent.nil? || parent.is_a?(ControlProxy::AreaProxy) }
      end
      
      def path_proxy
        find_parent_in_ancestors { |parent| parent.nil? || parent.is_a?(ControlProxy::PathProxy) }
      end
      
      def composite_shape
        find_parent_in_ancestors { |parent| parent.nil? || parent.is_a?(CompositeShape) }
      end
    
      def fill(*args)
        path_proxy.fill(*args)
      end
      alias fill= fill
      alias set_fill fill
      
      def stroke(*args)
        path_proxy.stroke(*args)
      end
      alias stroke= stroke
      alias set_stroke stroke
      
      def transform(matrix = nil)
        path_proxy.transform(matrix)
      end
      alias transform= transform
      alias set_transform transform
      
      def absolute_x
        if composite_shape
          composite_shape.relative_x(x)
        else
          x
        end
      end
      
      def absolute_y
        if composite_shape
          composite_shape.relative_y(y)
        else
          y
        end
      end
      
      def absolute_point_array
        # TODO Consider moving this method into a module mixed into all shapes having point_array
        return unless respond_to?(:point_array)
        
        point_array = self.point_array || []
        require 'perfect-shape'
        point_array = PerfectShape::MultiPoint.normalize_point_array(point_array)
        point_array.map do |point|
          if composite_shape
            composite_shape.relative_point(*point)
          else
            point
          end
        end
      end
      
      def absolute_x_center
        # TODO Consider moving this method into a module mixed into all shapes having x_center
        return unless respond_to?(:x_center)
        
        if composite_shape
          composite_shape.relative_x(x_center)
        else
          x_center
        end
      end
      
      def absolute_y_center
        # TODO Consider moving this method into a module mixed into all shapes having y_center
        return unless respond_to?(:y_center)
        
        if composite_shape
          composite_shape.relative_y(y_center)
        else
          y_center
        end
      end
      
      def absolute_c1_x
        # TODO Consider moving this method into a module mixed into all shapes having c1_x
        return unless respond_to?(:c1_x)
        
        if composite_shape
          composite_shape.relative_x(c1_x)
        else
          c1_x
        end
      end
      
      def absolute_c1_y
        # TODO Consider moving this method into a module mixed into all shapes having c1_y
        return unless respond_to?(:c1_y)
        
        if composite_shape
          composite_shape.relative_x(c1_y)
        else
          c1_y
        end
      end
      
      def absolute_c2_x
        # TODO Consider moving this method into a module mixed into all shapes having c2_x
        return unless respond_to?(:c2_x)
        
        if composite_shape
          composite_shape.relative_x(c2_x)
        else
          c2_x
        end
      end
      
      def absolute_c2_y
        # TODO Consider moving this method into a module mixed into all shapes having c2_y
        return unless respond_to?(:c2_y)
        
        if composite_shape
          composite_shape.relative_x(c2_y)
        else
          c2_y
        end
      end
      
      def absolute_end_x
        # TODO Consider moving this method into a module mixed into all shapes having end_x
        return unless respond_to?(:end_x)
        
        if composite_shape
          composite_shape.relative_x(end_x)
        else
          end_x
        end
      end
      
      def absolute_end_y
        # TODO Consider moving this method into a module mixed into all shapes having end_y
        return unless respond_to?(:end_y)
        
        if composite_shape
          composite_shape.relative_x(end_y)
        else
          end_y
        end
      end
      
      def can_handle_listener?(listener_name)
        area_proxy&.can_handle_listener?(listener_name)
      end
      
      def handle_listener(listener_name, &listener)
        area_proxy&.handle_listener(listener_name) do |event|
          listener.call(event) if include?(event[:x], event[:y])
        end
      end

      def respond_to?(method_name, *args, &block)
        self.class.parameters.include?(method_name.to_s.sub(/=$/, '').sub(/^set_/, '').to_sym) or
          super(method_name, true)
      end
      
      def method_missing(method_name, *args, &block)
        method_name_parameter = method_name.to_s.sub(/=$/, '').sub(/^set_/, '').to_sym
        if self.class.parameters.include?(method_name_parameter)
          method_name = method_name.to_s
          parameter_index = self.class.parameters.index(method_name_parameter)
          if method_name.start_with?('set_') || method_name.end_with?('=') || !args.empty?
            args = [args] if args.size > 1
            if args.first != @args[parameter_index]
              @args[parameter_index] = args.first
              request_auto_redraw
            end
          else
            @args[parameter_index]
          end
        else # TODO consider if there is a need to redirect anything to path proxy or delete this TODO
          super
        end
      end
      
      # indicates if nested directly under area or on_draw event (having an implicit path not an explicit path parent)
      def implicit_path?
        @implicit_path ||= !!(
          @parent.is_a?(Glimmer::LibUI::ControlProxy::AreaProxy) ||
          @parent.is_a?(Glimmer::LibUI::Shape::CompositeShape) ||
          (@parent.nil? && Glimmer::LibUI::ControlProxy::AreaProxy.current_area_draw_params)
        )
      end
      
      private
      
      def build_control
        block = Proc.new {} if dynamic?
        @parent = Glimmer::LibUI::ControlProxy::PathProxy.new('path', @parent, [], &block)
      end
      
      def dynamic?
        ((@parent.nil? || (@parent.is_a?(ControlProxy::PathProxy) && @parent.parent_proxy.nil?)) && Glimmer::LibUI::ControlProxy::AreaProxy.current_area_draw_params)
      end
      
      def set_parameter_defaults
        self.class.parameter_defaults.each_with_index do |default, i|
          @args[i] ||= default
        end
      end
      
      def find_parent_in_ancestors(&condition)
        found = self
        until condition.call(found)
          found = found.respond_to?(:parent_proxy) ? found.parent_proxy : found.parent
        end
        found
      end
    end
  end
end

Dir[File.expand_path("./#{File.basename(__FILE__, '.rb')}/*.rb", __dir__)].each {|f| require f}