whirled_peas 0.2.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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