whirled_peas 0.1.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +32 -0
  4. data/Gemfile +1 -0
  5. data/README.md +240 -79
  6. data/Rakefile +37 -3
  7. data/bin/debug +35 -0
  8. data/exe/whirled_peas +7 -0
  9. data/lib/whirled_peas.rb +17 -37
  10. data/lib/whirled_peas/command_line.rb +263 -0
  11. data/lib/whirled_peas/config.rb +21 -0
  12. data/lib/whirled_peas/debugger.rb +80 -0
  13. data/lib/whirled_peas/errors.rb +7 -0
  14. data/lib/whirled_peas/frame.rb +0 -8
  15. data/lib/whirled_peas/frame/consumer.rb +14 -51
  16. data/lib/whirled_peas/frame/debug_consumer.rb +30 -0
  17. data/lib/whirled_peas/frame/event_loop.rb +68 -38
  18. data/lib/whirled_peas/frame/producer.rb +37 -34
  19. data/lib/whirled_peas/graphics.rb +5 -0
  20. data/lib/whirled_peas/graphics/box_painter.rb +100 -0
  21. data/lib/whirled_peas/graphics/canvas.rb +107 -0
  22. data/lib/whirled_peas/graphics/container_coords.rb +61 -0
  23. data/lib/whirled_peas/graphics/container_dimensions.rb +65 -0
  24. data/lib/whirled_peas/graphics/container_painter.rb +116 -0
  25. data/lib/whirled_peas/graphics/debugger.rb +43 -0
  26. data/lib/whirled_peas/graphics/grid_painter.rb +56 -0
  27. data/lib/whirled_peas/graphics/mock_screen.rb +26 -0
  28. data/lib/whirled_peas/graphics/painter.rb +24 -0
  29. data/lib/whirled_peas/graphics/renderer.rb +49 -0
  30. data/lib/whirled_peas/graphics/screen.rb +65 -0
  31. data/lib/whirled_peas/graphics/text_dimensions.rb +15 -0
  32. data/lib/whirled_peas/graphics/text_painter.rb +38 -0
  33. data/lib/whirled_peas/settings.rb +5 -0
  34. data/lib/whirled_peas/settings/bg_color.rb +22 -0
  35. data/lib/whirled_peas/settings/border.rb +101 -0
  36. data/lib/whirled_peas/settings/box_settings.rb +8 -0
  37. data/lib/whirled_peas/settings/color.rb +69 -0
  38. data/lib/whirled_peas/settings/container_settings.rb +128 -0
  39. data/lib/whirled_peas/settings/debugger.rb +87 -0
  40. data/lib/whirled_peas/settings/display_flow.rb +25 -0
  41. data/lib/whirled_peas/settings/element_settings.rb +61 -0
  42. data/lib/whirled_peas/settings/grid_settings.rb +11 -0
  43. data/lib/whirled_peas/settings/margin.rb +8 -0
  44. data/lib/whirled_peas/settings/padding.rb +8 -0
  45. data/lib/whirled_peas/settings/position.rb +15 -0
  46. data/lib/whirled_peas/settings/spacing.rb +24 -0
  47. data/lib/whirled_peas/settings/text_align.rb +19 -0
  48. data/lib/whirled_peas/settings/text_color.rb +19 -0
  49. data/lib/whirled_peas/settings/text_settings.rb +15 -0
  50. data/lib/whirled_peas/template.rb +5 -0
  51. data/lib/whirled_peas/template/box_element.rb +8 -0
  52. data/lib/whirled_peas/template/composer.rb +68 -0
  53. data/lib/whirled_peas/template/container.rb +28 -0
  54. data/lib/whirled_peas/template/debugger.rb +34 -0
  55. data/lib/whirled_peas/template/element.rb +13 -0
  56. data/lib/whirled_peas/template/grid_element.rb +8 -0
  57. data/lib/whirled_peas/template/text_element.rb +24 -0
  58. data/lib/whirled_peas/utils.rb +5 -0
  59. data/lib/whirled_peas/utils/ansi.rb +53 -0
  60. data/lib/whirled_peas/utils/formatted_string.rb +64 -0
  61. data/lib/whirled_peas/utils/title_font.rb +75 -0
  62. data/lib/whirled_peas/version.rb +1 -1
  63. data/screen_test/rendered/elements/box.frame +1 -0
  64. data/screen_test/rendered/elements/box.rb +20 -0
  65. data/screen_test/rendered/elements/grid.frame +1 -0
  66. data/screen_test/rendered/elements/grid.rb +13 -0
  67. data/screen_test/rendered/elements/screen_overflow.rb +9 -0
  68. data/screen_test/rendered/elements/text.frame +1 -0
  69. data/screen_test/rendered/elements/text.rb +9 -0
  70. data/screen_test/rendered/elements/text_multiline.frame +1 -0
  71. data/screen_test/rendered/elements/text_multiline.rb +9 -0
  72. data/screen_test/rendered/settings/align/box.frame +1 -0
  73. data/screen_test/rendered/settings/align/box.rb +20 -0
  74. data/screen_test/rendered/settings/align/children_center.frame +1 -0
  75. data/screen_test/rendered/settings/align/children_center.rb +13 -0
  76. data/screen_test/rendered/settings/align/children_left.frame +1 -0
  77. data/screen_test/rendered/settings/align/children_left.rb +13 -0
  78. data/screen_test/rendered/settings/align/children_right.frame +1 -0
  79. data/screen_test/rendered/settings/align/children_right.rb +13 -0
  80. data/screen_test/rendered/settings/align/grid.frame +1 -0
  81. data/screen_test/rendered/settings/align/grid.rb +20 -0
  82. data/screen_test/rendered/settings/ansi/bold.frame +1 -0
  83. data/screen_test/rendered/settings/ansi/bold.rb +15 -0
  84. data/screen_test/rendered/settings/ansi/color.frame +1 -0
  85. data/screen_test/rendered/settings/ansi/color.rb +37 -0
  86. data/screen_test/rendered/settings/ansi/underline.frame +1 -0
  87. data/screen_test/rendered/settings/ansi/underline.rb +15 -0
  88. data/screen_test/rendered/settings/border.frame +1 -0
  89. data/screen_test/rendered/settings/border.rb +13 -0
  90. data/screen_test/rendered/settings/flow/l2r.frame +1 -0
  91. data/screen_test/rendered/settings/flow/l2r.rb +24 -0
  92. data/screen_test/rendered/settings/flow/t2b.frame +1 -0
  93. data/screen_test/rendered/settings/flow/t2b.rb +24 -0
  94. data/screen_test/rendered/settings/height/box.frame +1 -0
  95. data/screen_test/rendered/settings/height/box.rb +13 -0
  96. data/screen_test/rendered/settings/height/grid.frame +1 -0
  97. data/screen_test/rendered/settings/height/grid.rb +14 -0
  98. data/screen_test/rendered/settings/height/overflow_box.frame +1 -0
  99. data/screen_test/rendered/settings/height/overflow_box.rb +13 -0
  100. data/screen_test/rendered/settings/height/overflow_box_l2r.frame +1 -0
  101. data/screen_test/rendered/settings/height/overflow_box_l2r.rb +15 -0
  102. data/screen_test/rendered/settings/height/overflow_box_t2b.frame +1 -0
  103. data/screen_test/rendered/settings/height/overflow_box_t2b.rb +14 -0
  104. data/screen_test/rendered/settings/height/overflow_grid.frame +1 -0
  105. data/screen_test/rendered/settings/height/overflow_grid.rb +16 -0
  106. data/screen_test/rendered/settings/margin.frame +1 -0
  107. data/screen_test/rendered/settings/margin.rb +14 -0
  108. data/screen_test/rendered/settings/padding.frame +1 -0
  109. data/screen_test/rendered/settings/padding.rb +11 -0
  110. data/screen_test/rendered/settings/position/box_left.frame +1 -0
  111. data/screen_test/rendered/settings/position/box_left.rb +17 -0
  112. data/screen_test/rendered/settings/position/box_left_negative.frame +1 -0
  113. data/screen_test/rendered/settings/position/box_left_negative.rb +17 -0
  114. data/screen_test/rendered/settings/position/box_top.frame +1 -0
  115. data/screen_test/rendered/settings/position/box_top.rb +17 -0
  116. data/screen_test/rendered/settings/position/box_top_negative.frame +1 -0
  117. data/screen_test/rendered/settings/position/box_top_negative.rb +17 -0
  118. data/screen_test/rendered/settings/position/grid_left.frame +1 -0
  119. data/screen_test/rendered/settings/position/grid_left.rb +18 -0
  120. data/screen_test/rendered/settings/position/grid_left_negative.frame +1 -0
  121. data/screen_test/rendered/settings/position/grid_left_negative.rb +18 -0
  122. data/screen_test/rendered/settings/position/grid_top.frame +1 -0
  123. data/screen_test/rendered/settings/position/grid_top.rb +18 -0
  124. data/screen_test/rendered/settings/position/grid_top_negative.frame +1 -0
  125. data/screen_test/rendered/settings/position/grid_top_negative.rb +18 -0
  126. data/screen_test/rendered/settings/title_font.frame +1 -0
  127. data/screen_test/rendered/settings/title_font.rb +12 -0
  128. data/screen_test/rendered/settings/width/box.frame +1 -0
  129. data/screen_test/rendered/settings/width/box.rb +13 -0
  130. data/screen_test/rendered/settings/width/grid.frame +1 -0
  131. data/screen_test/rendered/settings/width/grid.rb +14 -0
  132. data/screen_test/rendered/settings/width/overflow_box.frame +1 -0
  133. data/screen_test/rendered/settings/width/overflow_box.rb +11 -0
  134. data/screen_test/rendered/settings/width/overflow_box_l2r.frame +1 -0
  135. data/screen_test/rendered/settings/width/overflow_box_l2r.rb +14 -0
  136. data/screen_test/rendered/settings/width/overflow_box_t2b.frame +1 -0
  137. data/screen_test/rendered/settings/width/overflow_box_t2b.rb +15 -0
  138. data/screen_test/rendered/settings/width/overflow_grid.frame +1 -0
  139. data/screen_test/rendered/settings/width/overflow_grid.rb +14 -0
  140. data/screen_test/screen_tester.rb +191 -0
  141. data/whirled_peas.gemspec +4 -2
  142. metadata +136 -20
  143. data/lib/whirled_peas/ui.rb +0 -7
  144. data/lib/whirled_peas/ui/ansi.rb +0 -154
  145. data/lib/whirled_peas/ui/canvas.rb +0 -35
  146. data/lib/whirled_peas/ui/element.rb +0 -199
  147. data/lib/whirled_peas/ui/painter.rb +0 -283
  148. data/lib/whirled_peas/ui/screen.rb +0 -62
  149. data/lib/whirled_peas/ui/settings.rb +0 -512
  150. data/lib/whirled_peas/ui/stroke.rb +0 -29
  151. data/sandbox/auto.rb +0 -13
  152. data/sandbox/box.rb +0 -19
  153. data/sandbox/grid.rb +0 -13
  154. data/sandbox/sandbox.rb +0 -17
  155. data/sandbox/text.rb +0 -33
