tuile 0.7.0 → 0.8.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 +11 -0
- data/examples/sampler.rb +109 -0
- data/ideas/back-buffer.md +217 -0
- data/lib/tuile/ansi.rb +16 -0
- data/lib/tuile/buffer.rb +412 -0
- data/lib/tuile/component/button.rb +2 -5
- data/lib/tuile/component/has_content.rb +0 -6
- data/lib/tuile/component/label.rb +8 -8
- data/lib/tuile/component/layout.rb +0 -12
- data/lib/tuile/component/list.rb +10 -11
- data/lib/tuile/component/log_window.rb +20 -5
- data/lib/tuile/component/picker_window.rb +4 -2
- data/lib/tuile/component/popup.rb +48 -13
- data/lib/tuile/component/text_area.rb +1 -1
- data/lib/tuile/component/text_field.rb +1 -1
- data/lib/tuile/component/text_input.rb +25 -9
- data/lib/tuile/component/text_view.rb +6 -7
- data/lib/tuile/component/window.rb +21 -38
- data/lib/tuile/component.rb +29 -25
- data/lib/tuile/fake_screen.rb +14 -1
- data/lib/tuile/screen.rb +90 -100
- data/lib/tuile/screen_pane.rb +80 -19
- data/lib/tuile/styled_string.rb +40 -30
- data/lib/tuile/version.rb +1 -1
- data/sig/tuile.rbs +511 -112
- metadata +4 -2
|
@@ -2,10 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
module Tuile
|
|
4
4
|
class Component
|
|
5
|
-
#
|
|
6
|
-
# paints nothing — it's a transparent host that handles
|
|
7
|
-
# ({#open} / {#close} / {#open?}, ESC/q to close)
|
|
8
|
-
#
|
|
5
|
+
# An overlay that wraps any {Component} as its content. Popup itself
|
|
6
|
+
# paints nothing — it's a transparent host that handles its lifecycle
|
|
7
|
+
# ({#open} / {#close} / {#open?}, ESC/q to close) and auto-sizes to the
|
|
8
|
+
# wrapped content.
|
|
9
|
+
#
|
|
10
|
+
# Modal by default: it centers on the screen, grabs focus, eats keys, and
|
|
11
|
+
# blocks clicks beneath it. Pass `modal: false` for a non-modal overlay
|
|
12
|
+
# that floats above the content (still painted on top, still auto-sized)
|
|
13
|
+
# without taking focus or capturing input — the caller positions it (via
|
|
14
|
+
# {#rect=}) and drives it from app code. That is the building block for an
|
|
15
|
+
# autocomplete/slash-command list anchored to a {Component::TextField} or
|
|
16
|
+
# {Component::TextArea} caret: typing keeps focus (and the cursor) in the
|
|
17
|
+
# input, an {Component::TextInput#on_change} listener refills the list, and
|
|
18
|
+
# an {Component::TextInput#on_key} interceptor forwards Up/Down/Enter to it.
|
|
9
19
|
#
|
|
10
20
|
# The wrapped content fills the popup's full {#rect}; if you want a frame
|
|
11
21
|
# and caption, wrap a {Component::Window} (or any subclass — including
|
|
@@ -27,12 +37,19 @@ module Tuile
|
|
|
27
37
|
|
|
28
38
|
# @param content [Component, nil] initial content; can be set later via
|
|
29
39
|
# {#content=}. When provided here, the popup auto-sizes to fit.
|
|
30
|
-
|
|
40
|
+
# @param modal [Boolean] true (default) for a centered, focus-grabbing,
|
|
41
|
+
# input-capturing modal; false for a non-modal overlay the caller
|
|
42
|
+
# positions and drives (see the class docs).
|
|
43
|
+
def initialize(content: nil, modal: true)
|
|
31
44
|
super()
|
|
45
|
+
@modal = modal
|
|
32
46
|
@content = nil
|
|
33
47
|
self.content = content unless content.nil?
|
|
34
48
|
end
|
|
35
49
|
|
|
50
|
+
# @return [Boolean] whether this popup is modal. See {#initialize}.
|
|
51
|
+
def modal? = @modal
|
|
52
|
+
|
|
36
53
|
def focusable? = true
|
|
37
54
|
|
|
38
55
|
# Reassigns the popup's rect, escalating to a full scene repaint when an
|
|
@@ -85,9 +102,20 @@ module Tuile
|
|
|
85
102
|
self.rect = rect.centered(screen.size)
|
|
86
103
|
end
|
|
87
104
|
|
|
88
|
-
# @return [Integer] max height the popup will grow to fit its content
|
|
89
|
-
#
|
|
90
|
-
|
|
105
|
+
# @return [Integer] max height the popup will grow to fit its content.
|
|
106
|
+
# Defers to the content's {Component#popup_max_height} advice when it
|
|
107
|
+
# gives one, else defaults to 12. Override in a subclass to allow
|
|
108
|
+
# taller popups regardless of content.
|
|
109
|
+
def max_height = @content&.popup_max_height || 12
|
|
110
|
+
|
|
111
|
+
# @return [Integer] min height the popup occupies even when its content
|
|
112
|
+
# is shorter. Defers to the content's {Component#popup_min_height}
|
|
113
|
+
# advice when it gives one, else defaults to 0 (size purely to
|
|
114
|
+
# content) — so a {Component::LogWindow} stays readable while only a
|
|
115
|
+
# few lines are in without callers wiring up a subclass. Override in a
|
|
116
|
+
# subclass to keep any popup from collapsing to a couple of rows.
|
|
117
|
+
# Capped at the same 4/5-of-screen ceiling {#update_rect} applies.
|
|
118
|
+
def min_height = @content&.popup_min_height || 0
|
|
91
119
|
|
|
92
120
|
# Sets the popup's content and auto-sizes the popup to fit.
|
|
93
121
|
# @param new_content [Component, nil]
|
|
@@ -114,11 +142,12 @@ module Tuile
|
|
|
114
142
|
child_hint.empty? ? prefix : "#{prefix} #{child_hint}"
|
|
115
143
|
end
|
|
116
144
|
|
|
145
|
+
# `q` and ESC close the popup. The popup sits on the focus chain of
|
|
146
|
+
# whatever it wraps, so the key reaches here by bubbling up from the
|
|
147
|
+
# focused content after that content declined to handle it.
|
|
117
148
|
# @param key [String]
|
|
118
149
|
# @return [Boolean] true if the key was handled.
|
|
119
150
|
def handle_key(key)
|
|
120
|
-
return true if super
|
|
121
|
-
|
|
122
151
|
if [Keys::ESC, "q"].include?(key)
|
|
123
152
|
close
|
|
124
153
|
true
|
|
@@ -147,10 +176,16 @@ module Tuile
|
|
|
147
176
|
# {#rect=}'s shrink/move detection fire a full repaint on every resize.
|
|
148
177
|
# @return [void]
|
|
149
178
|
def update_rect
|
|
179
|
+
ceiling = screen.size.height * 4 / 5
|
|
150
180
|
size = @content.content_size.clamp_height(max_height)
|
|
151
|
-
size = size.clamp(Size.new(screen.size.width * 4 / 5,
|
|
152
|
-
|
|
153
|
-
|
|
181
|
+
size = size.clamp(Size.new(screen.size.width * 4 / 5, ceiling))
|
|
182
|
+
floor = min_height.clamp(0, ceiling)
|
|
183
|
+
size = Size.new(size.width, floor) if size.height < floor
|
|
184
|
+
# A non-modal overlay is positioned by the caller, so an open one keeps
|
|
185
|
+
# its current top-left when its content resizes; a modal popup recenters.
|
|
186
|
+
origin = open? && !modal? ? Point.new(rect.left, rect.top) : Point.new(0, 0)
|
|
187
|
+
r = Rect.new(origin.x, origin.y, size.width, size.height)
|
|
188
|
+
r = r.centered(screen.size) if open? && modal?
|
|
154
189
|
self.rect = r
|
|
155
190
|
end
|
|
156
191
|
end
|
|
@@ -80,7 +80,7 @@ module Tuile
|
|
|
80
80
|
chunk = @text[r[:start], r[:length]] || ""
|
|
81
81
|
chunk + (" " * (rect.width - r[:length]))
|
|
82
82
|
end
|
|
83
|
-
screen.
|
|
83
|
+
screen.buffer.set_line(rect.left, rect.top + screen_row, background(line))
|
|
84
84
|
end
|
|
85
85
|
end
|
|
86
86
|
|
|
@@ -60,7 +60,7 @@ module Tuile
|
|
|
60
60
|
return if rect.empty?
|
|
61
61
|
|
|
62
62
|
padded = @text + (" " * (rect.width - @text.length))
|
|
63
|
-
screen.
|
|
63
|
+
screen.buffer.set_line(rect.left, rect.top, background(padded))
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
protected
|
|
@@ -31,6 +31,7 @@ module Tuile
|
|
|
31
31
|
@text = +""
|
|
32
32
|
@caret = 0
|
|
33
33
|
@on_change = nil
|
|
34
|
+
@on_key = nil
|
|
34
35
|
@on_escape = method(:default_on_escape)
|
|
35
36
|
end
|
|
36
37
|
|
|
@@ -49,6 +50,20 @@ module Tuile
|
|
|
49
50
|
# @return [Proc, Method, nil] one-arg callable, or nil.
|
|
50
51
|
attr_accessor :on_change
|
|
51
52
|
|
|
53
|
+
# Optional interceptor consulted before the input's own key handling.
|
|
54
|
+
# Receives the pressed key; return a truthy value to consume it (the
|
|
55
|
+
# input then ignores that key), falsy to let normal editing proceed.
|
|
56
|
+
#
|
|
57
|
+
# The keyboard analog of {#on_change}: it lets app code layer behavior
|
|
58
|
+
# onto an input without subclassing. The motivating case is an
|
|
59
|
+
# autocomplete / slash-command overlay (a non-modal {Component::Popup}):
|
|
60
|
+
# while it is open the interceptor claims Up/Down/Enter/ESC and forwards
|
|
61
|
+
# them to the overlay's list, but lets ordinary characters fall through
|
|
62
|
+
# so typing keeps editing the field (and {#on_change} keeps refilling the
|
|
63
|
+
# list).
|
|
64
|
+
# @return [Proc, Method, nil] one-arg callable, or nil.
|
|
65
|
+
attr_accessor :on_key
|
|
66
|
+
|
|
52
67
|
# Callback fired when ESC is pressed. Defaults to a closure that clears
|
|
53
68
|
# focus (`screen.focused = nil`) so ESC visibly cancels text entry instead
|
|
54
69
|
# of bubbling to the parent — and, in particular, instead of reaching the
|
|
@@ -88,14 +103,15 @@ module Tuile
|
|
|
88
103
|
invalidate
|
|
89
104
|
end
|
|
90
105
|
|
|
91
|
-
# Handles a key.
|
|
92
|
-
#
|
|
93
|
-
#
|
|
106
|
+
# Handles a key. An {#on_key} interceptor (if set) gets first refusal —
|
|
107
|
+
# a truthy return consumes the key — otherwise it delegates to
|
|
108
|
+
# {#handle_text_input_key}. Dispatch ({ScreenPane#handle_key}) only routes
|
|
109
|
+
# keys here when this input is on the focus chain, so there is no
|
|
110
|
+
# {#active?} gate.
|
|
94
111
|
# @param key [String]
|
|
95
112
|
# @return [Boolean]
|
|
96
113
|
def handle_key(key)
|
|
97
|
-
return
|
|
98
|
-
return true if super
|
|
114
|
+
return true if @on_key&.call(key)
|
|
99
115
|
|
|
100
116
|
handle_text_input_key(key)
|
|
101
117
|
end
|
|
@@ -103,13 +119,13 @@ module Tuile
|
|
|
103
119
|
protected
|
|
104
120
|
|
|
105
121
|
# Renders `text` on the field's background well, looked up from the
|
|
106
|
-
# current {Screen#theme} at paint time: {Theme#
|
|
107
|
-
# input is on the active (focus) chain, {Theme#
|
|
122
|
+
# current {Screen#theme} at paint time: {Theme#active_bg_color} when this
|
|
123
|
+
# input is on the active (focus) chain, {Theme#input_bg_color} otherwise —
|
|
108
124
|
# visibly a field either way, distinctly highlighted when active.
|
|
109
125
|
# @param text [String]
|
|
110
|
-
# @return [
|
|
126
|
+
# @return [StyledString] text on the field's background well.
|
|
111
127
|
def background(text)
|
|
112
|
-
active? ? screen.theme.
|
|
128
|
+
StyledString.styled(text, bg: active? ? screen.theme.active_bg_color : screen.theme.input_bg_color)
|
|
113
129
|
end
|
|
114
130
|
|
|
115
131
|
# Input filter for {#text=}. Subclasses override to truncate or reject
|
|
@@ -437,7 +437,7 @@ module Tuile
|
|
|
437
437
|
end
|
|
438
438
|
(0...rect.height).each do |row|
|
|
439
439
|
line = paintable_line(row + @top_line, row, scrollbar)
|
|
440
|
-
screen.
|
|
440
|
+
screen.buffer.set_line(rect.left, rect.top + row, line)
|
|
441
441
|
end
|
|
442
442
|
end
|
|
443
443
|
|
|
@@ -904,15 +904,14 @@ module Tuile
|
|
|
904
904
|
# @param index [Integer] 0-based index into `@physical_lines`.
|
|
905
905
|
# @param row_in_viewport [Integer] 0-based row within the viewport.
|
|
906
906
|
# @param scrollbar [VerticalScrollBar, nil]
|
|
907
|
-
# @return [
|
|
908
|
-
#
|
|
909
|
-
#
|
|
910
|
-
# ASCII-string concat of the scrollbar glyph when one is present.
|
|
907
|
+
# @return [StyledString] paintable line exactly `rect.width` columns wide.
|
|
908
|
+
# Body lines come pre-padded from {#rewrap}, so this reduces to a lookup
|
|
909
|
+
# plus a concat of the scrollbar glyph when one is present.
|
|
911
910
|
def paintable_line(index, row_in_viewport, scrollbar)
|
|
912
911
|
line = @physical_lines[index] || @blank_line
|
|
913
|
-
return line
|
|
912
|
+
return line unless scrollbar
|
|
914
913
|
|
|
915
|
-
line
|
|
914
|
+
line + StyledString.plain(scrollbar.scrollbar_char(row_in_viewport))
|
|
916
915
|
end
|
|
917
916
|
|
|
918
917
|
# A logical section of a {TextView}'s text — a contiguous run of
|
|
@@ -83,14 +83,6 @@ module Tuile
|
|
|
83
83
|
@footer.nil? ? super : super + [@footer]
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
-
# @param key [String]
|
|
87
|
-
# @return [Boolean]
|
|
88
|
-
def handle_key(key)
|
|
89
|
-
return @footer.handle_key(key) if @footer&.active?
|
|
90
|
-
|
|
91
|
-
super
|
|
92
|
-
end
|
|
93
|
-
|
|
94
86
|
# @param event [MouseEvent]
|
|
95
87
|
# @return [void]
|
|
96
88
|
def handle_mouse(event)
|
|
@@ -197,14 +189,31 @@ module Tuile
|
|
|
197
189
|
content.rect = Rect.new(rect.left + 1, rect.top + 1, rect.width - 1 - @border_right, rect.height - 2)
|
|
198
190
|
end
|
|
199
191
|
|
|
200
|
-
# Paints the window border.
|
|
192
|
+
# Paints the window border into the {Screen#buffer}. Title is clipped to
|
|
193
|
+
# the inner width so the box never overflows {#rect}; when the window is
|
|
194
|
+
# active the whole border is drawn in {Theme#active_border_color}.
|
|
201
195
|
# @return [void]
|
|
202
196
|
def repaint_border
|
|
203
197
|
return if rect.empty?
|
|
204
198
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
199
|
+
w = rect.width
|
|
200
|
+
h = rect.height
|
|
201
|
+
top = rect.top
|
|
202
|
+
left = rect.left
|
|
203
|
+
inner_w = [w - 2, 0].max
|
|
204
|
+
title = frame_caption.to_s
|
|
205
|
+
title = title[0, inner_w] if title.length > inner_w
|
|
206
|
+
dashes = "─" * (inner_w - title.length)
|
|
207
|
+
|
|
208
|
+
fg = active? ? screen.theme.active_border_color : nil
|
|
209
|
+
bar = StyledString::Style.new(fg: fg)
|
|
210
|
+
buf = screen.buffer
|
|
211
|
+
buf.set_line(left, top, StyledString.styled("┌#{title}#{dashes}┐", fg: fg))
|
|
212
|
+
(1..(h - 2)).each do |dy|
|
|
213
|
+
buf.set_char(left, top + dy, "│", bar)
|
|
214
|
+
buf.set_char(left + w - 1, top + dy, "│", bar)
|
|
215
|
+
end
|
|
216
|
+
buf.set_line(left, top + h - 1, StyledString.styled("└#{"─" * inner_w}┘", fg: fg)) if h >= 2
|
|
208
217
|
end
|
|
209
218
|
|
|
210
219
|
# The caption text as it appears in the rendered border, including the
|
|
@@ -215,32 +224,6 @@ module Tuile
|
|
|
215
224
|
key_shortcut.nil? ? c : "[#{key_shortcut}]-#{c}"
|
|
216
225
|
end
|
|
217
226
|
|
|
218
|
-
# Builds the border as a single string with embedded cursor-positioning
|
|
219
|
-
# escapes, mirroring the layout {TTY::Box.frame} used to produce. Title
|
|
220
|
-
# is clipped to fit the inner width so the box never overflows {#rect}.
|
|
221
|
-
# @param caption [String]
|
|
222
|
-
# @return [String]
|
|
223
|
-
def build_frame(caption)
|
|
224
|
-
w = @rect.width
|
|
225
|
-
h = @rect.height
|
|
226
|
-
top = @rect.top
|
|
227
|
-
left = @rect.left
|
|
228
|
-
inner_w = [w - 2, 0].max
|
|
229
|
-
|
|
230
|
-
title = caption.to_s
|
|
231
|
-
title = title[0, inner_w] if title.length > inner_w
|
|
232
|
-
dashes = "─" * (inner_w - title.length)
|
|
233
|
-
|
|
234
|
-
out = +""
|
|
235
|
-
out << TTY::Cursor.move_to(left, top) << "┌#{title}#{dashes}┐"
|
|
236
|
-
(1..(h - 2)).each do |dy|
|
|
237
|
-
out << TTY::Cursor.move_to(left, top + dy) << "│"
|
|
238
|
-
out << TTY::Cursor.move_to(left + w - 1, top + dy) << "│"
|
|
239
|
-
end
|
|
240
|
-
out << TTY::Cursor.move_to(left, top + h - 1) << "└#{"─" * inner_w}┘" if h >= 2
|
|
241
|
-
out
|
|
242
|
-
end
|
|
243
|
-
|
|
244
227
|
private
|
|
245
228
|
|
|
246
229
|
# Recomputes the window's natural size: content's natural size (or the
|
data/lib/tuile/component.rb
CHANGED
|
@@ -81,27 +81,22 @@ module Tuile
|
|
|
81
81
|
children.each { |c| screen.invalidate(c) }
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
-
# Called when a character is pressed on the keyboard.
|
|
84
|
+
# Called when a character is pressed on the keyboard. The default does
|
|
85
|
+
# nothing and reports the key as unhandled; input components
|
|
86
|
+
# ({Component::TextField}, {Component::List}, {Component::Button}, …)
|
|
87
|
+
# override it to act on keys they care about.
|
|
85
88
|
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
# @param
|
|
89
|
+
# Dispatch is owned by {ScreenPane#handle_key}: a {#key_shortcut} match
|
|
90
|
+
# anywhere in the active scope is captured first (suppressed while a
|
|
91
|
+
# cursor-owner is mid-edit), then the key is delivered to {Screen#focused}
|
|
92
|
+
# and bubbles up its ancestor chain until some component handles it. A
|
|
93
|
+
# component therefore only ever receives keys when it is on the focus chain
|
|
94
|
+
# — or when app code hands it a key directly — so it acts on the key alone
|
|
95
|
+
# and must never gate on its own {#active?} state.
|
|
96
|
+
# @param _key [String] a key.
|
|
94
97
|
# @return [Boolean] true if the key was handled, false if not.
|
|
95
|
-
def handle_key(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
c = find_shortcut_component(key)
|
|
99
|
-
if !c.nil?
|
|
100
|
-
screen.focused = c
|
|
101
|
-
true
|
|
102
|
-
else
|
|
103
|
-
false
|
|
104
|
-
end
|
|
98
|
+
def handle_key(_key)
|
|
99
|
+
false
|
|
105
100
|
end
|
|
106
101
|
|
|
107
102
|
# A global keyboard shortcut. When pressed, will focus this component.
|
|
@@ -293,6 +288,20 @@ module Tuile
|
|
|
293
288
|
# topmost popup. Empty by default; override to advertise shortcuts.
|
|
294
289
|
def keyboard_hint = ""
|
|
295
290
|
|
|
291
|
+
# Advice to a wrapping {Component::Popup} on the minimum height this
|
|
292
|
+
# component prefers to occupy when shown in a popup. `nil` (the default)
|
|
293
|
+
# means no preference — the popup uses its own {Component::Popup#min_height}.
|
|
294
|
+
# Override in a content component that should not collapse to a couple of
|
|
295
|
+
# rows when sparse (e.g. {Component::LogWindow}).
|
|
296
|
+
# @return [Integer, nil]
|
|
297
|
+
def popup_min_height = nil
|
|
298
|
+
|
|
299
|
+
# Advice to a wrapping {Component::Popup} on the maximum height this
|
|
300
|
+
# component may grow to when shown in a popup. `nil` (the default) means
|
|
301
|
+
# no preference — the popup uses its own {Component::Popup#max_height}.
|
|
302
|
+
# @return [Integer, nil]
|
|
303
|
+
def popup_max_height = nil
|
|
304
|
+
|
|
296
305
|
protected
|
|
297
306
|
|
|
298
307
|
# @param parent [Component, nil]
|
|
@@ -354,12 +363,7 @@ module Tuile
|
|
|
354
363
|
# component's rect.
|
|
355
364
|
# @return [void]
|
|
356
365
|
def clear_background
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
spaces = " " * rect.width
|
|
360
|
-
(rect.top..(rect.top + rect.height - 1)).each do |row|
|
|
361
|
-
screen.print TTY::Cursor.move_to(rect.left, row), spaces
|
|
362
|
-
end
|
|
366
|
+
screen.buffer.fill(rect)
|
|
363
367
|
end
|
|
364
368
|
end
|
|
365
369
|
end
|
data/lib/tuile/fake_screen.rb
CHANGED
|
@@ -25,10 +25,14 @@ module Tuile
|
|
|
25
25
|
super
|
|
26
26
|
@event_queue = FakeEventQueue.new
|
|
27
27
|
@size = Size.new(160, 50)
|
|
28
|
+
@buffer.resize(@size) # super sized it to the test runner's TTY
|
|
28
29
|
@prints = []
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
# @return [Array<String>] whatever {#print}
|
|
32
|
+
# @return [Array<String>] whatever {#print} / {#emit} produced so far.
|
|
33
|
+
# Component painting lands in {#buffer}, not here — assert on
|
|
34
|
+
# {Buffer#row_text} / {Buffer#row_ansi} / {Buffer#cell} for content, and
|
|
35
|
+
# on `prints` for cursor and housekeeping escapes.
|
|
32
36
|
attr_reader :prints
|
|
33
37
|
|
|
34
38
|
# @return [void]
|
|
@@ -46,6 +50,15 @@ module Tuile
|
|
|
46
50
|
@prints += args
|
|
47
51
|
end
|
|
48
52
|
|
|
53
|
+
# Captures the assembled repaint frame instead of writing to the test
|
|
54
|
+
# runner's TTY. Lands in {#prints} so cursor/sync escapes can be asserted;
|
|
55
|
+
# painted content is read from {#buffer}.
|
|
56
|
+
# @param str [String]
|
|
57
|
+
# @return [void]
|
|
58
|
+
def emit(str)
|
|
59
|
+
@prints << str
|
|
60
|
+
end
|
|
61
|
+
|
|
49
62
|
# @param component [Component] the component to check.
|
|
50
63
|
# @return [Boolean]
|
|
51
64
|
def invalidated?(component) = @invalidated.include?(component)
|