whirled_peas 0.7.0 → 0.10.0

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