@@ -0,0 +1,7 @@
1
+ module WhirledPeas
2
+ class Error < StandardError; end
3
+
4
+ class ConfigurationError < Error; end
5
+
6
+ class SettingsError < Error; end
7
+ end
@@ -1,13 +1,5 @@
1
- require_relative 'frame/consumer'
2
- require_relative 'frame/producer'
3
-
4
1
  module WhirledPeas
5
2
  module Frame
6
- TERMINATE = '__term__'
7
- EOF = '__EOF__'
8
-
9
- DEFAULT_ADDRESS = 'localhost'
10
- DEFAULT_PORT = 8765
11
3
  end
12
4
 
13
5
  private_constant :Frame
@@ -1,67 +1,30 @@
1
- require 'socket'
2
- require 'json'
3
-
4
- require_relative 'event_loop'
5
-
6
1
  module WhirledPeas
7
2
  module Frame
3
+ # Abstract class for consuming frame events.
8
4
  class Consumer
9
- LOGGER_ID = 'CONSUMER'
5
+ EOF = '__EOF__'
10
6
 
11
- def initialize(template_factory, refresh_rate, logger=NullLogger.new)
12
- @event_loop = EventLoop.new(template_factory, refresh_rate, logger)
13
- @logger = logger
14
- @running = false
15
- @mutex = Mutex.new
7
+ def enqueue(name, duration, args)
8
+ raise NotImplemented, "#{self.class} must implement #enqueue"
16
9
  end
