whirled_peas 0.11.0 → 0.11.1

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -2
  3. data/CHANGELOG.md +5 -0
  4. data/README.md +7 -1156
  5. data/doc/application.md +92 -0
  6. data/doc/cli.md +115 -0
  7. data/doc/components.md +55 -0
  8. data/doc/easing.md +257 -0
  9. data/doc/elements.md +69 -0
  10. data/doc/screen_test.md +23 -0
  11. data/doc/settings.md +466 -0
  12. data/doc/template_factory.md +53 -0
  13. data/doc/themes.md +15 -0
  14. data/lib/data/themes.yaml +8 -4
  15. data/lib/whirled_peas/component/list_with_active.rb +3 -3
  16. data/lib/whirled_peas/graphics/container_coords.rb +2 -26
  17. data/lib/whirled_peas/graphics/container_dimensions.rb +24 -0
  18. data/lib/whirled_peas/graphics/container_painter.rb +26 -28
  19. data/lib/whirled_peas/graphics/debugger.rb +1 -0
  20. data/lib/whirled_peas/graphics/graph_painter.rb +31 -24
  21. data/lib/whirled_peas/graphics/scrollbar_helper.rb +31 -29
  22. data/lib/whirled_peas/settings/bg_color.rb +5 -1
  23. data/lib/whirled_peas/settings/border.rb +1 -1
  24. data/lib/whirled_peas/settings/color.rb +8 -4
  25. data/lib/whirled_peas/settings/debugger.rb +2 -0
  26. data/lib/whirled_peas/settings/element_settings.rb +10 -2
  27. data/lib/whirled_peas/settings/text_color.rb +5 -0
  28. data/lib/whirled_peas/settings/theme.rb +28 -7
  29. data/lib/whirled_peas/settings/theme_library.rb +10 -4
  30. data/lib/whirled_peas/version.rb +1 -1
  31. data/screen_test/components/list_with_active/l2r_position_start.frame +1 -1
  32. data/screen_test/components/list_with_active/l2r_separator.frame +1 -1
  33. data/screen_test/components/list_with_active/t2b_position_start.frame +1 -1
  34. data/screen_test/components/list_with_active/t2b_separator.frame +1 -1
  35. data/screen_test/elements/{graph.frame → graph_asc.frame} +1 -1
  36. data/screen_test/elements/{graph.rb → graph_asc.rb} +0 -0
  37. data/screen_test/elements/graph_desc.frame +1 -0
  38. data/screen_test/elements/graph_desc.rb +12 -0
  39. data/screen_test/elements/graph_horiz.frame +1 -0
  40. data/screen_test/elements/graph_horiz.rb +12 -0
  41. data/screen_test/elements/graph_sin.frame +1 -0
  42. data/screen_test/elements/graph_sin.rb +12 -0
  43. data/screen_test/elements/theme.frame +1 -1
  44. data/screen_test/settings/scroll/horiz_box.frame +1 -1
  45. data/screen_test/settings/scroll/vert_box.frame +1 -1
  46. metadata +19 -6
  47. data/bin/easing +0 -33
  48. data/lib/whirled_peas/graphics/mock_screen.rb +0 -26
