whirled_peas 0.2.0 → 0.6.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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -0
  3. data/CHANGELOG.md +38 -0
  4. data/Gemfile +1 -0
  5. data/README.md +275 -81
  6. data/Rakefile +38 -3
  7. data/examples/intro.rb +52 -0
  8. data/examples/scrolling.rb +53 -0
  9. data/exe/whirled_peas +7 -0
  10. data/lib/whirled_peas.rb +12 -42
  11. data/lib/whirled_peas/command_line.rb +255 -0
  12. data/lib/whirled_peas/config.rb +21 -0
  13. data/lib/whirled_peas/errors.rb +7 -0
  14. data/lib/whirled_peas/frame.rb +0 -8
  15. data/lib/whirled_peas/frame/consumer.rb +14 -50
  16. data/lib/whirled_peas/frame/debug_consumer.rb +30 -0
  17. data/lib/whirled_peas/frame/event_loop.rb +68 -38
  18. data/lib/whirled_peas/frame/producer.rb +36 -32
  19. data/lib/whirled_peas/graphics.rb +5 -0
  20. data/lib/whirled_peas/graphics/box_painter.rb +108 -0
  21. data/lib/whirled_peas/graphics/canvas.rb +107 -0
  22. data/lib/whirled_peas/graphics/composer.rb +80 -0
  23. data/lib/whirled_peas/graphics/container_coords.rb +71 -0
  24. data/lib/whirled_peas/graphics/container_dimensions.rb +75 -0
  25. data/lib/whirled_peas/graphics/container_painter.rb +234 -0
  26. data/lib/whirled_peas/graphics/debugger.rb +52 -0
  27. data/lib/whirled_peas/graphics/grid_painter.rb +68 -0
  28. data/lib/whirled_peas/graphics/mock_screen.rb +26 -0
  29. data/lib/whirled_peas/graphics/painter.rb +19 -0
  30. data/lib/whirled_peas/graphics/renderer.rb +21 -0
  31. data/lib/whirled_peas/graphics/screen.rb +70 -0
  32. data/lib/whirled_peas/graphics/text_dimensions.rb +15 -0
  33. data/lib/whirled_peas/graphics/text_painter.rb +40 -0
  34. data/lib/whirled_peas/settings.rb +5 -0
  35. data/lib/whirled_peas/settings/bg_color.rb +22 -0
  36. data/lib/whirled_peas/settings/border.rb +101 -0
  37. data/lib/whirled_peas/settings/box_settings.rb +8 -0
  38. data/lib/whirled_peas/settings/color.rb +69 -0
  39. data/lib/whirled_peas/settings/container_settings.rb +139 -0
  40. data/lib/whirled_peas/settings/debugger.rb +96 -0
  41. data/lib/whirled_peas/settings/display_flow.rb +25 -0
  42. data/lib/whirled_peas/settings/element_settings.rb +61 -0
  43. data/lib/whirled_peas/settings/grid_settings.rb +15 -0
  44. data/lib/whirled_peas/settings/margin.rb +8 -0
  45. data/lib/whirled_peas/settings/padding.rb +8 -0
  46. data/lib/whirled_peas/settings/position.rb +15 -0
  47. data/lib/whirled_peas/settings/scrollbar.rb +15 -0
  48. data/lib/whirled_peas/settings/spacing.rb +24 -0
  49. data/lib/whirled_peas/settings/text_align.rb +19 -0
  50. data/lib/whirled_peas/settings/text_color.rb +19 -0
  51. data/lib/whirled_peas/settings/text_settings.rb +15 -0
  52. data/lib/whirled_peas/utils.rb +5 -0
  53. data/lib/whirled_peas/utils/ansi.rb +53 -0
  54. data/lib/whirled_peas/utils/formatted_string.rb +64 -0
  55. data/lib/whirled_peas/utils/title_font.rb +75 -0
  56. data/lib/whirled_peas/version.rb +1 -1
  57. data/screen_test/rendered/elements/box.frame +1 -0
  58. data/screen_test/rendered/elements/box.rb +20 -0
  59. data/screen_test/rendered/elements/grid.frame +1 -0
  60. data/screen_test/rendered/elements/grid.rb +13 -0
  61. data/screen_test/rendered/elements/screen_overflow.frame +1 -0
  62. data/screen_test/rendered/elements/screen_overflow.rb +9 -0
  63. data/screen_test/rendered/elements/text.frame +1 -0
  64. data/screen_test/rendered/elements/text.rb +9 -0
  65. data/screen_test/rendered/elements/text_multiline.frame +1 -0
  66. data/screen_test/rendered/elements/text_multiline.rb +9 -0
  67. data/screen_test/rendered/settings/align/box.frame +1 -0
  68. data/screen_test/rendered/settings/align/box.rb +24 -0
  69. data/screen_test/rendered/settings/align/children_center.frame +1 -0
  70. data/screen_test/rendered/settings/align/children_center.rb +13 -0
  71. data/screen_test/rendered/settings/align/children_left.frame +1 -0
  72. data/screen_test/rendered/settings/align/children_left.rb +13 -0
  73. data/screen_test/rendered/settings/align/children_right.frame +1 -0
  74. data/screen_test/rendered/settings/align/children_right.rb +13 -0
  75. data/screen_test/rendered/settings/align/grid.frame +1 -0
  76. data/screen_test/rendered/settings/align/grid.rb +20 -0
  77. data/screen_test/rendered/settings/ansi/bold.frame +1 -0
  78. data/screen_test/rendered/settings/ansi/bold.rb +15 -0
  79. data/screen_test/rendered/settings/ansi/color.frame +1 -0
  80. data/screen_test/rendered/settings/ansi/color.rb +37 -0
  81. data/screen_test/rendered/settings/ansi/underline.frame +1 -0
  82. data/screen_test/rendered/settings/ansi/underline.rb +15 -0
  83. data/screen_test/rendered/settings/border.frame +1 -0
  84. data/screen_test/rendered/settings/border.rb +13 -0
  85. data/screen_test/rendered/settings/flow/box_b2t.frame +1 -0
  86. data/screen_test/rendered/settings/flow/box_b2t.rb +24 -0
  87. data/screen_test/rendered/settings/flow/box_l2r.frame +1 -0
  88. data/screen_test/rendered/settings/flow/box_l2r.rb +24 -0
  89. data/screen_test/rendered/settings/flow/box_r2l.frame +1 -0
  90. data/screen_test/rendered/settings/flow/box_r2l.rb +24 -0
  91. data/screen_test/rendered/settings/flow/box_t2b.frame +1 -0
  92. data/screen_test/rendered/settings/flow/box_t2b.rb +24 -0
  93. data/screen_test/rendered/settings/flow/grid_b2t.frame +1 -0
  94. data/screen_test/rendered/settings/flow/grid_b2t.rb +14 -0
  95. data/screen_test/rendered/settings/flow/grid_l2r.frame +1 -0
  96. data/screen_test/rendered/settings/flow/grid_l2r.rb +14 -0
  97. data/screen_test/rendered/settings/flow/grid_r2l.frame +1 -0
  98. data/screen_test/rendered/settings/flow/grid_r2l.rb +14 -0
  99. data/screen_test/rendered/settings/flow/grid_t2b.frame +1 -0
  100. data/screen_test/rendered/settings/flow/grid_t2b.rb +14 -0
  101. data/screen_test/rendered/settings/height/box.frame +1 -0
  102. data/screen_test/rendered/settings/height/box.rb +13 -0
  103. data/screen_test/rendered/settings/height/grid.frame +1 -0
  104. data/screen_test/rendered/settings/height/grid.rb +14 -0
  105. data/screen_test/rendered/settings/height/overflow_box.frame +1 -0
  106. data/screen_test/rendered/settings/height/overflow_box.rb +13 -0
  107. data/screen_test/rendered/settings/height/overflow_box_l2r.frame +1 -0
  108. data/screen_test/rendered/settings/height/overflow_box_l2r.rb +15 -0
  109. data/screen_test/rendered/settings/height/overflow_box_t2b.frame +1 -0
  110. data/screen_test/rendered/settings/height/overflow_box_t2b.rb +14 -0
  111. data/screen_test/rendered/settings/height/overflow_grid.frame +1 -0
  112. data/screen_test/rendered/settings/height/overflow_grid.rb +16 -0
  113. data/screen_test/rendered/settings/margin.frame +1 -0
  114. data/screen_test/rendered/settings/margin.rb +14 -0
  115. data/screen_test/rendered/settings/padding.frame +1 -0
  116. data/screen_test/rendered/settings/padding.rb +11 -0
  117. data/screen_test/rendered/settings/position/box_left.frame +1 -0
  118. data/screen_test/rendered/settings/position/box_left.rb +17 -0
  119. data/screen_test/rendered/settings/position/box_left_negative.frame +1 -0
  120. data/screen_test/rendered/settings/position/box_left_negative.rb +17 -0
  121. data/screen_test/rendered/settings/position/box_top.frame +1 -0
  122. data/screen_test/rendered/settings/position/box_top.rb +17 -0
  123. data/screen_test/rendered/settings/position/box_top_negative.frame +1 -0
  124. data/screen_test/rendered/settings/position/box_top_negative.rb +17 -0
  125. data/screen_test/rendered/settings/position/grid_left.frame +1 -0
  126. data/screen_test/rendered/settings/position/grid_left.rb +18 -0
  127. data/screen_test/rendered/settings/position/grid_left_negative.frame +1 -0
  128. data/screen_test/rendered/settings/position/grid_left_negative.rb +18 -0
  129. data/screen_test/rendered/settings/position/grid_top.frame +1 -0
  130. data/screen_test/rendered/settings/position/grid_top.rb +18 -0
  131. data/screen_test/rendered/settings/position/grid_top_negative.frame +1 -0
  132. data/screen_test/rendered/settings/position/grid_top_negative.rb +18 -0
  133. data/screen_test/rendered/settings/scroll/horiz_box.frame +1 -0
  134. data/screen_test/rendered/settings/scroll/horiz_box.rb +15 -0
  135. data/screen_test/rendered/settings/scroll/vert_box.frame +1 -0
  136. data/screen_test/rendered/settings/scroll/vert_box.rb +18 -0
  137. data/screen_test/rendered/settings/title_font.frame +1 -0
  138. data/screen_test/rendered/settings/title_font.rb +12 -0
  139. data/screen_test/rendered/settings/width/box.frame +1 -0
  140. data/screen_test/rendered/settings/width/box.rb +13 -0
  141. data/screen_test/rendered/settings/width/grid.frame +1 -0
  142. data/screen_test/rendered/settings/width/grid.rb +14 -0
  143. data/screen_test/rendered/settings/width/overflow_box.frame +1 -0
  144. data/screen_test/rendered/settings/width/overflow_box.rb +11 -0
  145. data/screen_test/rendered/settings/width/overflow_box_l2r.frame +1 -0
  146. data/screen_test/rendered/settings/width/overflow_box_l2r.rb +14 -0
  147. data/screen_test/rendered/settings/width/overflow_box_t2b.frame +1 -0
  148. data/screen_test/rendered/settings/width/overflow_box_t2b.rb +15 -0
  149. data/screen_test/rendered/settings/width/overflow_grid.frame +1 -0
  150. data/screen_test/rendered/settings/width/overflow_grid.rb +14 -0
  151. data/screen_test/screen_tester.rb +201 -0
  152. data/whirled_peas.gemspec +4 -2
  153. metadata +147 -20
  154. data/lib/whirled_peas/ui.rb +0 -7
  155. data/lib/whirled_peas/ui/ansi.rb +0 -154
  156. data/lib/whirled_peas/ui/canvas.rb +0 -35
  157. data/lib/whirled_peas/ui/element.rb +0 -225
  158. data/lib/whirled_peas/ui/painter.rb +0 -283
  159. data/lib/whirled_peas/ui/screen.rb +0 -62
  160. data/lib/whirled_peas/ui/settings.rb +0 -521
  161. data/lib/whirled_peas/ui/stroke.rb +0 -29
  162. data/sandbox/auto.rb +0 -13
  163. data/sandbox/box.rb +0 -19
  164. data/sandbox/grid.rb +0 -13
  165. data/sandbox/sandbox.rb +0 -17
  166. data/sandbox/text.rb +0 -33
