whirled_peas 0.7.0 → 0.10.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 (220) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +25 -0
  4. data/README.md +467 -120
  5. data/Rakefile +1 -20
  6. data/bin/easing +33 -0
  7. data/bin/reset_cursor +11 -0
  8. data/bin/screen_test +68 -0
  9. data/examples/graph.rb +54 -0
  10. data/examples/intro.rb +3 -3
  11. data/examples/scrolling.rb +5 -4
  12. data/lib/data/themes.yaml +13 -0
  13. data/lib/whirled_peas.rb +12 -6
  14. data/lib/whirled_peas/animator.rb +5 -0
  15. data/lib/whirled_peas/animator/debug_consumer.rb +17 -0
  16. data/lib/whirled_peas/animator/easing.rb +72 -0
  17. data/lib/whirled_peas/animator/frame.rb +5 -0
  18. data/lib/whirled_peas/animator/frameset.rb +33 -0
  19. data/lib/whirled_peas/animator/producer.rb +35 -0
  20. data/lib/whirled_peas/animator/renderer_consumer.rb +31 -0
  21. data/lib/whirled_peas/command.rb +5 -0
  22. data/lib/whirled_peas/command/base.rb +86 -0
  23. data/lib/whirled_peas/command/config_command.rb +43 -0
  24. data/lib/whirled_peas/command/debug.rb +21 -0
  25. data/lib/whirled_peas/command/fonts.rb +22 -0
  26. data/lib/whirled_peas/command/frame_command.rb +34 -0
  27. data/lib/whirled_peas/command/frames.rb +24 -0
  28. data/lib/whirled_peas/command/help.rb +38 -0
  29. data/lib/whirled_peas/command/play.rb +111 -0
  30. data/lib/whirled_peas/command/record.rb +57 -0
  31. data/lib/whirled_peas/command/still.rb +29 -0
  32. data/lib/whirled_peas/command/themes.rb +76 -0
  33. data/lib/whirled_peas/command_line.rb +24 -212
  34. data/lib/whirled_peas/config.rb +56 -6
  35. data/lib/whirled_peas/device.rb +5 -0
  36. data/lib/whirled_peas/device/null_device.rb +8 -0
  37. data/lib/whirled_peas/device/output_file.rb +19 -0
  38. data/lib/whirled_peas/device/screen.rb +26 -0
  39. data/lib/whirled_peas/graphics/composer.rb +23 -2
  40. data/lib/whirled_peas/graphics/container_painter.rb +91 -0
  41. data/lib/whirled_peas/graphics/content_dimensions.rb +19 -0
  42. data/lib/whirled_peas/graphics/content_painter.rb +20 -0
  43. data/lib/whirled_peas/graphics/graph_dimensions.rb +12 -0
  44. data/lib/whirled_peas/graphics/graph_painter.rb +97 -0
  45. data/lib/whirled_peas/graphics/grid_painter.rb +1 -4
  46. data/lib/whirled_peas/graphics/painter.rb +10 -0
  47. data/lib/whirled_peas/graphics/renderer.rb +8 -2
  48. data/lib/whirled_peas/graphics/text_painter.rb +7 -11
  49. data/lib/whirled_peas/settings/border.rb +25 -4
  50. data/lib/whirled_peas/settings/container_settings.rb +2 -4
  51. data/lib/whirled_peas/settings/element_settings.rb +12 -3
  52. data/lib/whirled_peas/settings/graph_settings.rb +21 -0
  53. data/lib/whirled_peas/settings/grid_settings.rb +7 -0
  54. data/lib/whirled_peas/settings/text_settings.rb +1 -0
  55. data/lib/whirled_peas/settings/theme.rb +40 -0
  56. data/lib/whirled_peas/settings/theme_library.rb +63 -0
  57. data/lib/whirled_peas/utils/ansi.rb +13 -0
  58. data/lib/whirled_peas/utils/file_handler.rb +57 -0
  59. data/lib/whirled_peas/version.rb +1 -1
  60. data/screen_test/elements/box.frame +1 -1
  61. data/screen_test/elements/box.rb +1 -1
  62. data/screen_test/elements/graph.frame +1 -0
  63. data/screen_test/elements/graph.rb +12 -0
  64. data/screen_test/elements/grid.frame +1 -1
  65. data/screen_test/elements/grid.rb +1 -1
  66. data/screen_test/elements/screen_overflow_x.frame +1 -1
  67. data/screen_test/elements/screen_overflow_x.rb +1 -1
  68. data/screen_test/elements/screen_overflow_y.frame +1 -1
  69. data/screen_test/elements/screen_overflow_y.rb +1 -1
  70. data/screen_test/elements/text.frame +1 -1
  71. data/screen_test/elements/text.rb +1 -1
  72. data/screen_test/elements/text_multiline.frame +1 -1
  73. data/screen_test/elements/text_multiline.rb +1 -1
  74. data/screen_test/elements/theme.frame +1 -0
  75. data/screen_test/elements/theme.rb +26 -0
  76. data/screen_test/settings/align/box_around.frame +1 -1
  77. data/screen_test/settings/align/box_around.rb +1 -1
  78. data/screen_test/settings/align/box_between.frame +1 -1
  79. data/screen_test/settings/align/box_between.rb +1 -1
  80. data/screen_test/settings/align/box_center.frame +1 -1
  81. data/screen_test/settings/align/box_center.rb +1 -1
  82. data/screen_test/settings/align/box_default.frame +1 -1
  83. data/screen_test/settings/align/box_default.rb +1 -1
  84. data/screen_test/settings/align/box_evenly.frame +1 -1
  85. data/screen_test/settings/align/box_evenly.rb +1 -1
  86. data/screen_test/settings/align/box_left.frame +1 -1
  87. data/screen_test/settings/align/box_left.rb +1 -1
  88. data/screen_test/settings/align/box_right.frame +1 -1
  89. data/screen_test/settings/align/box_right.rb +1 -1
  90. data/screen_test/settings/align/children_center.frame +1 -1
  91. data/screen_test/settings/align/children_center.rb +1 -1
  92. data/screen_test/settings/align/children_left.frame +1 -1
  93. data/screen_test/settings/align/children_left.rb +1 -1
  94. data/screen_test/settings/align/children_right.frame +1 -1
  95. data/screen_test/settings/align/children_right.rb +1 -1
  96. data/screen_test/settings/align/grid_center.frame +1 -1
  97. data/screen_test/settings/align/grid_center.rb +1 -1
  98. data/screen_test/settings/align/grid_default.frame +1 -1
  99. data/screen_test/settings/align/grid_default.rb +1 -1
  100. data/screen_test/settings/align/grid_left.frame +1 -1
  101. data/screen_test/settings/align/grid_left.rb +1 -1
  102. data/screen_test/settings/align/grid_right.frame +1 -1
  103. data/screen_test/settings/align/grid_right.rb +1 -1
  104. data/screen_test/settings/ansi/bold.frame +1 -1
  105. data/screen_test/settings/ansi/bold.rb +1 -1
  106. data/screen_test/settings/ansi/color.frame +1 -1
  107. data/screen_test/settings/ansi/color.rb +1 -1
  108. data/screen_test/settings/ansi/underline.frame +1 -1
  109. data/screen_test/settings/ansi/underline.rb +1 -1
  110. data/screen_test/settings/border.frame +1 -1
  111. data/screen_test/settings/border.rb +1 -1
  112. data/screen_test/settings/flow/box_b2t.frame +1 -1
  113. data/screen_test/settings/flow/box_b2t.rb +1 -1
  114. data/screen_test/settings/flow/box_l2r.frame +1 -1
  115. data/screen_test/settings/flow/box_l2r.rb +1 -1
  116. data/screen_test/settings/flow/box_r2l.frame +1 -1
  117. data/screen_test/settings/flow/box_r2l.rb +1 -1
  118. data/screen_test/settings/flow/box_t2b.frame +1 -1
  119. data/screen_test/settings/flow/box_t2b.rb +1 -1
  120. data/screen_test/settings/flow/grid_b2t.frame +1 -1
  121. data/screen_test/settings/flow/grid_b2t.rb +2 -2
  122. data/screen_test/settings/flow/grid_l2r.frame +1 -1
  123. data/screen_test/settings/flow/grid_l2r.rb +2 -2
  124. data/screen_test/settings/flow/grid_r2l.frame +1 -1
  125. data/screen_test/settings/flow/grid_r2l.rb +2 -2
  126. data/screen_test/settings/flow/grid_t2b.frame +1 -1
  127. data/screen_test/settings/flow/grid_t2b.rb +2 -2
  128. data/screen_test/settings/height/box.frame +1 -1
  129. data/screen_test/settings/height/box.rb +1 -1
  130. data/screen_test/settings/height/box_border_sizing.frame +1 -1
  131. data/screen_test/settings/height/box_border_sizing.rb +1 -1
  132. data/screen_test/settings/height/grid.frame +1 -1
  133. data/screen_test/settings/height/grid.rb +1 -1
  134. data/screen_test/settings/height/overflow_box.frame +1 -1
  135. data/screen_test/settings/height/overflow_box.rb +1 -1
  136. data/screen_test/settings/height/overflow_box_l2r.frame +1 -1
  137. data/screen_test/settings/height/overflow_box_l2r.rb +1 -1
  138. data/screen_test/settings/height/overflow_box_t2b.frame +1 -1
  139. data/screen_test/settings/height/overflow_box_t2b.rb +1 -1
  140. data/screen_test/settings/height/overflow_grid.frame +1 -1
  141. data/screen_test/settings/height/overflow_grid.rb +1 -1
  142. data/screen_test/settings/margin.frame +1 -1
  143. data/screen_test/settings/margin.rb +1 -1
  144. data/screen_test/settings/padding.frame +1 -1
  145. data/screen_test/settings/padding.rb +1 -1
  146. data/screen_test/settings/position/box_left.frame +1 -1
  147. data/screen_test/settings/position/box_left.rb +1 -1
  148. data/screen_test/settings/position/box_left_negative.frame +1 -1
  149. data/screen_test/settings/position/box_left_negative.rb +1 -1
  150. data/screen_test/settings/position/box_top.frame +1 -1
  151. data/screen_test/settings/position/box_top.rb +1 -1
  152. data/screen_test/settings/position/box_top_negative.frame +1 -1
  153. data/screen_test/settings/position/box_top_negative.rb +1 -1
  154. data/screen_test/settings/position/grid_left.frame +1 -1
  155. data/screen_test/settings/position/grid_left.rb +1 -1
  156. data/screen_test/settings/position/grid_left_negative.frame +1 -1
  157. data/screen_test/settings/position/grid_left_negative.rb +1 -1
  158. data/screen_test/settings/position/grid_top.frame +1 -1
  159. data/screen_test/settings/position/grid_top.rb +1 -1
  160. data/screen_test/settings/position/grid_top_negative.frame +1 -1
  161. data/screen_test/settings/position/grid_top_negative.rb +1 -1
  162. data/screen_test/settings/scroll/horiz_box.frame +1 -1
  163. data/screen_test/settings/scroll/horiz_box.rb +1 -1
  164. data/screen_test/settings/scroll/horiz_box_align_center.rb +1 -1
  165. data/screen_test/settings/scroll/horiz_box_align_right.rb +1 -1
  166. data/screen_test/settings/scroll/vert_box.frame +1 -1
  167. data/screen_test/settings/scroll/vert_box.rb +1 -1
  168. data/screen_test/settings/title_font.frame +1 -1
  169. data/screen_test/settings/title_font.rb +1 -1
  170. data/screen_test/settings/valign/box_around.frame +1 -1
  171. data/screen_test/settings/valign/box_around.rb +1 -1
  172. data/screen_test/settings/valign/box_between.frame +1 -1
  173. data/screen_test/settings/valign/box_between.rb +1 -1
  174. data/screen_test/settings/valign/box_bottom.frame +1 -1
  175. data/screen_test/settings/valign/box_bottom.rb +1 -1
  176. data/screen_test/settings/valign/box_default.frame +1 -1
  177. data/screen_test/settings/valign/box_default.rb +1 -1
  178. data/screen_test/settings/valign/box_evenly.frame +1 -1
  179. data/screen_test/settings/valign/box_evenly.rb +1 -1
  180. data/screen_test/settings/valign/box_middle.frame +1 -1
  181. data/screen_test/settings/valign/box_middle.rb +1 -1
  182. data/screen_test/settings/valign/box_top.frame +1 -1
  183. data/screen_test/settings/valign/box_top.rb +1 -1
  184. data/screen_test/settings/valign/grid_bottom.frame +1 -1
  185. data/screen_test/settings/valign/grid_bottom.rb +1 -1
  186. data/screen_test/settings/valign/grid_default.frame +1 -1
  187. data/screen_test/settings/valign/grid_default.rb +1 -1
  188. data/screen_test/settings/valign/grid_middle.frame +1 -1
  189. data/screen_test/settings/valign/grid_middle.rb +1 -1
  190. data/screen_test/settings/valign/grid_top.frame +1 -1
  191. data/screen_test/settings/valign/grid_top.rb +1 -1
  192. data/screen_test/settings/width/box_border_sizing.frame +1 -1
  193. data/screen_test/settings/width/box_border_sizing.rb +1 -1
  194. data/screen_test/settings/width/box_content.frame +1 -1
  195. data/screen_test/settings/width/box_content.rb +1 -1
  196. data/screen_test/settings/width/box_default.frame +1 -1
  197. data/screen_test/settings/width/box_default.rb +1 -1
  198. data/screen_test/settings/width/grid.frame +1 -1
  199. data/screen_test/settings/width/grid.rb +1 -1
  200. data/screen_test/settings/width/overflow_align_center.frame +1 -1
  201. data/screen_test/settings/width/overflow_align_center.rb +1 -1
  202. data/screen_test/settings/width/overflow_align_right.frame +1 -1
  203. data/screen_test/settings/width/overflow_align_right.rb +1 -1
  204. data/screen_test/settings/width/overflow_box.frame +1 -1
  205. data/screen_test/settings/width/overflow_box.rb +1 -1
  206. data/screen_test/settings/width/overflow_box_l2r.frame +1 -1
  207. data/screen_test/settings/width/overflow_box_l2r.rb +1 -1
  208. data/screen_test/settings/width/overflow_box_t2b.frame +1 -1
  209. data/screen_test/settings/width/overflow_box_t2b.rb +1 -1
  210. data/screen_test/settings/width/overflow_grid.frame +1 -1
  211. data/screen_test/settings/width/overflow_grid.rb +1 -1
  212. data/tools/whirled_peas/tools/screen_tester.rb +131 -65
  213. metadata +42 -9
  214. data/lib/whirled_peas/frame.rb +0 -6
  215. data/lib/whirled_peas/frame/consumer.rb +0 -30
  216. data/lib/whirled_peas/frame/debug_consumer.rb +0 -30
  217. data/lib/whirled_peas/frame/event_loop.rb +0 -90
  218. data/lib/whirled_peas/frame/producer.rb +0 -67
  219. data/lib/whirled_peas/graphics/screen.rb +0 -70
  220. data/lib/whirled_peas/graphics/text_dimensions.rb +0 -15
