tuile 0.9.0 → 0.10.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/DECISIONS.md +1961 -0
- data/README.md +31 -24
- data/book/04-event-loop.md +86 -0
- data/book/05-focus.md +82 -51
- data/book/06-theming.md +53 -1
- data/book/07-components.md +363 -9
- data/book/08-testing.md +21 -8
- data/book/09-styled-text.md +132 -0
- data/book/README.md +19 -13
- data/examples/sampler.rb +435 -20
- data/ideas/new-components.md +109 -0
- data/ideas/per-component-buffers.md +55 -0
- data/lib/tuile/buffer.rb +52 -51
- data/lib/tuile/color.rb +4 -10
- data/lib/tuile/component/{text_input.rb → abstract_string_field.rb} +113 -20
- data/lib/tuile/component/button.rb +25 -21
- data/lib/tuile/component/checkbox.rb +133 -0
- data/lib/tuile/component/checkbox_group.rb +188 -0
- data/lib/tuile/component/combo_box.rb +281 -0
- data/lib/tuile/component/has_caption.rb +44 -0
- data/lib/tuile/component/has_content.rb +5 -5
- data/lib/tuile/component/has_value.rb +64 -0
- data/lib/tuile/component/integer_field.rb +135 -0
- data/lib/tuile/component/label.rb +13 -10
- data/lib/tuile/component/layout.rb +3 -17
- data/lib/tuile/component/list.rb +8 -7
- data/lib/tuile/component/list_dropdown.rb +106 -0
- data/lib/tuile/component/password_field.rb +105 -0
- data/lib/tuile/component/popup.rb +10 -14
- data/lib/tuile/component/progress_bar.rb +278 -0
- data/lib/tuile/component/radio_group.rb +188 -0
- data/lib/tuile/component/text_area.rb +189 -65
- data/lib/tuile/component/text_field.rb +170 -32
- data/lib/tuile/component/text_view.rb +57 -114
- data/lib/tuile/component/window.rb +32 -47
- data/lib/tuile/component.rb +250 -87
- data/lib/tuile/event_queue.rb +14 -17
- data/lib/tuile/fake_event_queue.rb +11 -2
- data/lib/tuile/fake_screen.rb +4 -5
- data/lib/tuile/fraction.rb +6 -10
- data/lib/tuile/screen.rb +202 -104
- data/lib/tuile/screen_pane.rb +51 -41
- data/lib/tuile/styled_string.rb +112 -83
- data/lib/tuile/theme.rb +78 -41
- data/lib/tuile/version.rb +1 -1
- data/sig/tuile.rbs +2043 -614
- metadata +18 -7
|
@@ -3,31 +3,25 @@
|
|
|
3
3
|
module Tuile
|
|
4
4
|
class Component
|
|
5
5
|
# A read-only viewer for prose: chunks of formatted text that scroll
|
|
6
|
-
# vertically. Shape-wise a hybrid between {Label} (string
|
|
7
|
-
#
|
|
6
|
+
# vertically. Shape-wise a hybrid between {Label} (string content via
|
|
7
|
+
# {#text=}) and {List} (scroll keys, optional scrollbar, auto-scroll).
|
|
8
8
|
#
|
|
9
|
-
# Text is
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# ANSI
|
|
13
|
-
#
|
|
14
|
-
# so embedded ANSI is honored) or a {StyledString} directly; {#text}
|
|
15
|
-
# always returns the {StyledString}.
|
|
9
|
+
# Text is a {StyledString}: embedded `\n` are hard line breaks, longer lines
|
|
10
|
+
# are word-wrapped via {StyledString#wrap} with style spans preserved across
|
|
11
|
+
# wrap boundaries. {#text=} takes a {String} (parsed via {StyledString.parse},
|
|
12
|
+
# honoring embedded ANSI) or a {StyledString}; {#text} always returns the
|
|
13
|
+
# {StyledString}.
|
|
16
14
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# {#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# caller streaming reformattable content (e.g. partially-rendered
|
|
25
|
-
# Markdown that may need to retract its last paragraph) can replace
|
|
26
|
-
# the tail without rewriting the whole text. Turn on {#auto_scroll}
|
|
27
|
-
# to keep the latest content in view.
|
|
15
|
+
# Pick the right incremental primitive: {#append} (aliased `<<`) concatenates
|
|
16
|
+
# a chunk verbatim onto the buffer (stream-friendly, `\n` → hard breaks);
|
|
17
|
+
# {#add_line} starts the chunk on a fresh line (the "log entry" convenience);
|
|
18
|
+
# {#remove_last_n_lines} pops hard lines off the tail, so a caller streaming
|
|
19
|
+
# reformattable content can retract and rewrite it; {#replace} / {#insert}
|
|
20
|
+
# splice a range in place. Turn on {#auto_scroll} to keep the latest content
|
|
21
|
+
# in view.
|
|
28
22
|
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
23
|
+
# Meant to be the content of a {Window} — focus indication and keyboard-hint
|
|
24
|
+
# surfacing rely on the surrounding window chrome.
|
|
31
25
|
class TextView < Component
|
|
32
26
|
def initialize
|
|
33
27
|
super
|
|
@@ -55,21 +49,14 @@ module Tuile
|
|
|
55
49
|
@auto_scroll = false
|
|
56
50
|
@follow = true
|
|
57
51
|
@scrollbar_visibility = :gone
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
# code that never calls {#create_region} sees the same behavior as
|
|
61
|
-
# before (a single region containing everything); apps that do call
|
|
62
|
-
# {#create_region} stack additional regions at the spatial tail.
|
|
52
|
+
# Always ≥1 region; the implicit default owns any hard lines no
|
|
53
|
+
# app-created region claims. See {Region}.
|
|
63
54
|
@regions = [Region.send(:new, self)]
|
|
64
55
|
end
|
|
65
56
|
|
|
66
|
-
# @return [StyledString] the current text
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
# whole buffer; the joined {StyledString} returned here is
|
|
70
|
-
# reconstructed on first read after a mutation and cached, so
|
|
71
|
-
# repeated reads are O(1) but the first read after {#append} pays
|
|
72
|
-
# O(total spans).
|
|
57
|
+
# @return [StyledString] the current text (empty by default). Rebuilt
|
|
58
|
+
# lazily on the first read after a mutation (O(total spans)), then
|
|
59
|
+
# cached — repeated reads are O(1).
|
|
73
60
|
def text
|
|
74
61
|
@text ||= build_text
|
|
75
62
|
end
|
|
@@ -144,19 +131,12 @@ module Tuile
|
|
|
144
131
|
# @return [Boolean] true iff {#text} is empty (no hard lines).
|
|
145
132
|
def empty? = @hard_lines.empty?
|
|
146
133
|
|
|
147
|
-
# Appends `str` verbatim. Embedded `\n`
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
# For the "add an entry on a new line" pattern use {#add_line}.
|
|
154
|
-
#
|
|
155
|
-
# Cost is O(appended + width-of-current-last-hard-line) — the
|
|
156
|
-
# previously last hard line is re-wrapped (because the extension may
|
|
157
|
-
# cause it to wrap differently), any additional hard lines created by
|
|
158
|
-
# embedded `\n` are wrapped fresh. The cached {#text} is invalidated
|
|
159
|
-
# and rebuilt on demand.
|
|
134
|
+
# Appends `str` verbatim. Embedded `\n` become hard line breaks; otherwise
|
|
135
|
+
# the text is concatenated onto the current last hard line. Designed for
|
|
136
|
+
# streaming use (feed each partial chunk straight in). Accepts the same
|
|
137
|
+
# input forms as {#text=}; empty/`nil` is a no-op. For the "entry on a new
|
|
138
|
+
# line" pattern use {#add_line}. Cost is O(appended + width of the last
|
|
139
|
+
# hard line), which is re-wrapped since the extension may wrap differently.
|
|
160
140
|
# @param str [String, StyledString, nil]
|
|
161
141
|
# @return [void]
|
|
162
142
|
def append(str)
|
|
@@ -221,19 +201,11 @@ module Tuile
|
|
|
221
201
|
end
|
|
222
202
|
end
|
|
223
203
|
|
|
224
|
-
# Drops the last `n` hard lines from the buffer
|
|
225
|
-
#
|
|
226
|
-
#
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
# by `append(new_tail)` to replace the damaged region in place.
|
|
230
|
-
#
|
|
231
|
-
# `n == 0` and the empty-buffer case are no-ops (no invalidation).
|
|
232
|
-
# `n >= hard-line count` empties the buffer.
|
|
233
|
-
#
|
|
234
|
-
# Operates on **hard lines** (the `\n`-delimited entries the
|
|
235
|
-
# buffer stores), not on wrapped physical rows — same granularity
|
|
236
|
-
# as {#add_line}. Cost is O(rendered-rows of the popped lines).
|
|
204
|
+
# Drops the last `n` hard lines from the buffer — the inverse of building
|
|
205
|
+
# up a tail with {#append} / {#add_line}, so a caller can `remove` then
|
|
206
|
+
# `append` to rewrite a damaged tail in place. Operates on **hard lines**
|
|
207
|
+
# (the `\n`-delimited entries), not wrapped physical rows. `n == 0` and the
|
|
208
|
+
# empty buffer are no-ops; `n >= hard-line count` empties the buffer.
|
|
237
209
|
# @param n [Integer] number of hard lines to drop; must be >= 0.
|
|
238
210
|
# @raise [TypeError] if `n` isn't an `Integer`.
|
|
239
211
|
# @raise [ArgumentError] if `n` is negative.
|
|
@@ -266,40 +238,21 @@ module Tuile
|
|
|
266
238
|
invalidate
|
|
267
239
|
end
|
|
268
240
|
|
|
269
|
-
# Replaces a contiguous range of hard lines with the parsed content
|
|
270
|
-
#
|
|
271
|
-
#
|
|
272
|
-
#
|
|
273
|
-
#
|
|
274
|
-
# `"\n"` in the replacement produces multiple hard lines, so a single
|
|
275
|
-
# `replace` can grow or shrink the buffer.
|
|
241
|
+
# Replaces a contiguous range of hard lines with the parsed content of
|
|
242
|
+
# `str` (parsed like {#text=}: `String` → {StyledString.parse}, `nil` →
|
|
243
|
+
# empty, so `nil` deletes the range). Embedded `"\n"` yields multiple hard
|
|
244
|
+
# lines, so one `replace` can grow or shrink the buffer. `range` selects
|
|
245
|
+
# which hard lines to swap out:
|
|
276
246
|
#
|
|
277
|
-
# `
|
|
278
|
-
#
|
|
279
|
-
# - an
|
|
280
|
-
#
|
|
281
|
-
# - a non-empty `Range` of hard-line indices replaces those lines;
|
|
282
|
-
# - an empty `Range` (e.g. `2...2`, or the canonical end-insertion
|
|
283
|
-
# `hard_lines.size...hard_lines.size`) is *insertion* at that
|
|
284
|
-
# position — no lines are removed. {#insert} is a thin alias for
|
|
285
|
-
# this case.
|
|
286
|
-
#
|
|
287
|
-
# Endpoints must be non-negative integers; `begin` may equal
|
|
288
|
-
# `hard-line count` (insertion at the end), `end` may not exceed
|
|
289
|
-
# `hard-line count - 1`. `nil` endpoints (beginless / endless ranges)
|
|
290
|
-
# are not accepted.
|
|
291
|
-
#
|
|
292
|
-
# Cost is roughly `O(from + length + new content)`: the splice
|
|
293
|
-
# updates only the affected slice of the physical-row buffer, using
|
|
294
|
-
# the per-hard-line wrap-count cache to locate the starting offset
|
|
295
|
-
# without re-wrapping preceding lines. Lines outside the splice are
|
|
296
|
-
# never re-wrapped. {#top_line} is clamped if the new line count
|
|
297
|
-
# puts it past the end; {#auto_scroll} pins it to the bottom as
|
|
298
|
-
# usual. The call is a no-op (no invalidation) when the parsed
|
|
299
|
-
# replacement equals the covered range (vacuously true for an empty
|
|
300
|
-
# range plus empty replacement, so `replace(n...n, "")` is a cheap
|
|
301
|
-
# no-op).
|
|
247
|
+
# - an `Integer` `n` is shorthand for `n..n` (replace one existing line);
|
|
248
|
+
# - a non-empty `Range` replaces those lines;
|
|
249
|
+
# - an empty `Range` (e.g. `2...2`, or `size...size` at the end) is
|
|
250
|
+
# *insertion* at that position — nothing removed. {#insert} aliases this.
|
|
302
251
|
#
|
|
252
|
+
# Splices in place — only the affected slice of the physical-row buffer is
|
|
253
|
+
# touched, no preceding lines re-wrapped (cost O(from + length + new
|
|
254
|
+
# content)). A no-op when the replacement equals the covered range, so
|
|
255
|
+
# `replace(n...n, "")` is cheap.
|
|
303
256
|
# @param range [Range, Integer] hard-line indices to replace.
|
|
304
257
|
# @param str [String, StyledString, nil] replacement content.
|
|
305
258
|
# @raise [TypeError] if `range` is neither an `Integer` nor a `Range`,
|
|
@@ -417,6 +370,8 @@ module Tuile
|
|
|
417
370
|
# Skips the {Component#repaint} default's auto-clear: every row is
|
|
418
371
|
# painted explicitly (with padded blanks past the last line), so the
|
|
419
372
|
# "fully draw over your rect" contract is met without an upfront wipe.
|
|
373
|
+
# Rows go through {Component#draw_line}, so content and blank rows inherit
|
|
374
|
+
# {Component#effective_bg_color} (a {#bg_color} set here or on an ancestor).
|
|
420
375
|
# @return [void]
|
|
421
376
|
def repaint
|
|
422
377
|
return if rect.empty?
|
|
@@ -426,7 +381,7 @@ module Tuile
|
|
|
426
381
|
end
|
|
427
382
|
(0...rect.height).each do |row|
|
|
428
383
|
line = paintable_line(row + @top_line, row, scrollbar)
|
|
429
|
-
|
|
384
|
+
draw_line(rect.left, rect.top + row, line)
|
|
430
385
|
end
|
|
431
386
|
end
|
|
432
387
|
|
|
@@ -645,19 +600,12 @@ module Tuile
|
|
|
645
600
|
invalidate
|
|
646
601
|
end
|
|
647
602
|
|
|
648
|
-
# Adjusts region line counts after a {@hard_lines} splice that
|
|
649
|
-
#
|
|
650
|
-
#
|
|
651
|
-
#
|
|
652
|
-
#
|
|
653
|
-
#
|
|
654
|
-
# region that lost lines — that's the natural home for the
|
|
655
|
-
# replacement content.
|
|
656
|
-
# 2. Credit `added_count` to that region. For pure insertions (no
|
|
657
|
-
# removal), there's no "first overlapping region" to pick from;
|
|
658
|
-
# walk regions and credit the latest one starting at `from` (the
|
|
659
|
-
# boundary tiebreaker matches the spatial-tail-routing of
|
|
660
|
-
# {#append}). Past-the-end inserts fall back to the tail region.
|
|
603
|
+
# Adjusts region line counts after a {@hard_lines} splice that removed
|
|
604
|
+
# `removed_count` lines at `from` and inserted `added_count`. Subtracts
|
|
605
|
+
# each region's overlap with the removed range, then credits the added
|
|
606
|
+
# lines to the first region that lost lines. Pure insertions have no such
|
|
607
|
+
# region — they credit the latest region starting at `from`, matching
|
|
608
|
+
# {#append}'s spatial-tail routing (past-the-end falls back to the tail).
|
|
661
609
|
# @param from [Integer]
|
|
662
610
|
# @param removed_count [Integer]
|
|
663
611
|
# @param added_count [Integer]
|
|
@@ -956,14 +904,9 @@ module Tuile
|
|
|
956
904
|
end
|
|
957
905
|
|
|
958
906
|
# Verbatim append into this region's tail. Same semantics as
|
|
959
|
-
# {TextView#append} but scoped
|
|
960
|
-
#
|
|
961
|
-
#
|
|
962
|
-
# is a no-op (but still raises when detached). When the region is
|
|
963
|
-
# the spatial tail of the view, this uses the incremental
|
|
964
|
-
# {TextView#append} path; mid-document regions splice the affected
|
|
965
|
-
# slice of the physical-row buffer (lines outside the region are
|
|
966
|
-
# not re-wrapped).
|
|
907
|
+
# {TextView#append} but scoped: embedded `"\n"` creates new hard lines
|
|
908
|
+
# within the region, other input extends the region's last hard line.
|
|
909
|
+
# Empty / `nil` is a no-op (but still raises when detached).
|
|
967
910
|
# @param str [String, StyledString, nil]
|
|
968
911
|
# @raise [RuntimeError] when the region is detached.
|
|
969
912
|
# @return [void]
|
|
@@ -13,12 +13,14 @@ module Tuile
|
|
|
13
13
|
# by {Component#invalidate}; subclasses don't need to re-check.)
|
|
14
14
|
class Window < Component
|
|
15
15
|
include Component::HasContent
|
|
16
|
+
include Component::HasCaption
|
|
16
17
|
|
|
17
|
-
# @param caption [String]
|
|
18
|
-
|
|
18
|
+
# @param caption [String, StyledString, nil] the border title, coerced
|
|
19
|
+
# the same way {HasCaption#caption=} coerces it.
|
|
20
|
+
def initialize(caption = nil)
|
|
19
21
|
super()
|
|
20
22
|
@border_right = 1
|
|
21
|
-
|
|
23
|
+
self.caption = caption
|
|
22
24
|
@content = nil
|
|
23
25
|
# Optional bottom-row widget slot (e.g. a search field), spanning the
|
|
24
26
|
# full inner width; and optional bottom-border chrome text embedded in
|
|
@@ -75,10 +77,12 @@ module Tuile
|
|
|
75
77
|
end
|
|
76
78
|
|
|
77
79
|
old = @footer
|
|
78
|
-
|
|
80
|
+
# Same slot-swap order as HasContent#content=: notified last, so the
|
|
81
|
+
# focus repair cascades into the new occupant rather than the old.
|
|
82
|
+
detach_child(old) unless old.nil?
|
|
79
83
|
@footer = new_footer
|
|
80
84
|
unless new_footer.nil?
|
|
81
|
-
new_footer
|
|
85
|
+
add_child(new_footer) # appended: the footer paints over the border row
|
|
82
86
|
new_footer.invalidate
|
|
83
87
|
layout_footer
|
|
84
88
|
end
|
|
@@ -86,11 +90,6 @@ module Tuile
|
|
|
86
90
|
on_child_removed(old) unless old.nil?
|
|
87
91
|
end
|
|
88
92
|
|
|
89
|
-
# @return [Array<Component>]
|
|
90
|
-
def children
|
|
91
|
-
@footer.nil? ? super : super + [@footer]
|
|
92
|
-
end
|
|
93
|
-
|
|
94
93
|
# @param event [MouseEvent]
|
|
95
94
|
# @return [void]
|
|
96
95
|
def handle_mouse(event)
|
|
@@ -122,16 +121,6 @@ module Tuile
|
|
|
122
121
|
layout(content)
|
|
123
122
|
end
|
|
124
123
|
|
|
125
|
-
# @return [String] the current caption, empty by default.
|
|
126
|
-
attr_reader :caption
|
|
127
|
-
|
|
128
|
-
# Sets new caption and repaints the window.
|
|
129
|
-
# @param new_caption [String]
|
|
130
|
-
def caption=(new_caption)
|
|
131
|
-
@caption = new_caption
|
|
132
|
-
invalidate
|
|
133
|
-
end
|
|
134
|
-
|
|
135
124
|
# Fully repaints the window: both frame and contents.
|
|
136
125
|
#
|
|
137
126
|
# Window deliberately paints over its entire rect (border around the
|
|
@@ -150,14 +139,6 @@ module Tuile
|
|
|
150
139
|
repaint_border
|
|
151
140
|
end
|
|
152
141
|
|
|
153
|
-
# @param key [String, nil]
|
|
154
|
-
# @return [void]
|
|
155
|
-
def key_shortcut=(key)
|
|
156
|
-
super
|
|
157
|
-
# The shortcut key is shown in the caption — repaint.
|
|
158
|
-
invalidate
|
|
159
|
-
end
|
|
160
|
-
|
|
161
142
|
protected
|
|
162
143
|
|
|
163
144
|
# @param content [Component]
|
|
@@ -166,9 +147,12 @@ module Tuile
|
|
|
166
147
|
content.rect = Rect.new(rect.left + 1, rect.top + 1, rect.width - 1 - @border_right, rect.height - 2)
|
|
167
148
|
end
|
|
168
149
|
|
|
169
|
-
# Paints the window border
|
|
170
|
-
#
|
|
171
|
-
#
|
|
150
|
+
# Paints the window border via {Component#draw_line}/{Component#draw_char},
|
|
151
|
+
# so the border cells inherit {Component#effective_bg_color} — a
|
|
152
|
+
# {Component#bg_color} on the window tints border and content alike. Both
|
|
153
|
+
# border lines are clipped by *display* width, so no caption overflows the
|
|
154
|
+
# box; when the window is active the whole border — the caption's own
|
|
155
|
+
# colors included — is drawn in {Theme#active_border_color}.
|
|
172
156
|
# @return [void]
|
|
173
157
|
def repaint_border
|
|
174
158
|
return if rect.empty?
|
|
@@ -178,19 +162,28 @@ module Tuile
|
|
|
178
162
|
top = rect.top
|
|
179
163
|
left = rect.left
|
|
180
164
|
inner_w = [w - 2, 0].max
|
|
181
|
-
title = frame_caption.to_s
|
|
182
|
-
title = title[0, inner_w] if title.length > inner_w
|
|
183
|
-
dashes = "─" * (inner_w - title.length)
|
|
184
165
|
|
|
185
166
|
fg = active? ? screen.theme.active_border_color : nil
|
|
186
167
|
bar = StyledString::Style.new(fg: fg)
|
|
187
|
-
|
|
188
|
-
buf.set_line(left, top, StyledString.styled("┌#{title}#{dashes}┐", fg: fg))
|
|
168
|
+
draw_line(left, top, top_border(inner_w, fg).slice(0, w))
|
|
189
169
|
(1..(h - 2)).each do |dy|
|
|
190
|
-
|
|
191
|
-
|
|
170
|
+
draw_char(left, top + dy, "│", bar)
|
|
171
|
+
draw_char(left + w - 1, top + dy, "│", bar)
|
|
192
172
|
end
|
|
193
|
-
|
|
173
|
+
draw_line(left, top + h - 1, bottom_border(inner_w, fg).slice(0, w)) if h >= 2
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Builds the top border line: corners, {#caption} embedded at its own
|
|
177
|
+
# width, dashes filling the remainder. The caption keeps its own styling
|
|
178
|
+
# unless `fg` is set — an active window's border claims it.
|
|
179
|
+
# @param inner_w [Integer] the border's interior width.
|
|
180
|
+
# @param fg [Color, nil] the active-border color, or nil when inactive.
|
|
181
|
+
# @return [StyledString]
|
|
182
|
+
def top_border(inner_w, fg)
|
|
183
|
+
title = caption.slice(0, inner_w)
|
|
184
|
+
title = title.with_fg(fg) if fg
|
|
185
|
+
dashes = StyledString.styled("─" * (inner_w - title.display_width), fg: fg)
|
|
186
|
+
StyledString.styled("┌", fg: fg) + title + dashes + StyledString.styled("┐", fg: fg)
|
|
194
187
|
end
|
|
195
188
|
|
|
196
189
|
# Builds the bottom border line. The corners take the border color; the
|
|
@@ -212,14 +205,6 @@ module Tuile
|
|
|
212
205
|
StyledString.styled("└", fg: fg) + interior + StyledString.styled("┘", fg: fg)
|
|
213
206
|
end
|
|
214
207
|
|
|
215
|
-
# The caption text as it appears in the rendered border, including the
|
|
216
|
-
# shortcut prefix when {#key_shortcut} is set.
|
|
217
|
-
# @return [String]
|
|
218
|
-
def frame_caption
|
|
219
|
-
c = @caption || ""
|
|
220
|
-
key_shortcut.nil? ? c : "[#{key_shortcut}]-#{c}"
|
|
221
|
-
end
|
|
222
|
-
|
|
223
208
|
private
|
|
224
209
|
|
|
225
210
|
# Positions the footer over the bottom border row, spanning the full
|