whirled_peas 0.10.0 → 0.11.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +88 -24
  4. data/examples/components.rb +35 -0
  5. data/examples/scrolling.rb +8 -19
  6. data/lib/whirled_peas.rb +30 -16
  7. data/lib/whirled_peas/command/config_command.rb +3 -2
  8. data/lib/whirled_peas/command/play.rb +3 -2
  9. data/lib/whirled_peas/command/themes.rb +3 -2
  10. data/lib/whirled_peas/component.rb +35 -0
  11. data/lib/whirled_peas/component/list_with_active.rb +88 -0
  12. data/lib/whirled_peas/graphics/box_painter.rb +4 -4
  13. data/lib/whirled_peas/graphics/composer.rb +4 -0
  14. data/lib/whirled_peas/graphics/container_coords.rb +24 -2
  15. data/lib/whirled_peas/graphics/container_painter.rb +9 -119
  16. data/lib/whirled_peas/graphics/debugger.rb +1 -7
  17. data/lib/whirled_peas/graphics/scrollbar_helper.rb +124 -0
  18. data/lib/whirled_peas/null_logger.rb +0 -1
  19. data/lib/whirled_peas/settings/container_settings.rb +12 -5
  20. data/lib/whirled_peas/settings/debugger.rb +1 -1
  21. data/lib/whirled_peas/settings/position.rb +17 -5
  22. data/lib/whirled_peas/version.rb +1 -1
  23. data/screen_test/components/list_with_active/l2r_active_color.frame +1 -0
  24. data/screen_test/components/list_with_active/l2r_active_color.rb +18 -0
  25. data/screen_test/components/list_with_active/l2r_position_end.frame +1 -0
  26. data/screen_test/components/list_with_active/l2r_position_end.rb +16 -0
  27. data/screen_test/components/list_with_active/l2r_position_middle.frame +1 -0
  28. data/screen_test/components/list_with_active/l2r_position_middle.rb +16 -0
  29. data/screen_test/components/list_with_active/l2r_position_start.frame +1 -0
  30. data/screen_test/components/list_with_active/l2r_position_start.rb +16 -0
  31. data/screen_test/components/list_with_active/l2r_separator.frame +1 -0
  32. data/screen_test/components/list_with_active/l2r_separator.rb +17 -0
  33. data/screen_test/components/list_with_active/t2b_active_color.frame +1 -0
  34. data/screen_test/components/list_with_active/t2b_active_color.rb +19 -0
  35. data/screen_test/components/list_with_active/t2b_position_end.frame +1 -0
  36. data/screen_test/components/list_with_active/t2b_position_end.rb +17 -0
  37. data/screen_test/components/list_with_active/t2b_position_middle.frame +1 -0
  38. data/screen_test/components/list_with_active/t2b_position_middle.rb +17 -0
  39. data/screen_test/components/list_with_active/t2b_position_start.frame +1 -0
  40. data/screen_test/components/list_with_active/t2b_position_start.rb +17 -0
  41. data/screen_test/components/list_with_active/t2b_separator.frame +1 -0
  42. data/screen_test/components/list_with_active/t2b_separator.rb +18 -0
  43. data/screen_test/elements/theme.rb +1 -1
  44. data/screen_test/settings/{position/box_top_negative.frame → content_start/box_bottom.frame} +0 -0
  45. data/screen_test/settings/content_start/box_bottom.rb +17 -0
  46. data/screen_test/settings/{position → content_start}/box_left.frame +0 -0
  47. data/screen_test/settings/{position/box_top.rb → content_start/box_left.rb} +1 -1
  48. data/screen_test/settings/{position → content_start}/box_left_negative.frame +0 -0
  49. data/screen_test/settings/{position → content_start}/box_left_negative.rb +1 -1
  50. data/screen_test/settings/content_start/box_right.frame +1 -0
  51. data/screen_test/settings/content_start/box_right.rb +17 -0
  52. data/screen_test/settings/{position → content_start}/box_top.frame +0 -0
  53. data/screen_test/settings/{position/box_left.rb → content_start/box_top.rb} +1 -1
  54. data/screen_test/settings/content_start/box_top_negative.frame +1 -0
  55. data/screen_test/settings/{position → content_start}/box_top_negative.rb +1 -1
  56. data/screen_test/settings/{position → content_start}/grid_left.frame +0 -0
  57. data/screen_test/settings/{position → content_start}/grid_left.rb +1 -1
  58. data/screen_test/settings/{position → content_start}/grid_left_negative.frame +0 -0
  59. data/screen_test/settings/{position → content_start}/grid_left_negative.rb +1 -1
  60. data/screen_test/settings/{position → content_start}/grid_top.frame +0 -0
  61. data/screen_test/settings/{position → content_start}/grid_top.rb +1 -1
  62. data/screen_test/settings/{position → content_start}/grid_top_negative.frame +0 -0
  63. data/screen_test/settings/{position → content_start}/grid_top_negative.rb +1 -1
  64. data/screen_test/settings/scroll/horiz_box.frame +1 -1
  65. data/screen_test/settings/scroll/horiz_box.rb +2 -4
  66. data/screen_test/settings/scroll/horiz_box_align_center.rb +2 -4
  67. data/screen_test/settings/scroll/horiz_box_align_right.rb +2 -4
  68. data/screen_test/settings/scroll/vert_box.frame +1 -1
  69. data/screen_test/settings/scroll/vert_box.rb +5 -7
  70. metadata +46 -18