@@ -0,0 +1,19 @@
1
+ require 'whirled_peas/utils/file_handler'
2
+
3
+ module WhirledPeas
4
+ module Device
5
+ class OutputFile
6
+ def initialize(file)
7
+ @file = file
8
+ end
9
+
10
+ def handle_renders(renders)
11
+ Utils::FileHandler.write(file, renders)
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :file
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ require 'highline'
2
+
3
+ module WhirledPeas
4
+ module Device
5
+ class Screen
6
+ def initialize(refresh_rate, output: STDOUT)
7
+ @refresh_rate = refresh_rate
8
+ @output = output
9
+ end
10
+
11
+ def handle_renders(renders)
12
+ renders.each do |strokes|
13
+ frame_at = Time.now
14
+ output.print(strokes)
15
+ output.flush
16
+ next_frame_at = frame_at + 1.0 / refresh_rate
17
+ sleep([0, next_frame_at - Time.now].max)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :refresh_rate, :output
24
+ end
25
+ end
26
+ end
@@ -1,8 +1,11 @@
1
1
  require 'whirled_peas/settings/box_settings'
2
+ require 'whirled_peas/settings/graph_settings'
2
3
  require 'whirled_peas/settings/grid_settings'
3
4
  require 'whirled_peas/settings/text_settings'
