tuile 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +378 -0
- data/examples/file_commander.rb +196 -0
- data/examples/hello_world.rb +29 -0
- data/lib/tuile/component/has_content.rb +69 -0
- data/lib/tuile/component/info_window.rb +30 -0
- data/lib/tuile/component/label.rb +63 -0
- data/lib/tuile/component/layout.rb +98 -0
- data/lib/tuile/component/list.rb +583 -0
- data/lib/tuile/component/log_window.rb +59 -0
- data/lib/tuile/component/picker_window.rb +97 -0
- data/lib/tuile/component/popup.rb +127 -0
- data/lib/tuile/component/text_field.rb +209 -0
- data/lib/tuile/component/window.rb +215 -0
- data/lib/tuile/component.rb +236 -0
- data/lib/tuile/event_queue.rb +192 -0
- data/lib/tuile/fake_event_queue.rb +31 -0
- data/lib/tuile/fake_screen.rb +58 -0
- data/lib/tuile/keys.rb +63 -0
- data/lib/tuile/mouse_event.rb +49 -0
- data/lib/tuile/point.rb +14 -0
- data/lib/tuile/rect.rb +58 -0
- data/lib/tuile/screen.rb +377 -0
- data/lib/tuile/screen_pane.rb +174 -0
- data/lib/tuile/size.rb +42 -0
- data/lib/tuile/version.rb +6 -0
- data/lib/tuile/vertical_scroll_bar.rb +46 -0
- data/lib/tuile.rb +37 -0
- data/sig/tuile.rbs +1502 -0
- metadata +197 -0
data/sig/tuile.rbs
ADDED
|
@@ -0,0 +1,1502 @@
|
|
|
1
|
+
# Tuile is a small component-oriented terminal UI framework, built on top of
|
|
2
|
+
# the TTY toolkit. The name is French for a roof tile — a small piece that
|
|
3
|
+
# composes into a larger whole, which mirrors how Tuile UIs are built from
|
|
4
|
+
# {Component}s nested under a single {Screen}.
|
|
5
|
+
module Tuile
|
|
6
|
+
VERSION: String
|
|
7
|
+
|
|
8
|
+
def self.logger: () -> Logger
|
|
9
|
+
|
|
10
|
+
# The logger Tuile writes to. Defaults to a null logger, so the gem is
|
|
11
|
+
# silent unless the host app opts in via `Tuile.logger = ...`. Any object
|
|
12
|
+
# duck-typing the stdlib `Logger` interface (`debug/info/warn/error/fatal`
|
|
13
|
+
# taking a string) works — including `TTY::Logger`.
|
|
14
|
+
def self.logger=: (Logger value) -> Logger
|
|
15
|
+
|
|
16
|
+
class Error < StandardError
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Constants for keys returned by {.getkey} and helpers for reading them from
|
|
20
|
+
# stdin. The constants are the raw escape sequences emitted by the terminal;
|
|
21
|
+
# see https://en.wikipedia.org/wiki/ANSI_escape_code for the encoding.
|
|
22
|
+
module Keys
|
|
23
|
+
DOWN_ARROW: String
|
|
24
|
+
UP_ARROW: String
|
|
25
|
+
DOWN_ARROWS: ::Array[String]
|
|
26
|
+
UP_ARROWS: ::Array[String]
|
|
27
|
+
LEFT_ARROW: String
|
|
28
|
+
RIGHT_ARROW: String
|
|
29
|
+
ESC: String
|
|
30
|
+
HOME: String
|
|
31
|
+
END_: String
|
|
32
|
+
PAGE_UP: String
|
|
33
|
+
PAGE_DOWN: String
|
|
34
|
+
BACKSPACE: String
|
|
35
|
+
DELETE: String
|
|
36
|
+
CTRL_H: String
|
|
37
|
+
BACKSPACES: ::Array[String]
|
|
38
|
+
CTRL_U: String
|
|
39
|
+
CTRL_D: String
|
|
40
|
+
ENTER: String
|
|
41
|
+
|
|
42
|
+
# Grabs a key from stdin and returns it. Blocks until the key is obtained.
|
|
43
|
+
# Reads a full ESC key sequence; see constants above for some values returned
|
|
44
|
+
# by this function.
|
|
45
|
+
#
|
|
46
|
+
# _@return_ — key, such as {DOWN_ARROW}.
|
|
47
|
+
def self.getkey: () -> String
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# A rectangle, with integer `left`, `top`, `width` and `height`, all 0-based.
|
|
51
|
+
#
|
|
52
|
+
# @!attribute [r] left
|
|
53
|
+
# @return [Integer] left edge, 0-based.
|
|
54
|
+
# @!attribute [r] top
|
|
55
|
+
# @return [Integer] top edge, 0-based.
|
|
56
|
+
# @!attribute [r] width
|
|
57
|
+
# @return [Integer] width.
|
|
58
|
+
# @!attribute [r] height
|
|
59
|
+
# @return [Integer] height.
|
|
60
|
+
class Rect
|
|
61
|
+
def to_s: () -> String
|
|
62
|
+
|
|
63
|
+
# _@return_ — true if either {#width} or {#height} is zero or negative.
|
|
64
|
+
def empty?: () -> bool
|
|
65
|
+
|
|
66
|
+
# _@param_ `point` — new top-left corner.
|
|
67
|
+
#
|
|
68
|
+
# _@return_ — positioned at the new `left`/`top`.
|
|
69
|
+
def at: (Point point) -> Rect
|
|
70
|
+
|
|
71
|
+
# Centers the rectangle — keeps {#width} and {#height} but modifies
|
|
72
|
+
# {#top} and {#left} so that the rectangle is centered on a screen.
|
|
73
|
+
#
|
|
74
|
+
# _@param_ `screen_size` — screen size
|
|
75
|
+
#
|
|
76
|
+
# _@return_ — moved rectangle.
|
|
77
|
+
def centered: (Size screen_size) -> Rect
|
|
78
|
+
|
|
79
|
+
# Clamp both width and height and return a rectangle.
|
|
80
|
+
#
|
|
81
|
+
# _@param_ `max_size` — the max size
|
|
82
|
+
def clamp: (Size max_size) -> Rect
|
|
83
|
+
|
|
84
|
+
# _@param_ `point`
|
|
85
|
+
def contains?: (Point point) -> bool
|
|
86
|
+
|
|
87
|
+
def size: () -> Size
|
|
88
|
+
|
|
89
|
+
def top_left: () -> Point
|
|
90
|
+
|
|
91
|
+
# _@return_ — left edge, 0-based.
|
|
92
|
+
attr_reader left: Integer
|
|
93
|
+
|
|
94
|
+
# _@return_ — top edge, 0-based.
|
|
95
|
+
attr_reader top: Integer
|
|
96
|
+
|
|
97
|
+
# _@return_ — width.
|
|
98
|
+
attr_reader width: Integer
|
|
99
|
+
|
|
100
|
+
# _@return_ — height.
|
|
101
|
+
attr_reader height: Integer
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# A size with integer `width` and `height`.
|
|
105
|
+
#
|
|
106
|
+
# @!attribute [r] width
|
|
107
|
+
# @return [Integer] width.
|
|
108
|
+
# @!attribute [r] height
|
|
109
|
+
# @return [Integer] height.
|
|
110
|
+
class Size
|
|
111
|
+
ZERO: Size
|
|
112
|
+
|
|
113
|
+
def to_s: () -> String
|
|
114
|
+
|
|
115
|
+
# _@return_ — true if either {#width} or {#height} is zero or negative.
|
|
116
|
+
def empty?: () -> bool
|
|
117
|
+
|
|
118
|
+
# _@param_ `width`
|
|
119
|
+
#
|
|
120
|
+
# _@param_ `height`
|
|
121
|
+
def plus: (Integer width, Integer height) -> Size
|
|
122
|
+
|
|
123
|
+
# Clamp both width and height and return a size.
|
|
124
|
+
#
|
|
125
|
+
# _@param_ `max_size` — the max size
|
|
126
|
+
def clamp: (Size max_size) -> Size
|
|
127
|
+
|
|
128
|
+
# Clamp height and return a size.
|
|
129
|
+
#
|
|
130
|
+
# _@param_ `max_height` — the max height
|
|
131
|
+
def clamp_height: (Integer max_height) -> Size
|
|
132
|
+
|
|
133
|
+
# _@return_ — width.
|
|
134
|
+
attr_reader width: Integer
|
|
135
|
+
|
|
136
|
+
# _@return_ — height.
|
|
137
|
+
attr_reader height: Integer
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# A point with `x` and `y` integer coordinates, both 0-based.
|
|
141
|
+
#
|
|
142
|
+
# @!attribute [r] x
|
|
143
|
+
# @return [Integer] x coordinate, 0-based.
|
|
144
|
+
# @!attribute [r] y
|
|
145
|
+
# @return [Integer] y coordinate, 0-based.
|
|
146
|
+
class Point
|
|
147
|
+
def to_s: () -> String
|
|
148
|
+
|
|
149
|
+
# _@return_ — x coordinate, 0-based.
|
|
150
|
+
attr_reader x: Integer
|
|
151
|
+
|
|
152
|
+
# _@return_ — y coordinate, 0-based.
|
|
153
|
+
attr_reader y: Integer
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# The TTY screen. There is exactly one screen per app.
|
|
157
|
+
#
|
|
158
|
+
# A screen runs the event loop; call {#run_event_loop} to do that.
|
|
159
|
+
#
|
|
160
|
+
# A screen holds the screen lock; any UI modifications must be called from
|
|
161
|
+
# the event queue.
|
|
162
|
+
#
|
|
163
|
+
# All UI lives under a single {ScreenPane} owned by the screen. Set tiled
|
|
164
|
+
# content via {#content=}; the pane fills the entire terminal and is
|
|
165
|
+
# responsible for laying out its children.
|
|
166
|
+
#
|
|
167
|
+
# Modal popups are supported too, via {Component::Popup#open}. They
|
|
168
|
+
# auto-size to their wrapped content and are drawn centered over the
|
|
169
|
+
# tiled content.
|
|
170
|
+
#
|
|
171
|
+
# The drawing procedure is very simple: when a window needs repaint, it
|
|
172
|
+
# invalidates itself, but won't draw immediately. After the keyboard press
|
|
173
|
+
# event processing is done in the event loop, {#repaint} is called which
|
|
174
|
+
# then repaints all invalidated windows. This prevents repeated paintings.
|
|
175
|
+
class Screen
|
|
176
|
+
# rubocop:disable Style/ClassVars
|
|
177
|
+
def initialize: () -> void
|
|
178
|
+
|
|
179
|
+
# _@return_ — the singleton instance.
|
|
180
|
+
def self.instance: () -> Screen
|
|
181
|
+
|
|
182
|
+
# _@return_ — tiled content (forwarded to {ScreenPane}).
|
|
183
|
+
def content: () -> Component?
|
|
184
|
+
|
|
185
|
+
# _@param_ `content`
|
|
186
|
+
def content=: (Component content) -> void
|
|
187
|
+
|
|
188
|
+
# _@return_ — currently active popup components (forwarded
|
|
189
|
+
# to {ScreenPane}). The array must not be modified!
|
|
190
|
+
def popups: () -> ::Array[Component]
|
|
191
|
+
|
|
192
|
+
# Checks that the UI lock is held and the current code runs in the "UI
|
|
193
|
+
# thread".
|
|
194
|
+
def check_locked: () -> void
|
|
195
|
+
|
|
196
|
+
# Clears the TTY screen.
|
|
197
|
+
def clear: () -> void
|
|
198
|
+
|
|
199
|
+
# Invalidates a component: causes the component to be repainted on next
|
|
200
|
+
# call to {#repaint}.
|
|
201
|
+
#
|
|
202
|
+
# _@param_ `component`
|
|
203
|
+
def invalidate: (Component component) -> void
|
|
204
|
+
|
|
205
|
+
# Internal — use {Component::Popup#open} instead. Adds the popup to
|
|
206
|
+
# {#pane}, centers and focuses it.
|
|
207
|
+
#
|
|
208
|
+
# _@param_ `window`
|
|
209
|
+
def add_popup: (Component::Popup window) -> void
|
|
210
|
+
|
|
211
|
+
# Runs event loop – waits for keys and sends them to active window. The
|
|
212
|
+
# function exits when the 'ESC' or 'q' key is pressed.
|
|
213
|
+
def run_event_loop: () -> void
|
|
214
|
+
|
|
215
|
+
# _@return_ — current active tiled component.
|
|
216
|
+
def active_window: () -> Component?
|
|
217
|
+
|
|
218
|
+
# Internal — use {Component::Popup#close} instead. Removes the popup
|
|
219
|
+
# from {#pane}, repairs focus, and repaints the scene.
|
|
220
|
+
#
|
|
221
|
+
# Does nothing if the window is not open on this screen.
|
|
222
|
+
#
|
|
223
|
+
# _@param_ `window`
|
|
224
|
+
def remove_popup: (Component::Popup window) -> void
|
|
225
|
+
|
|
226
|
+
# Internal — use {Component::Popup#open?} instead.
|
|
227
|
+
#
|
|
228
|
+
# _@param_ `window`
|
|
229
|
+
#
|
|
230
|
+
# _@return_ — true if this popup is currently mounted.
|
|
231
|
+
def has_popup?: (Component::Popup window) -> bool
|
|
232
|
+
|
|
233
|
+
# Testing only — creates new screen, locks the UI, and prevents any
|
|
234
|
+
# redraws, so that test TTY is not painted over. {FakeScreen#initialize}
|
|
235
|
+
# self-installs as the singleton, so subsequent {Screen.instance} calls
|
|
236
|
+
# return the same object.
|
|
237
|
+
def self.fake: () -> FakeScreen
|
|
238
|
+
|
|
239
|
+
def close: () -> void
|
|
240
|
+
|
|
241
|
+
def self.close: () -> void
|
|
242
|
+
|
|
243
|
+
# Prints given strings.
|
|
244
|
+
#
|
|
245
|
+
# _@param_ `args` — stuff to print.
|
|
246
|
+
def print: (*String args) -> void
|
|
247
|
+
|
|
248
|
+
# Repaints the screen; tries to be as effective as possible, by only
|
|
249
|
+
# considering invalidated windows.
|
|
250
|
+
def repaint: () -> void
|
|
251
|
+
|
|
252
|
+
# Returns the absolute screen coordinates where the hardware cursor should
|
|
253
|
+
# sit, or nil if it should be hidden. Only the {#focused} component owns
|
|
254
|
+
# the cursor: there can be multiple active components (the focus path),
|
|
255
|
+
# but only one focused.
|
|
256
|
+
def cursor_position: () -> Point?
|
|
257
|
+
|
|
258
|
+
# Collects a component and all its descendants in tree order
|
|
259
|
+
# (parent before children).
|
|
260
|
+
#
|
|
261
|
+
# _@param_ `component`
|
|
262
|
+
def collect_subtree: (Component component) -> ::Array[Component]
|
|
263
|
+
|
|
264
|
+
# Hides or moves the hardware cursor based on the current focus state.
|
|
265
|
+
def position_cursor: () -> void
|
|
266
|
+
|
|
267
|
+
# Recalculates positions of all windows, and repaints the scene.
|
|
268
|
+
# Automatically called whenever terminal size changes. Call when the app
|
|
269
|
+
# starts. {#size} provides correct size of the terminal.
|
|
270
|
+
def layout: () -> void
|
|
271
|
+
|
|
272
|
+
# Called after a popup is closed. Since a popup can cover any window,
|
|
273
|
+
# top-level component or other popups, we need to redraw everything.
|
|
274
|
+
def needs_full_repaint: () -> void
|
|
275
|
+
|
|
276
|
+
# A key has been pressed on the keyboard. Handle it, or forward to active
|
|
277
|
+
# window.
|
|
278
|
+
#
|
|
279
|
+
# _@param_ `key`
|
|
280
|
+
#
|
|
281
|
+
# _@return_ — true if the key was handled by some window.
|
|
282
|
+
def handle_key: (String key) -> bool
|
|
283
|
+
|
|
284
|
+
# Finds target window and calls {Component::Window#handle_mouse}.
|
|
285
|
+
#
|
|
286
|
+
# _@param_ `event`
|
|
287
|
+
def handle_mouse: (MouseEvent event) -> void
|
|
288
|
+
|
|
289
|
+
def event_loop: () -> void
|
|
290
|
+
|
|
291
|
+
# _@return_ — the structural root of the component tree.
|
|
292
|
+
attr_reader pane: ScreenPane
|
|
293
|
+
|
|
294
|
+
# Handler invoked when a {StandardError} escapes an event handler inside
|
|
295
|
+
# the event loop (e.g. a {Component::TextField}'s `on_change` raises).
|
|
296
|
+
#
|
|
297
|
+
# The default re-raises, so the exception propagates out of
|
|
298
|
+
# {#run_event_loop} and crashes the script with a stacktrace — unhandled
|
|
299
|
+
# exceptions are bugs and should be surfaced loudly.
|
|
300
|
+
#
|
|
301
|
+
# Replace it when the host has somewhere visible to put errors, e.g. a
|
|
302
|
+
# {Component::LogWindow} wired to {Tuile.logger}:
|
|
303
|
+
#
|
|
304
|
+
# screen.on_error = lambda do |e|
|
|
305
|
+
# Tuile.logger.error("#{e.class}: #{e.message}\n#{e.backtrace&.join("\n")}")
|
|
306
|
+
# end
|
|
307
|
+
#
|
|
308
|
+
# The handler runs on the event-loop thread with the UI lock held.
|
|
309
|
+
# Returning normally keeps the loop alive; raising from within the handler
|
|
310
|
+
# tears the loop down and propagates out of {#run_event_loop}.
|
|
311
|
+
#
|
|
312
|
+
# _@return_ — one-arg callable receiving the {StandardError} instance.
|
|
313
|
+
attr_accessor on_error: Proc
|
|
314
|
+
|
|
315
|
+
# _@return_ — current screen size.
|
|
316
|
+
attr_reader size: Size
|
|
317
|
+
|
|
318
|
+
# _@return_ — the event queue.
|
|
319
|
+
attr_reader event_queue: EventQueue
|
|
320
|
+
|
|
321
|
+
# _@return_ — currently focused component.
|
|
322
|
+
attr_accessor focused: Component?
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# A UI component which is positioned on the screen and draws characters into
|
|
326
|
+
# its bounding rectangle (in {#repaint}).
|
|
327
|
+
#
|
|
328
|
+
# Component is considered invisible if {#rect} is empty or one of left/top is
|
|
329
|
+
# negative. The component won't draw when invisible.
|
|
330
|
+
class Component
|
|
331
|
+
def initialize: () -> void
|
|
332
|
+
|
|
333
|
+
# _@return_ — the screen which owns this component.
|
|
334
|
+
def screen: () -> Screen
|
|
335
|
+
|
|
336
|
+
# Focuses this component. Equivalent to `screen.focused = self`.
|
|
337
|
+
def focus: () -> void
|
|
338
|
+
|
|
339
|
+
# Repaints the component. Default implementation does nothing.
|
|
340
|
+
#
|
|
341
|
+
# The component must fully draw over {#rect}, and must not draw outside of
|
|
342
|
+
# {#rect}.
|
|
343
|
+
#
|
|
344
|
+
# Tip: use {#clear_background} to clear component background before painting.
|
|
345
|
+
def repaint: () -> void
|
|
346
|
+
|
|
347
|
+
# Called when a character is pressed on the keyboard.
|
|
348
|
+
#
|
|
349
|
+
# Also called for inactive components. Inactive component should just return
|
|
350
|
+
# false.
|
|
351
|
+
#
|
|
352
|
+
# Default implementation searches for a component with {#key_shortcut} and
|
|
353
|
+
# focuses it. The shortcut search is suppressed while the focused component
|
|
354
|
+
# owns the hardware cursor (e.g. a {Component::TextField} the user is
|
|
355
|
+
# typing into) so that hotkeys don't steal printable keys from the editor.
|
|
356
|
+
#
|
|
357
|
+
# _@param_ `key` — a key.
|
|
358
|
+
#
|
|
359
|
+
# _@return_ — true if the key was handled, false if not.
|
|
360
|
+
def handle_key: (String key) -> bool
|
|
361
|
+
|
|
362
|
+
# _@param_ `key` — keyboard key to look up.
|
|
363
|
+
#
|
|
364
|
+
# _@return_ — the component whose {#key_shortcut} matches `key`,
|
|
365
|
+
# or nil.
|
|
366
|
+
def find_shortcut_component: (String key) -> Component?
|
|
367
|
+
|
|
368
|
+
# Handles mouse event. Default implementation focuses this component when
|
|
369
|
+
# clicked (if {#focusable?}).
|
|
370
|
+
#
|
|
371
|
+
# _@param_ `event`
|
|
372
|
+
def handle_mouse: (MouseEvent event) -> void
|
|
373
|
+
|
|
374
|
+
# _@return_ — true if the component is on the active chain — i.e. it
|
|
375
|
+
# is the focused component or an ancestor of it. Set by {Screen#focused=}.
|
|
376
|
+
def active?: () -> bool
|
|
377
|
+
|
|
378
|
+
# _@param_ `active` — true if active. Set by {Screen#focused=} as it marks the focus chain (root → focused); not meant to be called directly.
|
|
379
|
+
def active=: (bool active) -> void
|
|
380
|
+
|
|
381
|
+
# Whether this component is a valid focus target. `false` by default —
|
|
382
|
+
# passive components like {Label} are decoration and don't accept focus.
|
|
383
|
+
# The flag gates click-to-focus ({#handle_mouse}) and the focus-cascade
|
|
384
|
+
# in container components ({HasContent#on_focus}, {Layout#on_focus}).
|
|
385
|
+
# Independent from {#active?}: every component carries the active flag, but
|
|
386
|
+
# only focusable ones can become a focus target that puts themselves and
|
|
387
|
+
# their ancestors on the active chain.
|
|
388
|
+
#
|
|
389
|
+
# _@return_ — true if this component can be focused.
|
|
390
|
+
def focusable?: () -> bool
|
|
391
|
+
|
|
392
|
+
# _@return_ — the distance from the root component; 0 if {#parent}
|
|
393
|
+
# is nil.
|
|
394
|
+
def depth: () -> Integer
|
|
395
|
+
|
|
396
|
+
# _@return_ — the root component of this component hierarchy.
|
|
397
|
+
def root: () -> Component
|
|
398
|
+
|
|
399
|
+
# List of child components, defaults to an empty array.
|
|
400
|
+
#
|
|
401
|
+
# _@return_ — child components. Must not be mutated! May be
|
|
402
|
+
# empty.
|
|
403
|
+
def children: () -> ::Array[Component]
|
|
404
|
+
|
|
405
|
+
# Calls block for this component and for every descendant component.
|
|
406
|
+
def on_tree: () ?{ (Component component) -> void } -> void
|
|
407
|
+
|
|
408
|
+
# Called when the component receives focus.
|
|
409
|
+
def on_focus: () -> void
|
|
410
|
+
|
|
411
|
+
# _@return_ — true if this component's tree is currently mounted on
|
|
412
|
+
# the {Screen}, i.e. its root is the {ScreenPane}.
|
|
413
|
+
def attached?: () -> bool
|
|
414
|
+
|
|
415
|
+
# Called by container components after `child` has been detached from
|
|
416
|
+
# `self.children` (its `parent` is already nil and it is no longer in the
|
|
417
|
+
# children list). Default behavior repairs dangling focus: if the focused
|
|
418
|
+
# component lived inside the removed subtree, focus shifts to `self` so the
|
|
419
|
+
# cursor doesn't dangle on a detached component. No-op if `self` is not
|
|
420
|
+
# attached to the screen — focus state in a detached subtree is moot.
|
|
421
|
+
#
|
|
422
|
+
# _@param_ `child` — the just-detached child.
|
|
423
|
+
def on_child_removed: (Component child) -> void
|
|
424
|
+
|
|
425
|
+
# The {Size} big enough to show the entire component contents without
|
|
426
|
+
# scrolling. Plain components have no intrinsic content and report
|
|
427
|
+
# {Size::ZERO}; container/decorative components (e.g. {Label}, {List},
|
|
428
|
+
# {Layout}, {Window}) override this to fold in their content's natural
|
|
429
|
+
# extent. Used by callers like {Component::Popup} to auto-size to
|
|
430
|
+
# whatever content was assigned, regardless of its concrete type.
|
|
431
|
+
def content_size: () -> Size
|
|
432
|
+
|
|
433
|
+
# Where the hardware terminal cursor should sit when this component is the
|
|
434
|
+
# cursor owner. Returns `nil` to indicate the cursor should be hidden. The
|
|
435
|
+
# {Screen} positions the hardware cursor after each repaint cycle by
|
|
436
|
+
# consulting the {Screen#focused} component only.
|
|
437
|
+
#
|
|
438
|
+
# _@return_ — absolute screen coordinates, or nil to hide.
|
|
439
|
+
def cursor_position: () -> Point?
|
|
440
|
+
|
|
441
|
+
# _@return_ — formatted keyboard hint surfaced in the status bar by
|
|
442
|
+
# {Screen} when this component is the active tiled window or the
|
|
443
|
+
# topmost popup. Empty by default; override to advertise shortcuts.
|
|
444
|
+
def keyboard_hint: () -> String
|
|
445
|
+
|
|
446
|
+
# Called whenever the component width changes. Does nothing by default.
|
|
447
|
+
def on_width_changed: () -> void
|
|
448
|
+
|
|
449
|
+
# Invalidates the component: {Screen} records this component as
|
|
450
|
+
# needs-repaint and once all events are processed, will call {#repaint}.
|
|
451
|
+
def invalidate: () -> void
|
|
452
|
+
|
|
453
|
+
# Clears the background: prints spaces into all characters occupied by the
|
|
454
|
+
# component's rect.
|
|
455
|
+
def clear_background: () -> void
|
|
456
|
+
|
|
457
|
+
# _@return_ — the rectangle the component occupies on screen.
|
|
458
|
+
attr_accessor rect: Rect
|
|
459
|
+
|
|
460
|
+
# A global keyboard shortcut. When pressed, will focus this component.
|
|
461
|
+
#
|
|
462
|
+
# _@return_ — shortcut, `nil` by default.
|
|
463
|
+
attr_accessor key_shortcut: String?
|
|
464
|
+
|
|
465
|
+
# _@return_ — the parent component or nil if the component has
|
|
466
|
+
# no parent.
|
|
467
|
+
attr_accessor parent: Component?
|
|
468
|
+
|
|
469
|
+
# A scrollable list of String items with cursor support.
|
|
470
|
+
#
|
|
471
|
+
# Items are lines painted directly into the component's {#rect}. Lines are
|
|
472
|
+
# automatically clipped horizontally. Vertical scrolling is supported via
|
|
473
|
+
# {#top_line}; the list can also automatically scroll to the bottom if
|
|
474
|
+
# {#auto_scroll} is enabled.
|
|
475
|
+
#
|
|
476
|
+
# Cursor is supported; call {#cursor=} to change cursor behavior. The
|
|
477
|
+
# cursor responds to arrows, `jk`, Home/End, Ctrl+U/D and scrolls the list
|
|
478
|
+
# automatically.
|
|
479
|
+
class List < Component
|
|
480
|
+
def initialize: () -> void
|
|
481
|
+
|
|
482
|
+
# Sets new lines. Each entry is coerced via `#to_s`, split on `\n` into
|
|
483
|
+
# separate lines, and trailing whitespace stripped — symmetric with
|
|
484
|
+
# {#add_lines}, so the stored `@lines` is always `Array<String>`.
|
|
485
|
+
#
|
|
486
|
+
# _@param_ `lines` — new lines. Entries need only respond to `#to_s`.
|
|
487
|
+
def lines=: (::Array[untyped] lines) -> void
|
|
488
|
+
|
|
489
|
+
# Without a block, returns the current lines. With a block, fully
|
|
490
|
+
# re-populates the list:
|
|
491
|
+
# ```ruby
|
|
492
|
+
# list.lines do |buffer|
|
|
493
|
+
# buffer << "Hello!"
|
|
494
|
+
# end
|
|
495
|
+
# ```
|
|
496
|
+
#
|
|
497
|
+
# _@return_ — current lines (when called without a block).
|
|
498
|
+
def lines: () ?{ (::Array[String] buffer) -> void } -> ::Array[String]
|
|
499
|
+
|
|
500
|
+
# Adds a line.
|
|
501
|
+
#
|
|
502
|
+
# _@param_ `line`
|
|
503
|
+
def add_line: (String line) -> void
|
|
504
|
+
|
|
505
|
+
# Appends given lines. Each entry is coerced via `#to_s`, split on `\n`
|
|
506
|
+
# into separate lines, and trailing whitespace stripped — symmetric with
|
|
507
|
+
# {#lines=}.
|
|
508
|
+
#
|
|
509
|
+
# _@param_ `lines` — entries need only respond to `#to_s`.
|
|
510
|
+
def add_lines: (::Array[untyped] lines) -> void
|
|
511
|
+
|
|
512
|
+
def content_size: () -> Size
|
|
513
|
+
|
|
514
|
+
def focusable?: () -> bool
|
|
515
|
+
|
|
516
|
+
# _@param_ `key` — a key.
|
|
517
|
+
#
|
|
518
|
+
# _@return_ — true if the key was handled.
|
|
519
|
+
def handle_key: (String key) -> bool
|
|
520
|
+
|
|
521
|
+
# Moves the cursor to the next line whose text contains `query`
|
|
522
|
+
# (case-insensitive substring match). Search wraps around the end of the
|
|
523
|
+
# list. Only lines reachable by the current {#cursor} are considered.
|
|
524
|
+
#
|
|
525
|
+
# _@param_ `query` — substring to match. Empty query never matches.
|
|
526
|
+
#
|
|
527
|
+
# _@param_ `include_current` — when true, the current cursor position is eligible (useful when the query has just changed and the current line may still match); when false, the search starts after the current position (useful for "find next" key bindings that should advance past the current).
|
|
528
|
+
#
|
|
529
|
+
# _@return_ — true if a match was found.
|
|
530
|
+
def select_next: (String query, ?include_current: bool) -> bool
|
|
531
|
+
|
|
532
|
+
# Mirror of {#select_next} that walks the list backwards.
|
|
533
|
+
#
|
|
534
|
+
# _@param_ `query`
|
|
535
|
+
#
|
|
536
|
+
# _@param_ `include_current`
|
|
537
|
+
#
|
|
538
|
+
# _@return_ — true if a match was found.
|
|
539
|
+
def select_prev: (String query, ?include_current: bool) -> bool
|
|
540
|
+
|
|
541
|
+
# _@param_ `event`
|
|
542
|
+
def handle_mouse: (MouseEvent event) -> void
|
|
543
|
+
|
|
544
|
+
# Paints the list items into {#rect}.
|
|
545
|
+
def repaint: () -> void
|
|
546
|
+
|
|
547
|
+
# _@return_ — true if the cursor sits on a real content line.
|
|
548
|
+
def cursor_on_item?: () -> bool
|
|
549
|
+
|
|
550
|
+
# Calls {#on_item_chosen} with the cursor's current `(index, line)`.
|
|
551
|
+
# Caller must ensure {#cursor_on_item?}.
|
|
552
|
+
def fire_item_chosen: () -> void
|
|
553
|
+
|
|
554
|
+
# _@param_ `query`
|
|
555
|
+
#
|
|
556
|
+
# _@param_ `include_current`
|
|
557
|
+
#
|
|
558
|
+
# _@param_ `reverse`
|
|
559
|
+
def search_and_go: (String query, include_current: bool, reverse: bool) -> bool
|
|
560
|
+
|
|
561
|
+
# Rotates `candidates` (sorted ascending) so iteration starts from the
|
|
562
|
+
# position appropriate for "find next" / "find prev" with optional
|
|
563
|
+
# inclusion of the current.
|
|
564
|
+
#
|
|
565
|
+
# _@param_ `candidates`
|
|
566
|
+
#
|
|
567
|
+
# _@param_ `current`
|
|
568
|
+
#
|
|
569
|
+
# _@param_ `include_current`
|
|
570
|
+
#
|
|
571
|
+
# _@param_ `reverse`
|
|
572
|
+
def order_for_search: (
|
|
573
|
+
::Array[Integer] candidates,
|
|
574
|
+
Integer current,
|
|
575
|
+
include_current: bool,
|
|
576
|
+
reverse: bool
|
|
577
|
+
) -> ::Array[Integer]
|
|
578
|
+
|
|
579
|
+
# Scrolls the viewport so the cursor is visible.
|
|
580
|
+
def move_viewport_to_cursor: () -> void
|
|
581
|
+
|
|
582
|
+
# _@return_ — the max value of {#top_line}.
|
|
583
|
+
def top_line_max: () -> Integer
|
|
584
|
+
|
|
585
|
+
# _@return_ — the number of visible lines.
|
|
586
|
+
def viewport_lines: () -> Integer
|
|
587
|
+
|
|
588
|
+
# Scrolls the list.
|
|
589
|
+
#
|
|
590
|
+
# _@param_ `delta` — negative scrolls up, positive scrolls down.
|
|
591
|
+
def move_top_line_by: (Integer delta) -> void
|
|
592
|
+
|
|
593
|
+
# If auto-scrolling, recalculate the top line.
|
|
594
|
+
def update_top_line_if_auto_scroll: () -> void
|
|
595
|
+
|
|
596
|
+
# _@return_ — whether the scrollbar should be drawn right now.
|
|
597
|
+
def scrollbar_visible?: () -> bool
|
|
598
|
+
|
|
599
|
+
# Trims string exactly to `width` columns.
|
|
600
|
+
#
|
|
601
|
+
# _@param_ `str`
|
|
602
|
+
#
|
|
603
|
+
# _@param_ `width`
|
|
604
|
+
def trim_to: (String str, Integer width) -> String
|
|
605
|
+
|
|
606
|
+
# _@param_ `index` — 0-based index into {#lines}.
|
|
607
|
+
#
|
|
608
|
+
# _@param_ `row_in_viewport` — 0-based row within the viewport.
|
|
609
|
+
#
|
|
610
|
+
# _@param_ `width` — number of columns the line should occupy.
|
|
611
|
+
#
|
|
612
|
+
# _@param_ `scrollbar` — scrollbar instance, or nil if not shown.
|
|
613
|
+
#
|
|
614
|
+
# _@return_ — paintable line exactly `width` columns wide;
|
|
615
|
+
# highlighted if cursor is here.
|
|
616
|
+
def paintable_line: (
|
|
617
|
+
Integer index,
|
|
618
|
+
Integer row_in_viewport,
|
|
619
|
+
Integer width,
|
|
620
|
+
VerticalScrollBar? scrollbar
|
|
621
|
+
) -> String
|
|
622
|
+
|
|
623
|
+
# _@return_ — callback fired when an item is chosen — by pressing
|
|
624
|
+
# Enter on the cursor's item, or by left-clicking an item. Called as
|
|
625
|
+
# `proc.call(index, line)` with the chosen 0-based index and its line.
|
|
626
|
+
# Never fires when the cursor's position is outside the content (e.g.
|
|
627
|
+
# {Cursor::None}, or empty content).
|
|
628
|
+
attr_accessor on_item_chosen: Proc?
|
|
629
|
+
|
|
630
|
+
# _@return_ — if true and a line is added or new content is set,
|
|
631
|
+
# auto-scrolls to the bottom.
|
|
632
|
+
attr_accessor auto_scroll: bool
|
|
633
|
+
|
|
634
|
+
# _@return_ — top line of the viewport. 0 or positive.
|
|
635
|
+
attr_accessor top_line: Integer
|
|
636
|
+
|
|
637
|
+
# _@return_ — the list's cursor.
|
|
638
|
+
attr_accessor cursor: Cursor
|
|
639
|
+
|
|
640
|
+
# _@return_ — scrollbar visibility: `:gone` or `:visible`.
|
|
641
|
+
attr_accessor scrollbar_visibility: Symbol
|
|
642
|
+
|
|
643
|
+
# _@return_ — when true, the cursor highlight is painted even while
|
|
644
|
+
# the list is inactive (e.g. when focus is on a sibling search field).
|
|
645
|
+
# Defaults to false.
|
|
646
|
+
attr_accessor show_cursor_when_inactive: bool
|
|
647
|
+
|
|
648
|
+
# Tracks cursor position within the list.
|
|
649
|
+
class Cursor
|
|
650
|
+
# _@param_ `position` — the initial cursor position.
|
|
651
|
+
def initialize: (?position: Integer) -> void
|
|
652
|
+
|
|
653
|
+
# _@param_ `line_count` — number of lines in the list.
|
|
654
|
+
#
|
|
655
|
+
# _@return_ — positions the cursor can land on, in
|
|
656
|
+
# ascending order.
|
|
657
|
+
def candidate_positions: (Integer line_count) -> ::Array[Integer]
|
|
658
|
+
|
|
659
|
+
# _@param_ `key` — pressed keyboard key.
|
|
660
|
+
#
|
|
661
|
+
# _@param_ `line_count` — number of lines in the list.
|
|
662
|
+
#
|
|
663
|
+
# _@param_ `viewport_lines` — number of visible lines.
|
|
664
|
+
#
|
|
665
|
+
# _@return_ — true if the cursor moved.
|
|
666
|
+
def handle_key: (String key, Integer line_count, Integer viewport_lines) -> bool
|
|
667
|
+
|
|
668
|
+
# _@param_ `line` — cursor is hovering over this line.
|
|
669
|
+
#
|
|
670
|
+
# _@param_ `event` — the event.
|
|
671
|
+
#
|
|
672
|
+
# _@param_ `line_count` — number of lines in the list.
|
|
673
|
+
#
|
|
674
|
+
# _@return_ — true if the event was handled.
|
|
675
|
+
def handle_mouse: (Integer line, MouseEvent event, Integer line_count) -> bool
|
|
676
|
+
|
|
677
|
+
# Moves the cursor to the new position. Public only because of testing.
|
|
678
|
+
#
|
|
679
|
+
# _@param_ `new_position` — new 0-based cursor position.
|
|
680
|
+
#
|
|
681
|
+
# _@return_ — true if the position changed.
|
|
682
|
+
def go: (Integer new_position) -> bool
|
|
683
|
+
|
|
684
|
+
# _@param_ `lines`
|
|
685
|
+
#
|
|
686
|
+
# _@param_ `line_count`
|
|
687
|
+
def go_down_by: (Integer lines, Integer line_count) -> bool
|
|
688
|
+
|
|
689
|
+
# _@param_ `lines`
|
|
690
|
+
def go_up_by: (Integer lines) -> bool
|
|
691
|
+
|
|
692
|
+
def go_to_first: () -> bool
|
|
693
|
+
|
|
694
|
+
# _@param_ `line_count`
|
|
695
|
+
def go_to_last: (Integer line_count) -> bool
|
|
696
|
+
|
|
697
|
+
# _@return_ — 0-based line index of the current cursor position.
|
|
698
|
+
attr_reader position: Integer
|
|
699
|
+
|
|
700
|
+
# No cursor — cursor is disabled.
|
|
701
|
+
class None < Cursor
|
|
702
|
+
def initialize: () -> void
|
|
703
|
+
|
|
704
|
+
# _@param_ `_key`
|
|
705
|
+
#
|
|
706
|
+
# _@param_ `_line_count`
|
|
707
|
+
#
|
|
708
|
+
# _@param_ `_viewport_lines`
|
|
709
|
+
def handle_key: (String _key, Integer _line_count, Integer _viewport_lines) -> bool
|
|
710
|
+
|
|
711
|
+
# _@param_ `_line`
|
|
712
|
+
#
|
|
713
|
+
# _@param_ `_event`
|
|
714
|
+
#
|
|
715
|
+
# _@param_ `_line_count`
|
|
716
|
+
def handle_mouse: (Integer _line, MouseEvent _event, Integer _line_count) -> bool
|
|
717
|
+
|
|
718
|
+
# _@param_ `_line_count`
|
|
719
|
+
def candidate_positions: (Integer _line_count) -> ::Array[Integer]
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
# Cursor which can only land on specific allowed lines.
|
|
723
|
+
class Limited < Cursor
|
|
724
|
+
# _@param_ `positions` — allowed positions. Must not be empty.
|
|
725
|
+
#
|
|
726
|
+
# _@param_ `position` — initial position.
|
|
727
|
+
def initialize: (::Array[Integer] positions, ?position: Integer) -> void
|
|
728
|
+
|
|
729
|
+
# _@param_ `line`
|
|
730
|
+
#
|
|
731
|
+
# _@param_ `event`
|
|
732
|
+
#
|
|
733
|
+
# _@param_ `_line_count`
|
|
734
|
+
def handle_mouse: (Integer line, MouseEvent event, Integer _line_count) -> bool
|
|
735
|
+
|
|
736
|
+
# _@param_ `line_count`
|
|
737
|
+
def candidate_positions: (Integer line_count) -> ::Array[Integer]
|
|
738
|
+
|
|
739
|
+
# _@param_ `lines`
|
|
740
|
+
#
|
|
741
|
+
# _@param_ `line_count`
|
|
742
|
+
def go_down_by: (Integer lines, Integer line_count) -> bool
|
|
743
|
+
|
|
744
|
+
# _@param_ `lines`
|
|
745
|
+
def go_up_by: (Integer lines) -> bool
|
|
746
|
+
|
|
747
|
+
def go_to_first: () -> bool
|
|
748
|
+
|
|
749
|
+
# _@param_ `_line_count`
|
|
750
|
+
def go_to_last: (Integer _line_count) -> bool
|
|
751
|
+
end
|
|
752
|
+
end
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
# A label which shows static text. No word-wrapping; clips long lines.
|
|
756
|
+
class Label < Component
|
|
757
|
+
def initialize: () -> void
|
|
758
|
+
|
|
759
|
+
# _@param_ `text` — draws this text. May contain ANSI formatting. Clipped automatically.
|
|
760
|
+
def text=: (String? text) -> void
|
|
761
|
+
|
|
762
|
+
def content_size: () -> Size
|
|
763
|
+
|
|
764
|
+
def repaint: () -> void
|
|
765
|
+
|
|
766
|
+
def on_width_changed: () -> void
|
|
767
|
+
|
|
768
|
+
def update_clipped_text: () -> void
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
# A modal overlay that wraps any {Component} as its content. Popup itself
|
|
772
|
+
# paints nothing — it's a transparent host that handles modality
|
|
773
|
+
# ({#open} / {#close} / {#open?}, ESC/q to close), centers itself on the
|
|
774
|
+
# screen, and auto-sizes to the wrapped content.
|
|
775
|
+
#
|
|
776
|
+
# The wrapped content fills the popup's full {#rect}; if you want a frame
|
|
777
|
+
# and caption, wrap a {Component::Window} (or any subclass — including
|
|
778
|
+
# {Component::LogWindow}) and let it draw its own border:
|
|
779
|
+
#
|
|
780
|
+
# window = Component::Window.new("Help")
|
|
781
|
+
# window.content = Component::List.new.tap { _1.lines = lines }
|
|
782
|
+
# Component::Popup.new(content: window).open
|
|
783
|
+
#
|
|
784
|
+
# Bare content also works (a {Component::Label}, a {Component::List}…), in
|
|
785
|
+
# which case the popup is borderless.
|
|
786
|
+
#
|
|
787
|
+
# `q` and ESC close the popup. Any nested {Component::TextField} that owns
|
|
788
|
+
# the hardware cursor swallows printable keys first via the standard
|
|
789
|
+
# cursor-owner suppression in {Component#handle_key}, so typing `q` into a
|
|
790
|
+
# text field doesn't dismiss the popup.
|
|
791
|
+
class Popup < Component
|
|
792
|
+
include Tuile::Component::HasContent
|
|
793
|
+
|
|
794
|
+
# _@param_ `content` — initial content; can be set later via {#content=}. When provided here, the popup auto-sizes to fit.
|
|
795
|
+
def initialize: (?content: Component?) -> void
|
|
796
|
+
|
|
797
|
+
def focusable?: () -> bool
|
|
798
|
+
|
|
799
|
+
# Mounts this popup on the {Screen}.
|
|
800
|
+
def open: () -> void
|
|
801
|
+
|
|
802
|
+
# Constructs and opens a popup in one call.
|
|
803
|
+
#
|
|
804
|
+
# _@param_ `content`
|
|
805
|
+
#
|
|
806
|
+
# _@return_ — the opened popup.
|
|
807
|
+
def self.open: (?content: Component?) -> Popup
|
|
808
|
+
|
|
809
|
+
# Removes this popup from the {Screen}. No-op if not currently open.
|
|
810
|
+
def close: () -> void
|
|
811
|
+
|
|
812
|
+
# _@return_ — true if this popup is currently mounted on the screen.
|
|
813
|
+
def open?: () -> bool
|
|
814
|
+
|
|
815
|
+
# Recenters the popup on the screen, preserving its current width/height.
|
|
816
|
+
# Called automatically by the screen's layout pass and by {#content=}
|
|
817
|
+
# when the popup is open.
|
|
818
|
+
def center: () -> void
|
|
819
|
+
|
|
820
|
+
# _@return_ — max height the popup will grow to fit its content,
|
|
821
|
+
# defaults to 12. Override in a subclass to allow taller popups.
|
|
822
|
+
def max_height: () -> Integer
|
|
823
|
+
|
|
824
|
+
# Sets the popup's content and auto-sizes the popup to fit.
|
|
825
|
+
#
|
|
826
|
+
# _@param_ `new_content`
|
|
827
|
+
def content=: (Component? new_content) -> void
|
|
828
|
+
|
|
829
|
+
# Hint for the status bar: own "q Close" plus the wrapped content's hint.
|
|
830
|
+
def keyboard_hint: () -> String
|
|
831
|
+
|
|
832
|
+
# _@param_ `key`
|
|
833
|
+
#
|
|
834
|
+
# _@return_ — true if the key was handled.
|
|
835
|
+
def handle_key: (String key) -> bool
|
|
836
|
+
|
|
837
|
+
# Content fills the popup's full rect — Popup has no border to subtract.
|
|
838
|
+
#
|
|
839
|
+
# _@param_ `content`
|
|
840
|
+
def layout: (Component content) -> void
|
|
841
|
+
|
|
842
|
+
# Recompute width/height from {#content}'s natural size and recenter
|
|
843
|
+
# if currently open. Called whenever content is (re)assigned.
|
|
844
|
+
def update_rect: () -> void
|
|
845
|
+
|
|
846
|
+
# _@param_ `event`
|
|
847
|
+
def handle_mouse: (MouseEvent event) -> void
|
|
848
|
+
|
|
849
|
+
def children: () -> ::Array[Component]
|
|
850
|
+
|
|
851
|
+
# _@param_ `rect`
|
|
852
|
+
def rect=: (Rect rect) -> void
|
|
853
|
+
|
|
854
|
+
def on_focus: () -> void
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
# A layout doesn't paint anything by itself: its job is to position child
|
|
858
|
+
# components.
|
|
859
|
+
#
|
|
860
|
+
# All children must completely cover the contents of a layout: that way,
|
|
861
|
+
# the layout itself doesn't have to draw and no clipping algorithm is
|
|
862
|
+
# necessary.
|
|
863
|
+
class Layout < Component
|
|
864
|
+
def initialize: () -> void
|
|
865
|
+
|
|
866
|
+
def children: () -> ::Array[Component]
|
|
867
|
+
|
|
868
|
+
# Adds a child component to this layout.
|
|
869
|
+
#
|
|
870
|
+
# _@param_ `child`
|
|
871
|
+
def add: ((Component | ::Array[Component]) child) -> void
|
|
872
|
+
|
|
873
|
+
# _@param_ `child`
|
|
874
|
+
def remove: (Component child) -> void
|
|
875
|
+
|
|
876
|
+
def content_size: () -> Size
|
|
877
|
+
|
|
878
|
+
def repaint: () -> void
|
|
879
|
+
|
|
880
|
+
# Dispatches the event to the child under the mouse cursor.
|
|
881
|
+
#
|
|
882
|
+
# _@param_ `event`
|
|
883
|
+
def handle_mouse: (MouseEvent event) -> void
|
|
884
|
+
|
|
885
|
+
# Called when a character is pressed on the keyboard.
|
|
886
|
+
#
|
|
887
|
+
# _@param_ `key` — a key.
|
|
888
|
+
#
|
|
889
|
+
# _@return_ — true if the key was handled, false if not.
|
|
890
|
+
def handle_key: (String key) -> bool
|
|
891
|
+
|
|
892
|
+
def on_focus: () -> void
|
|
893
|
+
|
|
894
|
+
# Absolute layout. Extend this class, register any children, and
|
|
895
|
+
# override {Component#rect=} to reposition the children.
|
|
896
|
+
class Absolute < Layout
|
|
897
|
+
end
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
# A window with a frame, a {#caption} and a content {Component}. Doesn't
|
|
901
|
+
# support overlapping with other windows: it paints its entire contents and
|
|
902
|
+
# doesn't clip if there are other overlapping windows.
|
|
903
|
+
#
|
|
904
|
+
# The window's `content` is unset by default; assign one via {#content=}.
|
|
905
|
+
#
|
|
906
|
+
# Window is considered invisible if {#rect} is empty or one of left/top is
|
|
907
|
+
# negative. The window won't draw when invisible.
|
|
908
|
+
class Window < Component
|
|
909
|
+
include Tuile::Component::HasContent
|
|
910
|
+
|
|
911
|
+
# _@param_ `caption`
|
|
912
|
+
def initialize: (?String caption) -> void
|
|
913
|
+
|
|
914
|
+
def focusable?: () -> bool
|
|
915
|
+
|
|
916
|
+
def children: () -> ::Array[Component]
|
|
917
|
+
|
|
918
|
+
# _@param_ `key`
|
|
919
|
+
def handle_key: (String key) -> bool
|
|
920
|
+
|
|
921
|
+
# _@param_ `event`
|
|
922
|
+
def handle_mouse: (MouseEvent event) -> void
|
|
923
|
+
|
|
924
|
+
# _@param_ `new_rect`
|
|
925
|
+
def rect=: (Rect new_rect) -> void
|
|
926
|
+
|
|
927
|
+
# _@param_ `value`
|
|
928
|
+
def scrollbar=: (bool value) -> void
|
|
929
|
+
|
|
930
|
+
# _@return_ — the size needed to fit the window's content, footer
|
|
931
|
+
# (width only — footer overlays the bottom border), and caption,
|
|
932
|
+
# plus the 2-character border. Returns {Size}`.new(2, 2)` when the
|
|
933
|
+
# window has no content, footer, or caption.
|
|
934
|
+
def content_size: () -> Size
|
|
935
|
+
|
|
936
|
+
# _@return_ — true if {#rect} is off screen and the window won't
|
|
937
|
+
# paint.
|
|
938
|
+
def visible?: () -> bool
|
|
939
|
+
|
|
940
|
+
# Fully repaints the window: both frame and contents.
|
|
941
|
+
def repaint: () -> void
|
|
942
|
+
|
|
943
|
+
# _@param_ `key`
|
|
944
|
+
def key_shortcut=: (String? key) -> void
|
|
945
|
+
|
|
946
|
+
# _@param_ `content`
|
|
947
|
+
def layout: (Component content) -> void
|
|
948
|
+
|
|
949
|
+
# Paints the window border.
|
|
950
|
+
def repaint_border: () -> void
|
|
951
|
+
|
|
952
|
+
# The caption text as it appears in the rendered border, including the
|
|
953
|
+
# shortcut prefix when {#key_shortcut} is set.
|
|
954
|
+
def frame_caption: () -> String
|
|
955
|
+
|
|
956
|
+
# Builds the border as a single string with embedded cursor-positioning
|
|
957
|
+
# escapes, mirroring the layout {TTY::Box.frame} used to produce. Title
|
|
958
|
+
# is clipped to fit the inner width so the box never overflows {#rect}.
|
|
959
|
+
#
|
|
960
|
+
# _@param_ `caption`
|
|
961
|
+
def build_frame: (String caption) -> String
|
|
962
|
+
|
|
963
|
+
def layout_footer: () -> void
|
|
964
|
+
|
|
965
|
+
def on_focus: () -> void
|
|
966
|
+
|
|
967
|
+
# _@return_ — optional component overlaying the bottom border
|
|
968
|
+
# row.
|
|
969
|
+
attr_accessor footer: Component?
|
|
970
|
+
|
|
971
|
+
# _@return_ — the current caption, empty by default.
|
|
972
|
+
attr_accessor caption: String
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
# Shows a log. Construct your logger pointed at a {LogWindow::IO} to route
|
|
976
|
+
# log lines into this window:
|
|
977
|
+
#
|
|
978
|
+
# log_window = Tuile::Component::LogWindow.new
|
|
979
|
+
# logger = Logger.new(Tuile::Component::LogWindow::IO.new(log_window))
|
|
980
|
+
#
|
|
981
|
+
# Any logger that writes formatted lines to an IO works the same way —
|
|
982
|
+
# for example `TTY::Logger` configured with the `:console` handler and
|
|
983
|
+
# `output: LogWindow::IO.new(window)`.
|
|
984
|
+
class LogWindow < Tuile::Component::Window
|
|
985
|
+
# _@param_ `caption`
|
|
986
|
+
def initialize: (?String caption) -> void
|
|
987
|
+
|
|
988
|
+
# IO-shaped adapter that forwards each log line to the owning {LogWindow}.
|
|
989
|
+
# Implements both {#write} (stdlib `Logger`) and {#puts} (loggers that
|
|
990
|
+
# call `output.puts`, e.g. `TTY::Logger`).
|
|
991
|
+
class IO
|
|
992
|
+
# _@param_ `window`
|
|
993
|
+
def initialize: (LogWindow window) -> void
|
|
994
|
+
|
|
995
|
+
# _@param_ `string`
|
|
996
|
+
def write: (String string) -> void
|
|
997
|
+
|
|
998
|
+
# _@param_ `string`
|
|
999
|
+
def puts: (String string) -> void
|
|
1000
|
+
|
|
1001
|
+
# Stdlib `Logger` only treats an object as an IO target when it
|
|
1002
|
+
# responds to both {#write} and {#close}; otherwise it tries to
|
|
1003
|
+
# interpret it as a filename. This is a no-op.
|
|
1004
|
+
def close: () -> void
|
|
1005
|
+
end
|
|
1006
|
+
end
|
|
1007
|
+
|
|
1008
|
+
# A single-line text input field with hardware-cursor caret.
|
|
1009
|
+
#
|
|
1010
|
+
# The field does not scroll. Any keystroke that would make {#text} longer
|
|
1011
|
+
# than `rect.width - 1` (the last column is reserved for the caret past the
|
|
1012
|
+
# last char) is rejected.
|
|
1013
|
+
#
|
|
1014
|
+
# The caret is a logical index in `0..text.length`. The hardware cursor is
|
|
1015
|
+
# positioned by {Screen} after each repaint cycle when this component is
|
|
1016
|
+
# focused; see {Component#cursor_position}.
|
|
1017
|
+
class TextField < Component
|
|
1018
|
+
def initialize: () -> void
|
|
1019
|
+
|
|
1020
|
+
def focusable?: () -> bool
|
|
1021
|
+
|
|
1022
|
+
def cursor_position: () -> Point?
|
|
1023
|
+
|
|
1024
|
+
# _@param_ `key`
|
|
1025
|
+
def handle_key: (String key) -> bool
|
|
1026
|
+
|
|
1027
|
+
# _@param_ `event`
|
|
1028
|
+
def handle_mouse: (MouseEvent event) -> void
|
|
1029
|
+
|
|
1030
|
+
def repaint: () -> void
|
|
1031
|
+
|
|
1032
|
+
def on_width_changed: () -> void
|
|
1033
|
+
|
|
1034
|
+
# Maximum number of characters {#text} can hold given current width.
|
|
1035
|
+
def max_text_length: () -> Integer
|
|
1036
|
+
|
|
1037
|
+
# _@param_ `char`
|
|
1038
|
+
def insert: (String char) -> bool
|
|
1039
|
+
|
|
1040
|
+
def delete_before_caret: () -> void
|
|
1041
|
+
|
|
1042
|
+
def delete_at_caret: () -> void
|
|
1043
|
+
|
|
1044
|
+
# _@param_ `key`
|
|
1045
|
+
def printable?: (String key) -> bool
|
|
1046
|
+
|
|
1047
|
+
# _@return_ — current text contents.
|
|
1048
|
+
attr_accessor text: String
|
|
1049
|
+
|
|
1050
|
+
# _@return_ — caret index in `0..text.length`.
|
|
1051
|
+
attr_accessor caret: Integer
|
|
1052
|
+
|
|
1053
|
+
# Optional callback fired when ESC is pressed. When set, ESC is consumed
|
|
1054
|
+
# by the field; when nil, ESC falls through to the parent (default
|
|
1055
|
+
# behavior).
|
|
1056
|
+
#
|
|
1057
|
+
# _@return_ — no-arg callable, or nil.
|
|
1058
|
+
attr_accessor on_escape: (Proc | Method)?
|
|
1059
|
+
|
|
1060
|
+
# Optional callback fired whenever {#text} changes. Receives the new text
|
|
1061
|
+
# as a single argument. Not fired by {#caret=} (text unchanged) and not
|
|
1062
|
+
# fired when a setter is a no-op.
|
|
1063
|
+
#
|
|
1064
|
+
# _@return_ — one-arg callable, or nil.
|
|
1065
|
+
attr_accessor on_change: (Proc | Method)?
|
|
1066
|
+
|
|
1067
|
+
# Optional callback fired when the UP arrow key is pressed. When set, UP
|
|
1068
|
+
# is consumed by the field; when nil, UP falls through to the parent
|
|
1069
|
+
# (default behavior). Only triggered by {Keys::UP_ARROW}, not by `k`,
|
|
1070
|
+
# since `k` is a printable character inserted into {#text}.
|
|
1071
|
+
#
|
|
1072
|
+
# _@return_ — no-arg callable, or nil.
|
|
1073
|
+
attr_accessor on_key_up: (Proc | Method)?
|
|
1074
|
+
|
|
1075
|
+
# Optional callback fired when the DOWN arrow key is pressed. When set,
|
|
1076
|
+
# DOWN is consumed by the field; when nil, DOWN falls through to the
|
|
1077
|
+
# parent (default behavior). Only triggered by {Keys::DOWN_ARROW}, not by
|
|
1078
|
+
# `j`, since `j` is a printable character inserted into {#text}.
|
|
1079
|
+
#
|
|
1080
|
+
# _@return_ — no-arg callable, or nil.
|
|
1081
|
+
attr_accessor on_key_down: (Proc | Method)?
|
|
1082
|
+
|
|
1083
|
+
# Optional callback fired when ENTER is pressed. When set, ENTER is
|
|
1084
|
+
# consumed by the field; when nil, ENTER falls through to the parent
|
|
1085
|
+
# (default behavior).
|
|
1086
|
+
#
|
|
1087
|
+
# _@return_ — no-arg callable, or nil.
|
|
1088
|
+
attr_accessor on_enter: (Proc | Method)?
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1091
|
+
# A mixin interface for a component with one child tops. The host must
|
|
1092
|
+
# provide a protected `layout(content)` method which repositions the
|
|
1093
|
+
# content component; the mixin manages `@content` itself.
|
|
1094
|
+
module HasContent
|
|
1095
|
+
# _@param_ `key` — a key.
|
|
1096
|
+
#
|
|
1097
|
+
# _@return_ — true if the key was handled, false if not.
|
|
1098
|
+
def handle_key: (String key) -> bool
|
|
1099
|
+
|
|
1100
|
+
# _@param_ `event`
|
|
1101
|
+
def handle_mouse: (MouseEvent event) -> void
|
|
1102
|
+
|
|
1103
|
+
def children: () -> ::Array[Component]
|
|
1104
|
+
|
|
1105
|
+
# _@param_ `rect`
|
|
1106
|
+
def rect=: (Rect rect) -> void
|
|
1107
|
+
|
|
1108
|
+
def on_focus: () -> void
|
|
1109
|
+
|
|
1110
|
+
# _@return_ — the current content component.
|
|
1111
|
+
attr_accessor content: Component?
|
|
1112
|
+
end
|
|
1113
|
+
|
|
1114
|
+
# A {Window} preconfigured with a {List} of static lines. Useful for
|
|
1115
|
+
# showing read-only information.
|
|
1116
|
+
#
|
|
1117
|
+
# Usable tiled (just add to a {Layout}) or as a popup via {.open}, which
|
|
1118
|
+
# wraps it in a {Popup}.
|
|
1119
|
+
class InfoWindow < Tuile::Component::Window
|
|
1120
|
+
# _@param_ `caption`
|
|
1121
|
+
#
|
|
1122
|
+
# _@param_ `lines` — initial content; each entry may contain Rainbow formatting.
|
|
1123
|
+
def initialize: (?String caption, ?::Array[String] lines) -> void
|
|
1124
|
+
|
|
1125
|
+
# Opens the info window as a popup.
|
|
1126
|
+
#
|
|
1127
|
+
# _@param_ `caption`
|
|
1128
|
+
#
|
|
1129
|
+
# _@param_ `lines` — the content, may contain formatting.
|
|
1130
|
+
#
|
|
1131
|
+
# _@return_ — the opened popup.
|
|
1132
|
+
def self.open: (String caption, ::Array[String] lines) -> Popup
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
# A {Window} that lists options identified by single keyboard keys, asks
|
|
1136
|
+
# the user to pick one, and fires a callback with the picked key.
|
|
1137
|
+
#
|
|
1138
|
+
# Usable tiled (just add to a {Layout} and read picks via the block) or
|
|
1139
|
+
# as a popup via {.open}, which wraps it in a {Popup} that closes itself
|
|
1140
|
+
# after a pick. ESC / `q` close without firing the callback.
|
|
1141
|
+
class PickerWindow < Tuile::Component::Window
|
|
1142
|
+
MAX_ITEMS: Integer
|
|
1143
|
+
|
|
1144
|
+
# _@param_ `caption` — the window caption.
|
|
1145
|
+
#
|
|
1146
|
+
# _@param_ `options` — pairs of keyboard key and option caption. No Rainbow formatting must be used.
|
|
1147
|
+
def initialize: (String caption, ::Array[[String, String]] options) ?{ (String key) -> void } -> void
|
|
1148
|
+
|
|
1149
|
+
# _@param_ `key`
|
|
1150
|
+
def handle_key: (String key) -> bool
|
|
1151
|
+
|
|
1152
|
+
def keyboard_hint: () -> String
|
|
1153
|
+
|
|
1154
|
+
# Opens a picker as a popup. Picking an option fires `block`, then
|
|
1155
|
+
# closes the popup; ESC / `q` close without firing `block`.
|
|
1156
|
+
#
|
|
1157
|
+
# _@param_ `caption`
|
|
1158
|
+
#
|
|
1159
|
+
# _@param_ `options`
|
|
1160
|
+
#
|
|
1161
|
+
# _@return_ — the wrapping popup.
|
|
1162
|
+
def self.open: (String caption, ::Array[[String, String]] options) ?{ (String key) -> void } -> Popup
|
|
1163
|
+
|
|
1164
|
+
# _@param_ `key`
|
|
1165
|
+
def select_option: (String key) -> void
|
|
1166
|
+
|
|
1167
|
+
# Callback invoked after the user picks an option (after the block
|
|
1168
|
+
# fires). The {Popup} returned by {.open} sets this to its own `close`.
|
|
1169
|
+
attr_accessor on_pick: Proc?
|
|
1170
|
+
|
|
1171
|
+
# One picker option.
|
|
1172
|
+
#
|
|
1173
|
+
# @!attribute [r] key
|
|
1174
|
+
# @return [String] the keyboard key that picks this option.
|
|
1175
|
+
# @!attribute [r] caption
|
|
1176
|
+
# @return [String] the option caption.
|
|
1177
|
+
class Option
|
|
1178
|
+
# _@return_ — the keyboard key that picks this option.
|
|
1179
|
+
attr_reader key: String
|
|
1180
|
+
|
|
1181
|
+
# _@return_ — the option caption.
|
|
1182
|
+
attr_reader caption: String
|
|
1183
|
+
end
|
|
1184
|
+
end
|
|
1185
|
+
end
|
|
1186
|
+
|
|
1187
|
+
# An event queue. The idea is that all UI-related updates run from the thread
|
|
1188
|
+
# which runs the event queue only; this removes any need for locking and/or
|
|
1189
|
+
# need for thread-safety mechanisms.
|
|
1190
|
+
#
|
|
1191
|
+
# Any events (keypress, timer, term resize – WINCH) are captured in background
|
|
1192
|
+
# threads; instead of processing the events directly the events are pushed
|
|
1193
|
+
# into the event queue: this causes the events to be processed centrally,
|
|
1194
|
+
# by a single thread only.
|
|
1195
|
+
class EventQueue
|
|
1196
|
+
# _@param_ `listen_for_keys` — if true, fires {KeyEvent}.
|
|
1197
|
+
def initialize: (?listen_for_keys: bool) -> void
|
|
1198
|
+
|
|
1199
|
+
# Posts event into the event queue. The event may be of any type. Since the
|
|
1200
|
+
# event is passed between threads, the event object should be frozen.
|
|
1201
|
+
#
|
|
1202
|
+
# The function may be called from any thread.
|
|
1203
|
+
#
|
|
1204
|
+
# _@param_ `event` — the event to post to the queue, should be frozen.
|
|
1205
|
+
def post: (Object event) -> void
|
|
1206
|
+
|
|
1207
|
+
# Submits block to be run in the event queue. Returns immediately.
|
|
1208
|
+
#
|
|
1209
|
+
# The function may be called from any thread.
|
|
1210
|
+
def submit: () -> void
|
|
1211
|
+
|
|
1212
|
+
# Awaits until the event queue is empty (all events have been processed).
|
|
1213
|
+
def await_empty: () -> void
|
|
1214
|
+
|
|
1215
|
+
# Runs the event loop and blocks. Must be run from at most one thread at the
|
|
1216
|
+
# same time. Blocks until some thread calls {#stop}. Calls block for all
|
|
1217
|
+
# events submitted via {#post}; the block is always called from the thread
|
|
1218
|
+
# running this function.
|
|
1219
|
+
#
|
|
1220
|
+
# Any exception raised by block is re-thrown, causing this function to
|
|
1221
|
+
# terminate.
|
|
1222
|
+
def run_loop: () ?{ (Object event) -> void } -> void
|
|
1223
|
+
|
|
1224
|
+
# _@return_ — true if this thread is running inside an event queue.
|
|
1225
|
+
def locked?: () -> bool
|
|
1226
|
+
|
|
1227
|
+
# Stops ongoing {#run_loop}. The stop may not be immediate: {#run_loop} may
|
|
1228
|
+
# process a bunch of events before terminating.
|
|
1229
|
+
#
|
|
1230
|
+
# Can be called from any thread, including the thread which runs the event
|
|
1231
|
+
# loop.
|
|
1232
|
+
def stop: () -> void
|
|
1233
|
+
|
|
1234
|
+
def event_loop: () -> void
|
|
1235
|
+
|
|
1236
|
+
# Starts listening for stdin, firing {KeyEvent} on keypress.
|
|
1237
|
+
def start_key_thread: () -> void
|
|
1238
|
+
|
|
1239
|
+
# Trap the WINCH signal (TTY resize signal) and fire {TTYSizeEvent}.
|
|
1240
|
+
def trap_winch: () -> void
|
|
1241
|
+
|
|
1242
|
+
# A keypress event. See {Keys} for a list of key codes.
|
|
1243
|
+
#
|
|
1244
|
+
# @!attribute [r] key
|
|
1245
|
+
# @return [String] key code.
|
|
1246
|
+
class KeyEvent
|
|
1247
|
+
# _@return_ — key code.
|
|
1248
|
+
attr_reader key: String
|
|
1249
|
+
end
|
|
1250
|
+
|
|
1251
|
+
# An error event, causes {EventQueue#run_loop} to throw `StandardError` with
|
|
1252
|
+
# {#error} as its origin.
|
|
1253
|
+
#
|
|
1254
|
+
# @!attribute [r] error
|
|
1255
|
+
# @return [StandardError] the underlying error.
|
|
1256
|
+
class ErrorEvent
|
|
1257
|
+
# _@return_ — the underlying error.
|
|
1258
|
+
attr_reader error: StandardError
|
|
1259
|
+
end
|
|
1260
|
+
|
|
1261
|
+
# TTY has been resized. Contains the current width and height of the TTY
|
|
1262
|
+
# terminal.
|
|
1263
|
+
#
|
|
1264
|
+
# @!attribute [r] width
|
|
1265
|
+
# @return [Integer] terminal width in columns.
|
|
1266
|
+
# @!attribute [r] height
|
|
1267
|
+
# @return [Integer] terminal height in rows.
|
|
1268
|
+
class TTYSizeEvent
|
|
1269
|
+
# _@param_ `width`
|
|
1270
|
+
#
|
|
1271
|
+
# _@param_ `height`
|
|
1272
|
+
def initialize: (width: Integer, height: Integer) -> void
|
|
1273
|
+
|
|
1274
|
+
# _@return_ — event with current TTY size.
|
|
1275
|
+
def self.create: () -> TTYSizeEvent
|
|
1276
|
+
|
|
1277
|
+
def size: () -> Size
|
|
1278
|
+
|
|
1279
|
+
# _@return_ — terminal width in columns.
|
|
1280
|
+
attr_reader width: Integer
|
|
1281
|
+
|
|
1282
|
+
# _@return_ — terminal height in rows.
|
|
1283
|
+
attr_reader height: Integer
|
|
1284
|
+
end
|
|
1285
|
+
|
|
1286
|
+
# Emitted once when the queue is cleared, all messages are processed and the
|
|
1287
|
+
# event loop will block waiting for more messages. Perfect time for
|
|
1288
|
+
# repainting windows.
|
|
1289
|
+
class EmptyQueueEvent
|
|
1290
|
+
include Singleton
|
|
1291
|
+
end
|
|
1292
|
+
end
|
|
1293
|
+
|
|
1294
|
+
# Testing only — a screen which doesn't paint anything and pretends that the
|
|
1295
|
+
# lock is held. This way, the TTY running the tests is not painted over.
|
|
1296
|
+
#
|
|
1297
|
+
# Intended for unit-testing individual components: instantiate a component,
|
|
1298
|
+
# mutate it, and assert against {#prints} or {#invalidated?}. It does not
|
|
1299
|
+
# run an event loop, so it is *not* suitable for system-testing whole apps
|
|
1300
|
+
# — for that, drive the real script through a PTY (see `spec/examples/`).
|
|
1301
|
+
#
|
|
1302
|
+
# Call {Screen.fake} to initialize the fake screen easily. Typical usage:
|
|
1303
|
+
#
|
|
1304
|
+
# before { Screen.fake }
|
|
1305
|
+
# after { Screen.close }
|
|
1306
|
+
#
|
|
1307
|
+
# it "paints its content" do
|
|
1308
|
+
# label = Component::Label.new.tap { |l| l.text = "hi" }
|
|
1309
|
+
# Screen.instance.content = Component::Window.new("Greeting").tap { |w| w.content = label }
|
|
1310
|
+
# Screen.instance.repaint
|
|
1311
|
+
# assert_includes Screen.instance.prints.join, "hi"
|
|
1312
|
+
# end
|
|
1313
|
+
class FakeScreen < Tuile::Screen
|
|
1314
|
+
def initialize: () -> void
|
|
1315
|
+
|
|
1316
|
+
def check_locked: () -> void
|
|
1317
|
+
|
|
1318
|
+
def clear: () -> void
|
|
1319
|
+
|
|
1320
|
+
# Doesn't print anything: collects all strings in {#prints}.
|
|
1321
|
+
#
|
|
1322
|
+
# _@param_ `args`
|
|
1323
|
+
def print: (*String args) -> void
|
|
1324
|
+
|
|
1325
|
+
# _@param_ `component` — the component to check.
|
|
1326
|
+
def invalidated?: (Component component) -> bool
|
|
1327
|
+
|
|
1328
|
+
def invalidated_clear: () -> void
|
|
1329
|
+
|
|
1330
|
+
# _@return_ — whatever {#print} printed so far.
|
|
1331
|
+
attr_reader prints: ::Array[String]
|
|
1332
|
+
end
|
|
1333
|
+
|
|
1334
|
+
# A mouse event.
|
|
1335
|
+
#
|
|
1336
|
+
# @!attribute [r] button
|
|
1337
|
+
# @return [Symbol, nil] one of `:left`, `:middle`, `:right`, `:scroll_up`,
|
|
1338
|
+
# `:scroll_down`; `nil` if not known.
|
|
1339
|
+
# @!attribute [r] x
|
|
1340
|
+
# @return [Integer] x coordinate, 0-based.
|
|
1341
|
+
# @!attribute [r] y
|
|
1342
|
+
# @return [Integer] y coordinate, 0-based.
|
|
1343
|
+
class MouseEvent
|
|
1344
|
+
# _@return_ — the event's position.
|
|
1345
|
+
def point: () -> Point
|
|
1346
|
+
|
|
1347
|
+
# Checks whether given key is a mouse event key
|
|
1348
|
+
#
|
|
1349
|
+
# _@param_ `key` — key read via {Keys.getkey}
|
|
1350
|
+
#
|
|
1351
|
+
# _@return_ — true if it is a mouse event
|
|
1352
|
+
def self.mouse_event?: (String key) -> bool
|
|
1353
|
+
|
|
1354
|
+
# _@param_ `key` — key read via {Keys.getkey}
|
|
1355
|
+
def self.parse: (String key) -> MouseEvent?
|
|
1356
|
+
|
|
1357
|
+
def self.start_tracking: () -> String
|
|
1358
|
+
|
|
1359
|
+
def self.stop_tracking: () -> String
|
|
1360
|
+
|
|
1361
|
+
# _@return_ — one of `:left`, `:middle`, `:right`, `:scroll_up`,
|
|
1362
|
+
# `:scroll_down`; `nil` if not known.
|
|
1363
|
+
attr_reader button: Symbol?
|
|
1364
|
+
|
|
1365
|
+
# _@return_ — x coordinate, 0-based.
|
|
1366
|
+
attr_reader x: Integer
|
|
1367
|
+
|
|
1368
|
+
# _@return_ — y coordinate, 0-based.
|
|
1369
|
+
attr_reader y: Integer
|
|
1370
|
+
end
|
|
1371
|
+
|
|
1372
|
+
# The structural root of the {Screen}'s component tree.
|
|
1373
|
+
#
|
|
1374
|
+
# {Screen} is a singleton runtime owner (event loop, lock, terminal IO,
|
|
1375
|
+
# invalidation set). All actual UI lives under a {ScreenPane}: the tiled
|
|
1376
|
+
# {#content}, the modal {#popups} stack, and the bottom {#status_bar}.
|
|
1377
|
+
# Putting them under a single Component parent gives focus traversal a real
|
|
1378
|
+
# root, makes {Component#attached?} a one-liner, and lets popup-focus repair
|
|
1379
|
+
# fall out of the standard {Component#on_child_removed} hook.
|
|
1380
|
+
#
|
|
1381
|
+
# The pane is not a {Component::Layout}: popups deliberately overlap content
|
|
1382
|
+
# (Z-ordered, full overdraw, no clipping) and key/mouse dispatch follows
|
|
1383
|
+
# modal-popup rules rather than active-child dispatch.
|
|
1384
|
+
class ScreenPane < Tuile::Component
|
|
1385
|
+
def initialize: () -> void
|
|
1386
|
+
|
|
1387
|
+
def focusable?: () -> bool
|
|
1388
|
+
|
|
1389
|
+
# Children for tree traversal: content first, popups in stacking order,
|
|
1390
|
+
# status bar last.
|
|
1391
|
+
def children: () -> ::Array[Component]
|
|
1392
|
+
|
|
1393
|
+
# Adds a popup, centers it, focuses it, and invalidates it for repaint.
|
|
1394
|
+
#
|
|
1395
|
+
# _@param_ `window`
|
|
1396
|
+
def add_popup: (Component::Popup window) -> void
|
|
1397
|
+
|
|
1398
|
+
# Removes a popup. If the popup held focus, focus shifts to the now-topmost
|
|
1399
|
+
# remaining popup, falling back to the focus snapshotted when the popup
|
|
1400
|
+
# was opened (if still attached), then to {#content}, then to nil.
|
|
1401
|
+
#
|
|
1402
|
+
# _@param_ `window`
|
|
1403
|
+
def remove_popup: (Component window) -> void
|
|
1404
|
+
|
|
1405
|
+
# _@param_ `window`
|
|
1406
|
+
#
|
|
1407
|
+
# _@return_ — true if this pane currently hosts the popup.
|
|
1408
|
+
def has_popup?: (Component window) -> bool
|
|
1409
|
+
|
|
1410
|
+
# Re-lays out children whenever the pane's own rect changes.
|
|
1411
|
+
#
|
|
1412
|
+
# _@param_ `new_rect`
|
|
1413
|
+
def rect=: (Rect new_rect) -> void
|
|
1414
|
+
|
|
1415
|
+
# Lays out content (full pane minus the bottom row) and the status bar
|
|
1416
|
+
# (bottom row). Popups self-position via {Component::Popup#center}.
|
|
1417
|
+
def layout: () -> void
|
|
1418
|
+
|
|
1419
|
+
# Pane paints nothing itself; its children paint over the entire rect.
|
|
1420
|
+
def repaint: () -> void
|
|
1421
|
+
|
|
1422
|
+
# Topmost popup is modal: it eats keys. Falls through to content only
|
|
1423
|
+
# when no popup is open.
|
|
1424
|
+
def handle_key: (String key) -> bool
|
|
1425
|
+
|
|
1426
|
+
# Mouse events check popups in reverse stacking order (topmost first), and
|
|
1427
|
+
# fall through to content only when no popup is hit *and* there are no
|
|
1428
|
+
# popups open. This preserves modal click-blocking: an open popup eats
|
|
1429
|
+
# clicks even outside its rect.
|
|
1430
|
+
#
|
|
1431
|
+
# _@param_ `event`
|
|
1432
|
+
def handle_mouse: (MouseEvent event) -> void
|
|
1433
|
+
|
|
1434
|
+
# Focus repair when a child detaches. Default {Component#on_child_removed}
|
|
1435
|
+
# would refocus to `self` (the pane), which isn't a useful focus target.
|
|
1436
|
+
# Instead, route focus to the now-topmost popup, then to the prior focus
|
|
1437
|
+
# snapshotted when this popup was opened (if still attached), then to
|
|
1438
|
+
# content, then nil.
|
|
1439
|
+
#
|
|
1440
|
+
# _@param_ `child`
|
|
1441
|
+
def on_child_removed: (Component child) -> void
|
|
1442
|
+
|
|
1443
|
+
# _@return_ — the tiled content component.
|
|
1444
|
+
attr_accessor content: Component?
|
|
1445
|
+
|
|
1446
|
+
# _@return_ — modal popups in stacking order; last is
|
|
1447
|
+
# topmost. The array must not be mutated by callers.
|
|
1448
|
+
attr_reader popups: ::Array[Component]
|
|
1449
|
+
|
|
1450
|
+
# _@return_ — the bottom status bar.
|
|
1451
|
+
attr_reader status_bar: Component::Label
|
|
1452
|
+
end
|
|
1453
|
+
|
|
1454
|
+
# A "synchronous" event queue – no loop is run, submitted blocks are run right
|
|
1455
|
+
# away and submitted events are thrown away. Intended for testing only.
|
|
1456
|
+
class FakeEventQueue
|
|
1457
|
+
def locked?: () -> bool
|
|
1458
|
+
|
|
1459
|
+
def stop: () -> void
|
|
1460
|
+
|
|
1461
|
+
def run_loop: () -> void
|
|
1462
|
+
|
|
1463
|
+
def await_empty: () -> void
|
|
1464
|
+
|
|
1465
|
+
def submit: () -> void
|
|
1466
|
+
|
|
1467
|
+
# _@param_ `event`
|
|
1468
|
+
def post: (Object event) -> void
|
|
1469
|
+
end
|
|
1470
|
+
|
|
1471
|
+
# A vertical scrollbar that computes which character to draw at each row.
|
|
1472
|
+
#
|
|
1473
|
+
# Uses `█` for the handle (filled track) and `░` for the empty track. There
|
|
1474
|
+
# are no up/down arrows; the full height is used as the track. Handle
|
|
1475
|
+
# geometry is precomputed in the constructor as {#handle_height},
|
|
1476
|
+
# {#handle_start}, and {#handle_end}.
|
|
1477
|
+
class VerticalScrollBar
|
|
1478
|
+
# _@param_ `height` — number of rows in the scrollbar (== viewport height).
|
|
1479
|
+
#
|
|
1480
|
+
# _@param_ `line_count` — total number of content lines.
|
|
1481
|
+
#
|
|
1482
|
+
# _@param_ `top_line` — index of the first visible content line.
|
|
1483
|
+
def initialize: (Integer height, line_count: Integer, top_line: Integer) -> void
|
|
1484
|
+
|
|
1485
|
+
# Returns the scrollbar character for the given viewport row.
|
|
1486
|
+
#
|
|
1487
|
+
# _@param_ `row_in_viewport` — 0-based row index within the viewport.
|
|
1488
|
+
#
|
|
1489
|
+
# _@return_ — single scrollbar character.
|
|
1490
|
+
def scrollbar_char: (Integer row_in_viewport) -> String
|
|
1491
|
+
|
|
1492
|
+
# _@return_ — number of track rows the handle occupies (height >= 1
|
|
1493
|
+
# only).
|
|
1494
|
+
attr_reader handle_height: Integer
|
|
1495
|
+
|
|
1496
|
+
# _@return_ — 0-based row where the handle starts (height >= 1 only).
|
|
1497
|
+
attr_reader handle_start: Integer
|
|
1498
|
+
|
|
1499
|
+
# _@return_ — 0-based row where the handle ends (height >= 1 only).
|
|
1500
|
+
attr_reader handle_end: Integer
|
|
1501
|
+
end
|
|
1502
|
+
end
|