whirled_peas 0.3.0 → 0.7.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 (221) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -0
  3. data/CHANGELOG.md +38 -0
  4. data/README.md +549 -85
  5. data/Rakefile +28 -3
  6. data/examples/intro.rb +52 -0
  7. data/examples/scrolling.rb +53 -0
  8. data/exe/whirled_peas +7 -0
  9. data/lib/whirled_peas.rb +12 -36
  10. data/lib/whirled_peas/command_line.rb +255 -0
  11. data/lib/whirled_peas/config.rb +21 -0
  12. data/lib/whirled_peas/errors.rb +7 -0
  13. data/lib/whirled_peas/frame.rb +0 -5
  14. data/lib/whirled_peas/frame/consumer.rb +30 -0
  15. data/lib/whirled_peas/frame/debug_consumer.rb +30 -0
  16. data/lib/whirled_peas/frame/event_loop.rb +69 -38
  17. data/lib/whirled_peas/frame/producer.rb +36 -19
  18. data/lib/whirled_peas/graphics.rb +19 -0
  19. data/lib/whirled_peas/graphics/box_painter.rb +101 -0
  20. data/lib/whirled_peas/graphics/canvas.rb +118 -0
  21. data/lib/whirled_peas/graphics/composer.rb +80 -0
  22. data/lib/whirled_peas/graphics/container_coords.rb +72 -0
  23. data/lib/whirled_peas/graphics/container_dimensions.rb +93 -0
  24. data/lib/whirled_peas/graphics/container_painter.rb +272 -0
  25. data/lib/whirled_peas/graphics/debugger.rb +52 -0
  26. data/lib/whirled_peas/graphics/grid_painter.rb +69 -0
  27. data/lib/whirled_peas/graphics/mock_screen.rb +26 -0
  28. data/lib/whirled_peas/graphics/painter.rb +23 -0
  29. data/lib/whirled_peas/graphics/renderer.rb +26 -0
  30. data/lib/whirled_peas/graphics/screen.rb +70 -0
  31. data/lib/whirled_peas/graphics/text_dimensions.rb +15 -0
  32. data/lib/whirled_peas/graphics/text_painter.rb +40 -0
  33. data/lib/whirled_peas/settings.rb +5 -0
  34. data/lib/whirled_peas/settings/alignment.rb +24 -0
  35. data/lib/whirled_peas/settings/bg_color.rb +24 -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 +68 -0
  39. data/lib/whirled_peas/settings/container_settings.rb +223 -0
  40. data/lib/whirled_peas/settings/debugger.rb +96 -0
  41. data/lib/whirled_peas/settings/display_flow.rb +27 -0
  42. data/lib/whirled_peas/settings/element_settings.rb +61 -0
  43. data/lib/whirled_peas/settings/grid_settings.rb +19 -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/sizing.rb +19 -0
  49. data/lib/whirled_peas/settings/spacing.rb +58 -0
  50. data/lib/whirled_peas/settings/text_color.rb +21 -0
  51. data/lib/whirled_peas/settings/text_settings.rb +15 -0
  52. data/lib/whirled_peas/settings/vert_alignment.rb +24 -0
  53. data/lib/whirled_peas/utils.rb +5 -0
  54. data/lib/whirled_peas/utils/ansi.rb +53 -0
  55. data/lib/whirled_peas/utils/formatted_string.rb +64 -0
  56. data/lib/whirled_peas/utils/title_font.rb +75 -0
  57. data/lib/whirled_peas/version.rb +1 -1
  58. data/screen_test/elements/box.frame +1 -0
  59. data/screen_test/elements/box.rb +20 -0
  60. data/screen_test/elements/grid.frame +1 -0
  61. data/screen_test/elements/grid.rb +13 -0
  62. data/screen_test/elements/screen_overflow_x.frame +1 -0
  63. data/screen_test/elements/screen_overflow_x.rb +9 -0
  64. data/screen_test/elements/screen_overflow_y.frame +1 -0
  65. data/screen_test/elements/screen_overflow_y.rb +9 -0
  66. data/screen_test/elements/text.frame +1 -0
  67. data/screen_test/elements/text.rb +9 -0
  68. data/screen_test/elements/text_multiline.frame +1 -0
  69. data/screen_test/elements/text_multiline.rb +9 -0
  70. data/screen_test/settings/align/box_around.frame +1 -0
  71. data/screen_test/settings/align/box_around.rb +16 -0
  72. data/screen_test/settings/align/box_between.frame +1 -0
  73. data/screen_test/settings/align/box_between.rb +16 -0
  74. data/screen_test/settings/align/box_center.frame +1 -0
  75. data/screen_test/settings/align/box_center.rb +21 -0
  76. data/screen_test/settings/align/box_default.frame +1 -0
  77. data/screen_test/settings/align/box_default.rb +20 -0
  78. data/screen_test/settings/align/box_evenly.frame +1 -0
  79. data/screen_test/settings/align/box_evenly.rb +16 -0
  80. data/screen_test/settings/align/box_left.frame +1 -0
  81. data/screen_test/settings/align/box_left.rb +21 -0
  82. data/screen_test/settings/align/box_right.frame +1 -0
  83. data/screen_test/settings/align/box_right.rb +21 -0
  84. data/screen_test/settings/align/children_center.frame +1 -0
  85. data/screen_test/settings/align/children_center.rb +15 -0
  86. data/screen_test/settings/align/children_left.frame +1 -0
  87. data/screen_test/settings/align/children_left.rb +15 -0
  88. data/screen_test/settings/align/children_right.frame +1 -0
  89. data/screen_test/settings/align/children_right.rb +15 -0
  90. data/screen_test/settings/align/grid_center.frame +1 -0
  91. data/screen_test/settings/align/grid_center.rb +18 -0
  92. data/screen_test/settings/align/grid_default.frame +1 -0
  93. data/screen_test/settings/align/grid_default.rb +17 -0
  94. data/screen_test/settings/align/grid_left.frame +1 -0
  95. data/screen_test/settings/align/grid_left.rb +18 -0
  96. data/screen_test/settings/align/grid_right.frame +1 -0
  97. data/screen_test/settings/align/grid_right.rb +18 -0
  98. data/screen_test/settings/ansi/bold.frame +1 -0
  99. data/screen_test/settings/ansi/bold.rb +14 -0
  100. data/screen_test/settings/ansi/color.frame +1 -0
  101. data/screen_test/settings/ansi/color.rb +37 -0
  102. data/screen_test/settings/ansi/underline.frame +1 -0
  103. data/screen_test/settings/ansi/underline.rb +14 -0
  104. data/screen_test/settings/border.frame +1 -0
  105. data/screen_test/settings/border.rb +13 -0
  106. data/screen_test/settings/flow/box_b2t.frame +1 -0
  107. data/screen_test/settings/flow/box_b2t.rb +26 -0
  108. data/screen_test/settings/flow/box_l2r.frame +1 -0
  109. data/screen_test/settings/flow/box_l2r.rb +26 -0
  110. data/screen_test/settings/flow/box_r2l.frame +1 -0
  111. data/screen_test/settings/flow/box_r2l.rb +26 -0
  112. data/screen_test/settings/flow/box_t2b.frame +1 -0
  113. data/screen_test/settings/flow/box_t2b.rb +26 -0
  114. data/screen_test/settings/flow/grid_b2t.frame +1 -0
  115. data/screen_test/settings/flow/grid_b2t.rb +14 -0
  116. data/screen_test/settings/flow/grid_l2r.frame +1 -0
  117. data/screen_test/settings/flow/grid_l2r.rb +14 -0
  118. data/screen_test/settings/flow/grid_r2l.frame +1 -0
  119. data/screen_test/settings/flow/grid_r2l.rb +14 -0
  120. data/screen_test/settings/flow/grid_t2b.frame +1 -0
  121. data/screen_test/settings/flow/grid_t2b.rb +14 -0
  122. data/screen_test/settings/height/box.frame +1 -0
  123. data/screen_test/settings/height/box.rb +13 -0
  124. data/screen_test/settings/height/box_border_sizing.frame +1 -0
  125. data/screen_test/settings/height/box_border_sizing.rb +15 -0
  126. data/screen_test/settings/height/grid.frame +1 -0
  127. data/screen_test/settings/height/grid.rb +14 -0
  128. data/screen_test/settings/height/overflow_box.frame +1 -0
  129. data/screen_test/settings/height/overflow_box.rb +13 -0
  130. data/screen_test/settings/height/overflow_box_l2r.frame +1 -0
  131. data/screen_test/settings/height/overflow_box_l2r.rb +17 -0
  132. data/screen_test/settings/height/overflow_box_t2b.frame +1 -0
  133. data/screen_test/settings/height/overflow_box_t2b.rb +16 -0
  134. data/screen_test/settings/height/overflow_grid.frame +1 -0
  135. data/screen_test/settings/height/overflow_grid.rb +16 -0
  136. data/screen_test/settings/margin.frame +1 -0
  137. data/screen_test/settings/margin.rb +16 -0
  138. data/screen_test/settings/padding.frame +1 -0
  139. data/screen_test/settings/padding.rb +13 -0
  140. data/screen_test/settings/position/box_left.frame +1 -0
  141. data/screen_test/settings/position/box_left.rb +17 -0
  142. data/screen_test/settings/position/box_left_negative.frame +1 -0
  143. data/screen_test/settings/position/box_left_negative.rb +17 -0
  144. data/screen_test/settings/position/box_top.frame +1 -0
  145. data/screen_test/settings/position/box_top.rb +17 -0
  146. data/screen_test/settings/position/box_top_negative.frame +1 -0
  147. data/screen_test/settings/position/box_top_negative.rb +17 -0
  148. data/screen_test/settings/position/grid_left.frame +1 -0
  149. data/screen_test/settings/position/grid_left.rb +18 -0
  150. data/screen_test/settings/position/grid_left_negative.frame +1 -0
  151. data/screen_test/settings/position/grid_left_negative.rb +18 -0
  152. data/screen_test/settings/position/grid_top.frame +1 -0
  153. data/screen_test/settings/position/grid_top.rb +18 -0
  154. data/screen_test/settings/position/grid_top_negative.frame +1 -0
  155. data/screen_test/settings/position/grid_top_negative.rb +18 -0
  156. data/screen_test/settings/scroll/horiz_box.frame +1 -0
  157. data/screen_test/settings/scroll/horiz_box.rb +17 -0
  158. data/screen_test/settings/scroll/horiz_box_align_center.rb +18 -0
  159. data/screen_test/settings/scroll/horiz_box_align_right.rb +18 -0
  160. data/screen_test/settings/scroll/vert_box.frame +1 -0
  161. data/screen_test/settings/scroll/vert_box.rb +20 -0
  162. data/screen_test/settings/title_font.frame +1 -0
  163. data/screen_test/settings/title_font.rb +12 -0
  164. data/screen_test/settings/valign/box_around.frame +1 -0
  165. data/screen_test/settings/valign/box_around.rb +17 -0
  166. data/screen_test/settings/valign/box_between.frame +1 -0
  167. data/screen_test/settings/valign/box_between.rb +17 -0
  168. data/screen_test/settings/valign/box_bottom.frame +1 -0
  169. data/screen_test/settings/valign/box_bottom.rb +17 -0
  170. data/screen_test/settings/valign/box_default.frame +1 -0
  171. data/screen_test/settings/valign/box_default.rb +16 -0
  172. data/screen_test/settings/valign/box_evenly.frame +1 -0
  173. data/screen_test/settings/valign/box_evenly.rb +17 -0
  174. data/screen_test/settings/valign/box_middle.frame +1 -0
  175. data/screen_test/settings/valign/box_middle.rb +17 -0
  176. data/screen_test/settings/valign/box_top.frame +1 -0
  177. data/screen_test/settings/valign/box_top.rb +17 -0
  178. data/screen_test/settings/valign/grid_bottom.frame +1 -0
  179. data/screen_test/settings/valign/grid_bottom.rb +15 -0
  180. data/screen_test/settings/valign/grid_default.frame +1 -0
  181. data/screen_test/settings/valign/grid_default.rb +14 -0
  182. data/screen_test/settings/valign/grid_middle.frame +1 -0
  183. data/screen_test/settings/valign/grid_middle.rb +15 -0
  184. data/screen_test/settings/valign/grid_top.frame +1 -0
  185. data/screen_test/settings/valign/grid_top.rb +15 -0
  186. data/screen_test/settings/width/box_border_sizing.frame +1 -0
  187. data/screen_test/settings/width/box_border_sizing.rb +15 -0
  188. data/screen_test/settings/width/box_content.frame +1 -0
  189. data/screen_test/settings/width/box_content.rb +15 -0
  190. data/screen_test/settings/width/box_default.frame +1 -0
  191. data/screen_test/settings/width/box_default.rb +14 -0
  192. data/screen_test/settings/width/grid.frame +1 -0
  193. data/screen_test/settings/width/grid.rb +14 -0
  194. data/screen_test/settings/width/overflow_align_center.frame +1 -0
  195. data/screen_test/settings/width/overflow_align_center.rb +14 -0
  196. data/screen_test/settings/width/overflow_align_right.frame +1 -0
  197. data/screen_test/settings/width/overflow_align_right.rb +14 -0
  198. data/screen_test/settings/width/overflow_box.frame +1 -0
  199. data/screen_test/settings/width/overflow_box.rb +13 -0
  200. data/screen_test/settings/width/overflow_box_l2r.frame +1 -0
  201. data/screen_test/settings/width/overflow_box_l2r.rb +16 -0
  202. data/screen_test/settings/width/overflow_box_t2b.frame +1 -0
  203. data/screen_test/settings/width/overflow_box_t2b.rb +17 -0
  204. data/screen_test/settings/width/overflow_grid.frame +1 -0
  205. data/screen_test/settings/width/overflow_grid.rb +14 -0
  206. data/tools/whirled_peas/tools/screen_tester.rb +233 -0
  207. data/whirled_peas.gemspec +4 -1
  208. metadata +215 -17
  209. data/lib/whirled_peas/ui.rb +0 -7
  210. data/lib/whirled_peas/ui/ansi.rb +0 -103
  211. data/lib/whirled_peas/ui/canvas.rb +0 -68
  212. data/lib/whirled_peas/ui/color.rb +0 -101
  213. data/lib/whirled_peas/ui/element.rb +0 -224
  214. data/lib/whirled_peas/ui/painter.rb +0 -283
  215. data/lib/whirled_peas/ui/screen.rb +0 -60
  216. data/lib/whirled_peas/ui/settings.rb +0 -523
  217. data/sandbox/auto.rb +0 -13
  218. data/sandbox/box.rb +0 -19
  219. data/sandbox/grid.rb +0 -13
  220. data/sandbox/sandbox.rb +0 -17
  221. 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,10 +1,5 @@