5
+ require 'whirled_peas/settings/theme_library'
4
6
 
5
7
  require_relative 'box_painter'
8
+ require_relative 'graph_painter'
6
9
  require_relative 'grid_painter'
7
10
  require_relative 'text_painter'
8
11
 
@@ -23,8 +26,10 @@ module WhirledPeas
23
26
  "Element-#{@counter}"
24
27
  end
25
28
 
26
- def self.build
27
- settings = Settings::BoxSettings.new
29
+ def self.build(theme_name=nil, &block)
30
+ theme_name ||= Settings::ThemeLibrary.default_name
31
+ theme = Settings::ThemeLibrary.get(theme_name)
32
+ settings = Settings::BoxSettings.new(theme)
28
33
  template = BoxPainter.new('TEMPLATE', settings)
29
34
  composer = Composer.new(template)
30
35
  value = yield composer, settings
@@ -45,6 +50,7 @@ module WhirledPeas
45
50
  child = TextPainter.new(name, child_settings)
46
51
  # TextPainters are not composable, so yield nil
47
52
  content = yield nil, child_settings
53
+ child_settings.validate!
48
54
  unless self.class.stringable?(content)
49
55
  raise ArgumentError, "Unsupported type for text: #{content.class}"
50
56
  end
@@ -52,11 +58,25 @@ module WhirledPeas
52
58
  painter.add_child(child)