@@ -55,8 +55,8 @@ module WhirledPeas
55
55
  private
56
56
 
57
57
  def paint_horizontally(canvas, canvas_coords, &block)
58
- stroke_left = canvas_coords.content_left
59
- stroke_top = canvas_coords.content_top
58
+ stroke_left = canvas_coords.offset_content_left
59
+ stroke_top = canvas_coords.offset_content_top
60
60
  children_width = 0
61
61
  each_child { |c| children_width += c.dimensions.outer_width }
62
62
  left_offset, spacing_offset = horiz_justify_offset(children_width)
@@ -76,8 +76,8 @@ module WhirledPeas
76
76
  end
77
77
 
78
78
  def paint_vertically(canvas, canvas_coords, &block)
79
- stroke_left = canvas_coords.content_left
80
- stroke_top = canvas_coords.content_top
79
+ stroke_left = canvas_coords.offset_content_left
80
+ stroke_top = canvas_coords.offset_content_top
81
81
  children_height = 0
82
82
  each_child { |c| children_height += c.dimensions.outer_height }
83
83
  top_offset, spacing_offset = vert_justify_offset(children_height)
@@ -71,6 +71,10 @@ module WhirledPeas
71
71
  painter.add_child(child)
72
72
  end
73
73
 
74
+ def add_component(component)
75
+ component.compose(self, settings)
76
+ end
77
+
74
78
  def add_box(name=self.class.next_name, &block)
75
79
  child_settings = Settings::BoxSettings.inherit(painter.settings)
76
80
  child = BoxPainter.new(name, child_settings)
@@ -9,11 +9,11 @@ module WhirledPeas
9
9
  end
10
10
 
11
11
  def left
12
- start_left + settings.position.left
12
+ start_left
13
13
  end
14
14
 
15
15
  def top
16
- start_top + settings.position.top
16
+ start_top
17
17
  end
18
18
 
19
19
  def border_left
@@ -36,10 +36,32 @@ module WhirledPeas
36
36
  padding_left + settings.padding.left + col_index * grid_width
37
37
  end
38
38
 
39
+ def offset_content_left(col_index=0)
40
+ if settings.content_start.left
41
+ content_left(col_index) + settings.content_start.left
42
+ elsif settings.content_start.right
43
+ left_offset = dimensions.content_width - dimensions.children_width
44
+ content_left(col_index) + left_offset - settings.content_start.right
45
+ else
46
+ content_left(col_index)
47
+ end
48
+ end
49
+
39
50
  def content_top(row_index=0)
40
51
  padding_top + settings.padding.top + row_index * grid_height
41
52
  end
42
53
 
54
+ def offset_content_top(row_index=0)
55
+ if settings.content_start.top
56
+ content_top(row_index) + settings.content_start.top
57
+ elsif settings.content_start.bottom
58
+ top_offset = dimensions.content_height - dimensions.children_height
59
+ content_top(row_index) + top_offset - settings.content_start.bottom
60
+ else
61
+ content_top(row_index)
62
+ end
63
+ end
64
+
43
65
  def inner_grid_width
44
66
  settings.padding.left +
45
67
  dimensions.content_width +
@@ -2,6 +2,7 @@ require 'whirled_peas/utils/formatted_string'
2
2
 
3
3
  require_relative 'container_coords'
4
4
  require_relative 'painter'
5
+ require_relative 'scrollbar_helper'
5
6
 
6
7
  module WhirledPeas
7
8
  module Graphics
@@ -216,16 +217,12 @@ module WhirledPeas
216
217
  settings.border.style.right_vert,
217
218
  ) do
218
219
  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
220
+ scrollbar_char = ScrollbarHelper.vert_char(
221
+ dimensions.children_height + dimensions.padding_height,
222
+ canvas_coords.inner_grid_height,
223
+ canvas_coords.top - canvas_coords.offset_content_top,
224
+ row_within_cell
225
+ )
229
226
  PADDING * canvas_coords.inner_grid_width + scrollbar_char