@@ -0,0 +1,53 @@
1
+ ## Template Factory
2
+
3
+ To render the frame events sent by the application, the application requires a template factory. This factory will be called for each frame event, with the frame name and the arguments supplied by the application. A template factory can be an instance of ruby class and thus can maintain state. Whirled Peas provides a few basic building blocks to make simple, yet elegant terminal-based UIs.
4
+
5
+ To build a template in a template factory, use `WhirledPeas.template(theme)`, which takes an optional [theme](themes.md) and yields a composer (used to add [elements](elements.md) to the template) and [settings](settings.md) (used to configure the element).
6
+
7
+ ### Full example
8
+
9
+ ```ruby
10
+ class TemplateFactory
11
+ def build(frame, args)
12
+ set_state(frame, args)
13
+ WhirledPeas.template do |composer, settings|
14
+ settings.flow = :l2r
15
+ settings.align = :center
16
+
17
+ composer.add_box('Title', &method(:title))
18
+ composer.add_box('Sum', &method(:sum))
19
+ composer.add_grid('NumberGrid', &method(:number_grid))
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def set_state(frame, args)
26
+ @frame = frame
27
+ @numbers = args.key?(:numbers) ? args[:numbers] || []
28
+ @sum = args[:sum] if args.key?(:sum)
29
+ @low = args[:low] if args.key?(:low)
30
+ @high = args[:high] if args.key?(:high)
31
+ end
32
+
33
+ def title(_composer, settings)
34
+ settings.underline = true
35
+ "Pair Finder"
36
+ end
37
+
38
+ def sum(_composer, settings)
39
+ settings.color = @frame == 'found-pair' ? :green : :red
40
+ @sum ? "Sum: #{@sum}" : 'N/A'
41
+ end
42
+
43
+ def number_grid(composer, settings)
44
+ settings.full_border
45
+ @numbers.each.with_index do |num, index|
46
+ composer.add_text do |_, settings|
47
+ settings.bg_color = (@low == index || @high == index) ? :cyan : :white
48
+ num
49
+ end
50
+ end
51
+ end
52
+ end
53
+ ```
@@ -0,0 +1,15 @@
1
+ ## Themes
2
+
3
+ The template builder (`WhirledPeas.template`) takes an optional `theme` argument. You can provide the name of a predefined theme (run `whirled_peas themes` to see a list with samples) or define you own with the following
4
+
5
+ ```ruby
6
+ WhirledPeas.register_theme(:my_theme) do |theme|
7
+ theme.bg_color = :bright_white
8
+ theme.color = :blue
9
+ theme.border_color = :bright_green
10
+ theme.axis_color = :bright_red
11
+ theme.title_font = :default
12
+ end
13
+ ```
14
+
15
+ Theme settings will be used as default settings throughout the template, however theme settings can be overridden on any element.
@@ -1,13 +1,17 @@
1
1
  bright:
2
+ axis_color: :red
2
3
  bg_color: :bright_white
3
- color: :blue
4
4
  border_color: :bright_blue
5
- axis_color: :red
5
+ border_style: :soft
6
+ color: :blue
6
7
  title_font: :default
7
8
 
8
9
  dark:
10
+ axis_color: :bright_white
9
11
  bg_color: :black
10
- color: :white
11
12
  border_color: :gray
12
- axis_color: :bright_white
13
+ border_style: :double
14
+ color: :white
15
+ highlight_bg_color: :bright_red
16
+ highlight_color: :black
13
17
  title_font: :swan
@@ -1,7 +1,7 @@
1
1
  module WhirledPeas
2
2
  module Component
3
3
  class ListWithActive
4
- attr_accessor :active_index, :active_color, :active_bg_color, :separator
4
+ attr_accessor :active_index, :separator
5
5
 
6
6
  attr_reader :items
7
7
 
@@ -75,8 +75,8 @@ module WhirledPeas
75
75
  composer.add_text { separator } if !separator.nil? && index > 0
76
76
  composer.add_text do |_, settings|
77
77
  if index == active_index
78
- settings.bg_color = active_bg_color
79
- settings.color = active_color
78
+ settings.bg_color = :highlight
79
+ settings.color = :highlight
80
80
  end
81
81
  item
82
82
  end
@@ -33,7 +33,7 @@ module WhirledPeas
33
33
  end
34
34
 
35
35
  def content_left(col_index=0)
36
- padding_left + settings.padding.left + col_index * grid_width
36
+ padding_left + settings.padding.left + col_index * dimensions.grid_width
37
37
  end
38
38
 
39
39
  def offset_content_left(col_index=0)
@@ -48,7 +48,7 @@ module WhirledPeas
48
48
  end
49
49
 
50
50
  def content_top(row_index=0)
51
- padding_top + settings.padding.top + row_index * grid_height
51
+ padding_top + settings.padding.top + row_index * dimensions.grid_height
52
52
  end
53
53
 
54
54
  def offset_content_top(row_index=0)
@@ -62,30 +62,6 @@ module WhirledPeas
62
62
  end
63
63
  end
64
64
 