53
59
  end
54
60
 
61
+ def add_graph(name=self.class.next_name, &block)
62
+ child_settings = Settings::GraphSettings.inherit(painter.settings)
63
+ child = GraphPainter.new(name, child_settings)
64
+ # GraphPainters are not composable, so yield nil
65
+ content = yield nil, child_settings
66
+ child_settings.validate!
67
+ unless content.is_a?(Array) && content.length > 0
68
+ raise ArgumentError, 'Graphs require a non-empty array as the content'
69
+ end
70
+ child.content = content
71
+ painter.add_child(child)
72
+ end
73
+
55
74
  def add_box(name=self.class.next_name, &block)
56
75
  child_settings = Settings::BoxSettings.inherit(painter.settings)
57
76
  child = BoxPainter.new(name, child_settings)
58
77
  composer = self.class.new(child)
59
78
  value = yield composer, child.settings
79
+ child_settings.validate!
60
80
  painter.add_child(child)
61
81
  if !child.children? && self.class.stringable?(value)
62
82
  composer.add_text("#{name}-Text") { value.to_s }
@@ -68,6 +88,7 @@ module WhirledPeas
68
88
  child = GridPainter.new(name, child_settings)
69
89
  composer = self.class.new(child)
70
90
  values = yield composer, child.settings
