whirled_peas 0.2.0 → 0.6.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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -0
  3. data/CHANGELOG.md +38 -0
  4. data/Gemfile +1 -0
  5. data/README.md +275 -81
  6. data/Rakefile +38 -3
  7. data/examples/intro.rb +52 -0
  8. data/examples/scrolling.rb +53 -0
  9. data/exe/whirled_peas +7 -0
  10. data/lib/whirled_peas.rb +12 -42
  11. data/lib/whirled_peas/command_line.rb +255 -0
  12. data/lib/whirled_peas/config.rb +21 -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 -50
  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 +36 -32
  19. data/lib/whirled_peas/graphics.rb +5 -0
  20. data/lib/whirled_peas/graphics/box_painter.rb +108 -0
  21. data/lib/whirled_peas/graphics/canvas.rb +107 -0
  22. data/lib/whirled_peas/graphics/composer.rb +80 -0
  23. data/lib/whirled_peas/graphics/container_coords.rb +71 -0
  24. data/lib/whirled_peas/graphics/container_dimensions.rb +75 -0
  25. data/lib/whirled_peas/graphics/container_painter.rb +234 -0
  26. data/lib/whirled_peas/graphics/debugger.rb +52 -0
  27. data/lib/whirled_peas/graphics/grid_painter.rb +68 -0
  28. data/lib/whirled_peas/graphics/mock_screen.rb +26 -0
  29. data/lib/whirled_peas/graphics/painter.rb +19 -0
  30. data/lib/whirled_peas/graphics/renderer.rb +21 -0
  31. data/lib/whirled_peas/graphics/screen.rb +70 -0
  32. data/lib/whirled_peas/graphics/text_dimensions.rb +15 -0
  33. data/lib/whirled_peas/graphics/text_painter.rb +40 -0
  34. data/lib/whirled_peas/settings.rb +5 -0
  35. data/lib/whirled_peas/settings/bg_color.rb +22 -0
  36. data/lib/whirled_peas/settings/border.rb +101 -0
  37. data/lib/whirled_peas/settings/box_settings.rb +8 -0
  38. data/lib/whirled_peas/settings/color.rb +69 -0
  39. data/lib/whirled_peas/settings/container_settings.rb +139 -0
  40. data/lib/whirled_peas/settings/debugger.rb +96 -0
  41. data/lib/whirled_peas/settings/display_flow.rb +25 -0
  42. data/lib/whirled_peas/settings/element_settings.rb +61 -0
  43. data/lib/whirled_peas/settings/grid_settings.rb +15 -0
  44. data/lib/whirled_peas/settings/margin.rb +8 -0
  45. data/lib/whirled_peas/settings/padding.rb +8 -0
  46. data/lib/whirled_peas/settings/position.rb +15 -0
  47. data/lib/whirled_peas/settings/scrollbar.rb +15 -0
  48. data/lib/whirled_peas/settings/spacing.rb +24 -0
  49. data/lib/whirled_peas/settings/text_align.rb +19 -0
  50. data/lib/whirled_peas/settings/text_color.rb +19 -0
  51. data/lib/whirled_peas/settings/text_settings.rb +15 -0
  52. data/lib/whirled_peas/utils.rb +5 -0
  53. data/lib/whirled_peas/utils/ansi.rb +53 -0
  54. data/lib/whirled_peas/utils/formatted_string.rb +64 -0
  55. data/lib/whirled_peas/utils/title_font.rb +75 -0
  56. data/lib/whirled_peas/version.rb +1 -1
  57. data/screen_test/rendered/elements/box.frame +1 -0
  58. data/screen_test/rendered/elements/box.rb +20 -0
  59. data/screen_test/rendered/elements/grid.frame +1 -0
  60. data/screen_test/rendered/elements/grid.rb +13 -0
  61. data/screen_test/rendered/elements/screen_overflow.frame +1 -0
  62. data/screen_test/rendered/elements/screen_overflow.rb +9 -0
  63. data/screen_test/rendered/elements/text.frame +1 -0
  64. data/screen_test/rendered/elements/text.rb +9 -0
  65. data/screen_test/rendered/elements/text_multiline.frame +1 -0
  66. data/screen_test/rendered/elements/text_multiline.rb +9 -0
  67. data/screen_test/rendered/settings/align/box.frame +1 -0
  68. data/screen_test/rendered/settings/align/box.rb +24 -0
  69. data/screen_test/rendered/settings/align/children_center.frame +1 -0
  70. data/screen_test/rendered/settings/align/children_center.rb +13 -0
  71. data/screen_test/rendered/settings/align/children_left.frame +1 -0
  72. data/screen_test/rendered/settings/align/children_left.rb +13 -0
  73. data/screen_test/rendered/settings/align/children_right.frame +1 -0
  74. data/screen_test/rendered/settings/align/children_right.rb +13 -0
  75. data/screen_test/rendered/settings/align/grid.frame +1 -0
  76. data/screen_test/rendered/settings/align/grid.rb +20 -0
  77. data/screen_test/rendered/settings/ansi/bold.frame +1 -0
  78. data/screen_test/rendered/settings/ansi/bold.rb +15 -0
  79. data/screen_test/rendered/settings/ansi/color.frame +1 -0
  80. data/screen_test/rendered/settings/ansi/color.rb +37 -0
  81. data/screen_test/rendered/settings/ansi/underline.frame +1 -0
  82. data/screen_test/rendered/settings/ansi/underline.rb +15 -0
  83. data/screen_test/rendered/settings/border.frame +1 -0
  84. data/screen_test/rendered/settings/border.rb +13 -0
  85. data/screen_test/rendered/settings/flow/box_b2t.frame +1 -0
  86. data/screen_test/rendered/settings/flow/box_b2t.rb +24 -0
  87. data/screen_test/rendered/settings/flow/box_l2r.frame +1 -0
  88. data/screen_test/rendered/settings/flow/box_l2r.rb +24 -0
  89. data/screen_test/rendered/settings/flow/box_r2l.frame +1 -0
  90. data/screen_test/rendered/settings/flow/box_r2l.rb +24 -0
  91. data/screen_test/rendered/settings/flow/box_t2b.frame +1 -0
  92. data/screen_test/rendered/settings/flow/box_t2b.rb +24 -0
  93. data/screen_test/rendered/settings/flow/grid_b2t.frame +1 -0
  94. data/screen_test/rendered/settings/flow/grid_b2t.rb +14 -0
  95. data/screen_test/rendered/settings/flow/grid_l2r.frame +1 -0
  96. data/screen_test/rendered/settings/flow/grid_l2r.rb +14 -0
  97. data/screen_test/rendered/settings/flow/grid_r2l.frame +1 -0
  98. data/screen_test/rendered/settings/flow/grid_r2l.rb +14 -0
  99. data/screen_test/rendered/settings/flow/grid_t2b.frame +1 -0
  100. data/screen_test/rendered/settings/flow/grid_t2b.rb +14 -0
  101. data/screen_test/rendered/settings/height/box.frame +1 -0
  102. data/screen_test/rendered/settings/height/box.rb +13 -0
  103. data/screen_test/rendered/settings/height/grid.frame +1 -0
  104. data/screen_test/rendered/settings/height/grid.rb +14 -0
  105. data/screen_test/rendered/settings/height/overflow_box.frame +1 -0
  106. data/screen_test/rendered/settings/height/overflow_box.rb +13 -0
  107. data/screen_test/rendered/settings/height/overflow_box_l2r.frame +1 -0
  108. data/screen_test/rendered/settings/height/overflow_box_l2r.rb +15 -0
  109. data/screen_test/rendered/settings/height/overflow_box_t2b.frame +1 -0
  110. data/screen_test/rendered/settings/height/overflow_box_t2b.rb +14 -0
  111. data/screen_test/rendered/settings/height/overflow_grid.frame +1 -0
  112. data/screen_test/rendered/settings/height/overflow_grid.rb +16 -0
  113. data/screen_test/rendered/settings/margin.frame +1 -0
  114. data/screen_test/rendered/settings/margin.rb +14 -0
  115. data/screen_test/rendered/settings/padding.frame +1 -0
  116. data/screen_test/rendered/settings/padding.rb +11 -0
  117. data/screen_test/rendered/settings/position/box_left.frame +1 -0
  118. data/screen_test/rendered/settings/position/box_left.rb +17 -0
  119. data/screen_test/rendered/settings/position/box_left_negative.frame +1 -0
  120. data/screen_test/rendered/settings/position/box_left_negative.rb +17 -0
  121. data/screen_test/rendered/settings/position/box_top.frame +1 -0
  122. data/screen_test/rendered/settings/position/box_top.rb +17 -0
  123. data/screen_test/rendered/settings/position/box_top_negative.frame +1 -0
  124. data/screen_test/rendered/settings/position/box_top_negative.rb +17 -0
  125. data/screen_test/rendered/settings/position/grid_left.frame +1 -0
  126. data/screen_test/rendered/settings/position/grid_left.rb +18 -0
  127. data/screen_test/rendered/settings/position/grid_left_negative.frame +1 -0
  128. data/screen_test/rendered/settings/position/grid_left_negative.rb +18 -0
  129. data/screen_test/rendered/settings/position/grid_top.frame +1 -0
  130. data/screen_test/rendered/settings/position/grid_top.rb +18 -0
  131. data/screen_test/rendered/settings/position/grid_top_negative.frame +1 -0
  132. data/screen_test/rendered/settings/position/grid_top_negative.rb +18 -0
  133. data/screen_test/rendered/settings/scroll/horiz_box.frame +1 -0
  134. data/screen_test/rendered/settings/scroll/horiz_box.rb +15 -0
  135. data/screen_test/rendered/settings/scroll/vert_box.frame +1 -0
  136. data/screen_test/rendered/settings/scroll/vert_box.rb +18 -0
  137. data/screen_test/rendered/settings/title_font.frame +1 -0
  138. data/screen_test/rendered/settings/title_font.rb +12 -0
  139. data/screen_test/rendered/settings/width/box.frame +1 -0
  140. data/screen_test/rendered/settings/width/box.rb +13 -0
  141. data/screen_test/rendered/settings/width/grid.frame +1 -0
  142. data/screen_test/rendered/settings/width/grid.rb +14 -0
  143. data/screen_test/rendered/settings/width/overflow_box.frame +1 -0
  144. data/screen_test/rendered/settings/width/overflow_box.rb +11 -0
  145. data/screen_test/rendered/settings/width/overflow_box_l2r.frame +1 -0
  146. data/screen_test/rendered/settings/width/overflow_box_l2r.rb +14 -0
  147. data/screen_test/rendered/settings/width/overflow_box_t2b.frame +1 -0
  148. data/screen_test/rendered/settings/width/overflow_box_t2b.rb +15 -0
  149. data/screen_test/rendered/settings/width/overflow_grid.frame +1 -0
  150. data/screen_test/rendered/settings/width/overflow_grid.rb +14 -0
  151. data/screen_test/screen_tester.rb +201 -0
  152. data/whirled_peas.gemspec +4 -2
  153. metadata +147 -20
  154. data/lib/whirled_peas/ui.rb +0 -7
  155. data/lib/whirled_peas/ui/ansi.rb +0 -154
  156. data/lib/whirled_peas/ui/canvas.rb +0 -35
  157. data/lib/whirled_peas/ui/element.rb +0 -225
  158. data/lib/whirled_peas/ui/painter.rb +0 -283
  159. data/lib/whirled_peas/ui/screen.rb +0 -62
  160. data/lib/whirled_peas/ui/settings.rb +0 -521
  161. data/lib/whirled_peas/ui/stroke.rb +0 -29
  162. data/sandbox/auto.rb +0 -13
  163. data/sandbox/box.rb +0 -19
  164. data/sandbox/grid.rb +0 -13
  165. data/sandbox/sandbox.rb +0 -17
  166. data/sandbox/text.rb +0 -33