17
10
 
18
- def start(host:, port:)
19
- mutex.synchronize { @running = true }
20
- loop_thread = Thread.new do
21
- Thread.current.report_on_exception = false
22
- event_loop.start
23
- end
24
- socket = TCPSocket.new(host, port)
25
- logger.info(LOGGER_ID) { "Connected to #{host}:#{port}" }
26
- while @running && event_loop.running?
27
- line = socket.gets
28
- if line.nil?
29
- sleep(0.001)
30
- next
31
- end
32
- args = JSON.parse(line)
33
- name = args.delete('name')
34
- if [Frame::EOF, Frame::TERMINATE].include?(name)
35
- logger.info(LOGGER_ID) { "Received #{name} event, stopping..." }
36
- event_loop.stop if name == Frame::TERMINATE
37
- @running = false
38
- else
39
- duration = args.delete('duration')
40
- event_loop.enqueue(name, duration, args)
41
- end
42
- end
43
- logger.info(LOGGER_ID) { 'Exited normally' }
44
- logger.info(LOGGER_ID) { 'Waiting for loop thread to exit' }
45
- loop_thread.join
46
- rescue => e
47
- event_loop.stop if event_loop.running?
48
- logger.warn(LOGGER_ID) { 'Exited with error' }
49
- logger.error(LOGGER_ID) { e.message }
50
- logger.error(LOGGER_ID) { e.backtrace.join("\n") }
51
- raise
52
- ensure
53
- logger.info(LOGGER_ID) { 'Closing socket' }
54
- socket.close if socket
11
+ def running?
12
+ @running == true
13
+ end
14
+
15
+ def start
16
+ self.running = true
55
17
  end