91
+ child_settings.validate!
71
92
  painter.add_child(child)
72
93
  if !child.children? && values.is_a?(Array)
73
94
  values.each.with_index do |value, index|
@@ -5,6 +5,8 @@ require_relative 'painter'
5
5
 
6
6
  module WhirledPeas
7
7
  module Graphics
8
+ # Abstract Painter for containers. Containers (as the name implies) contain other child
9
+ # elements and must delegate painting of the children to the children themselves.
8
10
  class ContainerPainter < Painter
9
11
  PADDING = ' '
10
12
 
@@ -13,38 +15,65 @@ module WhirledPeas
13
15
  @children = []
14
16
  end
15
17
 
18
+ # Paint the common attributes of containers (e.g. border and background color). Any
19
+ # class that inherits from this one should call `super` at the start of its #paint
20
+ # method, before painting its children.
16
21
  def paint(canvas, left, top, &block)
17
22
  return unless canvas.writable?
18
23
  return unless needs_printing?
19
24
  canvas_coords = coords(left, top)
25
+
26
+ # Paint the border, background color, and scrollbar starting from the top left
27
+ # border position, moving down row by row until we reach the bottom border
28
+ # position
20
29
  stroke_left = canvas_coords.border_left
21
30
  stroke_top = canvas_coords.border_top
31
+
32
+ # All strokes will have the same formatting options
22
33
  formatting = [*settings.border.color, *settings.bg_color]
34
+
35
+ # Paint the top border if the settings call for it
23
36
  if settings.border.top?
24
37
  canvas.stroke(stroke_left, stroke_top, top_border_stroke(canvas_coords), formatting, &block)
25
38
  stroke_top += 1
26
39
  end
40
+ # Precalculate the middle border container grids with more than 1 row
27
41
  middle_border = dimensions.num_rows > 1 ? middle_border_stroke(canvas_coords) : ''
42
+
43
+ # Paint each grid row by row
28
44
  dimensions.num_rows.times do |row_num|
45
+ # In a grid with N rows, we will need to paint N - 1 inner horizontal borders.
46
+ # This code treats the inner horizontal border as the top of each row except for
47
+ # the first one.
29
48
  if row_num > 0 && settings.border.inner_horiz?
30
49
  canvas.stroke(stroke_left, stroke_top, middle_border, formatting, &block)