@@ -0,0 +1,107 @@
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, :start_left, :start_top
8
+
9
+ def self.unwritable
10
+ new(-1, -1, 0, 0, -1, -1)
11
+ end
12
+
13
+ def initialize(left, top, width, height, start_left, start_top)
14
+ @left = left
15
+ @top = top
16
+ @width = width
17
+ @height = height
18
+ @start_left = start_left
19
+ @start_top = start_top
20
+ end
21
+
22
+ def writable?
23
+ width > 0 || height > 0
24
+ end
25
+
26
+ def child(start_left, start_top, child_width, child_height)
27
+ child_left = start_left
28
+ child_top = start_top
29
+ if child_left >= left + width
30
+ self.class.unwritable
31
+ elsif child_left + child_width <= left
32
+ self.class.unwritable
33
+ elsif child_top >= top + height
34
+ self.class.unwritable
35
+ elsif child_top + child_height <= top
36
+ self.class.unwritable
37
+ else
38
+ if child_left < left
39
+ child_width -= left - child_left
40
+ child_left = left
41
+ end
42
+ child_width = [width - (child_left - left), child_width].min
43
+ if child_top < top
44
+ child_height -= top - child_top
45
+ child_top = top
46
+ end
47
+ child_height = [height - (child_top - top), child_height].min
48
+ self.class.new(
49
+ child_left,
50
+ child_top,
51
+ child_width,
52
+ child_height,
53
+ start_left,
54
+ start_top
55
+ )
56
+ end
57
+ end
58
+
59
+ # Yields a single line of formatted characters positioned on the canvas,
60
+ # verifying only characters within the canvas are included.
61
+ def stroke(stroke_left, stroke_top, raw, formatting=[], &block)
62
+ if stroke_left >= left + width
63
+ # The stroke starts to the right of the canvas
64
+ fstring = Utils::FormattedString.blank
65
+ elsif stroke_left + raw.length <= left
66
+ # The stroke ends to the left of the canvas
67
+ fstring = Utils::FormattedString.blank
68
+ elsif stroke_top < top
69
+ # The stroke is above the canvas
70
+ fstring = Utils::FormattedString.blank
71
+ elsif stroke_top >= top + height
72
+ # The stroke is below the canvas
73
+ fstring = Utils::FormattedString.blank
74
+ else
75
+ # In this section, we know that at least part of the stroke should be visible
76
+ # on the canvas. Chop off parts of the raw string that aren't within the
77
+ # canvas boundary and ensure the stroke start position is also within the
78
+ # canvas boundary
79
+
80
+ # If the stroke starts to the left of the canvas, set the start index to the
81
+ # first value that will be on the canvas, then update stroke_left to be on
82
+ # the canvas
83
+ start_index = stroke_left < left ? left - stroke_left : 0
84
+ stroke_left = left if stroke_left <= left
85
+
86
+ # Determine how many characters from the stroke will fit on the canvas
87
+ visible_length = [raw.length, width - (stroke_left - left)].min
88
+ end_index = start_index + visible_length - 1
89
+ fstring = Utils::FormattedString.new(raw[start_index..end_index], formatting)
90
+ end
91
+ yield stroke_left, stroke_top, fstring
92
+ end
93
+
94
+ def hash
95
+ [left, top, width, height].hash
96
+ end
97
+
98
+ def ==(other)
99
+ other.is_a?(self.class) && hash == other.hash
100
+ end
101
+
102
+ def inspect
103
+ "Canvas(left=#{left}, top=#{top}, width=#{width}, height=#{height})"
104
+ end
105
+ end
106
+ end
107
+ 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,71 @@
1
+ module WhirledPeas
2
+ module Graphics
3
+ class ContainerCoords
4
+ def initialize(canvas, dimensions, settings)
5
+ @canvas = canvas
6
+ @dimensions = dimensions
7
+ @settings = settings
8
+ end
9
+
10
+ def left
11
+ canvas.start_left + settings.position.left
12
+ end
13
+
14
+ def top
15
+ canvas.start_top + settings.position.top
16
+ end
17
+
18
+ def border_left
19
+ left + settings.margin.left
20
+ end
21
+
22
+ def border_top
23
+ top + settings.margin.top
24
+ end
25
+
26
+ def padding_left
27
+ border_left + (settings.border.left? ? 1 : 0)
28
+ end
29
+
30
+ def padding_top
31
+ border_top + (settings.border.top? ? 1 : 0)
32
+ end
33
+
34
+ def content_left(col_index=0)
35
+ padding_left + settings.padding.left + col_index * grid_width
36
+ end
37
+
38
+ def content_top(row_index=0)
39
+ padding_top + settings.padding.top + row_index * grid_height
40
+ end
41
+
42
+ def inner_grid_width
43
+ settings.padding.left +
44
+ dimensions.content_width +
45
+ settings.padding.right
46
+ end
47
+
48
+ def grid_width
49
+ (settings.border.inner_vert? ? 1 : 0) +
50
+ inner_grid_width +
51
+ (settings.scrollbar.vert? ? 1 : 0)
52
+ end
53
+
54
+ def inner_grid_height
55
+ settings.padding.top +
56
+ dimensions.content_height +
57
+ settings.padding.bottom
58
+ end
59
+
60
+ def grid_height
61
+ (settings.border.inner_horiz? ? 1 : 0) +
62
+ inner_grid_height +
63
+ (settings.scrollbar.horiz? ? 1 : 0)
64
+ end
65
+
66
+ private
67
+
68
+ attr_reader :canvas, :settings, :dimensions
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,75 @@
1
+ module WhirledPeas
2
+ module Graphics
3
+ class ContainerDimensions
4
+ attr_reader :content_width, :content_height, :children_width, :children_height, :num_cols, :num_rows
5
+
6
+ def initialize(settings, content_width, content_height, num_cols=1, num_rows=1)
7
+ @content_width = settings.width || content_width
8
+ @content_height = settings.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 outer_width
17
+ @outer_width ||= margin_width +
18
+ outer_border_width +
19
+ num_cols * (padding_width + content_width + vert_scroll_width) +
20
+ (num_cols - 1) * inner_border_width
21
+ end
22
+
23
+ def outer_height
24
+ @outer_height ||= margin_height +
25
+ outer_border_height +
26
+ num_rows * (padding_height + content_height + horiz_scroll_height) +
27
+ (num_rows - 1) * inner_border_height
28
+ end
29
+
30
+ def margin_width
31
+ settings.margin.left + settings.margin.right
32
+ end
33
+
34
+ def margin_height
35
+ settings.margin.top + settings.margin.bottom
36
+ end
37
+
38
+ def outer_border_width
39
+ (settings.border.left? ? 1 : 0) + (settings.border.right? ? 1 : 0)
40
+ end
41
+
42
+ def outer_border_height
43
+ (settings.border.top? ? 1 : 0) + (settings.border.bottom? ? 1 : 0)
44
+ end
45
+
46
+ def inner_border_width
47
+ settings.border.inner_vert? ? 1 : 0
48
+ end
49
+
50
+ def inner_border_height
51
+ settings.border.inner_horiz? ? 1 : 0
52
+ end
53
+
54
+ def padding_width
55
+ settings.padding.left + settings.padding.right
56
+ end
57
+
58
+ def padding_height
59
+ settings.padding.top + settings.padding.bottom
60
+ end
61
+
62
+ def vert_scroll_width
63
+ settings.scrollbar.vert? ? 1 : 0
64
+ end
65
+
66
+ def horiz_scroll_height
67
+ settings.scrollbar.horiz? ? 1 : 0
68
+ end
69
+
70
+ private
71
+
72
+ attr_reader :settings
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,234 @@
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
+ class ContainerPainter < Painter
9
+ PADDING = ' '
10
+
11
+ def initialize(name, settings)
12
+ super
13
+ @children = []
14
+ end
15
+
16
+ def paint(canvas, &block)
17
+ return unless canvas.writable?
18
+ return unless needs_printing?
19
+ canvas_coords = coords(canvas)
20
+ stroke_left = canvas_coords.border_left
21
+ stroke_top = canvas_coords.border_top
22
+ formatting = [*settings.border.color, *settings.bg_color]
23
+ if settings.border.top?
24
+ canvas.stroke(stroke_left, stroke_top, top_border_stroke(canvas_coords), formatting, &block)
25
+ stroke_top += 1
26
+ end
27
+ middle_border = dimensions.num_rows > 1 ? middle_border_stroke(canvas_coords) : ''
28
+ dimensions.num_rows.times do |row_num|
29
+ if row_num > 0 && settings.border.inner_horiz?
30
+ canvas.stroke(stroke_left, stroke_top, middle_border, formatting, &block)
31
+ stroke_top += 1
32
+ end
33
+ canvas_coords.inner_grid_height.times do |row_within_cell|
34
+ canvas.stroke(stroke_left, stroke_top, content_line_stroke(canvas_coords, row_within_cell), formatting, &block)
35
+ stroke_top += 1
36
+ end
37
+ if settings.scrollbar.horiz?
38
+ canvas.stroke(stroke_left, stroke_top, bottom_scroll_stroke(canvas_coords), formatting, &block)
39
+ stroke_top += 1
40
+ end
41
+ end
42
+ if settings.border.bottom?
43
+ canvas.stroke(stroke_left, stroke_top, bottom_border_stroke(canvas_coords), formatting, &block)
44
+ stroke_top += 1
45
+ end
46
+ end
47
+
48
+ def add_child(child)
49
+ children << child
50
+ end
51
+
52
+ def num_children
53
+ children.length
54
+ end
55
+
56
+ def children?
57
+ num_children > 0
58
+ end
59
+
60
+ def each_child(&block)
61
+ children.each(&block)
62
+ end
63
+
64
+ private
65
+
66
+ attr_reader :children
67
+
68
+ def needs_printing?
69
+ return true if settings.bg_color
70
+ return true if settings.border.outer?
71
+ return true if dimensions.num_cols > 1 && settings.border.inner_vert?
72
+ return true if dimensions.num_rows > 1 && settings.border.inner_horiz?
73
+ settings.scrollbar.horiz? || settings.scrollbar.vert?
74
+ end
75
+
76
+ def coords(canvas)
77
+ ContainerCoords.new(canvas, dimensions, settings)
78
+ end
79
+
80
+ def line_stroke(left_border, junc_border, right_border, &block)
81
+ stroke = ''
82
+ stroke += left_border if settings.border.left?
83
+ dimensions.num_cols.times do |col_num|
84
+ stroke += junc_border if col_num > 0 && settings.border.inner_vert?
85
+ stroke += yield
86
+ end
87
+ stroke += right_border if settings.border.right?
88
+ stroke
89
+ end
90
+
91
+ def top_border_stroke(canvas_coords)
92
+ line_stroke(
93
+ settings.border.style.top_left,
94
+ settings.border.style.top_junc,
95
+ settings.border.style.top_right
96
+ ) do
97
+ settings.border.style.top_horiz * (canvas_coords.inner_grid_width + (settings.scrollbar.vert? ? 1 : 0))
98
+ end
99
+ end
100
+
101
+ def middle_border_stroke(canvas_coords)
102
+ line_stroke(
103
+ settings.border.style.left_junc,
104
+ settings.border.style.cross_junc,
105
+ settings.border.style.right_junc
106
+ ) do
107
+ settings.border.style.middle_horiz * (canvas_coords.inner_grid_width + (settings.scrollbar.vert? ? 1 : 0))
108
+ end
109
+ end
110
+
111
+ def bottom_border_stroke(canvas_coords)
112
+ line_stroke(
113
+ settings.border.style.bottom_left,
114
+ settings.border.style.bottom_junc,
115
+ settings.border.style.bottom_right
116
+ ) do
117
+ settings.border.style.bottom_horiz * (canvas_coords.inner_grid_width + (settings.scrollbar.vert? ? 1 : 0))
118
+ end
119
+ end
120
+
121
+ def content_line_stroke(canvas_coords, row_within_cell)
122
+ line_stroke(
123
+ settings.border.style.left_vert,
124
+ settings.border.style.middle_vert,
125
+ settings.border.style.right_vert,
126
+ ) do
127
+ if settings.scrollbar.vert?
128
+ if dimensions.children_height <= canvas_coords.grid_height || children.first.settings.position.top > 0
129
+ scrollbar_char = GUTTER
130
+ else
131
+ scrollbar_char = vert_scroll_char(
132
+ dimensions.children_height + dimensions.padding_height,
133
+ canvas_coords.inner_grid_height,
134
+ -children.first.settings.position.top,
135
+ row_within_cell
136
+ )
137
+ end
138
+ PADDING * canvas_coords.inner_grid_width + scrollbar_char
139
+ else
140
+ PADDING * canvas_coords.inner_grid_width
141
+ end
142
+ end
143
+ end
144
+
145
+ def bottom_scroll_stroke(canvas_coords)
146
+ line_stroke(
147
+ settings.border.style.left_vert,
148
+ settings.border.style.middle_vert,
149
+ settings.border.style.right_vert,
150
+ ) do
151
+ canvas_coords.inner_grid_width.times.map do |col_within_cell|
152
+ horiz_scroll_char(
153
+ dimensions.children_width + dimensions.padding_width,
154
+ canvas_coords.inner_grid_width,
155
+ -children.first.settings.position.left,
156
+ col_within_cell
157
+ )
158
+ end.join
159
+ end
160
+ end
161
+
162
+ GUTTER = ' '
163
+ HORIZONTAL = %w[▗ ▄ ▖]
164
+ VERTICAL = %w[
165
+
166
+
167
+
168
+ ]
169
+
170
+ def horiz_scroll_char(col_count, viewable_col_count, first_visible_col, curr_col)
171
+ scroll_char(col_count, viewable_col_count, first_visible_col, curr_col, HORIZONTAL)
172
+ end
173
+
174
+ def vert_scroll_char(row_count, viewable_row_count, first_visible_row, curr_row)
175
+ scroll_char(row_count, viewable_row_count, first_visible_row, curr_row, VERTICAL)
176
+ end
177
+
178
+ private
179
+
180
+ def scroll_char(total_count, viewable_count, first_visible, curr, chars)
181
+ return GUTTER unless total_count > 0 && viewable_count > 0
182
+ # The scroll handle has the exact same relative size and position in the scroll gutter
183
+ # that the viewable content has in the total content area. For example, a content area
184
+ # that is 50 columns wide with a view port that is 20 columns wide might look like
185
+ #
186
+ # +---------1-----****2*********3******---4---------+
187
+ # | * * |
188
+ # | hidden * viewable * hidden |
189
+ # | * * |
190
+ # +---------1-----****2*********3******---4---------+
191
+ #
192
+ # The scoll gutter, would look like
193
+ #
194
+ # |......********.....|
195
+ #
196
+ # Scrolling all the way to the right results in
197
+ #
198
+ # +---------1---------2---------3*********4*********+
199
+ # | * *
200
+ # | hidden * viewable *
201
+ # | * *
202
+ # +---------1---------2---------3*********4*********+
203
+ # |...........********|
204
+
205
+ # The first task of determining how much of the handle is visible in a row/column is to
206
+ # calculate the range (as a precentage of the total) of viewable items
207
+ viewable_start = first_visible.to_f / total_count
208
+ viewable_end = (first_visible + viewable_count).to_f / total_count
209
+
210
+ # Always use the same length for the scroll bar so it does not give an inchworm effect
211
+ # as it scrolls along.
212
+ #
213
+ # Also, double the value now to get granularity for half width
214
+ # scrollbar characters.
215
+ scrollbar_length = ((2 * viewable_count ** 2).to_f / total_count).ceil
216
+ scrollbar_start = ((2 * first_visible * viewable_count).to_f / total_count).floor
217
+
218
+ first_half = (scrollbar_start...scrollbar_start + scrollbar_length).include?(2 * curr)
219
+ second_half = (scrollbar_start...scrollbar_start + scrollbar_length).include?(2 * curr + 1)
220
+
221
+ if first_half && second_half
222
+ chars[1]
223
+ elsif first_half
224
+ chars[2]
225
+ elsif second_half
226
+ chars[0]
227
+ else
228
+ GUTTER
229
+ end
230
+ end
231
+ end
232
+ private_constant :ContainerPainter
233
+ end
234
+ end