65
- def inner_grid_width
66
- settings.padding.left +
67
- dimensions.content_width +
68
- settings.padding.right
69
- end
70
-
71
- def grid_width
72
- (settings.border.inner_vert? ? 1 : 0) +
73
- inner_grid_width +
74
- (settings.scrollbar.vert? ? 1 : 0)
75
- end
76
-
77
- def inner_grid_height
78
- settings.padding.top +
79
- dimensions.content_height +
80
- settings.padding.bottom
81
- end
82
-
83
- def grid_height
84
- (settings.border.inner_horiz? ? 1 : 0) +
85
- inner_grid_height +
86
- (settings.scrollbar.horiz? ? 1 : 0)
87
- end
88
-
89
65
  private
90
66
 
91
67
  attr_reader :settings, :dimensions, :start_left, :start_top
@@ -31,6 +31,30 @@ module WhirledPeas
31
31
  end
32
32
  end
33
33
 
34
+ def inner_grid_width
35
+ settings.padding.left +
36
+ content_width +
37
+ settings.padding.right
38
+ end
39
+
40
+ def grid_width
41
+ (settings.border.inner_vert? ? 1 : 0) +
42
+ inner_grid_width +
43
+ (settings.scrollbar.vert? ? 1 : 0)
44
+ end
45
+
46
+ def inner_grid_height
47
+ settings.padding.top +
48
+ content_height +
49
+ settings.padding.bottom
50
+ end
51
+
52
+ def grid_height
53
+ (settings.border.inner_horiz? ? 1 : 0) +
54
+ inner_grid_height +
55
+ (settings.scrollbar.horiz? ? 1 : 0)
56
+ end
57
+
34
58
  def outer_width
35
59
  @outer_width ||= margin_width +
36
60
  outer_border_width +
@@ -35,11 +35,17 @@ module WhirledPeas
35
35
 
36
36
  # Paint the top border if the settings call for it
37
37
  if settings.border.top?
38
- canvas.stroke(stroke_left, stroke_top, top_border_stroke(canvas_coords), formatting, &block)
38
+ canvas.stroke(stroke_left, stroke_top, top_border_stroke, formatting, &block)
39
39
  stroke_top += 1
40
40
  end
41
41
  # Precalculate the middle border container grids with more than 1 row
42
- middle_border = dimensions.num_rows > 1 ? middle_border_stroke(canvas_coords) : ''
42
+ middle_border = dimensions.num_rows > 1 ? middle_border_stroke : ''
43
+
44
+ vert_scrollbar = settings.scrollbar.vert? ? ScrollbarHelper.vert(
45
+ dimensions.children_height + dimensions.padding_height,
46
+ dimensions.inner_grid_height,
47
+ canvas_coords.content_top - canvas_coords.offset_content_top,
48
+ ) : []
43
49
 
44
50
  # Paint each grid row by row
45
51
  dimensions.num_rows.times do |row_num|
@@ -53,8 +59,9 @@ module WhirledPeas
53
59
 
54
60
  # Paint the interior of each row (horizontal borders, veritical scroll bar and
55
61
  # background color for the padding and content area)
56
- canvas_coords.inner_grid_height.times do |row_within_cell|
57
- canvas.stroke(stroke_left, stroke_top, content_line_stroke(canvas_coords, row_within_cell), formatting, &block)
62
+ dimensions.inner_grid_height.times do |row_within_cell|
63
+ content_line = content_line_stroke(row_within_cell, vert_scrollbar)
64
+ canvas.stroke(stroke_left, stroke_top, content_line, formatting, &block)
58
65
  stroke_top += 1
59
66
  end
60
67
 
@@ -67,7 +74,7 @@ module WhirledPeas
67
74
 
68
75
  # Paint the bottom border if the settings call for it
69
76
  if settings.border.bottom?
70
- canvas.stroke(stroke_left, stroke_top, bottom_border_stroke(canvas_coords), formatting, &block)
77
+ canvas.stroke(stroke_left, stroke_top, bottom_border_stroke, formatting, &block)
71
78
  stroke_top += 1
72
79
  end
73
80
  end
@@ -177,55 +184,49 @@ module WhirledPeas
177
184
  end
178
185
 
179
186
  # Return the stroke for the top border
180
- def top_border_stroke(canvas_coords)
187
+ def top_border_stroke
181
188
  line_stroke(
182
189
  settings.border.style.top_left,
183
190
  settings.border.style.top_junc,
184
191
  settings.border.style.top_right
185
192
  ) do
