# @provides MotionKit::Constraint # @provides MotionKit::CompoundConstraint module MotionKit class InvalidRelationshipError < Exception end class InvalidAttributeError < Exception end class InvalidPriorityError < Exception end class Constraint attr_accessor :target attr_accessor :attribute attr_accessor :relationship attr_accessor :relative_to attr_accessor :attribute2 attr_reader :multiplier attr_reader :constant attr_writer :priority attr_writer :identifier Priorities = { required: 1000, # NSLayoutPriorityRequired high: 750, # NSLayoutPriorityDefaultHigh medium: 500, low: 250, # NSLayoutPriorityDefaultLow } Relationships = { equal: NSLayoutRelationEqual, lte: NSLayoutRelationLessThanOrEqual, gte: NSLayoutRelationGreaterThanOrEqual, } Attributes = { none: NSLayoutAttributeNotAnAttribute, left: NSLayoutAttributeLeft, right: NSLayoutAttributeRight, top: NSLayoutAttributeTop, bottom: NSLayoutAttributeBottom, leading: NSLayoutAttributeLeading, trailing: NSLayoutAttributeTrailing, width: NSLayoutAttributeWidth, height: NSLayoutAttributeHeight, center_x: NSLayoutAttributeCenterX, center_y: NSLayoutAttributeCenterY, baseline: NSLayoutAttributeBaseline, } def initialize(target, attribute=nil, relationship) @target = target @attribute = attribute @attribute2 = attribute @relative_to = nil @relationship = relationship @multiplier = 1 @constant = 0 @priority = nil @compare_flag = false @active = true end # like `equals`, but also sets `compare_flag` to true, so you can use ==, # <=, and >= # # @example # x.is(10) # x.is(10).priority(:required) # x.is == 10 # (x.is == 10).priority :required # width.is >= 100 # height.is <= 200 def is(value=nil) if value self.equals(value) end @compare_flag = true self end def ==(compare) if @compare_flag equals(compare) self else super end end def >=(compare) if @compare_flag gte(compare) self else super end end def <=(compare) if @compare_flag lte(compare) self else super end end def equals(target, attribute2=nil) self.relationship ||= :equal if Constraint.constant?(target) self.constant = target elsif Constraint.calculate?(target) calc = Calculator.scan(target) self.multiplier = calc.factor self.constant = calc.constant || 0 else self.relative_to = target if attribute2 self.attribute2 = attribute2 end end @compare_flag = false self end alias is_equal_to equals alias equal_to equals def lte(target, attribute2=nil) self.relationship = :lte if Constraint.constant?(target) self.constant = target elsif Constraint.calculate?(target) calc = Calculator.scan(target) self.multiplier = calc.factor self.constant = calc.constant || 0 else self.relative_to = target if attribute2 self.attribute2 = attribute2 end end @compare_flag = false self end alias is_at_most lte alias at_most lte def gte(target, attribute2=nil) self.relationship = :gte if Constraint.constant?(target) self.constant = target elsif Constraint.calculate?(target) calc = Calculator.scan(target) self.multiplier = calc.factor self.constant = calc.constant || 0 else self.relative_to = target if attribute2 self.attribute2 = attribute2 end end @compare_flag = false self end alias is_at_least gte alias at_least gte # width.is('100%').of(:view, :width) def of(target, attribute2=nil) self.relative_to = target if attribute2 self.attribute2 = attribute2 end self end def constant=(constant) @constant = constant case Constraint.attribute_lookup(self.attribute) when NSLayoutAttributeLeft, NSLayoutAttributeRight, NSLayoutAttributeTop, NSLayoutAttributeBottom, NSLayoutAttributeLeading, NSLayoutAttributeTrailing, NSLayoutAttributeCenterX, NSLayoutAttributeCenterY, NSLayoutAttributeBaseline self.relative_to ||= :superview end self.update_constraint end def multiplier=(multiplier) @multiplier = multiplier self.update_constraint end def times(multiplier) self.multiplier *= multiplier self end def divided_by(multiplier) times 1.0/multiplier end # If no relationship has been set, the "use case" here is: # # c.plus(10).equals(:view, :x) # # Which is the same as # # c.equals(:view, :x).minus(10) def plus(constant) unless self.relationship constant = -constant end self.constant += constant self end def minus(constant) plus -constant end def priority(priority=nil) return @priority if priority.nil? @priority = priority self end def identifier(identifier=nil) return @identifier if identifier.nil? @identifier = identifier self end alias identified_by identifier def update_constraint if @resolved constraint = @resolved[0] constraint.constant = self.constant end end def resolve_all(layout, view) @resolved ||= begin item = Constraint.view_lookup(layout, view, self.target) rel_item = Constraint.view_lookup(layout, view, self.relative_to) relationship = Constraint.relationship_lookup(self.relationship) attribute1 = Constraint.attribute_lookup(self.attribute) attribute2 = Constraint.attribute_lookup(self.attribute2) nsconstraint = NSLayoutConstraint.constraintWithItem(item, attribute: attribute1, relatedBy: relationship, toItem: rel_item, attribute: attribute2, multiplier: self.multiplier, constant: self.constant ) if self.priority nsconstraint.priority = Constraint.priority_lookup(self.priority) end if self.identifier nsconstraint.setIdentifier(self.identifier) end [nsconstraint] end end def active @active end def active=(active) if @resolved if active @resolved.each do |constraint| common_ancestor.addConstraint(constraint) end else @resolved.each do |constraint| common_ancestor.removeConstraint(constraint) end end else @active = active end end def activate self.active = true self end def deactivate self.active = false self end def common_ancestor if @resolved @common_ancestor ||= begin constraint = @resolved[0] base_view_class = MotionKit.base_view_class if constraint.firstItem.is_a?(base_view_class) && constraint.secondItem.is_a?(base_view_class) common_ancestor = nil ancestors = [constraint.firstItem] parent_view = constraint.firstItem while parent_view = parent_view.superview ancestors << parent_view end current_view = constraint.secondItem while current_view if ancestors.include? current_view common_ancestor = current_view break end current_view = current_view.superview end unless common_ancestor raise NoCommonAncestorError.new("No common ancestors between #{constraint.firstItem} and #{constraint.secondItem}") end else common_ancestor = constraint.firstItem end common_ancestor end end end class << self def axis_lookup(axis) case axis when :horizontal UILayoutConstraintAxisHorizontal when :vertical UILayoutConstraintAxisVertical else axis end end def orientation_lookup(orientation) case orientation when :horizontal NSLayoutConstraintOrientationHorizontal when :vertical NSLayoutConstraintOrientationVertical else orientation end end def view_lookup(layout, view, target) if ! target || target.is_a?(MotionKit.base_view_class) target elsif target == :self view elsif target == :superview view.superview else layout.get_view(target) end end def attribute_lookup(attribute) if attribute.is_a? Fixnum return attribute end unless Attributes.key? attribute raise InvalidAttributeError.new("Unsupported attribute #{attribute.inspect}") end Attributes[attribute] end def priority_lookup(priority) if priority.is_a? Fixnum return priority end unless Priorities.key? priority raise InvalidPriorityError.new("Unsupported priority #{priority.inspect}") end Priorities[priority] end def relationship_lookup(relationship) if relationship.is_a? Fixnum return relationship end unless Relationships.key? relationship raise InvalidRelationshipError.new("Unsupported relationship #{relationship.inspect}") end Relationships[relationship] end def attribute_reverse(attribute) Attributes.key(attribute) || :none end def relationship_reverse(relationship) Relationships.key(relationship) end def calculate?(value) value.is_a?(String) end def constant?(value) value.is_a?(Numeric) || value.is_a?(Hash) || value.is_a?(Array) end end end class CompoundConstraint < Constraint def initialize(target, attribute=nil, relationship=:equal) super @constant = [0, 0] @multiplier = [1, 1] end def update_constraint if @resolved [0, 1].each do |index| constraint = @resolved[index] constraint.constant = self.constant[index] end end end end end