230
227
  else
231
228
  PADDING * canvas_coords.inner_grid_width
@@ -241,122 +238,15 @@ module WhirledPeas
241
238
  settings.border.style.right_vert,
242
239
  ) do
243
240
  canvas_coords.inner_grid_width.times.map do |col_within_cell|
244
- horiz_scroll_char(
241
+ ScrollbarHelper.horiz_char(
245
242
  dimensions.children_width + dimensions.padding_width,
246
243
  canvas_coords.inner_grid_width,
247
- -children.first.settings.position.left,
244
+ canvas_coords.left - canvas_coords.offset_content_left,
248
245
  col_within_cell
249
246
  )
250
247
  end.join
251
248
  end
252
249
  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
250
  end
361
251
  private_constant :ContainerPainter
362
252
  end
@@ -18,13 +18,7 @@ module WhirledPeas
18
18
  info << "#{indent + ' '}- Settings"
19
19
  info << Settings::Debugger.new(painter.settings).debug(indent + ' ')
20
20
  if painter.is_a?(TextPainter)
21
- content = if painter.content.length > 1
22
- '<multiline>'
23
- elsif painter.content.first.length > 12
24
- "#{painter.content.first[0..9]}...".inspect
25
- else
26
- painter.content.first.inspect
27
- end
21
+ content = painter.content
28
22
  info << "#{indent + ' '}- Content(value=#{content})"
29
23
  elsif painter.is_a?(ContainerPainter)
30
24
  info << "#{indent + ' '}- Children"
@@ -0,0 +1,124 @@
1
+ module WhirledPeas
2
+ module Graphics
3
+ module ScrollbarHelper
4
+ # Contants to paint scrollbars
5
+ HORIZONTAL = [' ', '▗', '▖', '▄']
6
+ VERTICAL = [
7
+ ' ',
8
+ '▗',
9
+ '▝',
10
+ '▐'
11
+ ]
12
+
13
+ # The number of units of scrollbar a single character can be divided into
14
+ SCROLL_HANDLE_SCALE = 2
15
+ private_constant :SCROLL_HANDLE_SCALE
16
+
17
+ class << self
18
+ # Determine the character to paint the horizontal scroll bar with for the given column
19
+ #
20
+ # @see #scroll_char for more details
21
+ def horiz_char(col_count, viewable_col_count, first_visible_col, curr_col)
22
+ scroll_char(col_count, viewable_col_count, first_visible_col, curr_col, HORIZONTAL)
23
+ end
24
+
25
+ # Determine the character to paint the vertical scroll bar with for the given row
26
+ #
27
+ # @see #scroll_char for more details
28
+ def vert_char(row_count, viewable_row_count, first_visible_row, curr_row)
29
+ scroll_char(row_count, viewable_row_count, first_visible_row, curr_row, VERTICAL)
30
+ end
31
+
32
+ private
33
+
34
+ # Determine which character to paint a for a scrollbar
35
+ #
36
+ # @param total_count [Integer] total number of rows/columns in the content
37
+ # @param viewable_count [Integer] number of rows/columns visible in the viewport
38
+ # @param first_visible [Integer] zero-based index of the first row/column that is visible
39
+ # in the viewport
40
+ # @param curr [Integer] zero-based index of the row/column (relative to the first visible
41
+ # row/column) that the painted character is being requested for
42
+ # @param chars [Array<String>] an array with three 1-character strings, the frist is the
43
+ # "second half" scrollbar character, the second is the "full" scrollbar character, and
44
+ # the third is the "first half" scrollbar character.
45
+ def scroll_char(total_count, viewable_count, first_visible, curr, chars)
46
+ return chars[0] if total_count == 0
47
+ return chars[0] if viewable_count == 0
48
+ return chars[0] if viewable_count >= total_count
49
+
50
+ # The scroll handle has the exact same relative size and position in the scroll gutter
51
+ # that the viewable content has in the total content area. For example, a content area
52
+ # that is 50 columns wide with a view port that is 20 columns wide might look like
53
+ #
54
+ # +---------1-----****2*********3******---4---------+
55
+ # | * * |
56
+ # | hidden * viewable * hidden |
57
+ # | * * |
58
+ # +---------1-----****2*********3******---4---------+
59
+ #
60
+ # The scoll gutter, would look like
61
+ #
62
+ # |......********.....|
63
+ #
64
+ # Scrolling all the way to the right results in
65
+ #
66
+ # +---------1---------2---------3*********4*********+
67
+ # | * *
68
+ # | hidden * viewable *
69
+ # | * *
70
+ # +---------1---------2---------3*********4*********+
71
+ # |...........********|
72
+ #
73
+ # Returning to the first example, we can match up the arguments to this method to the
74
+ # diagram
75
+ #
76
+ # total_count = 50
77
+ # |<----------------------------------------------->|
78
+ # | |
79
+ # | veiwable_count = 20 |
80
+ # | |<----------------->| |
81
+ # ↓ ↓ ↓ ↓
82
+ # +---------1-----****2*********3******---4---------+
83
+ # | * * |
84
+ # | hidden * viewable * hidden |
85
+ # | * * |
86
+ # +---------1-----****2*********3******---4---------+
87
+ # |......****?***.....|
88
+ # ↑ ↑
89
+ # first_visible = 16 |
90
+ # curr = 11
91
+
92
+ # Always use the same length for the scrollbar so it does not give an inchworm effect
93
+ # as it scrolls along. This will calculate the "ideal" length of the scroll bar if we
94
+ # could infinitely divide a character.
95
+ scrollbar_length = (viewable_count ** 2).to_f / total_count
96
+
97
+ # Round the length to the nearst "scaled" value
98
+ scrollbar_length = (SCROLL_HANDLE_SCALE * scrollbar_length).round.to_f / SCROLL_HANDLE_SCALE
99
+
100
+ # Ensure we have a scrollbar!
101
+ scrollbar_length = 1.0 / SCROLL_HANDLE_SCALE if scrollbar_length == 0
102
+
103
+ # Find the "ideal" position of where the scrollbar should start.
104
+ scrollbar_start = first_visible * viewable_count.to_f / total_count
105
+
106
+ # Round the start to the nearest "scaled" value
107
+ scrollbar_start = (SCROLL_HANDLE_SCALE * scrollbar_start).round.to_f / SCROLL_HANDLE_SCALE
108
+
109
+ # Make sure we didn't scroll off the page!
110
+ scrollbar_start -= scrollbar_length if scrollbar_start == viewable_count
111
+
112
+ # Create "scaled" indexes for the subdivided current character
113
+ curr_0 = curr
114
+ curr_1 = curr + 1.0 / SCROLL_HANDLE_SCALE
115
+
116
+ first_half = scrollbar_start <= curr_0 && curr_0 < scrollbar_start + scrollbar_length ? 2 : 0
117
+ second_half = scrollbar_start <= curr_1 && curr_1 < scrollbar_start + scrollbar_length ? 1 : 0
118
+
119
+ chars[second_half | first_half]
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -15,6 +15,5 @@ module WhirledPeas
15
15
  def warn(*)
16
16
  end
17
17
  end
18
-
19
18
  private_constant :NullLogger
20
19
  end
@@ -175,13 +175,20 @@ module WhirledPeas
175
175
  @_padding ||= Padding.new
176
176
  end
177
177
 
178
- def set_position(left: nil, top: nil)
179
- position.left = left if left
180
- position.top = top if top
178
+ def set_content_start(left: nil, top: nil, right: nil, bottom: nil)
179
+ if left && right
180
+ raise ArgumentError, 'Setting left and right content_start is not supported'
181
+ elsif top && bottom
182
+ raise ArgumentError, 'Setting top and bottom content_start is not supported'
183
+ end
184
+ content_start.left = left if left
185
+ content_start.top = top if top
186
+ content_start.right = right if right
187
+ content_start.bottom = bottom if bottom
181
188
  end
182
189
 
183
- def position
184
- @_position ||= Position.new
190
+ def content_start
191
+ @_content_start ||= Position.new
185
192
  end
186
193
 
187
194
  def set_scrollbar(horiz: nil, vert: nil)
@@ -73,7 +73,7 @@ module WhirledPeas
73
73
  def border_value(border)
74
74
  values = non_defaults(
75
75
  border,
76
- Border.new,
76
+ Border.new(settings.theme),
77
77
  %i[left? top? right? bottom? inner_horiz? inner_vert? style color]
78
78
  )
79
79
  return if values == ''
@@ -1,14 +1,26 @@
1
1
  module WhirledPeas
2
2
  module Settings
3
3
  class Position
4
- attr_writer :left, :top
4
+ attr_reader :left, :top, :right, :bottom
5
5
 
6
- def left
7
- @left || 0
6
+ def left=(value)
7
+ @right = nil
8
+ @left = value
8
9
  end
9
10
 
10
- def top
11
- @top || 0
11
+ def top=(value)
12
+ @bottom = nil
13
+ @top = value
14
+ end
15
+
16
+ def right=(value)
17
+ @left = nil
18
+ @right = value
19
+ end
20
+
21
+ def bottom=(value)
22
+ @top = nil
23
+ @bottom = value
12
24
  end
13
25
  end
14
26
  end