whirled_peas 0.4.1 → 0.8.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 (238) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +1 -0
  4. data/CHANGELOG.md +37 -0
  5. data/README.md +529 -156
  6. data/Rakefile +9 -3
  7. data/bin/reset_cursor +11 -0
  8. data/bin/screen_test +68 -0
  9. data/examples/intro.rb +52 -0
  10. data/examples/scrolling.rb +54 -0
  11. data/lib/whirled_peas.rb +6 -12
  12. data/lib/whirled_peas/animator.rb +5 -0
  13. data/lib/whirled_peas/animator/debug_consumer.rb +17 -0
  14. data/lib/whirled_peas/animator/easing.rb +72 -0
  15. data/lib/whirled_peas/animator/frame.rb +5 -0
  16. data/lib/whirled_peas/animator/frameset.rb +33 -0
  17. data/lib/whirled_peas/animator/producer.rb +35 -0
  18. data/lib/whirled_peas/animator/renderer_consumer.rb +31 -0
  19. data/lib/whirled_peas/command.rb +5 -0
  20. data/lib/whirled_peas/command/base.rb +86 -0
  21. data/lib/whirled_peas/command/config_command.rb +44 -0
  22. data/lib/whirled_peas/command/debug.rb +21 -0
  23. data/lib/whirled_peas/command/fonts.rb +22 -0
  24. data/lib/whirled_peas/command/frame_command.rb +34 -0
  25. data/lib/whirled_peas/command/frames.rb +24 -0
  26. data/lib/whirled_peas/command/help.rb +38 -0
  27. data/lib/whirled_peas/command/play.rb +108 -0
  28. data/lib/whirled_peas/command/record.rb +57 -0
  29. data/lib/whirled_peas/command/still.rb +29 -0
  30. data/lib/whirled_peas/command_line.rb +23 -228
  31. data/lib/whirled_peas/config.rb +56 -6
  32. data/lib/whirled_peas/device.rb +5 -0
  33. data/lib/whirled_peas/device/null_device.rb +8 -0
  34. data/lib/whirled_peas/device/output_file.rb +19 -0
  35. data/lib/whirled_peas/device/screen.rb +26 -0
  36. data/lib/whirled_peas/errors.rb +2 -0
  37. data/lib/whirled_peas/graphics.rb +19 -0
  38. data/lib/whirled_peas/graphics/box_painter.rb +101 -0
  39. data/lib/whirled_peas/graphics/canvas.rb +118 -0
  40. data/lib/whirled_peas/graphics/composer.rb +80 -0
  41. data/lib/whirled_peas/graphics/container_coords.rb +72 -0
  42. data/lib/whirled_peas/graphics/container_dimensions.rb +93 -0
  43. data/lib/whirled_peas/graphics/container_painter.rb +363 -0
  44. data/lib/whirled_peas/graphics/debugger.rb +52 -0
  45. data/lib/whirled_peas/graphics/grid_painter.rb +69 -0
  46. data/lib/whirled_peas/graphics/mock_screen.rb +26 -0
  47. data/lib/whirled_peas/graphics/painter.rb +33 -0
  48. data/lib/whirled_peas/graphics/renderer.rb +32 -0
  49. data/lib/whirled_peas/graphics/text_dimensions.rb +15 -0
  50. data/lib/whirled_peas/graphics/text_painter.rb +40 -0
  51. data/lib/whirled_peas/settings.rb +5 -0
  52. data/lib/whirled_peas/settings/alignment.rb +24 -0
  53. data/lib/whirled_peas/settings/bg_color.rb +24 -0
  54. data/lib/whirled_peas/settings/border.rb +101 -0
  55. data/lib/whirled_peas/settings/box_settings.rb +8 -0
  56. data/lib/whirled_peas/settings/color.rb +68 -0
  57. data/lib/whirled_peas/settings/container_settings.rb +223 -0
  58. data/lib/whirled_peas/settings/debugger.rb +96 -0
  59. data/lib/whirled_peas/settings/display_flow.rb +27 -0
  60. data/lib/whirled_peas/settings/element_settings.rb +61 -0
  61. data/lib/whirled_peas/settings/grid_settings.rb +19 -0
  62. data/lib/whirled_peas/settings/margin.rb +8 -0
  63. data/lib/whirled_peas/settings/padding.rb +8 -0
  64. data/lib/whirled_peas/settings/position.rb +15 -0
  65. data/lib/whirled_peas/settings/scrollbar.rb +15 -0
  66. data/lib/whirled_peas/settings/sizing.rb +19 -0
  67. data/lib/whirled_peas/settings/spacing.rb +58 -0
  68. data/lib/whirled_peas/settings/text_color.rb +21 -0
  69. data/lib/whirled_peas/settings/text_settings.rb +15 -0
  70. data/lib/whirled_peas/settings/vert_alignment.rb +24 -0
  71. data/lib/whirled_peas/utils/ansi.rb +19 -56
  72. data/lib/whirled_peas/utils/file_handler.rb +57 -0
  73. data/lib/whirled_peas/utils/formatted_string.rb +64 -0
  74. data/lib/whirled_peas/utils/title_font.rb +1 -1
  75. data/lib/whirled_peas/version.rb +1 -1
  76. data/screen_test/elements/box.frame +1 -0
  77. data/screen_test/elements/box.rb +20 -0
  78. data/screen_test/elements/grid.frame +1 -0
  79. data/screen_test/elements/grid.rb +13 -0
  80. data/screen_test/elements/screen_overflow_x.frame +1 -0
  81. data/screen_test/elements/screen_overflow_x.rb +9 -0
  82. data/screen_test/elements/screen_overflow_y.frame +1 -0
  83. data/screen_test/elements/screen_overflow_y.rb +9 -0
  84. data/screen_test/elements/text.frame +1 -0
  85. data/screen_test/elements/text.rb +9 -0
  86. data/screen_test/elements/text_multiline.frame +1 -0
  87. data/screen_test/elements/text_multiline.rb +9 -0
  88. data/screen_test/settings/align/box_around.frame +1 -0
  89. data/screen_test/settings/align/box_around.rb +16 -0
  90. data/screen_test/settings/align/box_between.frame +1 -0
  91. data/screen_test/settings/align/box_between.rb +16 -0
  92. data/screen_test/settings/align/box_center.frame +1 -0
  93. data/screen_test/settings/align/box_center.rb +21 -0
  94. data/screen_test/settings/align/box_default.frame +1 -0
  95. data/screen_test/settings/align/box_default.rb +20 -0
  96. data/screen_test/settings/align/box_evenly.frame +1 -0
  97. data/screen_test/settings/align/box_evenly.rb +16 -0
  98. data/screen_test/settings/align/box_left.frame +1 -0
  99. data/screen_test/settings/align/box_left.rb +21 -0
  100. data/screen_test/settings/align/box_right.frame +1 -0
  101. data/screen_test/settings/align/box_right.rb +21 -0
  102. data/screen_test/settings/align/children_center.frame +1 -0
  103. data/screen_test/settings/align/children_center.rb +15 -0
  104. data/screen_test/settings/align/children_left.frame +1 -0
  105. data/screen_test/settings/align/children_left.rb +15 -0
  106. data/screen_test/settings/align/children_right.frame +1 -0
  107. data/screen_test/settings/align/children_right.rb +15 -0
  108. data/screen_test/settings/align/grid_center.frame +1 -0
  109. data/screen_test/settings/align/grid_center.rb +18 -0
  110. data/screen_test/settings/align/grid_default.frame +1 -0
  111. data/screen_test/settings/align/grid_default.rb +17 -0
  112. data/screen_test/settings/align/grid_left.frame +1 -0
  113. data/screen_test/settings/align/grid_left.rb +18 -0
  114. data/screen_test/settings/align/grid_right.frame +1 -0
  115. data/screen_test/settings/align/grid_right.rb +18 -0
  116. data/screen_test/settings/ansi/bold.frame +1 -0
  117. data/screen_test/settings/ansi/bold.rb +14 -0
  118. data/screen_test/settings/ansi/color.frame +1 -0
  119. data/screen_test/settings/ansi/color.rb +37 -0
  120. data/screen_test/settings/ansi/underline.frame +1 -0
  121. data/screen_test/settings/ansi/underline.rb +14 -0
  122. data/screen_test/settings/border.frame +1 -0
  123. data/screen_test/settings/border.rb +13 -0
  124. data/screen_test/settings/flow/box_b2t.frame +1 -0
  125. data/screen_test/settings/flow/box_b2t.rb +26 -0
  126. data/screen_test/settings/flow/box_l2r.frame +1 -0
  127. data/screen_test/settings/flow/box_l2r.rb +26 -0
  128. data/screen_test/settings/flow/box_r2l.frame +1 -0
  129. data/screen_test/settings/flow/box_r2l.rb +26 -0
  130. data/screen_test/settings/flow/box_t2b.frame +1 -0
  131. data/screen_test/settings/flow/box_t2b.rb +26 -0
  132. data/screen_test/settings/flow/grid_b2t.frame +1 -0
  133. data/screen_test/settings/flow/grid_b2t.rb +14 -0
  134. data/screen_test/settings/flow/grid_l2r.frame +1 -0
  135. data/screen_test/settings/flow/grid_l2r.rb +14 -0
  136. data/screen_test/settings/flow/grid_r2l.frame +1 -0
  137. data/screen_test/settings/flow/grid_r2l.rb +14 -0
  138. data/screen_test/settings/flow/grid_t2b.frame +1 -0
  139. data/screen_test/settings/flow/grid_t2b.rb +14 -0
  140. data/screen_test/settings/height/box.frame +1 -0
  141. data/screen_test/settings/height/box.rb +13 -0
  142. data/screen_test/settings/height/box_border_sizing.frame +1 -0
  143. data/screen_test/settings/height/box_border_sizing.rb +15 -0
  144. data/screen_test/settings/height/grid.frame +1 -0
  145. data/screen_test/settings/height/grid.rb +14 -0
  146. data/screen_test/settings/height/overflow_box.frame +1 -0
  147. data/screen_test/settings/height/overflow_box.rb +13 -0
  148. data/screen_test/settings/height/overflow_box_l2r.frame +1 -0
  149. data/screen_test/settings/height/overflow_box_l2r.rb +17 -0
  150. data/screen_test/settings/height/overflow_box_t2b.frame +1 -0
  151. data/screen_test/settings/height/overflow_box_t2b.rb +16 -0
  152. data/screen_test/settings/height/overflow_grid.frame +1 -0
  153. data/screen_test/settings/height/overflow_grid.rb +16 -0
  154. data/screen_test/settings/margin.frame +1 -0
  155. data/screen_test/settings/margin.rb +16 -0
  156. data/screen_test/settings/padding.frame +1 -0
  157. data/screen_test/settings/padding.rb +13 -0
  158. data/screen_test/settings/position/box_left.frame +1 -0
  159. data/screen_test/settings/position/box_left.rb +17 -0
  160. data/screen_test/settings/position/box_left_negative.frame +1 -0
  161. data/screen_test/settings/position/box_left_negative.rb +17 -0
  162. data/screen_test/settings/position/box_top.frame +1 -0
  163. data/screen_test/settings/position/box_top.rb +17 -0
  164. data/screen_test/settings/position/box_top_negative.frame +1 -0
  165. data/screen_test/settings/position/box_top_negative.rb +17 -0
  166. data/screen_test/settings/position/grid_left.frame +1 -0
  167. data/screen_test/settings/position/grid_left.rb +18 -0
  168. data/screen_test/settings/position/grid_left_negative.frame +1 -0
  169. data/screen_test/settings/position/grid_left_negative.rb +18 -0
  170. data/screen_test/settings/position/grid_top.frame +1 -0
  171. data/screen_test/settings/position/grid_top.rb +18 -0
  172. data/screen_test/settings/position/grid_top_negative.frame +1 -0
  173. data/screen_test/settings/position/grid_top_negative.rb +18 -0
  174. data/screen_test/settings/scroll/horiz_box.frame +1 -0
  175. data/screen_test/settings/scroll/horiz_box.rb +17 -0
  176. data/screen_test/settings/scroll/horiz_box_align_center.rb +18 -0
  177. data/screen_test/settings/scroll/horiz_box_align_right.rb +18 -0
  178. data/screen_test/settings/scroll/vert_box.frame +1 -0
  179. data/screen_test/settings/scroll/vert_box.rb +20 -0
  180. data/screen_test/settings/title_font.frame +1 -0
  181. data/screen_test/settings/title_font.rb +12 -0
  182. data/screen_test/settings/valign/box_around.frame +1 -0
  183. data/screen_test/settings/valign/box_around.rb +17 -0
  184. data/screen_test/settings/valign/box_between.frame +1 -0
  185. data/screen_test/settings/valign/box_between.rb +17 -0
  186. data/screen_test/settings/valign/box_bottom.frame +1 -0
  187. data/screen_test/settings/valign/box_bottom.rb +17 -0
  188. data/screen_test/settings/valign/box_default.frame +1 -0
  189. data/screen_test/settings/valign/box_default.rb +16 -0
  190. data/screen_test/settings/valign/box_evenly.frame +1 -0
  191. data/screen_test/settings/valign/box_evenly.rb +17 -0
  192. data/screen_test/settings/valign/box_middle.frame +1 -0
  193. data/screen_test/settings/valign/box_middle.rb +17 -0
  194. data/screen_test/settings/valign/box_top.frame +1 -0
  195. data/screen_test/settings/valign/box_top.rb +17 -0
  196. data/screen_test/settings/valign/grid_bottom.frame +1 -0
  197. data/screen_test/settings/valign/grid_bottom.rb +15 -0
  198. data/screen_test/settings/valign/grid_default.frame +1 -0
  199. data/screen_test/settings/valign/grid_default.rb +14 -0
  200. data/screen_test/settings/valign/grid_middle.frame +1 -0
  201. data/screen_test/settings/valign/grid_middle.rb +15 -0
  202. data/screen_test/settings/valign/grid_top.frame +1 -0
  203. data/screen_test/settings/valign/grid_top.rb +15 -0
  204. data/screen_test/settings/width/box_border_sizing.frame +1 -0
  205. data/screen_test/settings/width/box_border_sizing.rb +15 -0
  206. data/screen_test/settings/width/box_content.frame +1 -0
  207. data/screen_test/settings/width/box_content.rb +15 -0
  208. data/screen_test/settings/width/box_default.frame +1 -0
  209. data/screen_test/settings/width/box_default.rb +14 -0
  210. data/screen_test/settings/width/grid.frame +1 -0
  211. data/screen_test/settings/width/grid.rb +14 -0
  212. data/screen_test/settings/width/overflow_align_center.frame +1 -0
  213. data/screen_test/settings/width/overflow_align_center.rb +14 -0
  214. data/screen_test/settings/width/overflow_align_right.frame +1 -0
  215. data/screen_test/settings/width/overflow_align_right.rb +14 -0
  216. data/screen_test/settings/width/overflow_box.frame +1 -0
  217. data/screen_test/settings/width/overflow_box.rb +13 -0
  218. data/screen_test/settings/width/overflow_box_l2r.frame +1 -0
  219. data/screen_test/settings/width/overflow_box_l2r.rb +16 -0
  220. data/screen_test/settings/width/overflow_box_t2b.frame +1 -0
  221. data/screen_test/settings/width/overflow_box_t2b.rb +17 -0
  222. data/screen_test/settings/width/overflow_grid.frame +1 -0
  223. data/screen_test/settings/width/overflow_grid.rb +14 -0
  224. data/tools/whirled_peas/tools/screen_tester.rb +285 -0
  225. metadata +213 -15
  226. data/bin/title_fonts +0 -6
  227. data/lib/whirled_peas/frame.rb +0 -7
  228. data/lib/whirled_peas/frame/event_loop.rb +0 -91
  229. data/lib/whirled_peas/frame/print_consumer.rb +0 -33
  230. data/lib/whirled_peas/frame/producer.rb +0 -61
  231. data/lib/whirled_peas/template.rb +0 -5
  232. data/lib/whirled_peas/template/element.rb +0 -230
  233. data/lib/whirled_peas/template/settings.rb +0 -530
  234. data/lib/whirled_peas/ui.rb +0 -5
  235. data/lib/whirled_peas/ui/canvas.rb +0 -68
  236. data/lib/whirled_peas/ui/painter.rb +0 -287
  237. data/lib/whirled_peas/ui/screen.rb +0 -63
  238. data/lib/whirled_peas/utils/color.rb +0 -101
