whirled_peas 0.4.1 → 0.8.0

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