31
50
  stroke_top += 1
32
51
  end
52
+
53
+ # Paint the interior of each row (horizontal borders, veritical scroll bar and
54
+ # background color for the padding and content area)
33
55
  canvas_coords.inner_grid_height.times do |row_within_cell|
34
56
  canvas.stroke(stroke_left, stroke_top, content_line_stroke(canvas_coords, row_within_cell), formatting, &block)
35
57
  stroke_top += 1
36
58
  end
59
+
60
+ # Paint the horizontal scroll bar is the settings call for it
37
61
  if settings.scrollbar.horiz?
38
62
  canvas.stroke(stroke_left, stroke_top, bottom_scroll_stroke(canvas_coords), formatting, &block)
39
63
  stroke_top += 1
40
64
  end
41
65
  end
66
+
67
+ # Paint the bottom border if the settings call for it
42
68
  if settings.border.bottom?
43
69
  canvas.stroke(stroke_left, stroke_top, bottom_border_stroke(canvas_coords), formatting, &block)
44
70
  stroke_top += 1
45
71
  end
46
72
  end
47
73
 
74
+ # Tightly manage access to the children (rather than simply exposing the underlying
75
+ # array). This allows subclasses to easily modify behavior based on that element's
76
+ # specific settings.
48
77
  def add_child(child)
49
78
  children << child
50
79
  end
@@ -65,6 +94,8 @@ module WhirledPeas
65
94
 
66
95
  attr_reader :children
67
96
 
97
+ # Determine if there is anything to print for the container (this does not accont for
98
+ # children, just the border, scrollbar, and background color)
68
99
  def needs_printing?
69
100
  return true if settings.bg_color
70
101
  return true if settings.border.outer?
@@ -73,10 +104,15 @@ module WhirledPeas
73
104
  settings.scrollbar.horiz? || settings.scrollbar.vert?
74
105
  end
75
106
 
107
+ # Return an object that allows easy access to important coordinates within the container,
108
+ # e.g. the left position where the left border is printed
76
109
  def coords(left, top)
77
110
  ContainerCoords.new(dimensions, settings, left, top)
78
111
  end
79
112
 
113
+ # @return [Array<Integer>] a two-item array, the first being the amount of horizontal
114
+ # spacing to paint *before the first* child and the second being the amount of spacing
115
+ # to paint *between each* child
80
116
  def horiz_justify_offset(containing_width)
81
117
  if settings.align_center?
82
118
  [(dimensions.content_width - containing_width) / 2, 0]
@@ -96,6 +132,9 @@ module WhirledPeas
96
132
  end
97
133
  end
98
134
 
135
+ # @return [Array<Integer>] a two-item array, the first being the amount of vertical
136
+ # spacing to paint *above the first* child and the second being the amount of spacing
137
+ # to paint *between each* child
99
138
  def vert_justify_offset(containing_height)
100
139
  if settings.valign_middle?
101
140
  [(dimensions.content_height - containing_height) / 2, 0]
@@ -115,6 +154,16 @@ module WhirledPeas
115
154
  end
116
155
  end
117
156
 
157
+ # Return a stroke for one line of the container
158
+ #
159
+ # @param left_border [String] the character to print as the first character if there
160
+ # is a left border
161
+ # @param junc_border [String] the character to print as the junction between two grid
162
+ # columns if there is an inner vertical border
163
+ # @param right_border [String] the character to print as the last character if there
164
+ # is a right border
165
+ # @block [String] the block should yield a string that represents the interior
166
+ # (including padding) of a grid cell
118
167
  def line_stroke(left_border, junc_border, right_border, &block)
119
168
  stroke = ''
120
169
  stroke += left_border if settings.border.left?
@@ -126,6 +175,7 @@ module WhirledPeas
126
175
  stroke
127
176
  end
128
177
 
178
+ # Return the stroke for the top border
129
179
  def top_border_stroke(canvas_coords)