@@ -0,0 +1,21 @@
1
+ module WhirledPeas
2
+ class Config
3
+ attr_writer :driver, :template_factory
4
+ attr_accessor :loading_template_factory
5
+
6
+ def driver
7
+ unless @driver
8
+ raise ConfigurationError, 'driver must be configured'
9
+ end
10
+ @driver
11
+ end
12
+
13
+ def template_factory
14
+ unless @template_factory
15
+ raise ConfigurationError, 'template_factory must be configured'
16
+ end
17
+ @template_factory
18
+ end
19
+ end
20
+ private_constant :Config
21
+ end
@@ -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,66 +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 }
50
- raise
51
- ensure
52
- logger.info(LOGGER_ID) { 'Closing socket' }
53
- socket.close if socket
11
+ def running?
12
+ @running == true
13
+ end
14
+
15
+ def start
16
+ self.running = true
54
17
  end
55
18
 
56
19
  def stop
57
- logger.info(LOGGER_ID) { 'Stopping...' }
58
- mutex.synchronize { @running = false }
20
+ enqueue(EOF, nil, {})
59
21
  end
60
22
 
61
23
  private
62
24
 
63
- attr_reader :event_loop, :logger, :mutex
25
+ attr_writer :running
64
26
  end
27
+
28
+ private_constant :Consumer
65
29
  end
