Class: Tuile::Component::ProgressBar
- Inherits:
-
Component
- Object
- Component
- Tuile::Component::ProgressBar
- Defined in:
- lib/tuile/component/progress_bar.rb,
sig/tuile.rbs
Overview
A one-row progress bar: a run of █ growing left to right across #rect,
over a ░ track.
████████░░░░░░░░░░░░
= Component::ProgressBar.new(range: 0..files.size)
label = Component::Label.new
add()
add(label)
def rect=(new_rect) # the enclosing Layout positions both
super
.rect = Rect.new(rect.left, rect.top, rect.width, 1)
label.rect = Rect.new(rect.left, rect.top + 1, rect.width, 1)
end
.value = done
label.text = "#{.percent}% — #{done}/#{files.size}"
The bar paints no text of its own: put a Label beside it and feed it #percent or #fraction, so the app words it ("42% — 3/7 files") and places it freely. Display-only — not focusable, no keys, no mouse.
While the total is still unknown, #indeterminate= swaps the fill for a block sliding across the bar:
░░░░░░░████░░░░░░░░░
Both endpoints are exact: the bar is full only at #max and empty only at #min, so a full bar always means done. Assign a one-row #rect; a taller one paints the bar on its first row and leaves the rest to the background.
Implementation details
The █/░ pair is the same one VerticalScrollBar uses — East-Asian
Ambiguous and Neutral respectively, so under an ambiguous-as-wide terminal
the rendered length would vary with the fill level. Shipped anyway, per
DECISIONS.md D-ambiguous-width: a bar that rhymes with the scrollbar
beats a third convention, and if that bet is ever reversed both swap
together.
Constant Summary collapse
- DEFAULT_RANGE =
Range covering the whole bar when none is given.
(0.0..1.0)
- INDETERMINATE_FPS =
Frames per second of the indeterminate animation. The block advances one cell per frame, so this is also its speed in cells/second.
5- BLOCK_DIVISOR =
The indeterminate block is this fraction of the bar, at least one cell.
5
Instance Attribute Summary collapse
-
#bar_color ⇒ Color, ...
@return — the value as set, so a Theme::Ref comes back unresolved.
-
#max ⇒ Float
readonly
@return — upper bound of #range.
-
#min ⇒ Float
readonly
@return — lower bound of #range.
-
#value ⇒ Float, Numeric
@return — the progress, clamped into #range when assigned — so
bar.value = 999on a0..250bar reads back as250.0.
Instance Method Summary collapse
-
#block_at(width) ⇒ [Integer, Integer]
Where the sliding block sits this frame: it enters at the left edge and leaves at the right, one cell per frame, then loops.
-
#fraction ⇒ Float
@return — #value as
0.0..1.0. -
#glyphs(width) ⇒ String
@param
width— columns available. -
#indeterminate=(flag) ⇒ void
Switches between the fill and the sliding block.
-
#indeterminate? ⇒ Boolean
@return — whether the sliding-block animation is showing.
-
#initialize(range: DEFAULT_RANGE, value: nil, indeterminate: false) ⇒ ProgressBar
constructor
@param
range— initial #range=. - #on_attached ⇒ void
- #on_detached ⇒ void
-
#percent ⇒ Integer
@return — #fraction as
0..100, floored —100means done and nothing else does, matching the painted bar exactly. -
#range ⇒ ::Range[untyped]
@return — the scale #value is measured against.
-
#range=(new_range) ⇒ void
Replaces the scale, re-clamping #value into it.
-
#repaint ⇒ void
Paints the bar on the first row of #rect and blanks the rest.
- #resolved_bar_color ⇒ Color?
- #scale(steps) ⇒ Integer
-
#sync_ticker ⇒ void
Brings the ticker in line with "animating and on screen".
Constructor Details
#initialize(range: DEFAULT_RANGE, value: nil, indeterminate: false) ⇒ ProgressBar
@param range — initial #range=.
@param value — initial #value=; nil starts at the range's lower bound.
@param indeterminate — initial #indeterminate=.
62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/tuile/component/progress_bar.rb', line 62 def initialize(range: DEFAULT_RANGE, value: nil, indeterminate: false) super() @value = 0.0 @min = 0.0 @max = 1.0 @phase = 0 @ticker = nil @indeterminate = false @bar_color = nil self.range = range self.value = value unless value.nil? self.indeterminate = indeterminate end |
Instance Attribute Details
#bar_color ⇒ Color, ...
@return — the value as set, so a Theme::Ref comes back
unresolved. Both glyphs paint in it; nil (the default) is the
terminal's default foreground.
85 86 87 |
# File 'lib/tuile/component/progress_bar.rb', line 85 def @bar_color end |
#max ⇒ Float (readonly)
@return — upper bound of #range.
80 81 82 |
# File 'lib/tuile/component/progress_bar.rb', line 80 def max @max end |
#min ⇒ Float (readonly)
@return — lower bound of #range.
77 78 79 |
# File 'lib/tuile/component/progress_bar.rb', line 77 def min @min end |
#value ⇒ Float, Numeric
@return — the progress, clamped into #range when assigned — so
bar.value = 999 on a 0..250 bar reads back as 250.0.
119 120 121 |
# File 'lib/tuile/component/progress_bar.rb', line 119 def value @value end |
Instance Method Details
#block_at(width) ⇒ [Integer, Integer]
Where the sliding block sits this frame: it enters at the left edge and
leaves at the right, one cell per frame, then loops. The period is one
short of width + block so at least one cell is always lit — a full
width + block blanks the bar for exactly one frame per cycle.
@param width — columns available.
@return — start column and length, clipped.
242 243 244 245 246 247 248 |
# File 'lib/tuile/component/progress_bar.rb', line 242 def block_at(width) block = [width / BLOCK_DIVISOR, 1].max start = (@phase % (width + block - 1)) - (block - 1) first = [start, 0].max last = [start + block, width].min [first, last - first] end |
#fraction ⇒ Float
@return — #value as 0.0..1.0. 1.0 when the range is empty.
139 140 141 142 143 |
# File 'lib/tuile/component/progress_bar.rb', line 139 def fraction return 1.0 if @max == @min (@value - @min) / (@max - @min) end |
#glyphs(width) ⇒ String
@param width — columns available.
@return — the row, width glyphs wide.
231 232 233 234 |
# File 'lib/tuile/component/progress_bar.rb', line 231 def glyphs(width) start, length = @indeterminate ? block_at(width) : [0, scale(width)] [("░" * start), ("█" * length), ("░" * (width - start - length))].join end |
#indeterminate=(flag) ⇒ void
This method returns an undefined value.
Switches between the fill and the sliding block. #value keeps working while indeterminate — it is simply not painted — so switching back shows the progress that accumulated meanwhile.
The animation only runs while the bar is attached, and stops on detach. It also keeps the event loop awake at INDETERMINATE_FPS, so turn it off (or remove the bar) when the job ends.
@param flag — coerced; truthiness decides.
185 186 187 188 189 190 191 192 |
# File 'lib/tuile/component/progress_bar.rb', line 185 def indeterminate=(flag) flag = flag ? true : false return if @indeterminate == flag @indeterminate = flag sync_ticker invalidate # the picture changes now, not on the next frame end |
#indeterminate? ⇒ Boolean
@return — whether the sliding-block animation is showing.
173 |
# File 'lib/tuile/component/progress_bar.rb', line 173 def indeterminate? = @indeterminate |
#on_attached ⇒ void
This method returns an undefined value.
195 |
# File 'lib/tuile/component/progress_bar.rb', line 195 def on_attached = sync_ticker |
#on_detached ⇒ void
This method returns an undefined value.
198 |
# File 'lib/tuile/component/progress_bar.rb', line 198 def on_detached = sync_ticker |
#percent ⇒ Integer
@return — #fraction as 0..100, floored — 100 means done and
nothing else does, matching the painted bar exactly.
147 |
# File 'lib/tuile/component/progress_bar.rb', line 147 def percent = scale(100) |
#range ⇒ ::Range[untyped]
@return — the scale #value is measured against.
88 |
# File 'lib/tuile/component/progress_bar.rb', line 88 def range = @min..@max |
#range=(new_range) ⇒ void
This method returns an undefined value.
Replaces the scale, re-clamping #value into it. min == max is legal
and reads as complete — a zero-length job has nothing outstanding — so
bar.range = 0..files.size needs no special case for an empty list.
@param new_range — inclusive; endpoints Numeric and finite.
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/tuile/component/progress_bar.rb', line 100 def range=(new_range) raise ArgumentError, "range must be inclusive, got #{new_range.inspect}" if new_range.exclude_end? min = Float(new_range.begin) max = Float(new_range.end) raise ArgumentError, "range end #{max} is below its start #{min}" if max < min unless min.finite? && max.finite? raise ArgumentError, "range endpoints must be finite (use indeterminate = true)" end @min = min @max = max self.value = @value invalidate # the scale moved even when the clamped value did not end |
#repaint ⇒ void
This method returns an undefined value.
Paints the bar on the first row of Tuile::Component#rect and blanks the rest.
Deliberately not super: Tuile::Component#repaint's default blanks the
whole rect, which dirties every cell of the bar's own row before it is
painted over — so Buffer#flush re-emits the entire row every frame
instead of the one or two cells that actually moved.
207 208 209 210 211 212 |
# File 'lib/tuile/component/progress_bar.rb', line 207 def repaint return if rect.empty? draw_line(rect.left, rect.top, StyledString.styled(glyphs(rect.width), fg: )) clear_background(Rect.new(rect.left, rect.top + 1, rect.width, rect.height - 1)) if rect.height > 1 end |
#resolved_bar_color ⇒ Color?
251 252 253 |
# File 'lib/tuile/component/progress_bar.rb', line 251 def @bar_color.is_a?(Theme::Ref) ? @bar_color.resolve(screen.theme) : @bar_color end |
#scale(steps) ⇒ Integer
221 222 223 224 225 226 227 |
# File 'lib/tuile/component/progress_bar.rb', line 221 def scale(steps) return 0 if fraction <= 0.0 return steps if fraction >= 1.0 return 0 if steps < 2 # no interior to land in; fills only when done (fraction * steps).floor.clamp(1, steps - 1) end |
#sync_ticker ⇒ void
This method returns an undefined value.
Brings the ticker in line with "animating and on screen". The sole writer
of @ticker, and idempotent, so the attach/detach hooks and
#indeterminate= are all the same call and a repeated indeterminate = true cannot start a second one.
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 |
# File 'lib/tuile/component/progress_bar.rb', line 260 def sync_ticker want = attached? && @indeterminate return if want == !@ticker.nil? if want @ticker = screen.event_queue.tick_fps(INDETERMINATE_FPS) do |tick| # The paint that already happened is frame 0; a ticker's first # firing is one interval later, so it is frame 1. @phase = tick + 1 invalidate end else @ticker.cancel @ticker = nil end end |