tuile 0.2.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/README.md +141 -6
- data/examples/sampler.rb +33 -0
- data/lib/tuile/ansi.rb +14 -0
- data/lib/tuile/component/label.rb +64 -26
- data/lib/tuile/component/list.rb +197 -82
- data/lib/tuile/component/log_window.rb +12 -6
- data/lib/tuile/component/popup.rb +5 -5
- data/lib/tuile/component/text_area.rb +40 -137
- data/lib/tuile/component/text_field.rb +31 -151
- data/lib/tuile/component/text_input.rb +213 -0
- data/lib/tuile/component/text_view.rb +456 -0
- data/lib/tuile/component/window.rb +7 -12
- data/lib/tuile/component.rb +15 -3
- data/lib/tuile/keys.rb +91 -8
- data/lib/tuile/mouse_event.rb +23 -4
- data/lib/tuile/screen.rb +154 -12
- data/lib/tuile/styled_string.rb +774 -0
- data/lib/tuile/version.rb +1 -1
- data/sig/tuile.rbs +1026 -174
- metadata +5 -2
- data/lib/tuile/truncate.rb +0 -83
data/lib/tuile/component/list.rb
CHANGED
|
@@ -2,20 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
module Tuile
|
|
4
4
|
class Component
|
|
5
|
-
# A scrollable list of
|
|
5
|
+
# A scrollable list of items with cursor support.
|
|
6
6
|
#
|
|
7
|
-
# Items are
|
|
8
|
-
#
|
|
9
|
-
# {#
|
|
10
|
-
#
|
|
7
|
+
# Items are modeled as {StyledString}s and painted directly into the
|
|
8
|
+
# component's {#rect}. Lines wider than the viewport are ellipsized via
|
|
9
|
+
# {StyledString#ellipsize} (span styles are preserved across the cut —
|
|
10
|
+
# unlike the older ANSI-as-bytes truncation, color does *not* get
|
|
11
|
+
# dropped on the surviving characters). Vertical scrolling is supported
|
|
12
|
+
# via {#top_line}; the list can also automatically scroll to the bottom
|
|
13
|
+
# if {#auto_scroll} is enabled.
|
|
11
14
|
#
|
|
12
15
|
# Cursor is supported; call {#cursor=} to change cursor behavior. The
|
|
13
|
-
# cursor responds to arrows, `jk`, Home/End, Ctrl+U/D and scrolls the
|
|
14
|
-
# automatically.
|
|
16
|
+
# cursor responds to arrows, `jk`, Home/End, Ctrl+U/D and scrolls the
|
|
17
|
+
# list automatically. The cursor highlight overlays a dark background
|
|
18
|
+
# while preserving each span's foreground color.
|
|
15
19
|
class List < Component
|
|
20
|
+
# 256-color SGR index for the cursor-row background highlight. Matches
|
|
21
|
+
# what `Rainbow(...).bg(:darkslategray)` emits.
|
|
22
|
+
# @return [Integer]
|
|
23
|
+
CURSOR_BG = 59
|
|
24
|
+
|
|
16
25
|
def initialize
|
|
17
26
|
super
|
|
18
27
|
@lines = []
|
|
28
|
+
@padded_lines = []
|
|
29
|
+
@blank_padded = StyledString::EMPTY
|
|
19
30
|
@auto_scroll = false
|
|
20
31
|
@top_line = 0
|
|
21
32
|
@cursor = Cursor::None.new
|
|
@@ -28,19 +39,19 @@ module Tuile
|
|
|
28
39
|
|
|
29
40
|
# @return [Proc, nil] callback fired when an item is chosen — by pressing
|
|
30
41
|
# Enter on the cursor's item, or by left-clicking an item. Called as
|
|
31
|
-
# `proc.call(index, line)` with the chosen 0-based index and its
|
|
32
|
-
# Never fires when the cursor's position is
|
|
33
|
-
# {Cursor::None}, or empty content).
|
|
42
|
+
# `proc.call(index, line)` with the chosen 0-based index and its
|
|
43
|
+
# {StyledString} line. Never fires when the cursor's position is
|
|
44
|
+
# outside the content (e.g. {Cursor::None}, or empty content).
|
|
34
45
|
attr_accessor :on_item_chosen
|
|
35
46
|
|
|
36
47
|
# @return [Proc, nil] callback fired when the `(index, line)` tuple under
|
|
37
48
|
# the cursor changes. Called as `proc.call(index, line)` where `line`
|
|
38
|
-
# is `nil` when the cursor is
|
|
39
|
-
# or `index` past the last
|
|
40
|
-
#
|
|
41
|
-
# at the cursor's index
|
|
42
|
-
# flips). Useful for
|
|
43
|
-
# highlighted row.
|
|
49
|
+
# is the {StyledString} at the cursor, or `nil` when the cursor is
|
|
50
|
+
# off-content ({Cursor::None}, empty list, or `index` past the last
|
|
51
|
+
# line). Fires on cursor moves (key, mouse, search), on {#cursor=},
|
|
52
|
+
# and on {#lines=}/{#add_lines} when the line at the cursor's index
|
|
53
|
+
# changes (or its in-range/out-of-range status flips). Useful for
|
|
54
|
+
# keeping a details pane in sync with the highlighted row.
|
|
44
55
|
attr_accessor :on_cursor_changed
|
|
45
56
|
|
|
46
57
|
# @return [Boolean] if true and a line is added or new content is set,
|
|
@@ -77,6 +88,7 @@ module Tuile
|
|
|
77
88
|
return if @scrollbar_visibility == value
|
|
78
89
|
|
|
79
90
|
@scrollbar_visibility = value
|
|
91
|
+
rebuild_padded_lines
|
|
80
92
|
invalidate
|
|
81
93
|
end
|
|
82
94
|
|
|
@@ -109,16 +121,22 @@ module Tuile
|
|
|
109
121
|
invalidate
|
|
110
122
|
end
|
|
111
123
|
|
|
112
|
-
# Sets new lines. Each entry is coerced
|
|
113
|
-
#
|
|
114
|
-
# {
|
|
115
|
-
#
|
|
124
|
+
# Sets new lines. Each entry is coerced into a {StyledString} (a
|
|
125
|
+
# `String` is parsed via {StyledString.parse}, so embedded ANSI is
|
|
126
|
+
# honored; a {StyledString} is used as-is; anything else is stringified
|
|
127
|
+
# via `#to_s` first), then split on `\n` into separate lines via
|
|
128
|
+
# {StyledString#lines}, with trailing empty pieces dropped and trailing
|
|
129
|
+
# ASCII whitespace stripped — symmetric with {#add_lines}, so the
|
|
130
|
+
# stored `@lines` is always `Array<StyledString>`.
|
|
131
|
+
# @param lines [Array] entries are `String`, `StyledString`, or anything
|
|
132
|
+
# that responds to `#to_s`.
|
|
116
133
|
# @return [void]
|
|
117
134
|
def lines=(lines)
|
|
118
135
|
raise TypeError, "expected Array, got #{lines.inspect}" unless lines.is_a? Array
|
|
119
136
|
|
|
120
|
-
@lines = lines
|
|
137
|
+
@lines = parse_input_lines(lines)
|
|
121
138
|
@content_size = nil
|
|
139
|
+
rebuild_padded_lines
|
|
122
140
|
update_top_line_if_auto_scroll
|
|
123
141
|
notify_cursor_changed
|
|
124
142
|
invalidate
|
|
@@ -132,9 +150,11 @@ module Tuile
|
|
|
132
150
|
# end
|
|
133
151
|
# ```
|
|
134
152
|
# @yield [buffer]
|
|
135
|
-
# @yieldparam buffer [Array
|
|
153
|
+
# @yieldparam buffer [Array] mutable buffer to push lines into. Each
|
|
154
|
+
# entry is parsed the same way as the items passed to {#lines=}.
|
|
136
155
|
# @yieldreturn [void]
|
|
137
|
-
# @return [Array<
|
|
156
|
+
# @return [Array<StyledString>] current lines (when called without a
|
|
157
|
+
# block).
|
|
138
158
|
def lines
|
|
139
159
|
return @lines unless block_given?
|
|
140
160
|
|
|
@@ -144,21 +164,25 @@ module Tuile
|
|
|
144
164
|
end
|
|
145
165
|
|
|
146
166
|
# Adds a line.
|
|
147
|
-
# @param line [String]
|
|
167
|
+
# @param line [String, StyledString, #to_s]
|
|
148
168
|
# @return [void]
|
|
149
169
|
def add_line(line)
|
|
170
|
+
raise ArgumentError, "line is nil" if line.nil?
|
|
150
171
|
add_lines [line]
|
|
151
172
|
end
|
|
152
173
|
|
|
153
|
-
# Appends given lines. Each entry is
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
# @param lines [Array] entries
|
|
174
|
+
# Appends given lines. Each entry is parsed the same way as in
|
|
175
|
+
# {#lines=}: coerced to a {StyledString}, split on `\n`, with trailing
|
|
176
|
+
# empty pieces dropped and trailing ASCII whitespace stripped.
|
|
177
|
+
# @param lines [Array] entries are `String`, `StyledString`, or anything
|
|
178
|
+
# that responds to `#to_s`.
|
|
157
179
|
# @return [void]
|
|
158
180
|
def add_lines(lines)
|
|
159
181
|
screen.check_locked
|
|
160
|
-
|
|
182
|
+
new_lines = parse_input_lines(lines)
|
|
183
|
+
@lines += new_lines
|
|
161
184
|
@content_size = nil
|
|
185
|
+
@padded_lines += new_lines.map { |line| pad_to_row(line) }
|
|
162
186
|
update_top_line_if_auto_scroll
|
|
163
187
|
notify_cursor_changed
|
|
164
188
|
invalidate
|
|
@@ -167,8 +191,8 @@ module Tuile
|
|
|
167
191
|
# @return [Size]
|
|
168
192
|
def content_size
|
|
169
193
|
@content_size ||= begin
|
|
170
|
-
|
|
171
|
-
width = @lines.empty? ? 0 :
|
|
194
|
+
content_w = @lines.map(&:display_width).max || 0
|
|
195
|
+
width = @lines.empty? ? 0 : content_w + 2
|
|
172
196
|
Size.new(width, @lines.size)
|
|
173
197
|
end
|
|
174
198
|
end
|
|
@@ -206,6 +230,8 @@ module Tuile
|
|
|
206
230
|
# Moves the cursor to the next line whose text contains `query`
|
|
207
231
|
# (case-insensitive substring match). Search wraps around the end of the
|
|
208
232
|
# list. Only lines reachable by the current {#cursor} are considered.
|
|
233
|
+
# Matching uses the line's plain text — span styles do not affect the
|
|
234
|
+
# match.
|
|
209
235
|
#
|
|
210
236
|
# @param query [String] substring to match. Empty query never matches.
|
|
211
237
|
# @param include_current [Boolean] when true, the current cursor position
|
|
@@ -250,21 +276,19 @@ module Tuile
|
|
|
250
276
|
# Paints the list items into {#rect}.
|
|
251
277
|
#
|
|
252
278
|
# Skips the {Component#repaint} default's auto-clear: every row of
|
|
253
|
-
# {#rect} is painted below (with
|
|
279
|
+
# {#rect} is painted below (with blank padding past the last item),
|
|
254
280
|
# so the parent contract — "fully draw over your rect" — is met
|
|
255
281
|
# without an upfront wipe.
|
|
256
282
|
# @return [void]
|
|
257
283
|
def repaint
|
|
258
284
|
return if rect.empty?
|
|
259
285
|
|
|
260
|
-
width = rect.width
|
|
261
286
|
scrollbar = if scrollbar_visible?
|
|
262
287
|
VerticalScrollBar.new(rect.height, line_count: @lines.size, top_line: @top_line)
|
|
263
288
|
end
|
|
264
|
-
(0
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
screen.print TTY::Cursor.move_to(rect.left, line_no + rect.top), line
|
|
289
|
+
(0...rect.height).each do |row|
|
|
290
|
+
line = paintable_line(row + @top_line, row, scrollbar)
|
|
291
|
+
screen.print TTY::Cursor.move_to(rect.left, row + rect.top), line
|
|
268
292
|
end
|
|
269
293
|
end
|
|
270
294
|
|
|
@@ -303,6 +327,16 @@ module Tuile
|
|
|
303
327
|
def candidate_positions(_line_count)
|
|
304
328
|
[]
|
|
305
329
|
end
|
|
330
|
+
|
|
331
|
+
# Overridden so all movement funnels — base {Cursor#go_to_last},
|
|
332
|
+
# {Cursor#go_to_first}, etc., which all call {#go} — become safe
|
|
333
|
+
# no-ops on a disabled cursor. The instance is frozen, so a default
|
|
334
|
+
# mutating {#go} would raise.
|
|
335
|
+
# @param _new_position [Integer]
|
|
336
|
+
# @return [Boolean] always false.
|
|
337
|
+
def go(_new_position)
|
|
338
|
+
false
|
|
339
|
+
end
|
|
306
340
|
end
|
|
307
341
|
|
|
308
342
|
# @return [Integer] 0-based line index of the current cursor position.
|
|
@@ -361,6 +395,15 @@ module Tuile
|
|
|
361
395
|
true
|
|
362
396
|
end
|
|
363
397
|
|
|
398
|
+
# Moves the cursor to the last reachable position. For base {Cursor},
|
|
399
|
+
# the last line; {Limited} clamps to the last allowed position; {None}
|
|
400
|
+
# is a no-op.
|
|
401
|
+
# @param line_count [Integer] number of lines in the list.
|
|
402
|
+
# @return [Boolean] true if the position changed.
|
|
403
|
+
def go_to_last(line_count)
|
|
404
|
+
go(line_count - 1)
|
|
405
|
+
end
|
|
406
|
+
|
|
364
407
|
protected
|
|
365
408
|
|
|
366
409
|
# @param lines [Integer]
|
|
@@ -381,12 +424,6 @@ module Tuile
|
|
|
381
424
|
go(0)
|
|
382
425
|
end
|
|
383
426
|
|
|
384
|
-
# @param line_count [Integer]
|
|
385
|
-
# @return [Boolean]
|
|
386
|
-
def go_to_last(line_count)
|
|
387
|
-
go(line_count - 1)
|
|
388
|
-
end
|
|
389
|
-
|
|
390
427
|
# Cursor which can only land on specific allowed lines.
|
|
391
428
|
class Limited < Cursor
|
|
392
429
|
# @param positions [Array<Integer>] allowed positions. Must not be
|
|
@@ -419,6 +456,12 @@ module Tuile
|
|
|
419
456
|
@positions.select { it < line_count }
|
|
420
457
|
end
|
|
421
458
|
|
|
459
|
+
# @param _line_count [Integer]
|
|
460
|
+
# @return [Boolean]
|
|
461
|
+
def go_to_last(_line_count)
|
|
462
|
+
go(@positions.last)
|
|
463
|
+
end
|
|
464
|
+
|
|
422
465
|
protected
|
|
423
466
|
|
|
424
467
|
# @param lines [Integer]
|
|
@@ -444,17 +487,64 @@ module Tuile
|
|
|
444
487
|
def go_to_first
|
|
445
488
|
go(@positions.first)
|
|
446
489
|
end
|
|
447
|
-
|
|
448
|
-
# @param _line_count [Integer]
|
|
449
|
-
# @return [Boolean]
|
|
450
|
-
def go_to_last(_line_count)
|
|
451
|
-
go(@positions.last)
|
|
452
|
-
end
|
|
453
490
|
end
|
|
454
491
|
end
|
|
455
492
|
|
|
493
|
+
protected
|
|
494
|
+
|
|
495
|
+
# Rebuilds pre-padded lines when the wrap width changes. The wrap width
|
|
496
|
+
# depends on {#rect}`.width` and the scrollbar gutter, both of which
|
|
497
|
+
# trigger this hook. Also re-evaluates {#auto_scroll}: if items were
|
|
498
|
+
# appended while the rect was empty (e.g. a {Popup}-wrapped list got
|
|
499
|
+
# `add_line` calls before the popup was opened), the auto-scroll update
|
|
500
|
+
# was skipped because there was no viewport — re-run it now that there
|
|
501
|
+
# is one, so the list snaps to the bottom on first paint.
|
|
502
|
+
# @return [void]
|
|
503
|
+
def on_width_changed
|
|
504
|
+
super
|
|
505
|
+
rebuild_padded_lines
|
|
506
|
+
update_top_line_if_auto_scroll
|
|
507
|
+
end
|
|
508
|
+
|
|
456
509
|
private
|
|
457
510
|
|
|
511
|
+
# Coerces and flattens a list of input entries into trimmed
|
|
512
|
+
# {StyledString} lines. Each entry becomes a {StyledString} (String
|
|
513
|
+
# via {StyledString.parse}, StyledString passed through, anything else
|
|
514
|
+
# via `#to_s`), then split on `\n` via {StyledString#lines} — with
|
|
515
|
+
# trailing empty pieces dropped (matching `String#split("\n")`'s
|
|
516
|
+
# default behavior, so `add_line ""` is a no-op) — and trailing ASCII
|
|
517
|
+
# whitespace stripped on each resulting line.
|
|
518
|
+
# @param entries [Array]
|
|
519
|
+
# @return [Array<StyledString>]
|
|
520
|
+
def parse_input_lines(entries)
|
|
521
|
+
entries.flat_map { |entry| split_to_lines(entry) }
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# @param entry [Object]
|
|
525
|
+
# @return [Array<StyledString>]
|
|
526
|
+
def split_to_lines(entry)
|
|
527
|
+
styled = entry.is_a?(StyledString) ? entry : StyledString.parse(entry.to_s)
|
|
528
|
+
parts = styled.lines
|
|
529
|
+
parts.pop while parts.last && parts.last.empty?
|
|
530
|
+
parts.map { |line| rstrip_styled(line) }
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Returns `line` with trailing ASCII whitespace (space/tab) dropped,
|
|
534
|
+
# preserving span styles on the surviving prefix. Whitespace chars are
|
|
535
|
+
# all single-column ASCII, so byte-count delta equals column-count
|
|
536
|
+
# delta and {StyledString#slice} can do the cut.
|
|
537
|
+
# @param line [StyledString]
|
|
538
|
+
# @return [StyledString]
|
|
539
|
+
def rstrip_styled(line)
|
|
540
|
+
plain = line.to_s
|
|
541
|
+
trailing = plain.length - plain.rstrip.length
|
|
542
|
+
return line if trailing.zero?
|
|
543
|
+
return StyledString::EMPTY if trailing == plain.length
|
|
544
|
+
|
|
545
|
+
line.slice(0, line.display_width - trailing)
|
|
546
|
+
end
|
|
547
|
+
|
|
458
548
|
# @return [Boolean] true if the cursor sits on a real content line.
|
|
459
549
|
def cursor_on_item?
|
|
460
550
|
pos = @cursor.position
|
|
@@ -469,8 +559,9 @@ module Tuile
|
|
|
469
559
|
@on_item_chosen&.call(pos, @lines[pos])
|
|
470
560
|
end
|
|
471
561
|
|
|
472
|
-
# @return [Array((Integer,
|
|
473
|
-
# with `line` nil when the cursor is
|
|
562
|
+
# @return [Array((Integer, StyledString, nil))]
|
|
563
|
+
# `[position, line_at_position]`, with `line` nil when the cursor is
|
|
564
|
+
# off-content.
|
|
474
565
|
def cursor_state
|
|
475
566
|
pos = @cursor.position
|
|
476
567
|
line = pos >= 0 && pos < @lines.size ? @lines[pos] : nil
|
|
@@ -500,7 +591,7 @@ module Tuile
|
|
|
500
591
|
|
|
501
592
|
ordered = order_for_search(candidates, @cursor.position, include_current: include_current, reverse: reverse)
|
|
502
593
|
query_lc = query.downcase
|
|
503
|
-
match = ordered.find { |idx|
|
|
594
|
+
match = ordered.find { |idx| @lines[idx].to_s.downcase.include?(query_lc) }
|
|
504
595
|
return false unless match
|
|
505
596
|
|
|
506
597
|
@cursor.go(match)
|
|
@@ -566,10 +657,20 @@ module Tuile
|
|
|
566
657
|
invalidate
|
|
567
658
|
end
|
|
568
659
|
|
|
569
|
-
# If auto-scrolling, recalculate the top line
|
|
660
|
+
# If auto-scrolling, recalculate the top line and snap the cursor to the
|
|
661
|
+
# last reachable position. Without the cursor snap the viewport gets
|
|
662
|
+
# yanked back to wherever the cursor sat on the next arrow press,
|
|
663
|
+
# negating the auto-scroll. Skipped when {#rect} is empty: without a
|
|
664
|
+
# viewport the "lines minus viewport" formula yields `@lines.size`,
|
|
665
|
+
# which would leave `top_line` past the last item once a real rect
|
|
666
|
+
# arrives. {#on_width_changed} re-runs this hook when the rect grows so
|
|
667
|
+
# the snap-to-bottom intent is preserved.
|
|
570
668
|
# @return [void]
|
|
571
669
|
def update_top_line_if_auto_scroll
|
|
572
670
|
return unless @auto_scroll
|
|
671
|
+
return if rect.empty?
|
|
672
|
+
|
|
673
|
+
notify_cursor_changed if @cursor.go_to_last(@lines.size)
|
|
573
674
|
|
|
574
675
|
new_top_line = (@lines.size - viewport_lines).clamp(0, nil)
|
|
575
676
|
return unless @top_line != new_top_line
|
|
@@ -584,42 +685,56 @@ module Tuile
|
|
|
584
685
|
@scrollbar_visibility == :visible
|
|
585
686
|
end
|
|
586
687
|
|
|
587
|
-
#
|
|
588
|
-
#
|
|
589
|
-
#
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
688
|
+
# @return [Integer] column width available for line content (rect width
|
|
689
|
+
# minus the scrollbar gutter, when visible). `0` when {#rect}'s width
|
|
690
|
+
# is non-positive.
|
|
691
|
+
def content_width
|
|
692
|
+
return 0 if rect.width <= 0
|
|
693
|
+
|
|
694
|
+
rect.width - (scrollbar_visible? ? 1 : 0)
|
|
695
|
+
end
|
|
593
696
|
|
|
594
|
-
|
|
595
|
-
|
|
697
|
+
# Recomputes {@padded_lines} for the current rect width and scrollbar
|
|
698
|
+
# visibility. Each line is ellipsized to fit and pre-padded with
|
|
699
|
+
# single-space gutters on each side, so {#paintable_line} only has to
|
|
700
|
+
# apply the cursor highlight (if any) and append the scrollbar glyph.
|
|
701
|
+
# @return [void]
|
|
702
|
+
def rebuild_padded_lines
|
|
703
|
+
@padded_lines = @lines.map { |line| pad_to_row(line) }
|
|
704
|
+
@blank_padded = pad_to_row(StyledString::EMPTY)
|
|
705
|
+
end
|
|
596
706
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
707
|
+
# Pads `line` to one full row of the viewport (scrollbar gutter
|
|
708
|
+
# excluded). Lines wider than the content area are ellipsized via
|
|
709
|
+
# {StyledString#ellipsize} (span styles survive the cut); shorter
|
|
710
|
+
# lines are padded with default-styled spaces.
|
|
711
|
+
# @param line [StyledString]
|
|
712
|
+
# @return [StyledString] exactly {#content_width} display columns wide
|
|
713
|
+
# (or {StyledString::EMPTY} when content_width is non-positive).
|
|
714
|
+
def pad_to_row(line)
|
|
715
|
+
cw = content_width
|
|
716
|
+
return StyledString::EMPTY if cw <= 0
|
|
717
|
+
return StyledString.plain(" " * cw) if cw < 2
|
|
718
|
+
|
|
719
|
+
text_width = cw - 2
|
|
720
|
+
body = line.ellipsize(text_width)
|
|
721
|
+
fill = cw - 2 - body.display_width
|
|
722
|
+
StyledString.plain(" ") + body + StyledString.plain(" " * (fill + 1))
|
|
600
723
|
end
|
|
601
724
|
|
|
602
725
|
# @param index [Integer] 0-based index into {#lines}.
|
|
603
726
|
# @param row_in_viewport [Integer] 0-based row within the viewport.
|
|
604
|
-
# @param
|
|
605
|
-
#
|
|
606
|
-
#
|
|
607
|
-
#
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
content_width = scrollbar ? width - 1 : width
|
|
611
|
-
line = @lines[index] || ""
|
|
612
|
-
line = trim_to(line, content_width - 2)
|
|
613
|
-
line = " #{line} "
|
|
727
|
+
# @param scrollbar [VerticalScrollBar, nil] scrollbar instance, or nil
|
|
728
|
+
# if not shown.
|
|
729
|
+
# @return [String] paintable ANSI-encoded line exactly `rect.width`
|
|
730
|
+
# columns wide; highlighted if cursor is here.
|
|
731
|
+
def paintable_line(index, row_in_viewport, scrollbar)
|
|
732
|
+
base = index < @lines.size ? @padded_lines[index] : @blank_padded
|
|
614
733
|
is_cursor = (active? || @show_cursor_when_inactive) && index < @lines.size && @cursor.position == index
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
end
|
|
620
|
-
return line unless scrollbar
|
|
621
|
-
|
|
622
|
-
line + scrollbar.scrollbar_char(row_in_viewport)
|
|
734
|
+
styled = is_cursor ? base.with_bg(CURSOR_BG) : base
|
|
735
|
+
out = styled.to_ansi
|
|
736
|
+
out += scrollbar.scrollbar_char(row_in_viewport) if scrollbar
|
|
737
|
+
out
|
|
623
738
|
end
|
|
624
739
|
end
|
|
625
740
|
end
|
|
@@ -23,6 +23,16 @@ module Tuile
|
|
|
23
23
|
self.scrollbar = true
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Appends given line to the log. Can be called from any thread. Does nothing if nil is passed in.
|
|
27
|
+
# @param string [String, nil] the line (or multiple lines) to log.
|
|
28
|
+
# @return [void]
|
|
29
|
+
def log(string)
|
|
30
|
+
return if string.nil?
|
|
31
|
+
screen.event_queue.submit do
|
|
32
|
+
content.add_line(string)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
26
36
|
# IO-shaped adapter that forwards each log line to the owning {LogWindow}.
|
|
27
37
|
# Implements both {#write} (stdlib `Logger`) and {#puts} (loggers that
|
|
28
38
|
# call `output.puts`, e.g. `TTY::Logger`).
|
|
@@ -35,17 +45,13 @@ module Tuile
|
|
|
35
45
|
# @param string [String]
|
|
36
46
|
# @return [void]
|
|
37
47
|
def write(string)
|
|
38
|
-
@window.
|
|
39
|
-
@window.content.add_line(string.chomp)
|
|
40
|
-
end
|
|
48
|
+
@window.log(string.chomp)
|
|
41
49
|
end
|
|
42
50
|
|
|
43
51
|
# @param string [String]
|
|
44
52
|
# @return [void]
|
|
45
53
|
def puts(string)
|
|
46
|
-
@window.
|
|
47
|
-
@window.content.add_line(string)
|
|
48
|
-
end
|
|
54
|
+
@window.log(string)
|
|
49
55
|
end
|
|
50
56
|
|
|
51
57
|
# Stdlib `Logger` only treats an object as an IO target when it
|
|
@@ -30,17 +30,17 @@ module Tuile
|
|
|
30
30
|
def initialize(content: nil)
|
|
31
31
|
super()
|
|
32
32
|
@content = nil
|
|
33
|
-
# Off-screen sentinel until the content sets a real size and the popup
|
|
34
|
-
# is centered on open.
|
|
35
|
-
@rect = Rect.new(-1, -1, 0, 0)
|
|
36
33
|
self.content = content unless content.nil?
|
|
37
34
|
end
|
|
38
35
|
|
|
39
36
|
def focusable? = true
|
|
40
37
|
|
|
41
|
-
# Mounts this popup on the {Screen}.
|
|
38
|
+
# Mounts this popup on the {Screen}. Recomputes the popup's size from
|
|
39
|
+
# the current content first, so reopening a popup whose content has
|
|
40
|
+
# grown or shrunk while closed picks up the new size.
|
|
42
41
|
# @return [void]
|
|
43
42
|
def open
|
|
43
|
+
update_rect unless @content.nil?
|
|
44
44
|
screen.add_popup(self)
|
|
45
45
|
end
|
|
46
46
|
|
|
@@ -119,7 +119,7 @@ module Tuile
|
|
|
119
119
|
def update_rect
|
|
120
120
|
size = @content.content_size.clamp_height(max_height)
|
|
121
121
|
size = size.clamp(Size.new(screen.size.width * 4 / 5, screen.size.height * 4 / 5))
|
|
122
|
-
self.rect = Rect.new(
|
|
122
|
+
self.rect = Rect.new(0, 0, size.width, size.height)
|
|
123
123
|
center if open?
|
|
124
124
|
end
|
|
125
125
|
end
|