56
18
 
57
19
  def stop
58
- logger.info(LOGGER_ID) { 'Stopping...' }
59
- mutex.synchronize { @running = false }
20
+ enqueue(EOF, nil, {})
60
21
  end
61
22
 
62
23
  private
63
24
 
64
- attr_reader :event_loop, :logger, :mutex
25
+ attr_writer :running
65
26
  end
27
+
28
+ private_constant :Consumer
66
29
  end
67
30
  end
@@ -0,0 +1,30 @@
1
+ require 'whirled_peas/null_logger'
2
+
3
+ require_relative 'consumer'
4
+
5
+ module WhirledPeas
6
+ module Frame
7
+ class DebugConsumer < Consumer
8
+ LOGGER_ID = 'PRINTER'
9
+
10
+ def initialize(output=STDOUT, logger=NullLogger.new)
11
+ @output = output
12
+ @logger = logger
13
+ end
14
+
15
+ def enqueue(name, duration, args)
16
+ if name == EOF
17
+ output.puts "EOF frame detected"
18
+ else
19
+ displayed_for = duration ? "#{duration} second(s)" : '1 frame'
20
+ args_str = args.empty? ? '' : " '#{JSON.generate(args)}'"
21
+ output.puts "Frame '#{name}' displayed for #{displayed_for}#{args_str}"
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :output, :logger
28
+ end
29
+ end
30
+ end
@@ -1,60 +1,90 @@
1
+ require 'whirled_peas/null_logger'
2
+ require 'whirled_peas/graphics/screen'
3
+
4
+ require_relative 'consumer'
5
+
1
6
  module WhirledPeas
