Class: Layouter::Parent

Inherits:
Element show all
Defined in:
lib/layouter/parent.rb

Constant Summary collapse

DIM =
{
  rows: :height,
  cols: :width,
}.freeze

Instance Attribute Summary

Attributes inherited from Element

#calculated_height, #calculated_width

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Element

#layout?

Constructor Details

#initialize(orientation, children) ⇒ Parent

Returns a new instance of Parent.



9
10
11
12
13
14
15
16
17
18
19
# File 'lib/layouter/parent.rb', line 9

def initialize(orientation, children)
  super()
  unless DIM.keys.include?(orientation)
    raise(ArgumentError.new("Invalid orientation"))
  end
  unless children.is_a?(Array) && children.all? { |c| c.is_a?(Element) }
    raise(ArgumentError.new("Invalid children"))
  end
  @orientation = orientation
  @children = children
end

Class Method Details

.distribute(value, importances, maxs) ⇒ Object

Raises:



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/layouter/parent.rb', line 70

def self.distribute(value, importances, maxs)
  raise(AssertionError) unless value.is_a?(Integer) # Avoid endless loops.
  raise(AssertionError) unless value <= maxs.sum # Avoid endless loops.
  raise(AssertionError) unless importances.length == maxs.length
  unless maxs.all? { |v| v.is_a?(Integer) || v == Float::INFINITY }
    raise(AssertionError)
  end
  data = importances.zip(maxs).map.with_index do |(importance, max), i|
    { index: i, value: 0, importance: importance, max: max }
  end
  while true # Distribute based on the importance, adhering to the maxs.
    extra = value - data.map { |h| h[:value] }.sum
    break if extra <= Float::EPSILON * data.length
    candidates = data.select { |h| h[:value] < h[:max] }
    denominator = candidates.map { |h| h[:importance] }.sum.to_f
    candidates.each do |h|
      share = extra * h[:importance] / denominator
      h[:value] = h[:value] + share > h[:max] ? h[:max] : h[:value] + share
    end
  end
  data.each { |h| h[:value] = h[:value].round }
  while true # Fill up any extra or missing value from turning to integers.
    extra = value - data.map { |h| h[:value] }.sum
    break if extra == 0
    delta = extra > 0 ? 1 : -1
    data = data.sort_by { |h| h[:importance] }
    data = data.reverse if extra > 0
    data.cycle do |h|
      if h[:max] > h[:value] && h[:value] > 0
        h[:value] += delta
        extra -= delta
        break if extra == 0
      end
    end
  end
  raise(AssertionError) unless data.map { |h| h[:value] }.sum == value
  raise(AssertionError) unless data.all? { |h| h[:value] <= h[:max] }
  data.sort_by { |h| h[:index] }.map { |h| h[:value] }
end

Instance Method Details

#[](index) ⇒ Object



21
22
23
# File 'lib/layouter/parent.rb', line 21

def [](index)
  @children[index]
end

#importanceObject



56
57
58
# File 'lib/layouter/parent.rb', line 56

def importance
  @children.map(&:importance).max
end

#layout(width, height) ⇒ Object

Raises:



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/layouter/parent.rb', line 25

def layout(width, height)
  dim = DIM[@orientation]
  value = binding.local_variable_get(dim)
  min, max = send(:"min_#{dim}"), send(:"max_#{dim}")
  raise(AssertionError) if min > max
  raise(LayoutError.new(dim, :too_small)) unless value >= min
  raise(LayoutError.new(dim, :too_big)) unless value <= max
  importances = @children.map(&:importance)
  maxs = @children.map { |c| c.send(:"max_#{dim}") - c.send(:"min_#{dim}") }
  extras = self.class.distribute(value - min, importances, maxs)
  @children.zip(extras).each do |child, extra|
    child.layout(
      dim == :width ? child.min_width + extra : width,
      dim == :height ? child.min_height + extra : height,
    )
  end
  @calculated_width, @calculated_height = width, height
  self
end

#renderObject



60
61
62
63
64
65
66
67
68
# File 'lib/layouter/parent.rb', line 60

def render
  layout!
  if @orientation == :rows
    @children.map(&:render).join("\n")
  elsif @orientation == :cols
    tmp = @children.map { |child| child.render.split("\n") }
    tmp[0].zip(*tmp[1..-1]).map(&:join).join("\n")
  end
end