whirled_peas 0.2.0 → 0.6.0

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