2
7
  module Frame
3
- class EventLoop
4
- def initialize(template_factory, refresh_rate, logger=NullLogger.new)
8
+ class EventLoop < Consumer
9
+ DEFAULT_REFRESH_RATE = 30
10
+
11
+ LOGGER_ID = 'EVENT LOOP'
12
+
13
+ def initialize(
14
+ template_factory,
15
+ loading_template_factory=nil,
16
+ refresh_rate: DEFAULT_REFRESH_RATE,
17
+ logger: NullLogger.new,
18
+ screen: Graphics::Screen.new
19
+ )
5
20
  @template_factory = template_factory
21
+ @loading_template_factory = loading_template_factory
6
22
  @queue = Queue.new
7
- @refresh_rate = refresh_rate
23
+ @frame_duration = 1.0 / refresh_rate
8
24
  @logger = logger
25
+ @screen = screen
9
26
  end
10
27
 
11
28
  def enqueue(name, duration, args)
12
- queue.push([name, duration, args])
13
- end
14
-
15
- def running?
16
- @running
29
+ # If duration is nil, set it to the duration of a single frame
30
+ queue.push([name, duration || frame_duration, args])
17
31
  end
18
32
 
19
33
  def start
20
- logger.info('EVENT LOOP') { 'Starting' }
21
- @running = true
22
- screen = UI::Screen.new
23
- sleep(0.01) while queue.empty? # Wait for the first event
24
- remaining_frames = 1
25
- template = nil
26
- while @running && remaining_frames > 0
27
- frame_at = Time.now
28
- next_frame_at = frame_at + 1.0 / refresh_rate
29
- remaining_frames -= 1
30
- if remaining_frames > 0
31
- screen.refresh if screen.needs_refresh?
32
- elsif !queue.empty?
33
- name, duration, args = queue.pop
34
- remaining_frames = duration ? duration * refresh_rate : 1
35
- template = template_factory.build(name, args)
36
- screen.paint(template)
37
- end
38
- sleep(next_frame_at - Time.now)
39
- end
40
- logger.info('EVENT LOOP') { 'Exiting normally' }
34
+ super
35
+ wait_for_content
36
+ play_content
41
37
  rescue
42
- logger.warn('EVENT LOOP') { 'Exiting with error' }
43
- @running = false
38
+ self.running = false
39
+ logger.warn(LOGGER_ID) { 'Exiting with error' }
44
40
  raise
45
41
  ensure
46
- screen.finalize if screen
42
+ screen.finalize
47
43
  end
48
44
 
49
- def stop
50
- logger.info('EVENT LOOP') { 'Stopping...' }
51
- @running = false
45
+ private
46
+
47
+ attr_reader :template_factory, :loading_template_factory, :queue, :frame_duration, :logger, :screen
48
+
49
+ def wait_for_content
50
+ if loading_template_factory
51
+ play_loading_screen
52
+ else
53
+ sleep(frame_duration) while queue.empty?
54
+ end
52
55
  end
53
56
 
54
- private
57
+ def play_loading_screen
58
+ while queue.empty?
59
+ screen.paint(loading_template_factory.build)
60
+ sleep(frame_duration)
61
+ end
62
+ end
55
63
 
56
- attr_reader :template_factory, :queue, :refresh_rate, :logger
64
+ def play_content
65
+ template = nil
66
+ frame_until = Time.new(0) # Tell the loop to immediately pick up a new frame
67
+ while running?
68
+ frame_start = Time.now
69
+ next_frame_at = frame_start + frame_duration
70
+ if frame_until > frame_start
71
+ # While we're still displaying the previous frame, refresh the screen
72
+ screen.refresh
73
+ elsif !queue.empty?
74
+ name, duration, args = queue.pop
75
+ if name == EOF
76
+ self.running = false
77
+ else
78
+ frame_until = frame_start + duration
79
+ template = template_factory.build(name, args)
80
+ screen.paint(template)
81
+ end
82
+ else
83
+ wait_for_content
84
+ end
85
+ sleep([0, next_frame_at - Time.now].max)
86
+ end
87
+ end
57
88
  end