@@ -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
@@ -0,0 +1,80 @@
1
+ require 'whirled_peas/settings/box_settings'
2
+ require 'whirled_peas/settings/grid_settings'
3
+ require 'whirled_peas/settings/text_settings'
4
+
5
+ require_relative 'box_painter'
6
+ require_relative 'grid_painter'
7
+ require_relative 'text_painter'
8
+
9
+ module WhirledPeas
10
+ module Graphics
11
+ class Composer
12
+ # List of classes that convert nicely to a string
13
+ STRINGALBE_CLASSES = [FalseClass, Float, Integer, NilClass, String, Symbol, TrueClass]
14
+ private_constant :STRINGALBE_CLASSES
15
+
16
+ def self.stringable?(value)
17
+ STRINGALBE_CLASSES.include?(value.class)
18
+ end
19
+
20
+ def self.next_name
21
+ @counter ||= 0
22
+ @counter += 1
23
+ "Element-#{@counter}"
24
+ end
25
+
26
+ def self.build
27
+ settings = Settings::BoxSettings.new
28
+ template = BoxPainter.new('TEMPLATE', settings)
29
+ composer = Composer.new(template)
30
+ value = yield composer, settings
31
+ if !template.children? && stringable?(value)
32
+ composer.add_text { value.to_s }
33
+ end
34
+ template
35
+ end
36
+
37
+ attr_reader :painter
38
+
39
+ def initialize(painter)
40
+ @painter = painter
41
+ end
42
+
43
+ def add_text(name=self.class.next_name, &block)
44
+ child_settings = Settings::TextSettings.inherit(painter.settings)
45
+ child = TextPainter.new(name, child_settings)
46
+ # TextPainters are not composable, so yield nil
47
+ content = yield nil, child_settings
48
+ unless self.class.stringable?(content)
49
+ raise ArgumentError, "Unsupported type for text: #{content.class}"
50
+ end
51
+ child.content = content.to_s
52
+ painter.add_child(child)
53
+ end
54
+
55
+ def add_box(name=self.class.next_name, &block)
56
+ child_settings = Settings::BoxSettings.inherit(painter.settings)
57
+ child = BoxPainter.new(name, child_settings)
58
+ composer = self.class.new(child)
59
+ value = yield composer, child.settings
60
+ painter.add_child(child)
61
+ if !child.children? && self.class.stringable?(value)
62
+ composer.add_text("#{name}-Text") { value.to_s }
63
+ end
64
+ end
65
+
66
+ def add_grid(name=self.class.next_name, &block)
67
+ child_settings = Settings::GridSettings.inherit(painter.settings)
68
+ child = GridPainter.new(name, child_settings)
69
+ composer = self.class.new(child)
70
+ values = yield composer, child.settings
71
+ painter.add_child(child)
72
+ if !child.children? && values.is_a?(Array)
73
+ values.each.with_index do |value, index|
74
+ composer.add_text("#{name}-Text-#{index}") { value.to_s }
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,72 @@
1
+ module WhirledPeas
2
+ module Graphics
3
+ class ContainerCoords
4
+ def initialize(dimensions, settings, start_left, start_top)
5
+ @dimensions = dimensions
6
+ @settings = settings
7
+ @start_left = start_left
8
+ @start_top = start_top
9
+ end
10
+
11
+ def left
12
+ start_left + settings.position.left
13
+ end
14
+
15
+ def top
16
+ start_top + settings.position.top
17
+ end
18
+
19
+ def border_left
20
+ left + settings.margin.left
21
+ end
22
+
23
+ def border_top
24
+ top + settings.margin.top
25
+ end
26
+
27
+ def padding_left
28
+ border_left + (settings.border.left? ? 1 : 0)
29
+ end
30
+
31
+ def padding_top
32
+ border_top + (settings.border.top? ? 1 : 0)
33
+ end
34
+
35
+ def content_left(col_index=0)
36
+ padding_left + settings.padding.left + col_index * grid_width
37
+ end
38
+
39
+ def content_top(row_index=0)
40
+ padding_top + settings.padding.top + row_index * grid_height
41
+ end
42
+
43
+ def inner_grid_width
44
+ settings.padding.left +
45
+ dimensions.content_width +
46
+ settings.padding.right
47
+ end
48
+
49
+ def grid_width
50
+ (settings.border.inner_vert? ? 1 : 0) +
51
+ inner_grid_width +
52
+ (settings.scrollbar.vert? ? 1 : 0)
53
+ end
54
+
55
+ def inner_grid_height
56
+ settings.padding.top +
57
+ dimensions.content_height +
58
+ settings.padding.bottom
59
+ end
60
+
61
+ def grid_height
62
+ (settings.border.inner_horiz? ? 1 : 0) +
63
+ inner_grid_height +
64
+ (settings.scrollbar.horiz? ? 1 : 0)
65
+ end
66
+
67
+ private
68
+
69
+ attr_reader :settings, :dimensions, :start_left, :start_top
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,93 @@
1
+ module WhirledPeas
2
+ module Graphics
3
+ class ContainerDimensions
4
+ attr_reader :children_width, :children_height, :num_cols, :num_rows
5
+
6
+ def initialize(settings, content_width, content_height, num_cols=1, num_rows=1)
7
+ @orig_content_width = content_width
8
+ @orig_content_height = content_height
9
+ @children_width = content_width
10
+ @children_height = content_height
11
+ @num_cols = num_cols
12
+ @num_rows = num_rows
13
+ @settings = settings
14
+ end
15
+
16
+ def content_width
17
+ return orig_content_width unless settings.width
18
+ if settings.border_sizing?
19
+ settings.width - outer_border_width - scrollbar_width - padding_width
20
+ else
21
+ settings.width
22
+ end
23
+ end
24
+
25
+ def content_height
26
+ return orig_content_height unless settings.height
27
+ if settings.border_sizing?
28
+ settings.height - outer_border_height - scrollbar_height - padding_height
29
+ else
30
+ settings.height
31
+ end
32
+ end
33
+
34
+ def outer_width
35
+ @outer_width ||= margin_width +
36
+ outer_border_width +
37
+ num_cols * (padding_width + content_width + scrollbar_width) +
38
+ (num_cols - 1) * inner_border_width
39
+ end
40
+
41
+ def outer_height
42
+ @outer_height ||= margin_height +
43
+ outer_border_height +
44
+ num_rows * (padding_height + content_height + scrollbar_height) +
45
+ (num_rows - 1) * inner_border_height
46
+ end
47
+
48
+ def margin_width
49
+ settings.margin.left + settings.margin.right
50
+ end
51
+
52
+ def margin_height
53
+ settings.margin.top + settings.margin.bottom
54
+ end
55
+
56
+ def outer_border_width
57
+ (settings.border.left? ? 1 : 0) + (settings.border.right? ? 1 : 0)
58
+ end
59
+
60
+ def outer_border_height
61
+ (settings.border.top? ? 1 : 0) + (settings.border.bottom? ? 1 : 0)
62
+ end
63
+
64
+ def inner_border_width
65
+ settings.border.inner_vert? ? 1 : 0
66
+ end
67
+
68
+ def inner_border_height
69
+ settings.border.inner_horiz? ? 1 : 0
70
+ end
71
+
72
+ def padding_width
73
+ settings.padding.left + settings.padding.right
74
+ end
75
+
76
+ def padding_height
77
+ settings.padding.top + settings.padding.bottom
78
+ end
79
+
80
+ def scrollbar_width
81
+ settings.scrollbar.vert? ? 1 : 0
82
+ end
83
+
84
+ def scrollbar_height
85
+ settings.scrollbar.horiz? ? 1 : 0
86
+ end
87
+
88
+ private
89
+
90
+ attr_reader :settings, :orig_content_width, :orig_content_height
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,363 @@
1
+ require 'whirled_peas/utils/formatted_string'
2
+
3
+ require_relative 'container_coords'
4
+ require_relative 'painter'
5
+
6
+ module WhirledPeas
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.
10
+ class ContainerPainter < Painter
11
+ PADDING = ' '
12
+
13
+ def initialize(name, settings)
14
+ super
15
+ @children = []
16
+ end
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.
21
+ def paint(canvas, left, top, &block)
22
+ return unless canvas.writable?
23
+ return unless needs_printing?
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
29
+ stroke_left = canvas_coords.border_left
30
+ stroke_top = canvas_coords.border_top
31
+
32
+ # All strokes will have the same formatting options
33
+ formatting = [*settings.border.color, *settings.bg_color]
34
+
35
+ # Paint the top border if the settings call for it
36
+ if settings.border.top?
37
+ canvas.stroke(stroke_left, stroke_top, top_border_stroke(canvas_coords), formatting, &block)
38
+ stroke_top += 1
39
+ end
40
+ # Precalculate the middle border container grids with more than 1 row
41
+ middle_border = dimensions.num_rows > 1 ? middle_border_stroke(canvas_coords) : ''
42
+
43
+ # Paint each grid row by row
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.
48
+ if row_num > 0 && settings.border.inner_horiz?
49
+ canvas.stroke(stroke_left, stroke_top, middle_border, formatting, &block)
50
+ stroke_top += 1
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)
55
+ canvas_coords.inner_grid_height.times do |row_within_cell|
56
+ canvas.stroke(stroke_left, stroke_top, content_line_stroke(canvas_coords, row_within_cell), formatting, &block)
57
+ stroke_top += 1
58
+ end
59
+
60
+ # Paint the horizontal scroll bar is the settings call for it
61
+ if settings.scrollbar.horiz?
62
+ canvas.stroke(stroke_left, stroke_top, bottom_scroll_stroke(canvas_coords), formatting, &block)
63
+ stroke_top += 1
64
+ end
65
+ end
66
+
67
+ # Paint the bottom border if the settings call for it
68
+ if settings.border.bottom?
69
+ canvas.stroke(stroke_left, stroke_top, bottom_border_stroke(canvas_coords), formatting, &block)
70
+ stroke_top += 1
71
+ end
72
+ end
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.
77
+ def add_child(child)
78
+ children << child
79
+ end
80
+
81
+ def num_children
82
+ children.length
83
+ end
84
+
85
+ def children?
86
+ num_children > 0
87
+ end
88
+
89
+ def each_child(&block)
90
+ children.each(&block)
91
+ end
92
+
93
+ private
94
+
95
+ attr_reader :children
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)
99
+ def needs_printing?
100
+ return true if settings.bg_color
101
+ return true if settings.border.outer?
102
+ return true if dimensions.num_cols > 1 && settings.border.inner_vert?
103
+ return true if dimensions.num_rows > 1 && settings.border.inner_horiz?
104
+ settings.scrollbar.horiz? || settings.scrollbar.vert?
105
+ end
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
109
+ def coords(left, top)
110
+ ContainerCoords.new(dimensions, settings, left, top)
111
+ end
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
116
+ def horiz_justify_offset(containing_width)
117
+ if settings.align_center?
118
+ [(dimensions.content_width - containing_width) / 2, 0]
119
+ elsif settings.align_right?
120
+ [dimensions.content_width - containing_width, 0]
121
+ elsif settings.align_between?
122
+ return [0, 0] if num_children == 1
123
+ [0, (dimensions.content_width - containing_width) / (num_children - 1)]
124
+ elsif settings.align_around?
125
+ full_spacing = (dimensions.content_width - containing_width) / num_children
126
+ [full_spacing / 2, full_spacing]
127
+ elsif settings.align_evenly?
128
+ spacing = (dimensions.content_width - containing_width) / (num_children + 1)
129
+ [spacing, spacing]
130
+ else
131
+ [0, 0]
132
+ end
133
+ end
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
138
+ def vert_justify_offset(containing_height)
139
+ if settings.valign_middle?
140
+ [(dimensions.content_height - containing_height) / 2, 0]
141
+ elsif settings.valign_bottom?
142
+ [dimensions.content_height - containing_height, 0]
143
+ elsif settings.valign_between?
144
+ return [0, 0] if num_children == 1
145
+ [0, (dimensions.content_height - containing_height) / (num_children - 1)]
146
+ elsif settings.valign_around?
147
+ full_spacing = (dimensions.content_height - containing_height) / num_children
148
+ [full_spacing / 2, full_spacing]
149
+ elsif settings.valign_evenly?
150
+ spacing = (dimensions.content_height - containing_height) / (num_children + 1)
151
+ [spacing, spacing]
152
+ else
153
+ [0, 0]
154
+ end
155
+ end
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
167
+ def line_stroke(left_border, junc_border, right_border, &block)
168
+ stroke = ''
169
+ stroke += left_border if settings.border.left?
170
+ dimensions.num_cols.times do |col_num|
171
+ stroke += junc_border if col_num > 0 && settings.border.inner_vert?
172
+ stroke += yield
173
+ end
174
+ stroke += right_border if settings.border.right?
175
+ stroke
176
+ end
177
+
178
+ # Return the stroke for the top border
179
+ def top_border_stroke(canvas_coords)
180
+ line_stroke(
181
+ settings.border.style.top_left,
182
+ settings.border.style.top_junc,
183
+ settings.border.style.top_right
184
+ ) do
185
+ settings.border.style.top_horiz * (canvas_coords.inner_grid_width + (settings.scrollbar.vert? ? 1 : 0))
186
+ end
187
+ end
188
+
189
+ # Return the stroke for an inner horizontal border
190
+ def middle_border_stroke(canvas_coords)
191
+ line_stroke(
192
+ settings.border.style.left_junc,
193
+ settings.border.style.cross_junc,
194
+ settings.border.style.right_junc
195
+ ) do
196
+ settings.border.style.middle_horiz * (canvas_coords.inner_grid_width + (settings.scrollbar.vert? ? 1 : 0))
197
+ end
198
+ end
199
+
200
+ # Return the stroke for the bottom border
201
+ def bottom_border_stroke(canvas_coords)
202
+ line_stroke(
203
+ settings.border.style.bottom_left,
204
+ settings.border.style.bottom_junc,
205
+ settings.border.style.bottom_right
206
+ ) do
207
+ settings.border.style.bottom_horiz * (canvas_coords.inner_grid_width + (settings.scrollbar.vert? ? 1 : 0))
208
+ end
209
+ end
210
+
211
+ # Return the stroke for a grid row between any borders
212
+ def content_line_stroke(canvas_coords, row_within_cell)
213
+ line_stroke(
214
+ settings.border.style.left_vert,
215
+ settings.border.style.middle_vert,
216
+ settings.border.style.right_vert,
217
+ ) do
218
+ if settings.scrollbar.vert?
219
+ if dimensions.children_height <= canvas_coords.grid_height || children.first.settings.position.top > 0
220
+ scrollbar_char = GUTTER
221
+ else
222
+ scrollbar_char = vert_scroll_char(
223
+ dimensions.children_height + dimensions.padding_height,
224
+ canvas_coords.inner_grid_height,
225
+ -children.first.settings.position.top,
226
+ row_within_cell
227
+ )
228
+ end
229
+ PADDING * canvas_coords.inner_grid_width + scrollbar_char
230
+ else
231
+ PADDING * canvas_coords.inner_grid_width
232
+ end
233
+ end
234
+ end
235
+
236
+ # Return the stroke for the horizontal scroll bar
237
+ def bottom_scroll_stroke(canvas_coords)
238
+ line_stroke(
239
+ settings.border.style.left_vert,
240
+ settings.border.style.middle_vert,
241
+ settings.border.style.right_vert,
242
+ ) do
243
+ canvas_coords.inner_grid_width.times.map do |col_within_cell|
244
+ horiz_scroll_char(
245
+ dimensions.children_width + dimensions.padding_width,
246
+ canvas_coords.inner_grid_width,
247
+ -children.first.settings.position.left,
248
+ col_within_cell
249
+ )
250
+ end.join
251
+ end
252
+ end
253
+
254
+ # Contants to paint scrollbars
255
+ GUTTER = ' '
256
+ HORIZONTAL = %w[▗ ▄ ▖]
257
+ VERTICAL = %w[
258
+
259
+
260
+
261
+ ]
262
+
263
+ # Determine the character to paint the horizontal scroll bar with for the given column
264
+ #
265
+ # @see #scroll_char for more details
266
+ def horiz_scroll_char(col_count, viewable_col_count, first_visible_col, curr_col)
267
+ scroll_char(col_count, viewable_col_count, first_visible_col, curr_col, HORIZONTAL)
268
+ end
269
+
270
+ # Determine the character to paint the vertical scroll bar with for the given row
271
+ #
272
+ # @see #scroll_char for more details
273
+ def vert_scroll_char(row_count, viewable_row_count, first_visible_row, curr_row)
274
+ scroll_char(row_count, viewable_row_count, first_visible_row, curr_row, VERTICAL)
275
+ end
276
+
277
+ private
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.
290
+ def scroll_char(total_count, viewable_count, first_visible, curr, chars)
291
+ return GUTTER unless total_count > 0 && viewable_count > 0
292
+ # The scroll handle has the exact same relative size and position in the scroll gutter
293
+ # that the viewable content has in the total content area. For example, a content area
294
+ # that is 50 columns wide with a view port that is 20 columns wide might look like
295
+ #
296
+ # +---------1-----****2*********3******---4---------+
297
+ # | * * |
298
+ # | hidden * viewable * hidden |
299
+ # | * * |
300
+ # +---------1-----****2*********3******---4---------+
301
+ #
302
+ # The scoll gutter, would look like
303
+ #
304
+ # |......********.....|
305
+ #
306
+ # Scrolling all the way to the right results in
307
+ #
308
+ # +---------1---------2---------3*********4*********+
309
+ # | * *
310
+ # | hidden * viewable *
311
+ # | * *
312
+ # +---------1---------2---------3*********4*********+
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
333
+
334
+ # The first task of determining how much of the handle is visible in a row/column is to
335
+ # calculate the range (as a precentage of the total) of viewable items
336
+ viewable_start = first_visible.to_f / total_count
337
+ viewable_end = (first_visible + viewable_count).to_f / total_count
338
+
339
+ # Always use the same length for the scroll bar so it does not give an inchworm effect
340
+ # as it scrolls along.
341
+ #
342
+ # Also, double the value now to get granularity for half width
343
+ # scrollbar characters.
344
+ scrollbar_length = ((2 * viewable_count ** 2).to_f / total_count).ceil
345
+ scrollbar_start = ((2 * first_visible * viewable_count).to_f / total_count).floor
346
+
347
+ first_half = (scrollbar_start...scrollbar_start + scrollbar_length).include?(2 * curr)
348
+ second_half = (scrollbar_start...scrollbar_start + scrollbar_length).include?(2 * curr + 1)
349
+
350
+ if first_half && second_half
351
+ chars[1]
352
+ elsif first_half
353
+ chars[2]
354
+ elsif second_half
355
+ chars[0]
356
+ else
357
+ GUTTER
358
+ end
359
+ end
360
+ end
361
+ private_constant :ContainerPainter
362
+ end
363
+ end