130
180
  line_stroke(
131
181
  settings.border.style.top_left,
@@ -136,6 +186,7 @@ module WhirledPeas
136
186
  end
137
187
  end
138
188
 
189
+ # Return the stroke for an inner horizontal border
139
190
  def middle_border_stroke(canvas_coords)
140
191
  line_stroke(
141
192
  settings.border.style.left_junc,
@@ -146,6 +197,7 @@ module WhirledPeas
146
197
  end
147
198
  end
148
199
 
200
+ # Return the stroke for the bottom border
149
201
  def bottom_border_stroke(canvas_coords)
150
202
  line_stroke(
151
203
  settings.border.style.bottom_left,
@@ -156,6 +208,7 @@ module WhirledPeas
156
208
  end
157
209
  end
158
210
 
211
+ # Return the stroke for a grid row between any borders
159
212
  def content_line_stroke(canvas_coords, row_within_cell)
160
213
  line_stroke(
161
214
  settings.border.style.left_vert,
@@ -180,6 +233,7 @@ module WhirledPeas
180
233
  end
181
234
  end
182
235
 
236
+ # Return the stroke for the horizontal scroll bar
183
237
  def bottom_scroll_stroke(canvas_coords)
184
238
  line_stroke(
185
239
  settings.border.style.left_vert,
@@ -197,6 +251,7 @@ module WhirledPeas
197
251
  end
198
252
  end
199
253
 
254
+ # Contants to paint scrollbars
200
255
  GUTTER = ' '
201
256
  HORIZONTAL = %w[▗ ▄ ▖]
202
257
  VERTICAL = %w[
@@ -205,16 +260,33 @@ module WhirledPeas
205
260
 
206
261
  ]
207
262
 
263
+ # Determine the character to paint the horizontal scroll bar with for the given column
264
+ #
265
+ # @see #scroll_char for more details
208
266
  def horiz_scroll_char(col_count, viewable_col_count, first_visible_col, curr_col)
209
267
  scroll_char(col_count, viewable_col_count, first_visible_col, curr_col, HORIZONTAL)
210
268
  end
211
269
 
270
+ # Determine the character to paint the vertical scroll bar with for the given row
271
+ #
272
+ # @see #scroll_char for more details
212
273
  def vert_scroll_char(row_count, viewable_row_count, first_visible_row, curr_row)
213
274
  scroll_char(row_count, viewable_row_count, first_visible_row, curr_row, VERTICAL)
214
275
  end
215
276
 
216
277
  private
217
278
 
279
+ # Determine which character to paint a for a scrollbar
280
+ #
281
+ # @param total_count [Integer] total number of rows/columns in the content
282
+ # @param viewable_count [Integer] number of rows/columns visible in the viewport
283
+ # @param first_visible [Integer] zero-based index of the first row/column that is visible
284
+ # in the viewport
285
+ # @param curr [Integer] zero-based index of the row/column (relative to the first visible
286
+ # row/column) that the painted character is being requested for
287
+ # @param chars [Array<String>] an array with three 1-character strings, the frist is the
288
+ # "second half" scrollbar character, the second is the "full" scrollbar character, and
289
+ # the third is the "first half" scrollbar character.
218
290
  def scroll_char(total_count, viewable_count, first_visible, curr, chars)
219
291
  return GUTTER unless total_count > 0 && viewable_count > 0
220
292
  # The scroll handle has the exact same relative size and position in the scroll gutter
@@ -239,6 +311,25 @@ module WhirledPeas
239
311
  # | * *
240
312
  # +---------1---------2---------3*********4*********+
241
313
  # |...........********|
314
+ #
315
+ # Returning to the first example, we can match up the arguments to this method to the
316
+ # diagram
317
+ #
318
+ # total_count = 50
319
+ # |<----------------------------------------------->|
320
+ # | |
321
+ # | veiwable_count = 20 |
322
+ # | |<----------------->| |
323
+ # ↓ ↓ ↓ ↓
324
+ # +---------1-----****2*********3******---4---------+
325
+ # | * * |
326
+ # | hidden * viewable * hidden |
327
+ # | * * |
328
+ # +---------1-----****2*********3******---4---------+
329
+ # |......****?***.....|
330
+ # ↑ ↑
331
+ # first_visible = 16 |
332
+ # curr = 11
242
333
 
243
334
  # The first task of determining how much of the handle is visible in a row/column is to
244
335
  # calculate the range (as a precentage of the total) of viewable items
@@ -0,0 +1,19 @@
1
+ module WhirledPeas
2
+ module Graphics
3
+ class ContentDimensions
4
+ attr_reader :outer_width, :outer_height
5
+
6
+ def initialize(settings, content)
7
+ if settings.width
8
+ @outer_width = settings.width
9
+ else
10
+ @outer_width = 0
11
+ content.each do |line|
12
+ @outer_width = line.length if line.length > @outer_width
13
+ end
14
+ end
15
+ @outer_height = settings.height || content.length
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'painter'
2
+
3
+ module WhirledPeas
4
+ module Graphics
5
+ class ContentPainter < Painter
6
+ attr_accessor :content
7
+
8
+ def dimensions
9
+ ContentDimensions.new(settings, content_lines)
10
+ end
11
+
12
+ private
13
+
14
+ def content_lines
15
+ raise NotImplementedError, "#{self.class} must implement #content_lines"
16
+ end
17
+ end
18
+ private_constant :ContentPainter
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ module WhirledPeas
2
+ module Graphics
3
+ class TextDimensions
4
+ attr_reader :outer_width, :outer_height
5
+
6
+ def initialize(settings, content)
7
+ @outer_width = settings.width
8
+ @outer_height = settings.height
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,97 @@
1
+ require 'whirled_peas/utils/ansi'
2
+
3
+ require_relative 'content_painter'
4
+
5
+ module WhirledPeas
6
+ module Graphics
7
+ class GraphPainter < ContentPainter
8
+ def paint(canvas, left, top, &block)
9
+ axis_formatting = [*settings.axis_color, *settings.bg_color]
10
+ plot_formatting = [*settings.color, *settings.bg_color]
11
+ axes_lines.each.with_index do |axis_line, row_index|
12
+ canvas.stroke(left, top + row_index, axis_line, axis_formatting, &block)
13
+ next if row_index >= plot_lines.length
14
+ canvas.stroke(left + 1, top + row_index, plot_lines[row_index], plot_formatting, &block)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def content_lines
21
+ axes_lines
22
+ end
23
+
24
+ def plot_lines
25
+ return @plot_lines if @plot_lines
26
+ min_y = 1.0 / 0
27
+ max_y = -1.0 / 0
28
+ if settings.width
29
+ interpolated = inner_width.times.map do |i|
30
+ x = (i * (content.length - 1).to_f / inner_width).floor
31
+ max_y = content[x] if content[x] > max_y
32
+ min_y = content[x] if content[x] < min_y
33
+ content[x]
34
+ end
35
+ else
36
+ interpolated = content
37
+ content.each do |y|
38
+ max_y = y if y > max_y
39
+ min_y = y if y < min_y
40
+ end
41
+ end
42
+ scaled = interpolated.map do |y|
43
+ (2 * inner_height * (y - min_y).to_f / (max_y - min_y)).floor
44
+ end
45
+ @plot_lines = Array.new(inner_height) { '' }
46
+ scaled.each.with_index do |y, x_index|
47
+ @plot_lines.each.with_index do |row, row_index|
48
+ bottom_half_index = 2 * (inner_height - row_index - 1)
49
+ top_half_index = bottom_half_index + 1
50
+
51
+ asc, next_y = if scaled.length == 1
52
+ [true, y]
53
+ elsif x_index == scaled.length - 1
54
+ y >= scaled[x_index - 1]
55
+ [true, y]
56
+ else
57
+ scaled[x_index + 1] >= y
58
+ [true, scaled[x_index + 1]]
59
+ end
60
+ if asc
61
+ top_half = y == top_half_index || (y...next_y).include?(top_half_index)
62
+ bottom_half = y == bottom_half_index || (y...next_y).include?(bottom_half_index)
63
+ else
64
+ top_half = y == top_half_index || (next_y...y).include?(top_half_index)
65
+ bottom_half = y == bottom_half_index || (next_y...y).include?(bottom_half_index)
66
+ end
67
+ row << if top_half && bottom_half
68
+ '█'
69
+ elsif top_half
70
+ '▀'
71
+ elsif bottom_half
72
+ '▄'
73
+ else
74
+ ' '
75
+ end
76
+ end
77
+ end
78
+ @plot_lines
79
+ end
80
+
81
+ def inner_height
82
+ settings.height - 1
83
+ end
84
+
85
+ def inner_width
86
+ settings.width.nil? ? content.length : settings.width - 1
87
+ end
88
+
89
+ def axes_lines
90
+ return @axes_lines if @axes_lines
91
+ @axes_lines = inner_height.times.map { '┃' }
92
+ @axes_lines << '┗' + '━' * inner_width
93
+ @axes_lines
94
+ end
95
+ end
96
+ end
97
+ end