58
- private_constant :EventLoop
59
89
  end
60
90
  end
@@ -1,64 +1,67 @@
1
1
  require 'socket'
2
2
  require 'json'
3
3
 
4
+ require 'whirled_peas/null_logger'
5
+
4
6
  module WhirledPeas
5
7
  module Frame
8
+ # A Producer is the object given to the driver as the interface that allows
9
+ # the driver to emit frame events. The recommended way of creating a Producer
10
+ # is by invoking `Producer.produce` as it handles the lifecycle methods of
11
+ # the consumer.
6
12
  class Producer
7
13
  LOGGER_ID = 'PRODUCER'
8
14
 
9
- def self.start(logger: NullLogger.new, host:, port:, &block)
10
- server = TCPServer.new(host, port)
11
- client = server.accept
12
- logger.info(LOGGER_ID) { "Connected to #{host}:#{port}" }
13
- producer = new(client, logger)
15
+ # Manages the consumer lifecycle and yields a Producer to send frames to the
16
+ # consumer
17
+ #
18
+ # @param consumer [Consumer] instance that consumes frame events through
19
+ # `#enqueue`
20
+ def self.produce(consumer, logger=NullLogger.new)
21
+ producer = new(consumer, logger)
22
+ consumer_thread = Thread.new do
23
+ Thread.current.report_on_exception = false
24
+ consumer.start
25
+ end
14
26
  yield producer
15
- logger.info(LOGGER_ID) { 'Exited normally' }
27
+ producer.flush
16
28
  rescue => e
17
- producer.terminate
18
29
  logger.warn(LOGGER_ID) { 'Exited with error' }
19
- logger.error(LOGGER_ID) { e.message }
20
- logger.error(LOGGER_ID) { e.backtrace.join("\n") }
30
+ logger.error(LOGGER_ID) { e }
21
31
  raise
22
32
  ensure
23
- if client
24
- logger.info(LOGGER_ID) { 'Closing connection'}
25
- client.close
26
- end
33
+ consumer.stop
34
+ consumer_thread.join if consumer_thread
27
35
  end
28
36
 
29
- def initialize(client, logger=NullLogger.new)
30
- @client = client
37
+ def initialize(consumer, logger=NullLogger.new)
38
+ @consumer = consumer
31
39
  @logger = logger
32
40
  @queue = Queue.new
33
41
  end
34
42
 
35
- def send(name, duration: nil, args: {})
36
- client.puts(JSON.generate('name' => name, 'duration' => duration, **args))
37
- logger.debug(LOGGER_ID) { "Sending frame: #{name}" }
38
- end
39
-
40
- def enqueue(name, duration: nil, args: {})
43
+ # Buffer a frame to be played for the given duration. `#flush` must be called
44
+ # for frames to get pushed to the EventLoop.
45
+ #
46
+ # @param name [String] name of frame, which is passed to #build of the
47
+ # TemplateFactory
48
+ # @param duration [Float|Integer] duration in seconds the frame should be,
49
+ # displayed (default is nil, which results in a duration of a single refresh
50
+ # cycle)
51
+ # @param args [Hash] key/value pair of arguments, which is passed to #build of
52
+ # the TemplateFactory
53
+ def send_frame(name, duration: nil, args: {})
41
54
  queue.push([name, duration, args])
42
55
  end
43
56
 
57
+ # Send any buffered frames to the EventLoop
44
58
  def flush