186
- settings.border.style.top_horiz * (canvas_coords.inner_grid_width + (settings.scrollbar.vert? ? 1 : 0))
193
+ settings.border.style.top_horiz * (dimensions.inner_grid_width + (settings.scrollbar.vert? ? 1 : 0))
187
194
  end
188
195
  end
189
196
 
190
197
  # Return the stroke for an inner horizontal border
191
- def middle_border_stroke(canvas_coords)
198
+ def middle_border_stroke
192
199
  line_stroke(
193
200
  settings.border.style.left_junc,
194
201
  settings.border.style.cross_junc,
195
202
  settings.border.style.right_junc
196
203
  ) do
197
- settings.border.style.middle_horiz * (canvas_coords.inner_grid_width + (settings.scrollbar.vert? ? 1 : 0))
204
+ settings.border.style.middle_horiz * (dimensions.inner_grid_width + (settings.scrollbar.vert? ? 1 : 0))
198
205
  end
199
206
  end
200
207
 
201
208
  # Return the stroke for the bottom border
202
- def bottom_border_stroke(canvas_coords)
209
+ def bottom_border_stroke
203
210
  line_stroke(
204
211
  settings.border.style.bottom_left,
205
212
  settings.border.style.bottom_junc,
206
213
  settings.border.style.bottom_right
207
214
  ) do
208
- settings.border.style.bottom_horiz * (canvas_coords.inner_grid_width + (settings.scrollbar.vert? ? 1 : 0))
215
+ settings.border.style.bottom_horiz * (dimensions.inner_grid_width + (settings.scrollbar.vert? ? 1 : 0))
209
216
  end
210
217
  end
211
218
 
212
219
  # Return the stroke for a grid row between any borders
213
- def content_line_stroke(canvas_coords, row_within_cell)
220
+ def content_line_stroke(row_within_cell, vert_scrollbar)
214
221
  line_stroke(
215
222
  settings.border.style.left_vert,
216
223
  settings.border.style.middle_vert,
217
224
  settings.border.style.right_vert,
218
225
  ) do
219
226
  if settings.scrollbar.vert?
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
- )
226
- PADDING * canvas_coords.inner_grid_width + scrollbar_char
227
+ PADDING * dimensions.inner_grid_width + vert_scrollbar[row_within_cell]
227
228
  else
228
- PADDING * canvas_coords.inner_grid_width
229
+ PADDING * dimensions.inner_grid_width
229
230
  end
230
231
  end
231
232
  end
@@ -237,14 +238,11 @@ module WhirledPeas
237
238
  settings.border.style.middle_vert,
238
239
  settings.border.style.right_vert,
239
240
  ) do
240
- canvas_coords.inner_grid_width.times.map do |col_within_cell|
241
- ScrollbarHelper.horiz_char(
242
- dimensions.children_width + dimensions.padding_width,
243
- canvas_coords.inner_grid_width,
244
- canvas_coords.left - canvas_coords.offset_content_left,
245
- col_within_cell
246
- )
247
- end.join
241
+ ScrollbarHelper.horiz(
242
+ dimensions.children_width + dimensions.padding_width,
243
+ dimensions.inner_grid_width,
244
+ canvas_coords.content_left - canvas_coords.offset_content_left
245
+ ).join
248
246
  end
249
247
  end
250
248
  end
@@ -15,6 +15,7 @@ module WhirledPeas
15
15
  "#{indent}* #{painter.class}(#{painter.name})",
16
16
  ]
17
17
  info << "#{indent + ' '}- Dimensions(#{dimensions})"
18
+ info << "#{indent + ' '}- Theme=#{painter.settings.theme.inspect}" if indent == ''
18
19
  info << "#{indent + ' '}- Settings"
19
20
  info << Settings::Debugger.new(painter.settings).debug(indent + ' ')
20
21
  if painter.is_a?(TextPainter)
@@ -5,6 +5,10 @@ require_relative 'content_painter'
5
5
  module WhirledPeas
6
6
  module Graphics
7
7
  class GraphPainter < ContentPainter
8
+ # The number of units along the Y-axis a single character can be divided into
9
+ Y_AXIS_SCALE = 2
10
+ private_constant :Y_AXIS_SCALE
11
+
8
12
  def paint(canvas, left, top, &block)