66
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([0, next_frame_at - Time.now].max)
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,63 +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
30
  logger.error(LOGGER_ID) { e }
20
31
  raise
21
32
  ensure
22
- if client
23
- logger.info(LOGGER_ID) { 'Closing connection'}
24
- client.close
25
- end
33
+ consumer.stop
34
+ consumer_thread.join if consumer_thread
26
35
  end
27
36
 
28
- def initialize(client, logger=NullLogger.new)
29
- @client = client
37
+ def initialize(consumer, logger=NullLogger.new)
38
+ @consumer = consumer
30
39
  @logger = logger
31
40
  @queue = Queue.new
32
41
  end
33
42
 
34
- def send(name, duration: nil, args: {})
35
- client.puts(JSON.generate('name' => name, 'duration' => duration, **args))
36
- logger.debug(LOGGER_ID) { "Sending frame: #{name}" }
37
- end
38
-
39
- 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: {})
40
54
  queue.push([name, duration, args])
41
55
  end
42
56
 
57
+ # Send any buffered frames to the EventLoop
43
58
  def flush
44
- while !queue.empty?
45
- name, duration, args = queue.pop
46
- send(name, duration: duration, args: args)
47
- end
48
- end
49
-
50
- def stop
51
- send(Frame::EOF)
52
- end
53
-
54
- def terminate
55
- send(Frame::TERMINATE)
59
+ consumer.enqueue(*queue.pop) while !queue.empty?
56
60
  end
