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
@@ -1,283 +0,0 @@
1
- require_relative 'ansi'
2
- require_relative 'canvas'
3
- require_relative 'settings'
4
-
5
- module WhirledPeas
6
- module UI
7
- DEBUG_SPACING = ARGV.include?('--debug-spacing')
8
-
9
- class TextPainter
10
- JUSTIFICATION = DEBUG_SPACING ? 'j' : ' '
11
-
12
- def initialize(text, canvas)
13
- @text = text
14
- @canvas = canvas
15
- end
16
-
17
- def paint(&block)
18
- yield canvas.stroke(canvas.left, canvas.top, justified)
19
- end
20
-
21
- private
22
-
23
- attr_reader :text, :canvas
24
-
25
- def visible
26
- if text.value.length <= text.preferred_width
27
- text.value
28
- elsif text.settings.align == TextAlign::LEFT
29
- text.value[0..text.preferred_width - 1]
30
- elsif text.settings.align == TextAlign::CENTER
31
- left_chop = (text.value.length - text.preferred_width) / 2
32
- right_chop = text.value.length - text.preferred_width - left_chop
33
- text.value[left_chop..-right_chop - 1]
34
- else
35
- text.value[-text.preferred_width..-1]
36
- end
37
- end
38
-
39
- def justified
40
- format_settings = [*text.settings.color, *text.settings.bg_color]
41
- format_settings << TextFormat::BOLD if text.settings.bold?
42
- format_settings << TextFormat::UNDERLINE if text.settings.underline?
43
-
44
- ljust = case text.settings.align
45
- when TextAlign::LEFT
46
- 0
47
- when TextAlign::CENTER
48
- [0, (text.preferred_width - text.value.length) / 2].max
49
- when TextAlign::RIGHT
50
- [0, text.preferred_width - text.value.length].max
51
- end
52
- rjust = [0, text.preferred_width - text.value.length - ljust].max
53
- Ansi.format(JUSTIFICATION * ljust, [*text.settings.bg_color]) +
54
- Ansi.format(visible, format_settings) +
55
- Ansi.format(JUSTIFICATION * rjust, [*text.settings.bg_color])
56
- end
57
- end
58
- private_constant :TextPainter
59
-
60
- class ContainerPainter
61
- PADDING = DEBUG_SPACING ? 'p' : ' '
62
-
63
- def initialize(container, canvas)
64
- @container = container
65
- @settings = container.settings
66
- @canvas = canvas
67
- end
68
-
69
- def paint(&block)
70
- return if container.num_rows == 0 || container.num_cols == 0
71
- top = canvas.top + settings.margin.top
72
- if settings.auto_margin?
73
- left = canvas.left + (canvas.width - container.preferred_width) / 2
74
- else
75
- left = canvas.left + settings.margin.left
76
- end
77
- if settings.border.top?
78
- yield canvas.stroke(left, top, top_border)
79
- top += 1
80
- end
81
- container.num_rows.times do |row_num|
82
- if row_num > 0 && settings.border.inner_horiz?
83
- yield canvas.stroke(left, top, middle_border)
84
- top += 1
85
- end
86
- (settings.padding.top + container.row_height + settings.padding.bottom).times do
87
- yield canvas.stroke(left, top, content_line)
88
- top += 1
89
- end
90
- end
91
- if settings.border.bottom?
92
- yield canvas.stroke(left, top, bottom_border)
93
- top += 1
94
- end
95
- end
96
-
97
- private
98
-
99
- attr_reader :container, :settings, :canvas
100
-
101
- def line_stroke(left_border, horiz_border, junc_border, right_border)
102
- stroke = ''
103
- stroke += left_border if settings.border.left?
104
- container.num_cols.times do |col_num|
105
- stroke += junc_border if col_num > 0 && settings.border.inner_horiz?
106
- stroke += horiz_border * (container.col_width + settings.padding.left + settings.padding.right)
107
- end
108
- stroke += right_border if settings.border.right?
109
- Ansi.format(stroke, [*settings.border.color, *settings.bg_color])
110
- end
111
-
112
- def top_border
113
- line_stroke(
114
- settings.border.style.top_left,
115
- settings.border.style.top_horiz,
116
- settings.border.style.top_junc,
117
- settings.border.style.top_right
118
- )
119
- end
120
-
121
- def content_line
122
- line_stroke(
123
- settings.border.style.left_vert,
124
- PADDING,
125
- settings.border.style.middle_vert,
126
- settings.border.style.right_vert
127
- )
128
- end
129
-
130
- def middle_border
131
- line_stroke(
132
- settings.border.style.left_junc,
133
- settings.border.style.middle_horiz,
134
- settings.border.style.cross_junc,
135
- settings.border.style.right_junc
136
- )
137
- end
138
-
139
- def bottom_border
140
- line_stroke(
141
- settings.border.style.bottom_left,
142
- settings.border.style.bottom_horiz,
143
- settings.border.style.bottom_junc,
144
- settings.border.style.bottom_right
145
- )
146
- end
147
- end
148
-
149
- class BoxContainer
150
- attr_reader :settings, :num_cols, :num_rows, :col_width, :row_height, :preferred_width
151
-
152
- def initialize(box)
153
- @settings = ContainerSettings.merge(box.settings)
154
- @num_cols = 1
155
- @num_rows = 1
156
- @col_width = box.content_width
157
- @row_height = box.content_height
158
- @preferred_width = box.preferred_width
159
- end
160
- end
161
-
162
- class BoxPainter
163
- def initialize(box, canvas)
164
- @box = box
165
- @canvas = canvas
166
- end
167
-
168
- def paint(&block)
169
- container = BoxContainer.new(box)
170
- ContainerPainter.new(container, canvas).paint(&block)
171
- top = canvas.top + box.settings.margin.top + (box.settings.border.top? ? 1 : 0) + box.settings.padding.top
172
- if box.settings.auto_margin?
173
- margin = (canvas.width - box.preferred_width) / 2
174
- else
175
- margin = box.settings.margin.left
176
- end
177
- left = canvas.left + margin + (box.settings.border.left? ? 1 : 0) + box.settings.padding.left
178
- greedy_width = box.settings.vertical_flow? || box.children.length == 1
179
- children = box.children
180
- children = children.reverse if box.settings.reverse_flow?
181
- children.each do |child|
182
- if greedy_width
183
- width = box.content_width
184
- height = child.preferred_height
185
- else
186
- width = child.preferred_width
187
- height = box.content_height
188
- end
189
- child_canvas = Canvas.new(left, top, width, height)
190
- Painter.paint(child, child_canvas, &block)
191
- if box.settings.horizontal_flow?
192
- left += child.preferred_width
193
- else
194
- top += child.preferred_height
195
- end
196
- end
197
- end
198
-
199
- private
200
-
201
- attr_reader :box, :canvas
202
- end
203
-
204
- class GridContainer
205
- attr_reader :settings, :num_cols, :num_rows, :col_width, :row_height, :preferred_width
206
-
207
- def initialize(grid, num_cols, num_rows)
208
- @settings = ContainerSettings.merge(grid.settings)
209
- @num_cols = num_cols
210
- @num_rows = num_rows
211
- @col_width = grid.col_width
212
- @row_height = grid.row_height
213
- @preferred_width = grid.preferred_width
214
- end
215
- end
216
-
217
- class GridPainter
218
- def initialize(grid, canvas)
219
- @grid = grid
220
- @canvas = canvas
221
- available_width = grid.preferred_width - (grid.settings.margin.left || 0) - (grid.settings.margin.right || 0)
222
- @num_cols = grid.settings.num_cols || (available_width - (grid.settings.border.left? ? 1 : 0) - (grid.settings.border.right? ? 1 : 0) + (grid.border.inner_vert ? 1 : 0)) / (col_width + grid.settings.padding.left + grid.settings.right + (grid.border.inner_vert ? 1 : 0))
223
- end
224
-
225
- def paint(&block)
226
- return if grid.children.empty?
227
-
228
- container = GridContainer.new(grid, num_cols, (grid.children.length.to_f / num_cols).ceil)
229
- ContainerPainter.new(container, canvas).paint(&block)
230
-
231
- children = if grid.settings.transpose?
232
- grid.children.length.times.map do |i|
233
- grid.children[(i * num_cols) % grid.children.length + i / (grid.children.length / num_cols)]
234
- end.compact
235
- else
236
- grid.children
237
- end
238
-
239
- top = canvas.top + grid.settings.margin.top + (grid.settings.border.top? ? 1 : 0) + grid.settings.padding.top
240
- if grid.settings.auto_margin?
241
- margin = (canvas.width - grid.preferred_width) / 2
242
- else
243
- margin = grid.settings.margin.left
244
- end
245
- left = canvas.left + margin + (grid.settings.border.left? ? 1 : 0) + grid.settings.padding.left
246
- grid_height = grid.settings.padding.top + grid.row_height + grid.settings.padding.bottom + (grid.settings.border.inner_horiz? ? 1 : 0)
247
- grid_width = grid.settings.padding.left + grid.col_width + grid.settings.padding.right + (grid.settings.border.inner_vert? ? 1 : 0)
248
- children.each_slice(num_cols).each.with_index do |row, row_num|
249
- row_top = top + row_num * grid_height
250
- row.each.with_index do |element, col_num|
251
- col_left = left + col_num * grid_width
252
- child_canvas = Canvas.new(
253
- col_left,
254
- row_top,
255
- element.preferred_width,
256
- element.preferred_height
257
- )
258
- Painter.paint(element, child_canvas, &block)
259
- end
260
- end
261
- end
262
-
263
- private
264
-
265
- attr_reader :grid, :canvas, :num_cols
266
- end
267
-
268
- module Painter
269
- PAINTERS = {
270
- TextElement => TextPainter,
271
- BoxElement => BoxPainter,
272
- GridElement => GridPainter,
273
- }
274
-
275
- def self.paint(element, canvas, &block)
276
- if element.is_a?(Template)
277
- element = BoxElement.from_template(element, canvas.width, canvas.height)
278
- end
279
- PAINTERS[element.class].new(element, canvas).paint(&block)
280
- end
281
- end
282
- end
283
- end
@@ -1,62 +0,0 @@
1
- require 'highline'
2
- require 'tty-cursor'
3
-
4
- require_relative 'ansi'
5
- require_relative 'painter'
6
-
7
- module WhirledPeas
8
- module UI
9
- class Screen
10
- def initialize(print_output=true)
11
- @print_output = print_output
12
- @terminal = HighLine.new.terminal
13
- @cursor = TTY::Cursor
14
- @strokes = []
15
- refresh_size!
16
- Signal.trap('SIGWINCH', proc { self.refresh_size! })
17
- end
18
-
19
- def paint(template)
20
- @template = template
21
- refresh
22
- end
23
-
24
- def needs_refresh?
25
- @refreshed_width != width || @refreshed_height != height
26
- end
27
-
28
- def refresh
29
- strokes = [cursor.hide, cursor.move_to(0, 0), cursor.clear_screen_down]
30
- Painter.paint(@template, Canvas.new(0, 0, width, height)) do |stroke|
31
- unless stroke.chars.nil?
32
- strokes << cursor.move_to(stroke.left, stroke.top)
33
- strokes << stroke.chars
34
- end
35
- end
36
- return unless @print_output
37
- strokes.each(&method(:print))
38
- STDOUT.flush
39
- @refreshed_width = width
40
- @refreshed_height = height
41
- end
42
-
43
- def finalize
44
- return unless @print_output
45
- print UI::Ansi.clear
46
- print cursor.move_to(0, height - 1)
47
- print cursor.show
48
- STDOUT.flush
49
- end
50
-
51
- protected
52
-
53
- def refresh_size!
54
- @width, @height = terminal.terminal_size
55
- end
56
-
57
- private
58
-
59
- attr_reader :cursor, :terminal, :width, :height
60
- end
61
- end
62
- end
@@ -1,521 +0,0 @@
1
- require 'json'
2
-
3
- module WhirledPeas
4
- module UI
5
- module TextAlign
6
- LEFT = :left
7
- CENTER = :center
8
- RIGHT = :right
9
-
10
- def self.validate!(align)
11
- return unless align
12
- if [TextAlign::LEFT, TextAlign::CENTER, TextAlign::RIGHT].include?(align)
13
- align
14
- else
15
- raise ArgumentError, "Invalid alignment: #{align}"
16
- end
17
- end
18
- end
19
-
20
- module DisplayFlow
21
- LEFT_TO_RIGHT = :l2r
22
- RIGHT_TO_LEFT = :r2l
23
- TOP_TO_BOTTOM = :t2b
24
- BOTTOM_TO_TOP = :b2t
25
-
26
- def self.validate!(flow)
27
- return unless flow
28
- if [
29
- DisplayFlow::LEFT_TO_RIGHT,
30
- DisplayFlow::RIGHT_TO_LEFT,
31
- DisplayFlow::TOP_TO_BOTTOM,
32
- DisplayFlow::BOTTOM_TO_TOP
33
- ].include?(flow)
34
- flow
35
- else
36
- raise ArgumentError, "Invalid flow: #{flow}"
37
- end
38
- end
39
- end
40
-
41
- class Spacing
42
- def left
43
- @_left || 0
44
- end
45
-
46
- def left=(val)
47
- @_left = val
48
- end
49
-
50
- def top
51
- @_top || 0
52
- end
53
-
54
- def top=(val)
55
- @_top = val
56
- end
57
-
58
- def right
59
- @_right || 0
60
- end
61
-
62
- def right=(val)
63
- @_right = val
64
- end
65
-
66
- def bottom
67
- @_bottom || 0
68
- end
69
-
70
- def bottom=(val)
71
- @_bottom = val
72
- end
73
-
74
- def merge(parent)
75
- merged = Spacing.new
76
- merged._left = @_left || parent._left
77
- merged._top = @_top || parent._top
78
- merged._right = @_right || parent._right
79
- merged._bottom = @_bottom || parent._bottom
80
- merged
81
- end
82
-
83
- def inspect
84
- vals = {}
85
- vals[:left] = @_left if @_left
86
- vals[:top] = @_top if @_top
87
- vals[:right] = @_right if @_right
88
- vals[:bottom] = @_bottom if @_bottom
89
- JSON.generate(vals) if vals.any?
90
- end
91
-
92
- protected
93
-
94
- attr_accessor :_left, :_top, :_right, :_bottom
95
- end
96
- private_constant :Spacing
97
-
98
- class Padding < Spacing
99
- end
100
-
101
- class Margin < Spacing
102
- end
103
-
104
- BorderStyle = Struct.new(
105
- :top_left, :top_horiz, :top_junc, :top_right,
106
- :left_vert, :left_junc,
107
- :middle_vert, :cross_junc, :middle_horiz,
108
- :right_vert, :right_junc,
109
- :bottom_left, :bottom_horiz, :bottom_junc, :bottom_right
110
- )
111
-
112
- module BorderStyles
113
- BOLD = BorderStyle.new(
114
- '┏', '━', '┳', '┓',
115
- '┃', '┣',
116
- '┃', '╋', '━',
117
- '┃', '┫',
118
- '┗', '━', '┻', '┛'
119
- )
120
- SOFT = BorderStyle.new(
121
- '╭', '─', '┬', '╮',
122
- '│', '├',
123
- '│', '┼', '─',
124
- '│', '┤',
125
- '╰', '─', '┴', '╯'
126
- )
127
- DOUBLE = BorderStyle.new(
128
- '╔', '═', '╦', '╗',
129
- '║', '╠',
130
- '║', '╬', '═',
131
- '║', '╣',
132
- '╚', '═', '╩', '╝'
133
- )
134
-
135
- def self.validate!(style)
136
- return unless style
137
- if style.is_a?(Symbol)
138
- error_message = "Unsupported border style: #{style}"
139
- begin
140
- style = self.const_get(style.upcase)
141
- raise ArgumentError, error_message unless style.is_a?(BorderStyle)
142
- style
143
- rescue NameError
144
- raise ArgumentError, error_message
145
- end
146
- else
147
- style
148
- end
149
- end
150
- end
151
-
152
- class Border
153
- def left?
154
- @_left == true
155
- end
156
-
157
- def left=(val)
158
- @_left = val
159
- end
160
-
161
- def top?
162
- @_top == true
163
- end
164
-
165
- def top=(val)
166
- @_top = val
167
- end
168
-
169
- def right?
170
- @_right == true
171
- end
172
-
173
- def right=(val)
174
- @_right = val
175
- end
176
-
177
- def bottom?
178
- @_bottom == true
179
- end
180
-
181
- def bottom=(val)
182
- @_bottom = val
183
- end
184
-
185
- def inner_horiz?
186
- @_inner_horiz == true
187
- end
188
-
189
- def inner_horiz=(val)
190
- @_inner_horiz = val
191
- end
192
-
193
- def inner_vert?
194
- @_inner_vert == true
195
- end
196
-
197
- def inner_vert=(val)
198
- @_inner_vert = val
199
- end
200
-
201
- def style
202
- @_style || BorderStyles::BOLD
203
- end
204
-
205
- def style=(val)
206
- @_style = BorderStyles.validate!(val)
207
- end
208
-
209
- def color
210
- @_color || TextColor::WHITE
211
- end
212
-
213
- def color=(val)
214
- @_color = TextColor.validate!(val)
215
- end
216
-
217
- def merge(parent)
218
- merged = Border.new
219
- merged._left = @_left.nil? ? parent._left : @_left
220
- merged._top = @_top.nil? ? parent._top : @_top
221
- merged._right = @_right.nil? ? parent._right : @_right
222
- merged._bottom = @_bottom.nil? ? parent._bottom : @_bottom
223
- merged._inner_horiz = @_inner_horiz.nil? ? parent._inner_horiz : @_inner_horiz
224
- merged._inner_vert = @_inner_vert.nil? ? parent._inner_vert : @_inner_vert
225
- merged._style = @_style || parent._style
226
- merged._color = @_color || parent._color
227
- merged
228
- end
229
-
230
- def inspect
231
- vals = {}
232
- vals[:left] = @_left unless @_left.nil?
233
- vals[:top] = @_top unless @_top.nil?
234
- vals[:right] = @_right unless @_right.nil?
235
- vals[:bottom] = @_bottom unless @_bottom.nil?
236
- vals[:inner_horiz] = @_inner_horiz unless @_inner_horiz.nil?
237
- vals[:inner_vert] = @_inner_vert unless @_inner_vert.nil?
238
- JSON.generate(vals) if vals.any?
239
- end
240
-
241
- protected
242
-
243
- attr_accessor :_left, :_top, :_right, :_bottom, :_inner_horiz, :_inner_vert, :_style, :_color
244
- end
245
-
246
- class ElementSettings
247
- def self.merge(parent)
248
- self.new.merge(parent)
249
- end
250
-
251
- def color
252
- @_color
253
- end
254
-
255
- def color=(color)
256
- @_color = TextColor.validate!(color)
257
- end
258
-
259
- def bg_color
260
- @_bg_color
261
- end
262
-
263
- def bg_color=(color)
264
- @_bg_color = BgColor.validate!(color)
265
- end
266
-
267
- def bold?
268
- @_bold || false
269
- end
270
-
271
- def bold=(val)
272
- @_bold = val
273
- end
274
-
275
- def underline?
276
- @_underline || false
277
- end
278
-
279
- def underline=(val)
280
- @_underline = val
281
- end
282
-
283
- def merge(parent)
284
- merged = self.class.new
285
- merged.color = @color || parent.color
286
- merged.bg_color = @bg_color || parent.bg_color
287
- merged._bold = @_bold.nil? ? parent._bold : @_bold
288
- merged._underline = @_underline.nil? ? parent._underline : @_underline
289
- merged
290
- end
291
-
292
- def inspect(indent='')
293
- values = self.instance_variables.map do |v|
294
- unless instance_variable_get(v).nil? || instance_variable_get(v).inspect.nil?
295
- "#{indent} #{v} = #{instance_variable_get(v).inspect}"
296
- end
297
- end.compact
298
- details = values.length > 0 ? values.join("\n") : "#{indent} <default>"
299
- "#{indent}#{self.class.name}\n#{details}"
300
- end
301
-
302
- protected
303
-
304
- attr_accessor :_bold, :_underline
305
- end
306
- private_constant :ElementSettings
307
-
308
- module WidthSetting
309
- attr_accessor :width
310
- end
311
-
312
- module AlignSetting
313
- def align
314
- @_align || TextAlign::LEFT
315
- end
316
-
317
- def align=(align)
318
- @_align = TextAlign.validate!(align)
319
- end
320
-
321
- def merge(parent)
322
- merged = super
323
- merged._align = if @_align
324
- @_align
325
- elsif parent.is_a?(AlignSetting)
326
- parent._align
327
- end
328
- merged
329
- end
330
-
331
- protected
332
-
333
- attr_accessor :_align
334
- end
335
-
336
- module MarginSettings
337
- def set_margin(left: nil, top: nil, right: nil, bottom: nil)
338
- @_margin = Margin.new unless @_margin
339
- @_margin.left = left if left
340
- @_margin.top = top if top
341
- @_margin.right = right if right
342
- @_margin.bottom = bottom if bottom
343
- end
344
-
345
- def clear_margin
346
- set_margin(left: 0, top: 0, right: 0, bottom: 0)
347
- @_auto_margin = nil
348
- end
349
-
350
- def margin
351
- @_margin || Margin.new
352
- end
353
-
354
- def auto_margin=(val)
355
- @_auto_margin = val
356
- end
357
-
358
- def auto_margin?
359
- @_auto_margin || false
360
- end
361
-
362
- def merge(parent)
363
- merged = super
364
- if parent.is_a?(MarginSettings)
365
- merged._margin = margin.merge(parent.margin)
366
- merged._auto_margin = @_auto_margin.nil? ? parent._auto_margin : @_auto_margin
367
- else
368
- merged._margin = _margin
369
- merged._auto_margin = @_auto_margin
370
- end
371
- merged
372
- end
373
-
374
- protected
375
-
376
- attr_accessor :_margin, :_auto_margin
377
- end
378
- private_constant :MarginSettings
379
-
380
- module BorderSettings
381
- def set_border(
382
- left: nil, top: nil, right: nil, bottom: nil, inner_horiz: nil, inner_vert: nil, style: nil, color: nil
383
- )
384
- @_border = Border.new unless @_border
385
- @_border.left = left unless left.nil?
386
- @_border.top = top unless top.nil?
387
- @_border.right = right unless right.nil?
388
- @_border.bottom = bottom unless bottom.nil?
389
- @_border.inner_horiz = inner_horiz unless inner_horiz.nil?
390
- @_border.inner_vert = inner_vert unless inner_vert.nil?
391
- @_border.style = style unless style.nil?
392
- @_border.color = color unless color.nil?
393
- end
394
-
395
- def clear_border
396
- set_border(
397
- left: false, top: false, right: false, bottom: false, inner_horiz: false, inner_vert: false
398
- )
399
- end
400
-
401
- def full_border(style: nil, color: nil)
402
- set_border(
403
- left: true, top: true, right: true, bottom: true, inner_horiz: true, inner_vert: true, style: style, color: color
404
- )
405
- end
406
-
407
- def border
408
- @_border || Border.new
409
- end
410
-
411
- def merge(parent)
412
- merged = super
413
- merged._border = parent.is_a?(BorderSettings) ? border.merge(parent.border) : _border
414
- merged
415
- end
416
-
417
- protected
418
-
419
- attr_accessor :_border
420
- end
421
- private_constant :BorderSettings
422
-
423
- module PaddingSettings
424
- def set_padding(left: nil, top: nil, right: nil, bottom: nil)
425
- @_padding = Padding.new unless @_padding
426
- @_padding.left = left if left
427
- @_padding.top = top if top
428
- @_padding.right = right if right
429
- @_padding.bottom = bottom if bottom
430
- end
431
-
432
- def clear_padding
433
- set_padding(left: 0, top: 0, right: 0, bottom: 0)
434
- end
435
-
436
- def padding
437
- @_padding || Padding.new
438
- end
439
-
440
- def merge(parent)
441
- merged = super
442
- merged._padding = parent.is_a?(PaddingSettings) ? padding.merge(parent.padding) : _padding
443
- merged
444
- end
445
-
446
- protected
447
-
448
- attr_accessor :_padding
449
- end
450
- private_constant :PaddingSettings
451
-
452
- class TemplateSettings < ElementSettings
453
- end
454
-
455
- class TextSettings < ElementSettings
456
- include WidthSetting
457
- include AlignSetting
458
- end
459
-
460
- class ContainerSettings < ElementSettings
461
- include MarginSettings
462
- include BorderSettings
463
- include PaddingSettings
464
- end
465
-
466
- class BoxSettings < ContainerSettings
467
- include WidthSetting
468
- include AlignSetting
469
-
470
- def flow=(flow)
471
- @_flow = DisplayFlow.validate!(flow)
472
- end
473
-
474
- def flow
475
- @_flow || DisplayFlow::LEFT_TO_RIGHT
476
- end
477
-
478
- def horizontal_flow?
479
- %i[l2r r2l].include?(flow)
480
- end
481
-
482
- def vertical_flow?
483
- !horizontal_flow?
484
- end
485
-
486
- def forward_flow?
487
- %i[l2r t2b].include?(flow)
488
- end
489
-
490
- def reverse_flow?
491
- !forward_flow?
492
- end
493
-
494
- def merge(parent)
495
- merged = super
496
- merged._flow = if @_flow
497
- @_flow
498
- elsif parent.is_a?(BoxSettings)
499
- parent._flow
500
- end
501
- merged
502
- end
503
-
504
- protected
505
-
506
- attr_accessor :_flow
507
- end
508
-
509
- class GridSettings < ContainerSettings
510
- include WidthSetting
511
- include AlignSetting
512
-
513
- attr_accessor :num_cols
514
- attr_writer :transpose
515
-
516
- def transpose?
517
- @transpose || false
518
- end
519
- end
520
- end
521
- end