45
- while !queue.empty?
46
- name, duration, args = queue.pop
47
- send(name, duration: duration, args: args)
48
- end
49
- end
50
-
51
- def stop
52
- send(Frame::EOF)
53
- end
54
-
55
- def terminate
56
- send(Frame::TERMINATE)
59
+ consumer.enqueue(*queue.pop) while !queue.empty?
57
60
  end
58
61
 
59
62
  private
60
63
 
61
- attr_reader :client, :logger, :queue
64
+ attr_reader :consumer, :logger, :queue
62
65
  end
63
66
  end
64
67
  end
@@ -0,0 +1,5 @@
1
+ module WhirledPeas
2
+ module Graphics
3
+ end
4
+ private_constant :Graphics
5
+ end
@@ -0,0 +1,100 @@
1
+ require_relative 'container_painter'
2
+ require_relative 'container_dimensions'
3
+
4
+ module WhirledPeas
5
+ module Graphics
6
+ class BoxPainter < ContainerPainter
7
+ def paint(canvas, &block)
8
+ super
9
+ return unless canvas.writable?
10
+ if element.settings.horizontal_flow?
11
+ paint_horizontally(canvas, &block)
12
+ else
13
+ paint_vertically(canvas, &block)
14
+ end
15
+ end
16
+
17
+ def dimensions
18
+ @dimensions ||= begin
19
+ content_width = 0
20
+ content_height = 0
21
+ if settings.horizontal_flow?
22
+ each_child do |child|
23
+ content_width += child.dimensions.outer_width
24
+ if child.dimensions.outer_height > content_height
25
+ content_height = child.dimensions.outer_height
26
+ end
27
+ end
28
+ else
29
+ each_child do |child|
30
+ if child.dimensions.outer_width > content_width
31
+ content_width = child.dimensions.outer_width
32
+ end
33
+ content_height += child.dimensions.outer_height
34
+ end
35
+ end
36
+ ContainerDimensions.new(settings, content_width, content_height)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def paint_horizontally(canvas, &block)
43
+ stroke_top = coords(canvas).content_top
44
+ stroke_left = coords(canvas).content_left
45
+ total_child_width = 0
46
+ each_child { |c| total_child_width += c.dimensions.outer_width }
47
+ if settings.align_center?
48
+ stroke_left += (dimensions.content_width - total_child_width) / 2
49
+ elsif settings.align_right?
50
+ stroke_left += dimensions.content_width - total_child_width
51
+ end
52
+ given_width = 0
53
+ each_child do |child|
54
+ child_width = [
55
+ child.dimensions.outer_width,
56
+ dimensions.content_width - given_width
57
+ ].min
58
+ child_canvas = canvas.child(
59
+ stroke_left + given_width,
60
+ stroke_top,
61
+ child_width,
62
+ [dimensions.content_height, child.dimensions.outer_height].min
63
+ )
64
+ child.paint(child_canvas, &block)
65
+ given_width += child_width
66
+ break if given_width == dimensions.content_width
67
+ end
68
+ end
69
+
70
+ def paint_vertically(canvas, &block)
71
+ stroke_top = coords(canvas).content_top
72
+ stroke_left = coords(canvas).content_left
73
+ given_height = 0
74
+ each_child do |child|
75
+ if settings.align_center?
76
+ justify_offset = (dimensions.content_width - child.dimensions.outer_width) / 2
77
+ elsif settings.align_right?
78
+ justify_offset = dimensions.content_width - child.dimensions.outer_width
79
+ else
80
+ justify_offset = 0
81
+ end
82
+ child_height = [
83
+ child.dimensions.outer_height,
84
+ dimensions.content_height - given_height
85
+ ].min
86
+ child_canvas = canvas.child(
87
+ stroke_left + justify_offset,
88
+ stroke_top + given_height,
89
+ [dimensions.content_width, child.dimensions.outer_width].min,
90
+ child_height
91
+ )
92
+ child.paint(child_canvas, &block)
93
+ given_height += child_height
94
+ break if given_height == dimensions.content_height
95
+ end
96
+ end
97
+ end
98
+ private_constant :BoxPainter
99
+ end
100
+ end