57
61
 
58
62
  private
59
63
 
60
- attr_reader :client, :logger, :queue
64
+ attr_reader :consumer, :logger, :queue
61
65
  end
62
66
  end
63
67
  end
@@ -0,0 +1,5 @@
1
+ module WhirledPeas
2
+ module Graphics
3
+ end
4
+ private_constant :Graphics
5
+ end
@@ -0,0 +1,108 @@
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 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
+ def each_child(&block)
41
+ if settings.reverse_flow?
42
+ children.reverse.each(&block)
43
+ else
44
+ super
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def paint_horizontally(canvas, &block)
51
+ stroke_top = coords(canvas).content_top
52
+ stroke_left = coords(canvas).content_left
53
+ total_child_width = 0
54
+ each_child { |c| total_child_width += c.dimensions.outer_width }
55
+ if settings.align_center?
56
+ stroke_left += (dimensions.content_width - total_child_width) / 2
57
+ elsif settings.align_right?
58
+ stroke_left += dimensions.content_width - total_child_width
59
+ end
60
+ given_width = 0
61
+ each_child do |child|
62
+ child_width = [
63
+ child.dimensions.outer_width,
64
+ dimensions.content_width - given_width
65
+ ].min
66
+ child_canvas = canvas.child(
67
+ stroke_left + given_width,
68
+ stroke_top,
69
+ child_width,
70
+ [dimensions.content_height, child.dimensions.outer_height].min
71
+ )
72
+ child.paint(child_canvas, &block)
73
+ given_width += child_width
74
+ break if given_width == dimensions.content_width
75
+ end
76
+ end
77
+
78
+ def paint_vertically(canvas, &block)
79
+ stroke_top = coords(canvas).content_top
80
+ stroke_left = coords(canvas).content_left
81
+ given_height = 0
82
+ each_child do |child|
83
+ if settings.align_center?
84
+ justify_offset = (dimensions.content_width - child.dimensions.outer_width) / 2
85
+ elsif settings.align_right?
86
+ justify_offset = dimensions.content_width - child.dimensions.outer_width
87
+ else
88
+ justify_offset = 0
89
+ end
90
+ child_height = [
91
+ child.dimensions.outer_height,
92
+ dimensions.content_height - given_height
93
+ ].min
94
+ child_canvas = canvas.child(
95
+ stroke_left + justify_offset,
96
+ stroke_top + given_height,
97
+ [dimensions.content_width, child.dimensions.outer_width].min,
98
+ child_height
99
+ )
100
+ child.paint(child_canvas, &block)
101
+ given_height += child_height
102
+ break if given_height == dimensions.content_height
103
+ end
104
+ end
105
+ end
106
+ private_constant :BoxPainter
107
+ end
108
+ end