1
- require_relative 'frame/event_loop'
2
- require_relative 'frame/producer'
3
-
4
1
  module WhirledPeas
5
2
  module Frame
6
- TERMINATE = '__term__'
7
- EOF = '__EOF__'
8
3
  end
9
4
 
10
5
  private_constant :Frame
@@ -0,0 +1,30 @@
1
+ module WhirledPeas
2
+ module Frame
3
+ # Abstract class for consuming frame events.
4
+ class Consumer
5
+ EOF = '__EOF__'
6
+
7
+ def enqueue(name, duration, args)
8
+ raise NotImplemented, "#{self.class} must implement #enqueue"
9
+ end
10
+
11
+ def running?
12
+ @running == true
13
+ end
14
+
15
+ def start
16
+ self.running = true
17
+ end
18
+
19
+ def stop
20
+ enqueue(EOF, nil, {})
21
+ end
22
+
23
+ private
24
+
25
+ attr_writer :running
26
+ end
27
+
28
+ private_constant :Consumer
29
+ end
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,59 +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
- def enqueue(name:, duration:, args:)
12
- queue.push([name, duration, args])
13
- end
14
-
15
- def running?
16
- @running
28
+ def enqueue(name, duration, args)
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
89
  end
59
90
  end
@@ -1,50 +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.produce(event_loop:, logger: NullLogger.new)
10
- producer = new(event_loop, logger)
11
- logger.info(LOGGER_ID) { 'Starting' }
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
12
26
  yield producer