9
13
  axis_formatting = [*settings.axis_color, *settings.bg_color]
10
14
  plot_formatting = [*settings.color, *settings.bg_color]
@@ -26,8 +30,8 @@ module WhirledPeas
26
30
  min_y = 1.0 / 0
27
31
  max_y = -1.0 / 0
28
32
  if settings.width
29
- interpolated = inner_width.times.map do |i|
30
- x = (i * (content.length - 1).to_f / inner_width).floor
33
+ interpolated = (inner_width + 1).times.map do |i|
34
+ x = (i * (content.length - 1).to_f / (inner_width + 1)).floor
31
35
  max_y = content[x] if content[x] > max_y
32
36
  min_y = content[x] if content[x] < min_y
33
37
  content[x]
@@ -39,31 +43,34 @@ module WhirledPeas
39
43
  min_y = y if y < min_y
40
44
  end
41
45
  end
46
+ if min_y == max_y
47
+ min_y -= 1
48
+ max_y += 1
49
+ end
42
50
  scaled = interpolated.map do |y|
43
- (2 * inner_height * (y - min_y).to_f / (max_y - min_y)).floor
51
+ inner_height * (y - min_y).to_f / (max_y - min_y)
44
52
  end
53
+
45
54
  @plot_lines = Array.new(inner_height) { '' }
46
- scaled.each.with_index do |y, x_index|
55
+ scaled[0..-2].each.with_index do |y, x_index|
56
+ last = x_index == scaled.length - 2
57
+
58
+ prev_y = x_index == 0 ? y : scaled[x_index - 1]
59
+ next_y = scaled[x_index + 1]
60
+
61
+ min_y = [(prev_y + y) / 2, y, last ? next_y : (y + next_y) / 2].min
62
+ min_y = (Y_AXIS_SCALE * min_y).round.to_f / Y_AXIS_SCALE
63
+
64
+ max_y = [(prev_y + y) / 2, y, last ? next_y : (y + next_y) / 2].max
65
+ max_y = (Y_AXIS_SCALE * max_y).round.to_f / Y_AXIS_SCALE
66
+
47
67
  @plot_lines.each.with_index do |row, row_index|
48
- bottom_half_index = 2 * (inner_height - row_index - 1)
49
- top_half_index = bottom_half_index + 1
50
-
51
- asc, next_y = if scaled.length == 1
52
- [true, y]
53
- elsif x_index == scaled.length - 1
54
- y >= scaled[x_index - 1]
55
- [true, y]
56
- else
57
- scaled[x_index + 1] >= y
58
- [true, scaled[x_index + 1]]
59
- end
60
- if asc
61
- top_half = y == top_half_index || (y...next_y).include?(top_half_index)
62
- bottom_half = y == bottom_half_index || (y...next_y).include?(bottom_half_index)
63
- else
64
- top_half = y == top_half_index || (next_y...y).include?(top_half_index)
65
- bottom_half = y == bottom_half_index || (next_y...y).include?(bottom_half_index)
66
- end
68
+ bottom_value = inner_height - row_index - 1
69
+ top_value = bottom_value + 1.0 / Y_AXIS_SCALE
70
+
71
+ top_half = min_y == top_value || (min_y <= top_value && top_value < max_y)
72
+ bottom_half = min_y == bottom_value || (min_y <= bottom_value && bottom_value < max_y)
73
+
67
74
  row << if top_half && bottom_half
68
75
  '█'
69
76
  elsif top_half
@@ -83,7 +90,7 @@ module WhirledPeas
83
90
  end
84
91
 
85
92
  def inner_width
86
- settings.width.nil? ? content.length : settings.width - 1
93
+ (settings.width.nil? ? content.length : settings.width) - 1
87
94
  end
88
95
 
89
96
  def axes_lines
@@ -15,18 +15,18 @@ module WhirledPeas
15
15
  private_constant :SCROLL_HANDLE_SCALE
16
16
 
17
17
  class << self