13
- logger.info(LOGGER_ID) { 'Done with yield' }
14
- producer.send_frame(Frame::EOF)
15
- logger.info(LOGGER_ID) { 'Exited normally' }
27
+ producer.flush
16
28
  rescue => e
17
- producer.send_frame(Frame::TERMINATE)
18
29
  logger.warn(LOGGER_ID) { 'Exited with error' }
19
30
  logger.error(LOGGER_ID) { e }
20
31
  raise
32
+ ensure
33
+ consumer.stop
34
+ consumer_thread.join if consumer_thread
21
35
  end
22
36
 
23
- def initialize(event_loop, logger=NullLogger.new)
24
- @event_loop = event_loop
37
+ def initialize(consumer, logger=NullLogger.new)
38
+ @consumer = consumer
25
39
  @logger = logger
26
40
  @queue = Queue.new
27
41
  end
28
42
 
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
29
53
  def send_frame(name, duration: nil, args: {})
30
- event_loop.enqueue(name: name, duration: duration, args: args)
31
- logger.debug(LOGGER_ID) { "Sending frame: #{name}" }
32
- end
33
-
34
- def enqueue_frame(name, duration: nil, args: {})
35
54
  queue.push([name, duration, args])
36
55
  end
37
56
 
57
+ # Send any buffered frames to the EventLoop
38
58
  def flush