18
- # Determine the character to paint the horizontal scroll bar with for the given column
18
+ # Return the characters to paint the horizontal scroll bar with for the given column
19
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)
20
+ # @see #scroll_chars for more details
21
+ def horiz(col_count, viewable_col_count, first_visible_col)
22
+ scroll_chars(col_count, viewable_col_count, first_visible_col, HORIZONTAL)
23
23
  end
24
24
 
25
- # Determine the character to paint the vertical scroll bar with for the given row
25
+ # Return the characters to paint the vertical scroll bar with for the given row
26
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)
27
+ # @see #scroll_chars for more details
28
+ def vert(row_count, viewable_row_count, first_visible_row)
29
+ scroll_chars(row_count, viewable_row_count, first_visible_row, VERTICAL)
30
30
  end
31
31
 
32
32
  private
@@ -37,15 +37,16 @@ module WhirledPeas
37
37
  # @param viewable_count [Integer] number of rows/columns visible in the viewport
38
38
  # @param first_visible [Integer] zero-based index of the first row/column that is visible
39
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
40
+ # @param chars [Array<String>] an array with four 1-character strings, the frist is the
41
+ # gutter (i.e. no scrolbar handle visible), then the "second half" scrollbar character,
42
+ # then second is the "first half" scrollbar character, and finally the "full"
43
+ def scroll_chars(total_count, viewable_count, first_visible, chars)
44
+ # Start by initializing the scroll back to to all gutters
45
+ scrollbar = Array.new(viewable_count) { chars[0] }
46
+
47
+ return scrollbar if total_count == 0
48
+ return scrollbar if viewable_count == 0
49
+ return scrollbar if viewable_count >= total_count
49
50
 
50
51
  # The scroll handle has the exact same relative size and position in the scroll gutter
51
52
  # that the viewable content has in the total content area. For example, a content area
@@ -92,31 +93,32 @@ module WhirledPeas
92
93
  # Always use the same length for the scrollbar so it does not give an inchworm effect
93
94
  # as it scrolls along. This will calculate the "ideal" length of the scroll bar if we
94
95
  # could infinitely divide a character.
95
- scrollbar_length = (viewable_count ** 2).to_f / total_count
96
+ length = (viewable_count ** 2).to_f / total_count
96
97
 
97
98
  # Round the length to the nearst "scaled" value
98
- scrollbar_length = (SCROLL_HANDLE_SCALE * scrollbar_length).round.to_f / SCROLL_HANDLE_SCALE
99
+ length = (SCROLL_HANDLE_SCALE * length).round.to_f / SCROLL_HANDLE_SCALE
99
100
 
100
101
  # Ensure we have a scrollbar!
101
- scrollbar_length = 1.0 / SCROLL_HANDLE_SCALE if scrollbar_length == 0
102
+ length = 1.0 / SCROLL_HANDLE_SCALE if length == 0
102
103
 
103
104
  # Find the "ideal" position of where the scrollbar should start.
104
- scrollbar_start = first_visible * viewable_count.to_f / total_count
105
+ start = first_visible * viewable_count.to_f / total_count
105
106
 
106
107
  # Round the start to the nearest "scaled" value
107
- scrollbar_start = (SCROLL_HANDLE_SCALE * scrollbar_start).round.to_f / SCROLL_HANDLE_SCALE
108
+ start = (SCROLL_HANDLE_SCALE * start).round.to_f / SCROLL_HANDLE_SCALE
108
109
 
109
110
  # Make sure we didn't scroll off the page!
110
- scrollbar_start -= scrollbar_length if scrollbar_start == viewable_count
111
+ start -= length if start == viewable_count
111
112
 
112
- # Create "scaled" indexes for the subdivided current character
113
- curr_0 = curr
114
- curr_1 = curr + 1.0 / SCROLL_HANDLE_SCALE
113
+ (start.floor..[(start + length).floor, viewable_count - 1].min).each do |curr_0|
114
+ curr_1 = curr_0 + 1.0 / SCROLL_HANDLE_SCALE
115
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
116
+ first_half = start <= curr_0 && curr_0 < start + length ? 2 : 0
117
+ second_half = start <= curr_1 && curr_1 < start + length ? 1 : 0
118
118
 
119
- chars[second_half | first_half]
119
+ scrollbar[curr_0] = chars[second_half | first_half]
120
+ end
121
+ scrollbar
120
122
  end
121
123
  end
122
124
  end