39
- while !queue.empty?
40
- name, duration, args = queue.pop
41
- send_frame(name: name, duration: duration, args: args)
42
- end
59
+ consumer.enqueue(*queue.pop) while !queue.empty?
43
60
  end
44
61
 
45
62
  private
46
63
 
47
- attr_reader :event_loop, :logger
64
+ attr_reader :consumer, :logger, :queue
48
65
  end
49
66
  end
50
67
  end
@@ -0,0 +1,19 @@
1
+ module WhirledPeas
2
+ module Graphics
3
+ class << self
4
+ attr_accessor :debug
5
+
6
+ def debugger(string_or_proc)
7
+ return unless @debug
8
+ @debugger ||= Logger.new(STDOUT, level: Logger::DEBUG)
9
+ if string_or_proc.is_a?(Proc)
10
+ string = string_or_proc.call
11
+ else
12
+ string = string_or_proc
13
+ end
14
+ @debugger.debug(string)
15
+ end
16
+ end
17
+ end
18
+ private_constant :Graphics
19
+ end
@@ -0,0 +1,101 @@
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, left, top, &block)
8
+ super
9
+ canvas_coords = coords(left, top)
10
+ content_canvas = canvas.child(
11
+ canvas_coords.content_left,
12
+ canvas_coords.content_top,
13
+ dimensions.content_width,
14
+ dimensions.content_height
15
+ )
16
+ return unless content_canvas.writable?
17
+ if settings.horizontal_flow?
18
+ paint_horizontally(content_canvas, canvas_coords, &block)
19
+ else
20
+ paint_vertically(content_canvas, canvas_coords, &block)
21
+ end
22
+ end
23
+
24
+ def dimensions
25
+ @dimensions ||= begin
26
+ content_width = 0
27
+ content_height = 0
28
+ if settings.horizontal_flow?
29
+ each_child do |child|
30
+ content_width += child.dimensions.outer_width
31
+ if child.dimensions.outer_height > content_height
32
+ content_height = child.dimensions.outer_height
33
+ end
34
+ end
35
+ else
36
+ each_child do |child|
37
+ if child.dimensions.outer_width > content_width
38
+ content_width = child.dimensions.outer_width
39
+ end
40
+ content_height += child.dimensions.outer_height
41
+ end
42
+ end
43
+ ContainerDimensions.new(settings, content_width, content_height)
44
+ end
45
+ end
46
+
47
+ def each_child(&block)
48
+ if settings.reverse_flow?
49
+ children.reverse.each(&block)
50
+ else
51
+ super
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def paint_horizontally(canvas, canvas_coords, &block)
58
+ stroke_left = canvas_coords.content_left
59
+ stroke_top = canvas_coords.content_top
60
+ children_width = 0
61
+ each_child { |c| children_width += c.dimensions.outer_width }
62
+ left_offset, spacing_offset = horiz_justify_offset(children_width)
63
+ stroke_left += left_offset
64
+ each_child do |child|
65
+ top_offset, _ = vert_justify_offset(child.dimensions.outer_height)
66
+ child_width = child.dimensions.outer_width
67
+ child_canvas = canvas.child(
68
+ stroke_left,
69
+ stroke_top + top_offset,
70
+ child_width,
71
+ child.dimensions.outer_height
72
+ )
73
+ child.paint(child_canvas, stroke_left, stroke_top + top_offset, &block)
74
+ stroke_left += child_width + spacing_offset
75
+ end
76
+ end
77
+
78
+ def paint_vertically(canvas, canvas_coords, &block)
79
+ stroke_left = canvas_coords.content_left
80
+ stroke_top = canvas_coords.content_top
81
+ children_height = 0
82
+ each_child { |c| children_height += c.dimensions.outer_height }
83
+ top_offset, spacing_offset = vert_justify_offset(children_height)
84
+ stroke_top += top_offset
85
+ each_child do |child|
86
+ left_offset, _ = horiz_justify_offset(child.dimensions.outer_width)
87
+ child_height = child.dimensions.outer_height
88
+ child_canvas = canvas.child(
89
+ stroke_left + left_offset,
90
+ stroke_top ,
91
+ child.dimensions.outer_width,
92
+ child_height
93
+ )
94
+ child.paint(child_canvas, stroke_left + left_offset, stroke_top, &block)
95
+ stroke_top += child_height + spacing_offset
96
+ end
97
+ end
98
+ end
99
+ private_constant :BoxPainter
100
+ end
101
+ end
@@ -0,0 +1,118 @@
1
+ require 'whirled_peas/utils/formatted_string'
2
+
3
+ module WhirledPeas
4
+ module Graphics
5
+ # Canvas represent the area of the screen a painter can paint on.
6
+ class Canvas
7
+ attr_reader :left, :top, :width, :height
8
+
9
+ def self.unwritable
10
+ new(-1, -1, 0, 0)
11
+ end
12
+
13
+ def initialize(left, top, width, height)
14
+ @left = left
15
+ @top = top
16
+ @width = width
17
+ @height = height
18
+ end
19
+
20
+ def writable?
21
+ width > 0 || height > 0
22
+ end
23
+
24
+ def child(child_left, child_top, child_width, child_height)
25
+ Graphics.debugger(
26
+ proc do
27
+ "Create child: #{self.inspect}.child(left=#{child_left}, top=#{child_top}, width=#{child_width}, height=#{child_height})"
28
+ end
29
+ )
30
+ if child_left >= left + width
31
+ self.class.unwritable
32
+ elsif child_left + child_width <= left
33
+ self.class.unwritable
34
+ elsif child_top >= top + height
35
+ self.class.unwritable
36
+ elsif child_top + child_height <= top
37
+ self.class.unwritable
38
+ else
39
+ if child_left < left
40
+ child_width -= left - child_left
41
+ child_left = left
42
+ end
43
+ child_width = [width - (child_left - left), child_width].min
44
+ if child_top < top
45
+ child_height -= top - child_top
46
+ child_top = top
47
+ end
48
+ child_height = [height - (child_top - top), child_height].min
49
+ child_canvas = self.class.new(
50
+ child_left,
51
+ child_top,
52
+ child_width,
53
+ child_height,
54
+ )
55
+ Graphics.debugger(proc { " -> #{child_canvas.inspect}" })
56
+ child_canvas
57
+ end
58
+ end
59
+
60
+ # Yields a single line of formatted characters positioned on the canvas,
61
+ # verifying only characters within the canvas are included.
62
+ def stroke(stroke_left, stroke_top, raw, formatting=[], &block)
63
+ Graphics.debugger(
64
+ proc do
65
+ "Stroke: #{self.inspect}.stroke(left=#{stroke_left}, top=#{stroke_top}, length=#{raw.length})"
66
+ end
67
+ )
68
+ if stroke_left >= left + width
69
+ # The stroke starts to the right of the canvas
70
+ fstring = Utils::FormattedString.blank
71
+ elsif stroke_left + raw.length <= left
72
+ # The stroke ends to the left of the canvas
73
+ fstring = Utils::FormattedString.blank
74
+ elsif stroke_top < top
75
+ # The stroke is above the canvas
76
+ fstring = Utils::FormattedString.blank
77
+ elsif stroke_top >= top + height
78
+ # The stroke is below the canvas
79
+ fstring = Utils::FormattedString.blank
80
+ else
81
+ # In this section, we know that at least part of the stroke should be visible
82
+ # on the canvas. Chop off parts of the raw string that aren't within the
83
+ # canvas boundary and ensure the stroke start position is also within the
84
+ # canvas boundary
85
+
86
+ # If the stroke starts to the left of the canvas, set the start index to the
87
+ # first value that will be on the canvas, then update stroke_left to be on
88
+ # the canvas
89
+ start_index = stroke_left < left ? left - stroke_left : 0
90
+ stroke_left = left if stroke_left <= left
91
+
92
+ # Determine how many characters from the stroke will fit on the canvas
93
+ visible_length = [raw.length, width - (stroke_left - left)].min
94
+ end_index = start_index + visible_length - 1
95
+ fstring = Utils::FormattedString.new(raw[start_index..end_index], formatting)
96
+ end
97
+ Graphics.debugger(
98
+ proc do
99
+ " -> Stroke(left=#{stroke_left}, top=#{stroke_top}, length=#{fstring.length})"
100
+ end
101
+ )
102
+ yield stroke_left, stroke_top, fstring
103
+ end
104
+
105
+ def hash
106
+ [left, top, width, height].hash
107
+ end
108
+
109
+ def ==(other)
110
+ other.is_a?(self.class) && hash == other.hash
111
+ end
112
+
113
+ def inspect
114
+ "Canvas(left=#{left}, top=#{top}, width=#{width}, height=#{height})"
115
+ end
116
+ end
117
+ end
118
+ end