tuile 0.3.0 → 0.5.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 +34 -0
- data/README.md +137 -5
- data/lib/tuile/color.rb +127 -0
- data/lib/tuile/component/label.rb +34 -4
- data/lib/tuile/component/list.rb +43 -14
- data/lib/tuile/component/log_window.rb +12 -6
- data/lib/tuile/component/popup.rb +5 -5
- data/lib/tuile/component/text_area.rb +39 -134
- data/lib/tuile/component/text_field.rb +31 -148
- data/lib/tuile/component/text_input.rb +213 -0
- data/lib/tuile/component/text_view.rb +792 -48
- data/lib/tuile/component/window.rb +5 -10
- data/lib/tuile/component.rb +15 -3
- data/lib/tuile/event_queue.rb +104 -9
- data/lib/tuile/fake_event_queue.rb +69 -0
- data/lib/tuile/keys.rb +91 -8
- data/lib/tuile/mouse_event.rb +23 -4
- data/lib/tuile/screen.rb +156 -12
- data/lib/tuile/styled_string.rb +38 -58
- data/lib/tuile/version.rb +1 -1
- data/sig/tuile.rbs +932 -154
- metadata +3 -1
data/sig/tuile.rbs
CHANGED
|
@@ -45,14 +45,51 @@ module Tuile
|
|
|
45
45
|
PAGE_DOWN: String
|
|
46
46
|
BACKSPACE: String
|
|
47
47
|
DELETE: String
|
|
48
|
+
CTRL_A: String
|
|
49
|
+
CTRL_B: String
|
|
50
|
+
CTRL_C: String
|
|
51
|
+
CTRL_D: String
|
|
52
|
+
CTRL_E: String
|
|
53
|
+
CTRL_F: String
|
|
54
|
+
CTRL_G: String
|
|
48
55
|
CTRL_H: String
|
|
49
|
-
|
|
56
|
+
CTRL_I: String
|
|
57
|
+
CTRL_J: String
|
|
58
|
+
CTRL_K: String
|
|
59
|
+
CTRL_L: String
|
|
60
|
+
CTRL_M: String
|
|
61
|
+
CTRL_N: String
|
|
62
|
+
CTRL_O: String
|
|
63
|
+
CTRL_P: String
|
|
64
|
+
CTRL_Q: String
|
|
65
|
+
CTRL_R: String
|
|
66
|
+
CTRL_S: String
|
|
67
|
+
CTRL_T: String
|
|
50
68
|
CTRL_U: String
|
|
51
|
-
|
|
69
|
+
CTRL_V: String
|
|
70
|
+
CTRL_W: String
|
|
71
|
+
CTRL_X: String
|
|
72
|
+
CTRL_Y: String
|
|
73
|
+
CTRL_Z: String
|
|
74
|
+
BACKSPACES: ::Array[String]
|
|
52
75
|
ENTER: String
|
|
53
76
|
TAB: String
|
|
54
77
|
SHIFT_TAB: String
|
|
55
78
|
|
|
79
|
+
# True iff `key` is a single printable character — a one-character string
|
|
80
|
+
# whose codepoint is not in Unicode's C (Other) category. Rejects multi-
|
|
81
|
+
# character escape sequences ({UP_ARROW}, mouse events, …), control bytes
|
|
82
|
+
# ({TAB}, {ENTER}, {ESC}, {CTRL_A}..{CTRL_Z}, {BACKSPACE}), and the empty
|
|
83
|
+
# string; accepts ASCII letters/digits/punctuation/space *and* non-ASCII
|
|
84
|
+
# printables like "é".
|
|
85
|
+
#
|
|
86
|
+
# Used by {Screen#register_global_shortcut} to reject keys that would
|
|
87
|
+
# collide with typing, and by {Tuile::Component::TextField} to decide
|
|
88
|
+
# whether to insert a key at the caret.
|
|
89
|
+
#
|
|
90
|
+
# _@param_ `key`
|
|
91
|
+
def self.printable?: (String key) -> bool
|
|
92
|
+
|
|
56
93
|
# Grabs a key from stdin and returns it. Blocks until the key is obtained.
|
|
57
94
|
# Reads a full ESC key sequence; see constants above for some values returned
|
|
58
95
|
# by this function.
|
|
@@ -151,6 +188,70 @@ module Tuile
|
|
|
151
188
|
attr_reader height: Integer
|
|
152
189
|
end
|
|
153
190
|
|
|
191
|
+
# An immutable terminal color. Accepts the three forms ANSI/SGR understands:
|
|
192
|
+
#
|
|
193
|
+
# - a Symbol from {COLOR_SYMBOLS} — 8 standard + 8 bright named colors
|
|
194
|
+
# (SGR 30..37 / 90..97 for fg, 40..47 / 100..107 for bg)
|
|
195
|
+
# - an Integer 0..255 — the 256-color palette (SGR 38;5;N / 48;5;N)
|
|
196
|
+
# - an Array of three Integers 0..255 — 24-bit RGB (SGR 38;2;R;G;B / 48;2;R;G;B)
|
|
197
|
+
#
|
|
198
|
+
# A constant per named color is pre-defined (`Color::RED`, `Color::BRIGHT_BLUE`,
|
|
199
|
+
# …) so callers can reach for `Color::RED` instead of building one each time.
|
|
200
|
+
# {.coerce} accepts anything {.new} accepts plus `nil` (terminal default) and
|
|
201
|
+
# an existing {Color} (returned as-is), so APIs that accept colors typically
|
|
202
|
+
# take `[Color, nil]` and pass through {.coerce}.
|
|
203
|
+
#
|
|
204
|
+
# ```ruby
|
|
205
|
+
# Color.new(:red) # named
|
|
206
|
+
# Color.new(42) # 256-color palette
|
|
207
|
+
# Color.new([255, 100, 0]) # RGB
|
|
208
|
+
# Color::RED # constant
|
|
209
|
+
# Color.coerce(:red) # accepts raw forms, returns Color
|
|
210
|
+
# Color.coerce(nil) # nil → nil
|
|
211
|
+
# ```
|
|
212
|
+
#
|
|
213
|
+
# {#to_ansi} renders a full SGR escape (`"\e[31m"`); {#sgr_codes} returns the
|
|
214
|
+
# raw numeric codes so callers (notably {StyledString}) can combine them with
|
|
215
|
+
# other SGR attributes in a single sequence.
|
|
216
|
+
class Color
|
|
217
|
+
COLOR_SYMBOLS: ::Array[Symbol]
|
|
218
|
+
|
|
219
|
+
# Coerces the input to a {Color}. `nil` passes through unchanged (callers
|
|
220
|
+
# use `nil` for the terminal default); an existing {Color} is returned
|
|
221
|
+
# as-is; otherwise the value is fed to {.new}.
|
|
222
|
+
#
|
|
223
|
+
# _@param_ `value`
|
|
224
|
+
def self.coerce: ((Color | Symbol | Integer | ::Array[Integer])? value) -> Color?
|
|
225
|
+
|
|
226
|
+
# _@param_ `value` — see class-level docs for the three accepted forms.
|
|
227
|
+
def initialize: ((Symbol | Integer | ::Array[Integer]) value) -> void
|
|
228
|
+
|
|
229
|
+
# SGR parameter codes for emitting this color as either a foreground
|
|
230
|
+
# (`target: :fg`) or background (`target: :bg`). Returned as an array so
|
|
231
|
+
# callers can splice them into a multi-attribute SGR (e.g. bold + color).
|
|
232
|
+
#
|
|
233
|
+
# _@param_ `target` — `:fg` or `:bg`.
|
|
234
|
+
def sgr_codes: (?Symbol target) -> ::Array[Integer]
|
|
235
|
+
|
|
236
|
+
# Full SGR escape sequence for this color (e.g. `"\e[31m"`). Useful for
|
|
237
|
+
# `print`-style direct emission; for composing with other attributes use
|
|
238
|
+
# {#sgr_codes} instead.
|
|
239
|
+
#
|
|
240
|
+
# _@param_ `target` — `:fg` or `:bg`.
|
|
241
|
+
def to_ansi: (?Symbol target) -> String
|
|
242
|
+
|
|
243
|
+
# _@param_ `other`
|
|
244
|
+
def ==: (Object other) -> bool
|
|
245
|
+
|
|
246
|
+
def hash: () -> Integer
|
|
247
|
+
|
|
248
|
+
def inspect: () -> String
|
|
249
|
+
|
|
250
|
+
# The underlying raw representation — a Symbol, Integer, or frozen
|
|
251
|
+
# Array<Integer>.
|
|
252
|
+
attr_reader value: (Symbol | Integer | ::Array[Integer])
|
|
253
|
+
end
|
|
254
|
+
|
|
154
255
|
# A point with `x` and `y` integer coordinates, both 0-based.
|
|
155
256
|
#
|
|
156
257
|
# @!attribute [r] x
|
|
@@ -216,6 +317,22 @@ module Tuile
|
|
|
216
317
|
# _@param_ `component`
|
|
217
318
|
def invalidate: (Component component) -> void
|
|
218
319
|
|
|
320
|
+
# Rebuild the status-bar text from the current focus and global-shortcut
|
|
321
|
+
# registry. Called from {#focused=} and whenever the global registry
|
|
322
|
+
# changes. Popups own their own "q Close" prefix in `#keyboard_hint`;
|
|
323
|
+
# for the tiled case Screen tacks on the global "q quit" instead.
|
|
324
|
+
# Global-shortcut hints get spliced in too — see {#global_shortcut_hints}
|
|
325
|
+
# for the over_popups filter rule.
|
|
326
|
+
def refresh_status_bar: () -> void
|
|
327
|
+
|
|
328
|
+
# Status-bar hints from currently-registered global shortcuts.
|
|
329
|
+
# When a popup is open, only `over_popups: true` shortcuts contribute —
|
|
330
|
+
# the rest don't fire in that context, so showing them would be a lie.
|
|
331
|
+
# Insertion order is preserved (Hash iteration order).
|
|
332
|
+
#
|
|
333
|
+
# _@param_ `popup_open`
|
|
334
|
+
def global_shortcut_hints: (popup_open: bool) -> ::Array[String]
|
|
335
|
+
|
|
219
336
|
# Internal — use {Component::Popup#open} instead. Adds the popup to
|
|
220
337
|
# {#pane}, centers and focuses it.
|
|
221
338
|
#
|
|
@@ -224,7 +341,9 @@ module Tuile
|
|
|
224
341
|
|
|
225
342
|
# Runs event loop – waits for keys and sends them to active window. The
|
|
226
343
|
# function exits when the 'ESC' or 'q' key is pressed.
|
|
227
|
-
|
|
344
|
+
#
|
|
345
|
+
# _@param_ `capture_mouse` — when true (default), enables xterm mouse tracking so clicks and scroll wheel arrive as {MouseEvent}s and feed {Component#handle_mouse}. When false, no tracking escape sequence is written: the terminal keeps its native click handling, which is what you want if the app benefits more from select-to-copy than from click-to-focus. Components' `handle_mouse` is simply never invoked from the loop in that mode (the terminal stops sending the bytes).
|
|
346
|
+
def run_event_loop: (?capture_mouse: bool) -> void
|
|
228
347
|
|
|
229
348
|
# Advances focus to the next {Component#tab_stop?} in tree order, wrapping
|
|
230
349
|
# around. Scope is the topmost popup if one is open, otherwise {#content}
|
|
@@ -239,6 +358,51 @@ module Tuile
|
|
|
239
358
|
# _@return_ — true if focus moved.
|
|
240
359
|
def focus_previous: () -> bool
|
|
241
360
|
|
|
361
|
+
# Registers an app-level keyboard shortcut. When `key` arrives, the block
|
|
362
|
+
# is invoked on the event-loop thread (so it may freely mutate UI) before
|
|
363
|
+
# the key reaches any component. Re-registering the same key replaces the
|
|
364
|
+
# previous binding; use {#unregister_global_shortcut} to remove one.
|
|
365
|
+
#
|
|
366
|
+
# Only unprintable keys are accepted — control characters (Ctrl+letter,
|
|
367
|
+
# ESC, BACKSPACE, ENTER, …) and multi-character escape sequences (arrows,
|
|
368
|
+
# F-keys, …). Printable keys raise {ArgumentError}: they'd hijack typing
|
|
369
|
+
# into a {Component::TextField} and should be expressed as
|
|
370
|
+
# {Component#key_shortcut} instead, which the dispatcher suppresses while
|
|
371
|
+
# a text widget owns the hardware cursor. TAB and SHIFT_TAB are also
|
|
372
|
+
# rejected because {#handle_key} intercepts them for focus navigation
|
|
373
|
+
# before the global registry is consulted, so a binding on them would
|
|
374
|
+
# silently never fire.
|
|
375
|
+
#
|
|
376
|
+
# Pass `hint:` to surface the shortcut in the status bar. It's a
|
|
377
|
+
# preformatted string the caller fully owns (so colors and the key label
|
|
378
|
+
# style stay consistent with whatever the host app uses elsewhere). The
|
|
379
|
+
# framework splices it in like any other status hint: in the tiled case,
|
|
380
|
+
# right after `q quit` and before the active window's own hint; while a
|
|
381
|
+
# popup is open, only hints from `over_popups: true` shortcuts are
|
|
382
|
+
# shown, and they're prepended before the popup's `q Close`.
|
|
383
|
+
#
|
|
384
|
+
# Example — open a log popup with Ctrl+L from anywhere, even while a
|
|
385
|
+
# popup is already on screen:
|
|
386
|
+
#
|
|
387
|
+
# screen.register_global_shortcut(Keys::CTRL_L,
|
|
388
|
+
# over_popups: true,
|
|
389
|
+
# hint: "^L #{Rainbow("log").cadetblue}") do
|
|
390
|
+
# log_popup.open
|
|
391
|
+
# end
|
|
392
|
+
#
|
|
393
|
+
# _@param_ `key` — unprintable key (e.g. {Keys::CTRL_L}, {Keys::ESC}, {Keys::PAGE_UP}).
|
|
394
|
+
#
|
|
395
|
+
# _@param_ `over_popups` — when true, fires even while a modal popup is open (pre-empting the popup's own key handling). When false (default), the shortcut is suppressed while any popup is open and the popup gets the key instead.
|
|
396
|
+
#
|
|
397
|
+
# _@param_ `hint` — preformatted status-bar hint (e.g. `"^L #{Rainbow("log").cadetblue}"`). When nil (default) the shortcut is silent in the status bar.
|
|
398
|
+
def register_global_shortcut: (String key, ?over_popups: bool, ?hint: String?) -> void
|
|
399
|
+
|
|
400
|
+
# Removes a shortcut previously installed by {#register_global_shortcut}.
|
|
401
|
+
# No-op if `key` was not registered.
|
|
402
|
+
#
|
|
403
|
+
# _@param_ `key`
|
|
404
|
+
def unregister_global_shortcut: (String key) -> void
|
|
405
|
+
|
|
242
406
|
# _@return_ — current active tiled component.
|
|
243
407
|
def active_window: () -> Component?
|
|
244
408
|
|
|
@@ -330,10 +494,17 @@ module Tuile
|
|
|
330
494
|
# A key has been pressed on the keyboard. Handle it, or forward to active
|
|
331
495
|
# window.
|
|
332
496
|
#
|
|
333
|
-
#
|
|
334
|
-
#
|
|
335
|
-
#
|
|
336
|
-
#
|
|
497
|
+
# Dispatch order:
|
|
498
|
+
# 1. Tab / Shift+Tab — reserved focus navigation, intercepted before
|
|
499
|
+
# anything else so a focused {Component::TextField} (which would
|
|
500
|
+
# otherwise swallow printable keys via cursor-owner suppression)
|
|
501
|
+
# doesn't trap them.
|
|
502
|
+
# 2. App-level shortcuts from {#register_global_shortcut}. An entry
|
|
503
|
+
# registered with `over_popups: true` always fires; one with the
|
|
504
|
+
# default `over_popups: false` fires only when no popup is open
|
|
505
|
+
# (otherwise the popup receives the key normally).
|
|
506
|
+
# 3. {ScreenPane#handle_key}, which routes to the topmost popup or
|
|
507
|
+
# tiled content.
|
|
337
508
|
#
|
|
338
509
|
# _@param_ `key`
|
|
339
510
|
#
|
|
@@ -379,13 +550,29 @@ module Tuile
|
|
|
379
550
|
|
|
380
551
|
# _@return_ — currently focused component.
|
|
381
552
|
attr_accessor focused: Component?
|
|
553
|
+
|
|
554
|
+
# Entry in the global shortcut registry: the block to run, whether it
|
|
555
|
+
# pre-empts open popups, and an optional preformatted status-bar hint.
|
|
556
|
+
# @api private
|
|
557
|
+
class Shortcut < Data
|
|
558
|
+
# Returns the value of attribute block
|
|
559
|
+
attr_reader block: Object
|
|
560
|
+
|
|
561
|
+
# Returns the value of attribute over_popups
|
|
562
|
+
attr_reader over_popups: Object
|
|
563
|
+
|
|
564
|
+
# Returns the value of attribute hint
|
|
565
|
+
attr_reader hint: Object
|
|
566
|
+
end
|
|
382
567
|
end
|
|
383
568
|
|
|
384
569
|
# A UI component which is positioned on the screen and draws characters into
|
|
385
570
|
# its bounding rectangle (in {#repaint}).
|
|
386
571
|
#
|
|
387
|
-
#
|
|
388
|
-
#
|
|
572
|
+
# Painting is gated by attachment: a detached component (one whose {#root}
|
|
573
|
+
# isn't {Screen#pane}) is never enqueued for repaint via {#invalidate}, and
|
|
574
|
+
# any stale invalidation entries are filtered out at drain time. Subclasses
|
|
575
|
+
# can paint freely in {#repaint} without re-asserting attachment.
|
|
389
576
|
class Component
|
|
390
577
|
def initialize: () -> void
|
|
391
578
|
|
|
@@ -417,6 +604,8 @@ module Tuile
|
|
|
417
604
|
# responsibility for {#rect}. Everything else should call super.
|
|
418
605
|
#
|
|
419
606
|
# A component must not draw outside of {#rect}.
|
|
607
|
+
#
|
|
608
|
+
# Only called when the component is attached.
|
|
420
609
|
def repaint: () -> void
|
|
421
610
|
|
|
422
611
|
# Called when a character is pressed on the keyboard.
|
|
@@ -537,6 +726,12 @@ module Tuile
|
|
|
537
726
|
|
|
538
727
|
# Invalidates the component: {Screen} records this component as
|
|
539
728
|
# needs-repaint and once all events are processed, will call {#repaint}.
|
|
729
|
+
#
|
|
730
|
+
# No-op when the component is not {#attached?} — a detached component has
|
|
731
|
+
# no place on the screen to paint to, so {Screen} must never end up
|
|
732
|
+
# repainting it. Callers don't need to guard their own `invalidate` calls;
|
|
733
|
+
# mutating a detached component (e.g. setting `lines=` on a {List} sitting
|
|
734
|
+
# inside a closed {Component::Popup}) is silent.
|
|
540
735
|
def invalidate: () -> void
|
|
541
736
|
|
|
542
737
|
# Whether direct children fully tile {#rect}. Used by the default
|
|
@@ -665,7 +860,11 @@ module Tuile
|
|
|
665
860
|
|
|
666
861
|
# Rebuilds pre-padded lines when the wrap width changes. The wrap width
|
|
667
862
|
# depends on {#rect}`.width` and the scrollbar gutter, both of which
|
|
668
|
-
# trigger this hook.
|
|
863
|
+
# trigger this hook. Also re-evaluates {#auto_scroll}: if items were
|
|
864
|
+
# appended while the rect was empty (e.g. a {Popup}-wrapped list got
|
|
865
|
+
# `add_line` calls before the popup was opened), the auto-scroll update
|
|
866
|
+
# was skipped because there was no viewport — re-run it now that there
|
|
867
|
+
# is one, so the list snaps to the bottom on first paint.
|
|
669
868
|
def on_width_changed: () -> void
|
|
670
869
|
|
|
671
870
|
# Coerces and flattens a list of input entries into trimmed
|
|
@@ -744,7 +943,14 @@ module Tuile
|
|
|
744
943
|
# _@param_ `delta` — negative scrolls up, positive scrolls down.
|
|
745
944
|
def move_top_line_by: (Integer delta) -> void
|
|
746
945
|
|
|
747
|
-
# If auto-scrolling, recalculate the top line
|
|
946
|
+
# If auto-scrolling, recalculate the top line and snap the cursor to the
|
|
947
|
+
# last reachable position. Without the cursor snap the viewport gets
|
|
948
|
+
# yanked back to wherever the cursor sat on the next arrow press,
|
|
949
|
+
# negating the auto-scroll. Skipped when {#rect} is empty: without a
|
|
950
|
+
# viewport the "lines minus viewport" formula yields `@lines.size`,
|
|
951
|
+
# which would leave `top_line` past the last item once a real rect
|
|
952
|
+
# arrives. {#on_width_changed} re-runs this hook when the rect grows so
|
|
953
|
+
# the snap-to-bottom intent is preserved.
|
|
748
954
|
def update_top_line_if_auto_scroll: () -> void
|
|
749
955
|
|
|
750
956
|
# _@return_ — whether the scrollbar should be drawn right now.
|
|
@@ -853,6 +1059,15 @@ module Tuile
|
|
|
853
1059
|
# _@return_ — true if the position changed.
|
|
854
1060
|
def go: (Integer new_position) -> bool
|
|
855
1061
|
|
|
1062
|
+
# Moves the cursor to the last reachable position. For base {Cursor},
|
|
1063
|
+
# the last line; {Limited} clamps to the last allowed position; {None}
|
|
1064
|
+
# is a no-op.
|
|
1065
|
+
#
|
|
1066
|
+
# _@param_ `line_count` — number of lines in the list.
|
|
1067
|
+
#
|
|
1068
|
+
# _@return_ — true if the position changed.
|
|
1069
|
+
def go_to_last: (Integer line_count) -> bool
|
|
1070
|
+
|
|
856
1071
|
# _@param_ `lines`
|
|
857
1072
|
#
|
|
858
1073
|
# _@param_ `line_count`
|
|
@@ -863,9 +1078,6 @@ module Tuile
|
|
|
863
1078
|
|
|
864
1079
|
def go_to_first: () -> bool
|
|
865
1080
|
|
|
866
|
-
# _@param_ `line_count`
|
|
867
|
-
def go_to_last: (Integer line_count) -> bool
|
|
868
|
-
|
|
869
1081
|
# _@return_ — 0-based line index of the current cursor position.
|
|
870
1082
|
attr_reader position: Integer
|
|
871
1083
|
|
|
@@ -889,6 +1101,16 @@ module Tuile
|
|
|
889
1101
|
|
|
890
1102
|
# _@param_ `_line_count`
|
|
891
1103
|
def candidate_positions: (Integer _line_count) -> ::Array[Integer]
|
|
1104
|
+
|
|
1105
|
+
# Overridden so all movement funnels — base {Cursor#go_to_last},
|
|
1106
|
+
# {Cursor#go_to_first}, etc., which all call {#go} — become safe
|
|
1107
|
+
# no-ops on a disabled cursor. The instance is frozen, so a default
|
|
1108
|
+
# mutating {#go} would raise.
|
|
1109
|
+
#
|
|
1110
|
+
# _@param_ `_new_position`
|
|
1111
|
+
#
|
|
1112
|
+
# _@return_ — always false.
|
|
1113
|
+
def go: (Integer _new_position) -> bool
|
|
892
1114
|
end
|
|
893
1115
|
|
|
894
1116
|
# Cursor which can only land on specific allowed lines.
|
|
@@ -908,6 +1130,9 @@ module Tuile
|
|
|
908
1130
|
# _@param_ `line_count`
|
|
909
1131
|
def candidate_positions: (Integer line_count) -> ::Array[Integer]
|
|
910
1132
|
|
|
1133
|
+
# _@param_ `_line_count`
|
|
1134
|
+
def go_to_last: (Integer _line_count) -> bool
|
|
1135
|
+
|
|
911
1136
|
# _@param_ `lines`
|
|
912
1137
|
#
|
|
913
1138
|
# _@param_ `line_count`
|
|
@@ -917,9 +1142,6 @@ module Tuile
|
|
|
917
1142
|
def go_up_by: (Integer lines) -> bool
|
|
918
1143
|
|
|
919
1144
|
def go_to_first: () -> bool
|
|
920
|
-
|
|
921
|
-
# _@param_ `_line_count`
|
|
922
|
-
def go_to_last: (Integer _line_count) -> bool
|
|
923
1145
|
end
|
|
924
1146
|
end
|
|
925
1147
|
end
|
|
@@ -951,9 +1173,13 @@ module Tuile
|
|
|
951
1173
|
# Each line is ellipsized to fit, padded with trailing spaces out to
|
|
952
1174
|
# the full width, and pre-rendered to ANSI so {#repaint} is just a
|
|
953
1175
|
# lookup + screen.print per row. {@blank_line} covers rows past the
|
|
954
|
-
# last text line.
|
|
1176
|
+
# last text line. When {#bg} is set, every produced line (and the
|
|
1177
|
+
# blank row) has the bg applied uniformly.
|
|
955
1178
|
def update_clipped_lines: () -> void
|
|
956
1179
|
|
|
1180
|
+
# _@param_ `line`
|
|
1181
|
+
def apply_bg: (StyledString line) -> StyledString
|
|
1182
|
+
|
|
957
1183
|
# _@param_ `line`
|
|
958
1184
|
#
|
|
959
1185
|
# _@param_ `width`
|
|
@@ -962,6 +1188,11 @@ module Tuile
|
|
|
962
1188
|
# _@return_ — the current text. Defaults to an empty
|
|
963
1189
|
# {StyledString}.
|
|
964
1190
|
attr_accessor text: (StyledString | String)?
|
|
1191
|
+
|
|
1192
|
+
# _@return_ — background color applied uniformly across every
|
|
1193
|
+
# painted row (including padding past the text). `nil` (default)
|
|
1194
|
+
# leaves whatever bg the text's own styling carries.
|
|
1195
|
+
attr_accessor bg: (Color | Symbol | Integer | ::Array[Integer])?
|
|
965
1196
|
end
|
|
966
1197
|
|
|
967
1198
|
# A modal overlay that wraps any {Component} as its content. Popup itself
|
|
@@ -992,7 +1223,9 @@ module Tuile
|
|
|
992
1223
|
|
|
993
1224
|
def focusable?: () -> bool
|
|
994
1225
|
|
|
995
|
-
# Mounts this popup on the {Screen}.
|
|
1226
|
+
# Mounts this popup on the {Screen}. Recomputes the popup's size from
|
|
1227
|
+
# the current content first, so reopening a popup whose content has
|
|
1228
|
+
# grown or shrunk while closed picks up the new size.
|
|
996
1229
|
def open: () -> void
|
|
997
1230
|
|
|
998
1231
|
# Constructs and opens a popup in one call.
|
|
@@ -1150,8 +1383,9 @@ module Tuile
|
|
|
1150
1383
|
#
|
|
1151
1384
|
# The window's `content` is unset by default; assign one via {#content=}.
|
|
1152
1385
|
#
|
|
1153
|
-
# Window is considered invisible if {#rect} is empty
|
|
1154
|
-
#
|
|
1386
|
+
# Window is considered invisible if {#rect} is empty. The window won't
|
|
1387
|
+
# draw when invisible. (Repaint of detached windows is short-circuited
|
|
1388
|
+
# by {Component#invalidate}; subclasses don't need to re-check.)
|
|
1155
1389
|
class Window < Component
|
|
1156
1390
|
include Tuile::Component::HasContent
|
|
1157
1391
|
|
|
@@ -1180,10 +1414,6 @@ module Tuile
|
|
|
1180
1414
|
# window has no content, footer, or caption.
|
|
1181
1415
|
def content_size: () -> Size
|
|
1182
1416
|
|
|
1183
|
-
# _@return_ — true if {#rect} is off screen and the window won't
|
|
1184
|
-
# paint.
|
|
1185
|
-
def visible?: () -> bool
|
|
1186
|
-
|
|
1187
1417
|
# Fully repaints the window: both frame and contents.
|
|
1188
1418
|
#
|
|
1189
1419
|
# Window deliberately paints over its entire rect (border around the
|
|
@@ -1244,26 +1474,26 @@ module Tuile
|
|
|
1244
1474
|
# Currently only {#on_change} is wired; Enter inserts a newline as in any
|
|
1245
1475
|
# plain `<textarea>` or text editor. A future `on_enter`/`on_submit`
|
|
1246
1476
|
# callback may opt out of that by consuming Enter instead.
|
|
1247
|
-
class TextArea < Component
|
|
1477
|
+
class TextArea < Tuile::Component::TextInput
|
|
1248
1478
|
ACTIVE_BG_SGR: String
|
|
1249
1479
|
INACTIVE_BG_SGR: String
|
|
1250
1480
|
|
|
1251
1481
|
def initialize: () -> void
|
|
1252
1482
|
|
|
1253
|
-
def focusable?: () -> bool
|
|
1254
|
-
|
|
1255
|
-
def tab_stop?: () -> bool
|
|
1256
|
-
|
|
1257
1483
|
def cursor_position: () -> Point?
|
|
1258
1484
|
|
|
1259
|
-
# _@param_ `key`
|
|
1260
|
-
def handle_key: (String key) -> bool
|
|
1261
|
-
|
|
1262
1485
|
# _@param_ `event`
|
|
1263
1486
|
def handle_mouse: (MouseEvent event) -> void
|
|
1264
1487
|
|
|
1265
1488
|
def repaint: () -> void
|
|
1266
1489
|
|
|
1490
|
+
def on_text_mutated: () -> void
|
|
1491
|
+
|
|
1492
|
+
def on_caret_mutated: () -> void
|
|
1493
|
+
|
|
1494
|
+
# _@param_ `key`
|
|
1495
|
+
def handle_text_input_key: (String key) -> bool
|
|
1496
|
+
|
|
1267
1497
|
def on_width_changed: () -> void
|
|
1268
1498
|
|
|
1269
1499
|
# _@return_ — cached wrap of {#text} for the
|
|
@@ -1306,38 +1536,11 @@ module Tuile
|
|
|
1306
1536
|
# _@return_ — always true.
|
|
1307
1537
|
def insert_char: (String char) -> bool
|
|
1308
1538
|
|
|
1309
|
-
def delete_before_caret: () -> void
|
|
1310
|
-
|
|
1311
|
-
def delete_at_caret: () -> void
|
|
1312
|
-
|
|
1313
1539
|
# Keeps the caret visible by scrolling vertically.
|
|
1314
1540
|
def adjust_top_display_row: () -> void
|
|
1315
1541
|
|
|
1316
|
-
# _@param_ `key`
|
|
1317
|
-
def printable?: (String key) -> bool
|
|
1318
|
-
|
|
1319
|
-
# Same semantics as {TextField}'s ctrl+left.
|
|
1320
|
-
def word_left: () -> Integer
|
|
1321
|
-
|
|
1322
|
-
# Same semantics as {TextField}'s ctrl+right.
|
|
1323
|
-
def word_right: () -> Integer
|
|
1324
|
-
|
|
1325
|
-
# _@return_ — current text contents (may contain embedded `\n`).
|
|
1326
|
-
attr_accessor text: String
|
|
1327
|
-
|
|
1328
|
-
# _@return_ — caret index in `0..text.length`.
|
|
1329
|
-
attr_accessor caret: Integer
|
|
1330
|
-
|
|
1331
1542
|
# _@return_ — index of the topmost display row currently visible.
|
|
1332
1543
|
attr_reader top_display_row: Integer
|
|
1333
|
-
|
|
1334
|
-
# Optional callback fired whenever {#text} changes. Receives the new text
|
|
1335
|
-
# as a single argument. Not fired by {#caret=} (text unchanged), not
|
|
1336
|
-
# fired by a no-op setter, and not fired by a re-wrap caused by a width
|
|
1337
|
-
# change ({#text} itself is unchanged).
|
|
1338
|
-
#
|
|
1339
|
-
# _@return_ — one-arg callable, or nil.
|
|
1340
|
-
attr_accessor on_change: (Proc | Method)?
|
|
1341
1544
|
end
|
|
1342
1545
|
|
|
1343
1546
|
# A read-only viewer for prose: chunks of formatted text that scroll
|
|
@@ -1350,9 +1553,19 @@ module Tuile
|
|
|
1350
1553
|
# ANSI-as-bytes wrapping, color does *not* get dropped on continuation
|
|
1351
1554
|
# rows). {#text=} accepts a {String} (parsed via {StyledString.parse},
|
|
1352
1555
|
# so embedded ANSI is honored) or a {StyledString} directly; {#text}
|
|
1353
|
-
# always returns the {StyledString}.
|
|
1354
|
-
#
|
|
1355
|
-
#
|
|
1556
|
+
# always returns the {StyledString}.
|
|
1557
|
+
#
|
|
1558
|
+
# For incremental updates pick the right primitive: {#append} (aliased
|
|
1559
|
+
# as `<<`) is verbatim and stream-friendly — chunks are concatenated
|
|
1560
|
+
# straight onto the buffer, with embedded `\n` becoming hard breaks.
|
|
1561
|
+
# {#add_line} is the "log entry" convenience — it starts the content on
|
|
1562
|
+
# a fresh line by inserting a leading `\n` when the buffer is non-empty.
|
|
1563
|
+
# {#remove_last_n_lines} pops hard lines back off the tail — the
|
|
1564
|
+
# inverse of building up a region with {#append} / {#add_line}, so a
|
|
1565
|
+
# caller streaming reformattable content (e.g. partially-rendered
|
|
1566
|
+
# Markdown that may need to retract its last paragraph) can replace
|
|
1567
|
+
# the tail without rewriting the whole text. Turn on {#auto_scroll}
|
|
1568
|
+
# to keep the latest content in view.
|
|
1356
1569
|
#
|
|
1357
1570
|
# TextView is meant to be the content of a {Window} — focus indication and
|
|
1358
1571
|
# keyboard-hint surfacing rely on the surrounding window chrome.
|
|
@@ -1373,22 +1586,129 @@ module Tuile
|
|
|
1373
1586
|
# honored); a `StyledString` is used as-is; `nil` is coerced to an
|
|
1374
1587
|
# empty {StyledString}.
|
|
1375
1588
|
#
|
|
1589
|
+
# Detaches every existing {Region} (including the original default)
|
|
1590
|
+
# and installs a fresh internal default region that owns all the new
|
|
1591
|
+
# hard lines. Any handle the caller was holding becomes detached and
|
|
1592
|
+
# raises on use — see {Region#attached?}. The no-op short-circuit
|
|
1593
|
+
# (matching value, same {StyledString}) preserves existing regions.
|
|
1594
|
+
#
|
|
1376
1595
|
# _@param_ `value`
|
|
1377
1596
|
def text=: ((String | StyledString)? value) -> void
|
|
1378
1597
|
|
|
1379
|
-
#
|
|
1380
|
-
#
|
|
1381
|
-
#
|
|
1382
|
-
#
|
|
1598
|
+
# Creates a new empty {Region} at the spatial tail of the document
|
|
1599
|
+
# and returns its handle. Subsequent {#append} / {#<<} / {#add_line}
|
|
1600
|
+
# calls route through this new region (since it is now the spatial
|
|
1601
|
+
# tail). Earlier regions keep their content and their handles stay
|
|
1602
|
+
# valid; their {Region#range} shifts as later regions grow.
|
|
1603
|
+
#
|
|
1604
|
+
# Apps streaming logically-distinct sections (e.g. an LLM's "thinking"
|
|
1605
|
+
# vs. "assistant" output) create one region per section, hold the
|
|
1606
|
+
# handles, and call `region.append` / `region.text=` directly when
|
|
1607
|
+
# they need to grow or rewrite an earlier section.
|
|
1608
|
+
def create_region: () -> Region
|
|
1609
|
+
|
|
1610
|
+
# _@return_ — true iff {#text} is empty (no hard lines).
|
|
1611
|
+
def empty?: () -> bool
|
|
1612
|
+
|
|
1613
|
+
# Appends `str` verbatim. Embedded `\n` characters become hard line
|
|
1614
|
+
# breaks; otherwise the text is concatenated onto the current last
|
|
1615
|
+
# hard line. Designed for streaming use (e.g. an LLM chat window
|
|
1616
|
+
# receiving partial messages — feed each chunk straight in). Accepts
|
|
1617
|
+
# the same input forms as {#text=}; empty/`nil` input is a no-op.
|
|
1618
|
+
#
|
|
1619
|
+
# For the "add an entry on a new line" pattern use {#add_line}.
|
|
1383
1620
|
#
|
|
1384
|
-
# Cost is O(appended
|
|
1385
|
-
#
|
|
1386
|
-
#
|
|
1387
|
-
# {#text} is invalidated
|
|
1621
|
+
# Cost is O(appended + width-of-current-last-hard-line) — the
|
|
1622
|
+
# previously last hard line is re-wrapped (because the extension may
|
|
1623
|
+
# cause it to wrap differently), any additional hard lines created by
|
|
1624
|
+
# embedded `\n` are wrapped fresh. The cached {#text} is invalidated
|
|
1625
|
+
# and rebuilt on demand.
|
|
1388
1626
|
#
|
|
1389
1627
|
# _@param_ `str`
|
|
1390
1628
|
def append: ((String | StyledString)? str) -> void
|
|
1391
1629
|
|
|
1630
|
+
# Verbatim append, returning `self` for chainability (`view << a << b`).
|
|
1631
|
+
#
|
|
1632
|
+
# _@param_ `str`
|
|
1633
|
+
def <<: ((String | StyledString)? str) -> self
|
|
1634
|
+
|
|
1635
|
+
# Appends `str` as a new entry: starts a fresh hard line first (when
|
|
1636
|
+
# the buffer is non-empty) and then appends `str`. Equivalent to
|
|
1637
|
+
# `append("\n" + str)` on a non-empty buffer, or `append(str)` on an
|
|
1638
|
+
# empty one. `nil` and `""` produce a blank entry on a non-empty
|
|
1639
|
+
# buffer and a no-op on an empty buffer (matches the old `append`
|
|
1640
|
+
# semantics for "log line" callers).
|
|
1641
|
+
#
|
|
1642
|
+
# _@param_ `str`
|
|
1643
|
+
def add_line: ((String | StyledString)? str) -> void
|
|
1644
|
+
|
|
1645
|
+
# Drops the last `n` hard lines from the buffer. The inverse of
|
|
1646
|
+
# building up a tail region with {#append} / {#add_line}: a caller
|
|
1647
|
+
# streaming partially-rendered content whose tail must occasionally
|
|
1648
|
+
# be retracted (e.g. Markdown-to-ANSI where a new token reformats
|
|
1649
|
+
# the table being built) can call `remove_last_n_lines(k)` followed
|
|
1650
|
+
# by `append(new_tail)` to replace the damaged region in place.
|
|
1651
|
+
#
|
|
1652
|
+
# `n == 0` and the empty-buffer case are no-ops (no invalidation).
|
|
1653
|
+
# `n >= hard-line count` empties the buffer.
|
|
1654
|
+
#
|
|
1655
|
+
# Operates on **hard lines** (the `\n`-delimited entries the
|
|
1656
|
+
# buffer stores), not on wrapped physical rows — same granularity
|
|
1657
|
+
# as {#add_line}. Cost is O(rendered-rows of the popped lines).
|
|
1658
|
+
#
|
|
1659
|
+
# _@param_ `n` — number of hard lines to drop; must be >= 0.
|
|
1660
|
+
def remove_last_n_lines: (Integer n) -> void
|
|
1661
|
+
|
|
1662
|
+
# Replaces a contiguous range of hard lines with the parsed content
|
|
1663
|
+
# of `str`. The replacement is parsed exactly like {#text=} and
|
|
1664
|
+
# {#append}: a {String} is run through {StyledString.parse} (so
|
|
1665
|
+
# embedded ANSI is honored), a {StyledString} is used as-is, `nil`
|
|
1666
|
+
# behaves like an empty replacement (the range is deleted). Embedded
|
|
1667
|
+
# `"\n"` in the replacement produces multiple hard lines, so a single
|
|
1668
|
+
# `replace` can grow or shrink the buffer.
|
|
1669
|
+
#
|
|
1670
|
+
# `range` selects which hard lines to swap out:
|
|
1671
|
+
#
|
|
1672
|
+
# - an `Integer` `n` is shorthand for `n..n` (replace one existing
|
|
1673
|
+
# line — `n` must be in `[0, hard-line count)`);
|
|
1674
|
+
# - a non-empty `Range` of hard-line indices replaces those lines;
|
|
1675
|
+
# - an empty `Range` (e.g. `2...2`, or the canonical end-insertion
|
|
1676
|
+
# `hard_lines.size...hard_lines.size`) is *insertion* at that
|
|
1677
|
+
# position — no lines are removed. {#insert} is a thin alias for
|
|
1678
|
+
# this case.
|
|
1679
|
+
#
|
|
1680
|
+
# Endpoints must be non-negative integers; `begin` may equal
|
|
1681
|
+
# `hard-line count` (insertion at the end), `end` may not exceed
|
|
1682
|
+
# `hard-line count - 1`. `nil` endpoints (beginless / endless ranges)
|
|
1683
|
+
# are not accepted.
|
|
1684
|
+
#
|
|
1685
|
+
# Cost is roughly `O(from + length + new content)`: the splice
|
|
1686
|
+
# updates only the affected slice of the physical-row buffer, using
|
|
1687
|
+
# the per-hard-line wrap-count cache to locate the starting offset
|
|
1688
|
+
# without re-wrapping preceding lines. Lines outside the splice are
|
|
1689
|
+
# never re-wrapped. {#top_line} is clamped if the new line count
|
|
1690
|
+
# puts it past the end; {#auto_scroll} pins it to the bottom as
|
|
1691
|
+
# usual. The call is a no-op (no invalidation) when the parsed
|
|
1692
|
+
# replacement equals the covered range (vacuously true for an empty
|
|
1693
|
+
# range plus empty replacement, so `replace(n...n, "")` is a cheap
|
|
1694
|
+
# no-op).
|
|
1695
|
+
#
|
|
1696
|
+
# _@param_ `range` — hard-line indices to replace.
|
|
1697
|
+
#
|
|
1698
|
+
# _@param_ `str` — replacement content.
|
|
1699
|
+
def replace: ((::Range[untyped] | Integer) range, (String | StyledString)? str) -> void
|
|
1700
|
+
|
|
1701
|
+
# Inserts `str` at hard-line index `at`. Equivalent to
|
|
1702
|
+
# `replace(at...at, str)` — a no-removal splice that grows the buffer
|
|
1703
|
+
# by the parsed line count. `at == hard-line count` is allowed and
|
|
1704
|
+
# appends at the end; for that case {#append} / {#add_line} are
|
|
1705
|
+
# usually more idiomatic.
|
|
1706
|
+
#
|
|
1707
|
+
# _@param_ `at` — 0-based hard-line index in `[0, hard-line count]`.
|
|
1708
|
+
#
|
|
1709
|
+
# _@param_ `str` — content to insert.
|
|
1710
|
+
def insert: (Integer at, (String | StyledString)? str) -> void
|
|
1711
|
+
|
|
1392
1712
|
# Clears the text. Equivalent to `text = ""`.
|
|
1393
1713
|
def clear: () -> void
|
|
1394
1714
|
|
|
@@ -1414,30 +1734,164 @@ module Tuile
|
|
|
1414
1734
|
# this hook.
|
|
1415
1735
|
def on_width_changed: () -> void
|
|
1416
1736
|
|
|
1737
|
+
# Validates and unpacks a {#replace}-style range argument into
|
|
1738
|
+
# inclusive `[from, to]` line indices. An `Integer` `n` becomes
|
|
1739
|
+
# `[n, n]` (which must point at an existing line — `Integer` is
|
|
1740
|
+
# never insertion sugar). A `Range` is normalized for
|
|
1741
|
+
# `exclude_end?`; `to == from - 1` is a valid empty range
|
|
1742
|
+
# (insertion at `from`), and `from` may equal `size` for
|
|
1743
|
+
# end-insertion. Shared by {#replace} and {Region#replace};
|
|
1744
|
+
# `size` is the buffer or region line count, and `what` is the
|
|
1745
|
+
# entity name woven into error messages.
|
|
1746
|
+
#
|
|
1747
|
+
# _@param_ `range`
|
|
1748
|
+
#
|
|
1749
|
+
# _@param_ `size`
|
|
1750
|
+
#
|
|
1751
|
+
# _@param_ `what`
|
|
1752
|
+
def normalize_replace_range: ((::Range[untyped] | Integer) range, ?Integer size, ?String what) -> [Integer, Integer]
|
|
1753
|
+
|
|
1754
|
+
# Hard-line index where `region` begins in {@hard_lines} — derived
|
|
1755
|
+
# by summing the line counts of all regions that precede it.
|
|
1756
|
+
#
|
|
1757
|
+
# _@param_ `region`
|
|
1758
|
+
def region_start_index: (Region region) -> Integer
|
|
1759
|
+
|
|
1760
|
+
# Joined {StyledString} of the hard lines that `region` owns. Mirrors
|
|
1761
|
+
# {#text} but scoped to one region.
|
|
1762
|
+
#
|
|
1763
|
+
# _@param_ `region`
|
|
1764
|
+
def text_for_region: (Region region) -> StyledString
|
|
1765
|
+
|
|
1766
|
+
# Replaces all of `region`'s hard lines with the parsed content of
|
|
1767
|
+
# `value`. Symmetric with {#text=}, scoped to one region. Empty/nil
|
|
1768
|
+
# content empties the region (no visible blank line). Works on
|
|
1769
|
+
# already-empty regions (insertion at the region's position).
|
|
1770
|
+
#
|
|
1771
|
+
# _@param_ `region`
|
|
1772
|
+
#
|
|
1773
|
+
# _@param_ `value`
|
|
1774
|
+
def set_region_text: (Region region, (String | StyledString)? value) -> void
|
|
1775
|
+
|
|
1776
|
+
# Region-scoped {#replace}. Validates `range` against
|
|
1777
|
+
# `region.line_count`, translates region-relative indices to
|
|
1778
|
+
# absolute buffer indices, splices, and updates the region's count.
|
|
1779
|
+
#
|
|
1780
|
+
# _@param_ `region`
|
|
1781
|
+
#
|
|
1782
|
+
# _@param_ `range`
|
|
1783
|
+
#
|
|
1784
|
+
# _@param_ `str`
|
|
1785
|
+
def replace_in_region: (Region region, (::Range[untyped] | Integer) range, (String | StyledString)? str) -> void
|
|
1786
|
+
|
|
1787
|
+
# Verbatim append into `region`.
|
|
1788
|
+
#
|
|
1789
|
+
# _@param_ `region`
|
|
1790
|
+
#
|
|
1791
|
+
# _@param_ `str`
|
|
1792
|
+
def append_to_region: (Region region, (String | StyledString)? str) -> void
|
|
1793
|
+
|
|
1794
|
+
# Drops the last `n` hard lines from `region`'s tail via
|
|
1795
|
+
# {#splice_hard_lines}. `n` is clamped to the region's current
|
|
1796
|
+
# line count; callers guarantee `n > 0` and the region is
|
|
1797
|
+
# non-empty (the {Region#remove_last_n_lines} guard handles the
|
|
1798
|
+
# no-op cases).
|
|
1799
|
+
#
|
|
1800
|
+
# _@param_ `region`
|
|
1801
|
+
#
|
|
1802
|
+
# _@param_ `n`
|
|
1803
|
+
def remove_last_n_from_region: (Region region, Integer n) -> void
|
|
1804
|
+
|
|
1805
|
+
# Drops `region` from {@regions}: its hard lines are removed via
|
|
1806
|
+
# {#splice_hard_lines}, the handle is detached, and the always-one
|
|
1807
|
+
# default is restored if the removal would have left zero regions.
|
|
1808
|
+
# Skips the rewrap / invalidate work when the region was empty
|
|
1809
|
+
# (the buffer didn't change), but always detaches.
|
|
1810
|
+
#
|
|
1811
|
+
# _@param_ `region`
|
|
1812
|
+
def remove_region: (Region region) -> void
|
|
1813
|
+
|
|
1814
|
+
# Adjusts region line counts after a {@hard_lines} splice that
|
|
1815
|
+
# removed `removed_count` lines at index `from` and inserted
|
|
1816
|
+
# `added_count` in their place. Two passes:
|
|
1817
|
+
#
|
|
1818
|
+
# 1. Subtract each region's overlap with the removed range (uses
|
|
1819
|
+
# the original counts to compute positions). Remember the first
|
|
1820
|
+
# region that lost lines — that's the natural home for the
|
|
1821
|
+
# replacement content.
|
|
1822
|
+
# 2. Credit `added_count` to that region. For pure insertions (no
|
|
1823
|
+
# removal), there's no "first overlapping region" to pick from;
|
|
1824
|
+
# walk regions and credit the latest one starting at `from` (the
|
|
1825
|
+
# boundary tiebreaker matches the spatial-tail-routing of
|
|
1826
|
+
# {#append}). Past-the-end inserts fall back to the tail region.
|
|
1827
|
+
#
|
|
1828
|
+
# _@param_ `from`
|
|
1829
|
+
#
|
|
1830
|
+
# _@param_ `removed_count`
|
|
1831
|
+
#
|
|
1832
|
+
# _@param_ `added_count`
|
|
1833
|
+
def update_region_counts: (Integer from, Integer removed_count, Integer added_count) -> void
|
|
1834
|
+
|
|
1417
1835
|
# _@return_ — number of visible lines.
|
|
1418
1836
|
def viewport_lines: () -> Integer
|
|
1419
1837
|
|
|
1420
1838
|
# _@return_ — the max value of {#top_line} for scroll-key clamping.
|
|
1421
1839
|
def top_line_max: () -> Integer
|
|
1422
1840
|
|
|
1423
|
-
#
|
|
1424
|
-
#
|
|
1425
|
-
#
|
|
1426
|
-
#
|
|
1427
|
-
#
|
|
1428
|
-
# {@top_line} if the new line count puts it out of
|
|
1841
|
+
# Full rebuild of {@physical_lines} and {@hard_line_wrap_counts}
|
|
1842
|
+
# from {@hard_lines}. Called when wrap width changes (which
|
|
1843
|
+
# invalidates every cached row count) and from {#text=} (which
|
|
1844
|
+
# replaces the whole logical model). Mid-buffer mutators splice
|
|
1845
|
+
# incrementally via {#splice_hard_lines} and do *not* go through
|
|
1846
|
+
# here. Clamps {@top_line} if the new line count puts it out of
|
|
1847
|
+
# range.
|
|
1429
1848
|
def rewrap: () -> void
|
|
1430
1849
|
|
|
1431
|
-
# Wraps `hard_line` at `width` and
|
|
1432
|
-
#
|
|
1433
|
-
# and degenerate `width <= 0` both emit a single {@blank_line}
|
|
1434
|
-
# matching what `@text.wrap(width).map { |l| pad_to(l, width) }`
|
|
1435
|
-
# would have produced
|
|
1850
|
+
# Wraps `hard_line` at `width` and returns the padded physical rows
|
|
1851
|
+
# alongside the row count. Empty hard lines (e.g. from a `"\n\n"`
|
|
1852
|
+
# run) and degenerate `width <= 0` both emit a single {@blank_line}
|
|
1853
|
+
# row, matching what `@text.wrap(width).map { |l| pad_to(l, width) }`
|
|
1854
|
+
# would have produced.
|
|
1436
1855
|
#
|
|
1437
|
-
# _@param_ `hard_line`
|
|
1856
|
+
# _@param_ `hard_line`
|
|
1438
1857
|
#
|
|
1439
1858
|
# _@param_ `width`
|
|
1440
|
-
def
|
|
1859
|
+
def wrap_hard_line: (StyledString hard_line, Integer width) -> [::Array[StyledString], Integer]
|
|
1860
|
+
|
|
1861
|
+
# Appends `hard_line` to the tail of {@hard_lines}, updating the
|
|
1862
|
+
# wrap-count cache and {@physical_lines} in lockstep.
|
|
1863
|
+
#
|
|
1864
|
+
# _@param_ `hard_line`
|
|
1865
|
+
#
|
|
1866
|
+
# _@param_ `width`
|
|
1867
|
+
def push_hard_line: (StyledString hard_line, Integer width) -> void
|
|
1868
|
+
|
|
1869
|
+
# Pops the last hard line, the corresponding cache entry, and the
|
|
1870
|
+
# physical rows that hard line contributed. Returns the popped
|
|
1871
|
+
# hard line.
|
|
1872
|
+
def pop_hard_line: () -> StyledString
|
|
1873
|
+
|
|
1874
|
+
# Splices `new_hard_lines` into the buffer in place of the `count`
|
|
1875
|
+
# hard lines starting at index `from`. Updates {@hard_lines},
|
|
1876
|
+
# {@hard_line_wrap_counts}, and {@physical_lines} consistently.
|
|
1877
|
+
# The starting physical-row offset is computed in O(`from`) integer
|
|
1878
|
+
# adds via the cache — no wraps of preceding hard lines. Wraps are
|
|
1879
|
+
# done only for the new content, so total cost is
|
|
1880
|
+
# `O(from + count + new_hard_lines.sum(&:display_width))`.
|
|
1881
|
+
#
|
|
1882
|
+
# _@param_ `from`
|
|
1883
|
+
#
|
|
1884
|
+
# _@param_ `count` — number of existing hard lines to remove.
|
|
1885
|
+
#
|
|
1886
|
+
# _@param_ `new_hard_lines`
|
|
1887
|
+
def splice_hard_lines: (Integer from, Integer count, ::Array[StyledString] new_hard_lines) -> void
|
|
1888
|
+
|
|
1889
|
+
# _@param_ `idx`
|
|
1890
|
+
#
|
|
1891
|
+
# _@return_ — the {@physical_lines} index where the hard line
|
|
1892
|
+
# at {@hard_lines}`[idx]` starts. O(`idx`) integer adds via the
|
|
1893
|
+
# wrap-count cache.
|
|
1894
|
+
def phys_offset_at: (Integer idx) -> Integer
|
|
1441
1895
|
|
|
1442
1896
|
# Rebuilds the joined {StyledString} from {@hard_lines}, inserting a
|
|
1443
1897
|
# default-styled `"\n"` between hard lines. Called from the {#text}
|
|
@@ -1501,6 +1955,136 @@ module Tuile
|
|
|
1501
1955
|
# `Size.new(0, 0)`. Maintained incrementally by {#text=} and
|
|
1502
1956
|
# {#append}, so reads are O(1).
|
|
1503
1957
|
attr_reader content_size: Size
|
|
1958
|
+
|
|
1959
|
+
# A logical section of a {TextView}'s text — a contiguous run of
|
|
1960
|
+
# hard lines the app wants to address as a unit (e.g. an LLM's
|
|
1961
|
+
# "thinking" output vs. its assistant message). The view always
|
|
1962
|
+
# has at least one region, an internal default that owns whatever
|
|
1963
|
+
# hard lines aren't claimed by an app-created region.
|
|
1964
|
+
#
|
|
1965
|
+
# Apps don't construct regions directly; call {TextView#create_region}
|
|
1966
|
+
# to get one. The handle stays valid as long as the region is
|
|
1967
|
+
# attached — i.e. until {TextView#text=} (or {TextView#clear}) wipes
|
|
1968
|
+
# the slate and installs a fresh internal default. Detached regions
|
|
1969
|
+
# raise {RuntimeError} on every mutator and reader.
|
|
1970
|
+
#
|
|
1971
|
+
# A region's position is derived from its sibling order and counts,
|
|
1972
|
+
# so growing or shrinking an earlier region implicitly shifts the
|
|
1973
|
+
# ranges of all later regions. Empty regions occupy zero rows but
|
|
1974
|
+
# still hold a position in the sequence; `region.text = ""` collapses
|
|
1975
|
+
# a region's visible footprint without detaching it. Pre-creating
|
|
1976
|
+
# empty placeholder regions is supported and is the natural pattern
|
|
1977
|
+
# for "I'll fill this in later" layouts.
|
|
1978
|
+
class Region
|
|
1979
|
+
# _@param_ `view` — the owning view (never `nil` at construction).
|
|
1980
|
+
#
|
|
1981
|
+
# _@param_ `line_count` — number of hard lines this region owns.
|
|
1982
|
+
def initialize: (TextView view, ?Integer line_count) -> void
|
|
1983
|
+
|
|
1984
|
+
# _@return_ — `true` while the region is owned by its
|
|
1985
|
+
# {TextView}. Becomes `false` permanently once detached
|
|
1986
|
+
# (typically by {TextView#text=} / {TextView#clear}).
|
|
1987
|
+
def attached?: () -> bool
|
|
1988
|
+
|
|
1989
|
+
# _@return_ — true iff the region owns zero hard lines.
|
|
1990
|
+
# Empty regions render nothing — they still hold a position in
|
|
1991
|
+
# the sequence, so subsequent mutations route to them as usual.
|
|
1992
|
+
def empty?: () -> bool
|
|
1993
|
+
|
|
1994
|
+
# _@return_ — the joined content of just this region's
|
|
1995
|
+
# hard lines. Empty regions return {StyledString::EMPTY}.
|
|
1996
|
+
def text: () -> StyledString
|
|
1997
|
+
|
|
1998
|
+
# Replaces all of this region's hard lines with the parsed content
|
|
1999
|
+
# of `value`. Accepts the same inputs as {TextView#text=}; empty
|
|
2000
|
+
# or `nil` content collapses the region to zero hard lines.
|
|
2001
|
+
#
|
|
2002
|
+
# _@param_ `value`
|
|
2003
|
+
def text=: ((String | StyledString)? value) -> void
|
|
2004
|
+
|
|
2005
|
+
# Verbatim append into this region's tail. Same semantics as
|
|
2006
|
+
# {TextView#append} but scoped to the region: embedded `"\n"`
|
|
2007
|
+
# creates new hard lines within the region, no-leading-newline
|
|
2008
|
+
# input extends the region's last hard line. Empty / `nil` input
|
|
2009
|
+
# is a no-op (but still raises when detached). When the region is
|
|
2010
|
+
# the spatial tail of the view, this uses the incremental
|
|
2011
|
+
# {TextView#append} path; mid-document regions splice the affected
|
|
2012
|
+
# slice of the physical-row buffer (lines outside the region are
|
|
2013
|
+
# not re-wrapped).
|
|
2014
|
+
#
|
|
2015
|
+
# _@param_ `str`
|
|
2016
|
+
def append: ((String | StyledString)? str) -> void
|
|
2017
|
+
|
|
2018
|
+
# _@return_ — the hard-line indices this region currently
|
|
2019
|
+
# occupies — `start...(start + line_count)`. Empty regions
|
|
2020
|
+
# return a degenerate exclusive range at their position (e.g.
|
|
2021
|
+
# `5...5`). The result is computed on each call and so always
|
|
2022
|
+
# reflects sibling mutations.
|
|
2023
|
+
def range: () -> ::Range[untyped]
|
|
2024
|
+
|
|
2025
|
+
# Removes this region from its view. The region's hard lines (if
|
|
2026
|
+
# any) are deleted from the buffer — subsequent regions' ranges
|
|
2027
|
+
# shift up by `line_count` — and the handle detaches permanently.
|
|
2028
|
+
# The view keeps its always-≥1-region invariant: if this was the
|
|
2029
|
+
# only remaining region, a fresh internal default is installed
|
|
2030
|
+
# (the app doesn't get a handle to it; call
|
|
2031
|
+
# {TextView#create_region} again to start tracking).
|
|
2032
|
+
#
|
|
2033
|
+
# Idempotent: calling `remove` on an already-detached region is a
|
|
2034
|
+
# silent no-op (unlike the other mutators, which raise). This
|
|
2035
|
+
# lets cleanup paths blindly call `remove` without first checking
|
|
2036
|
+
# {#attached?}.
|
|
2037
|
+
def remove: () -> void
|
|
2038
|
+
|
|
2039
|
+
# Appends `str` as a new entry in this region: starts a fresh
|
|
2040
|
+
# hard line first (when the region is non-empty), then appends
|
|
2041
|
+
# `str`. Scoped equivalent of {TextView#add_line}. On an empty
|
|
2042
|
+
# region behaves like {#append}.
|
|
2043
|
+
#
|
|
2044
|
+
# _@param_ `str`
|
|
2045
|
+
def add_line: ((String | StyledString)? str) -> void
|
|
2046
|
+
|
|
2047
|
+
# Replaces a contiguous range of this region's hard lines with the
|
|
2048
|
+
# parsed content of `str`. Region-scoped counterpart of
|
|
2049
|
+
# {TextView#replace}: indices are 0-based **within the region**
|
|
2050
|
+
# (so `replace(0, "x")` rewrites the region's first line, not
|
|
2051
|
+
# the buffer's). Same range conventions apply — `Integer`,
|
|
2052
|
+
# inclusive/exclusive `Range`, empty range as insertion at
|
|
2053
|
+
# `begin`, and `begin == line_count` for end-insertion.
|
|
2054
|
+
#
|
|
2055
|
+
# _@param_ `range` — region-relative hard-line indices.
|
|
2056
|
+
#
|
|
2057
|
+
# _@param_ `str` — replacement content.
|
|
2058
|
+
def replace: ((::Range[untyped] | Integer) range, (String | StyledString)? str) -> void
|
|
2059
|
+
|
|
2060
|
+
# Inserts `str` at region-relative hard-line index `at`.
|
|
2061
|
+
# Equivalent to `replace(at...at, str)`. Region-scoped counterpart
|
|
2062
|
+
# of {TextView#insert}; `at == line_count` is allowed and appends
|
|
2063
|
+
# at the region's tail.
|
|
2064
|
+
#
|
|
2065
|
+
# _@param_ `at` — region-relative index in `[0, line_count]`.
|
|
2066
|
+
#
|
|
2067
|
+
# _@param_ `str`
|
|
2068
|
+
def insert: (Integer at, (String | StyledString)? str) -> void
|
|
2069
|
+
|
|
2070
|
+
# Drops the last `n` hard lines from this region's tail.
|
|
2071
|
+
# Subsequent regions' ranges shift up by the number actually
|
|
2072
|
+
# dropped. `n` is clamped to {#line_count}, so passing a large
|
|
2073
|
+
# `n` empties the region — the handle stays attached (use
|
|
2074
|
+
# {#remove} when the goal is to drop the region itself).
|
|
2075
|
+
# `n == 0` and an already-empty region are no-ops.
|
|
2076
|
+
#
|
|
2077
|
+
# _@param_ `n`
|
|
2078
|
+
def remove_last_n_lines: (Integer n) -> void
|
|
2079
|
+
|
|
2080
|
+
def detach!: () -> void
|
|
2081
|
+
|
|
2082
|
+
def check_attached: () -> void
|
|
2083
|
+
|
|
2084
|
+
# _@return_ — number of hard lines this region owns. Safe to
|
|
2085
|
+
# read on a detached region (no error raised).
|
|
2086
|
+
attr_accessor line_count: (Integer | untyped)
|
|
2087
|
+
end
|
|
1504
2088
|
end
|
|
1505
2089
|
|
|
1506
2090
|
# Shows a log. Construct your logger pointed at a {LogWindow::IO} to route
|
|
@@ -1516,6 +2100,11 @@ module Tuile
|
|
|
1516
2100
|
# _@param_ `caption`
|
|
1517
2101
|
def initialize: (?String caption) -> void
|
|
1518
2102
|
|
|
2103
|
+
# Appends given line to the log. Can be called from any thread. Does nothing if nil is passed in.
|
|
2104
|
+
#
|
|
2105
|
+
# _@param_ `string` — the line (or multiple lines) to log.
|
|
2106
|
+
def log: (String? string) -> void
|
|
2107
|
+
|
|
1519
2108
|
# IO-shaped adapter that forwards each log line to the owning {LogWindow}.
|
|
1520
2109
|
# Implements both {#write} (stdlib `Logger`) and {#puts} (loggers that
|
|
1521
2110
|
# call `output.puts`, e.g. `TTY::Logger`).
|
|
@@ -1545,26 +2134,28 @@ module Tuile
|
|
|
1545
2134
|
# The caret is a logical index in `0..text.length`. The hardware cursor is
|
|
1546
2135
|
# positioned by {Screen} after each repaint cycle when this component is
|
|
1547
2136
|
# focused; see {Component#cursor_position}.
|
|
1548
|
-
class TextField < Component
|
|
2137
|
+
class TextField < Tuile::Component::TextInput
|
|
1549
2138
|
ACTIVE_BG_SGR: String
|
|
1550
2139
|
INACTIVE_BG_SGR: String
|
|
1551
2140
|
|
|
1552
2141
|
def initialize: () -> void
|
|
1553
2142
|
|
|
1554
|
-
def focusable?: () -> bool
|
|
1555
|
-
|
|
1556
|
-
def tab_stop?: () -> bool
|
|
1557
|
-
|
|
1558
2143
|
def cursor_position: () -> Point?
|
|
1559
2144
|
|
|
1560
|
-
# _@param_ `key`
|
|
1561
|
-
def handle_key: (String key) -> bool
|
|
1562
|
-
|
|
1563
2145
|
# _@param_ `event`
|
|
1564
2146
|
def handle_mouse: (MouseEvent event) -> void
|
|
1565
2147
|
|
|
1566
2148
|
def repaint: () -> void
|
|
1567
2149
|
|
|
2150
|
+
# Truncate to fit `rect.width - 1` — single-line fields can't grow past
|
|
2151
|
+
# their width.
|
|
2152
|
+
#
|
|
2153
|
+
# _@param_ `new_text`
|
|
2154
|
+
def preprocess_text: (String new_text) -> String
|
|
2155
|
+
|
|
2156
|
+
# _@param_ `key`
|
|
2157
|
+
def handle_text_input_key: (String key) -> bool
|
|
2158
|
+
|
|
1568
2159
|
def on_width_changed: () -> void
|
|
1569
2160
|
|
|
1570
2161
|
# Maximum number of characters {#text} can hold given current width.
|
|
@@ -1573,12 +2164,110 @@ module Tuile
|
|
|
1573
2164
|
# _@param_ `char`
|
|
1574
2165
|
def insert: (String char) -> bool
|
|
1575
2166
|
|
|
2167
|
+
# Optional callback fired when the UP arrow key is pressed. When set, UP
|
|
2168
|
+
# is consumed by the field; when nil, UP falls through to the parent
|
|
2169
|
+
# (default behavior). Only triggered by {Keys::UP_ARROW}, not by `k`,
|
|
2170
|
+
# since `k` is a printable character inserted into {#text}.
|
|
2171
|
+
#
|
|
2172
|
+
# _@return_ — no-arg callable, or nil.
|
|
2173
|
+
attr_accessor on_key_up: (Proc | Method)?
|
|
2174
|
+
|
|
2175
|
+
# Optional callback fired when the DOWN arrow key is pressed. When set,
|
|
2176
|
+
# DOWN is consumed by the field; when nil, DOWN falls through to the
|
|
2177
|
+
# parent (default behavior). Only triggered by {Keys::DOWN_ARROW}, not by
|
|
2178
|
+
# `j`, since `j` is a printable character inserted into {#text}.
|
|
2179
|
+
#
|
|
2180
|
+
# _@return_ — no-arg callable, or nil.
|
|
2181
|
+
attr_accessor on_key_down: (Proc | Method)?
|
|
2182
|
+
|
|
2183
|
+
# Optional callback fired when ENTER is pressed. When set, ENTER is
|
|
2184
|
+
# consumed by the field; when nil, ENTER falls through to the parent
|
|
2185
|
+
# (default behavior).
|
|
2186
|
+
#
|
|
2187
|
+
# _@return_ — no-arg callable, or nil.
|
|
2188
|
+
attr_accessor on_enter: (Proc | Method)?
|
|
2189
|
+
end
|
|
2190
|
+
|
|
2191
|
+
# Abstract base for editable text components ({TextField}, {TextArea}).
|
|
2192
|
+
#
|
|
2193
|
+
# Holds the shared state — a mutable {#text} buffer, a {#caret} index,
|
|
2194
|
+
# {#on_change} and {#on_escape} callbacks — and the keyboard machinery
|
|
2195
|
+
# that single-line and multi-line inputs both need: ESC handling,
|
|
2196
|
+
# LEFT/RIGHT caret movement, CTRL+LEFT/CTRL+RIGHT word jumps, and the
|
|
2197
|
+
# `focusable?`/`tab_stop?` flags.
|
|
2198
|
+
#
|
|
2199
|
+
# Subclasses implement the layout-specific pieces ({#cursor_position},
|
|
2200
|
+
# {#repaint}) and add their own keys (HOME/END, ENTER, UP/DOWN,
|
|
2201
|
+
# printable insertion) by overriding the protected
|
|
2202
|
+
# {#handle_text_input_key} hook — `super` falls through to the common
|
|
2203
|
+
# navigation handling.
|
|
2204
|
+
#
|
|
2205
|
+
# The mutation pipeline is a template method: {#text=} and {#caret=}
|
|
2206
|
+
# detect no-ops, mutate state, fire {#on_change}, and invalidate.
|
|
2207
|
+
# Subclasses inject their own behavior via two protected hooks:
|
|
2208
|
+
#
|
|
2209
|
+
# - {#preprocess_text} — input filter (e.g. {TextField} truncates to
|
|
2210
|
+
# fit `rect.width - 1`).
|
|
2211
|
+
# - {#on_text_mutated} / {#on_caret_mutated} — post-mutation side
|
|
2212
|
+
# effects (e.g. {TextArea} invalidates its wrap cache and scrolls to
|
|
2213
|
+
# keep the caret visible).
|
|
2214
|
+
class TextInput < Component
|
|
2215
|
+
ACTIVE_BG_SGR: String
|
|
2216
|
+
INACTIVE_BG_SGR: String
|
|
2217
|
+
|
|
2218
|
+
def initialize: () -> void
|
|
2219
|
+
|
|
2220
|
+
# _@return_ — true iff {#text} is the empty string.
|
|
2221
|
+
def empty?: () -> bool
|
|
2222
|
+
|
|
2223
|
+
def focusable?: () -> bool
|
|
2224
|
+
|
|
2225
|
+
def tab_stop?: () -> bool
|
|
2226
|
+
|
|
2227
|
+
# Handles a key. Returns false when the component is inactive. Otherwise
|
|
2228
|
+
# first runs the {Component#handle_key} shortcut search via `super`, then
|
|
2229
|
+
# delegates to {#handle_text_input_key}.
|
|
2230
|
+
#
|
|
2231
|
+
# _@param_ `key`
|
|
2232
|
+
def handle_key: (String key) -> bool
|
|
2233
|
+
|
|
2234
|
+
# Input filter for {#text=}. Subclasses override to truncate or reject
|
|
2235
|
+
# invalid input. Default coerces to String.
|
|
2236
|
+
#
|
|
2237
|
+
# _@param_ `new_text`
|
|
2238
|
+
#
|
|
2239
|
+
# _@return_ — possibly transformed text.
|
|
2240
|
+
def preprocess_text: (String new_text) -> String
|
|
2241
|
+
|
|
2242
|
+
# Hook called after {#text} has been mutated, before invalidation /
|
|
2243
|
+
# {#on_change}. Default no-op. Subclasses use this to invalidate caches
|
|
2244
|
+
# ({TextArea}'s wrap cache) and update derived state.
|
|
2245
|
+
def on_text_mutated: () -> void
|
|
2246
|
+
|
|
2247
|
+
# Hook called after {#caret} has been mutated, before invalidation.
|
|
2248
|
+
# Default no-op. Subclasses use this to keep the caret visible
|
|
2249
|
+
# ({TextArea}'s vertical scroll).
|
|
2250
|
+
def on_caret_mutated: () -> void
|
|
2251
|
+
|
|
2252
|
+
# Dispatch hook for {#handle_key}. Handles ESC and the navigation keys
|
|
2253
|
+
# that have identical semantics in single-line and multi-line inputs:
|
|
2254
|
+
# LEFT/RIGHT arrows, CTRL+LEFT/CTRL+RIGHT for word jumps. Subclasses
|
|
2255
|
+
# override to add their own keys (HOME/END, UP/DOWN, ENTER, BACKSPACE/
|
|
2256
|
+
# DELETE, printable insertion) and call `super` to fall back to the
|
|
2257
|
+
# common navigation handling.
|
|
2258
|
+
#
|
|
2259
|
+
# _@param_ `key`
|
|
2260
|
+
#
|
|
2261
|
+
# _@return_ — true if the key was handled.
|
|
2262
|
+
def handle_text_input_key: (String key) -> bool
|
|
2263
|
+
|
|
1576
2264
|
def delete_before_caret: () -> void
|
|
1577
2265
|
|
|
1578
2266
|
def delete_at_caret: () -> void
|
|
1579
2267
|
|
|
1580
|
-
#
|
|
1581
|
-
|
|
2268
|
+
# Default {#on_escape} action: clear focus. Component deactivates; user
|
|
2269
|
+
# can re-focus by clicking or tabbing back in.
|
|
2270
|
+
def default_on_escape: () -> void
|
|
1582
2271
|
|
|
1583
2272
|
# Caret target for ctrl+left: skip whitespace going left, then a run of
|
|
1584
2273
|
# non-whitespace. Lands at the beginning of the current word, or the
|
|
@@ -1596,13 +2285,6 @@ module Tuile
|
|
|
1596
2285
|
# _@return_ — caret index in `0..text.length`.
|
|
1597
2286
|
attr_accessor caret: Integer
|
|
1598
2287
|
|
|
1599
|
-
# Optional callback fired when ESC is pressed. When set, ESC is consumed
|
|
1600
|
-
# by the field; when nil, ESC falls through to the parent (default
|
|
1601
|
-
# behavior).
|
|
1602
|
-
#
|
|
1603
|
-
# _@return_ — no-arg callable, or nil.
|
|
1604
|
-
attr_accessor on_escape: (Proc | Method)?
|
|
1605
|
-
|
|
1606
2288
|
# Optional callback fired whenever {#text} changes. Receives the new text
|
|
1607
2289
|
# as a single argument. Not fired by {#caret=} (text unchanged) and not
|
|
1608
2290
|
# fired when a setter is a no-op.
|
|
@@ -1610,28 +2292,14 @@ module Tuile
|
|
|
1610
2292
|
# _@return_ — one-arg callable, or nil.
|
|
1611
2293
|
attr_accessor on_change: (Proc | Method)?
|
|
1612
2294
|
|
|
1613
|
-
#
|
|
1614
|
-
#
|
|
1615
|
-
#
|
|
1616
|
-
#
|
|
1617
|
-
#
|
|
1618
|
-
# _@return_ — no-arg callable, or nil.
|
|
1619
|
-
attr_accessor on_key_up: (Proc | Method)?
|
|
1620
|
-
|
|
1621
|
-
# Optional callback fired when the DOWN arrow key is pressed. When set,
|
|
1622
|
-
# DOWN is consumed by the field; when nil, DOWN falls through to the
|
|
1623
|
-
# parent (default behavior). Only triggered by {Keys::DOWN_ARROW}, not by
|
|
1624
|
-
# `j`, since `j` is a printable character inserted into {#text}.
|
|
2295
|
+
# Callback fired when ESC is pressed. Defaults to a closure that clears
|
|
2296
|
+
# focus (`screen.focused = nil`) so ESC visibly cancels text entry instead
|
|
2297
|
+
# of bubbling to the parent — and, in particular, instead of reaching the
|
|
2298
|
+
# screen's default ESC-to-quit handler. Set to nil to let ESC fall through
|
|
2299
|
+
# to the parent again; set to any other callable to replace the default.
|
|
1625
2300
|
#
|
|
1626
2301
|
# _@return_ — no-arg callable, or nil.
|
|
1627
|
-
attr_accessor
|
|
1628
|
-
|
|
1629
|
-
# Optional callback fired when ENTER is pressed. When set, ENTER is
|
|
1630
|
-
# consumed by the field; when nil, ENTER falls through to the parent
|
|
1631
|
-
# (default behavior).
|
|
1632
|
-
#
|
|
1633
|
-
# _@return_ — no-arg callable, or nil.
|
|
1634
|
-
attr_accessor on_enter: (Proc | Method)?
|
|
2302
|
+
attr_accessor on_escape: (Proc | Method)?
|
|
1635
2303
|
end
|
|
1636
2304
|
|
|
1637
2305
|
# A mixin interface for a component with one child tops. The host must
|
|
@@ -1758,13 +2426,39 @@ module Tuile
|
|
|
1758
2426
|
# Awaits until the event queue is empty (all events have been processed).
|
|
1759
2427
|
def await_empty: () -> void
|
|
1760
2428
|
|
|
2429
|
+
# Schedules `block` to fire on the event-loop thread roughly `fps` times
|
|
2430
|
+
# per second, passing a 0-based monotonically increasing tick counter. Use
|
|
2431
|
+
# it for animations (e.g. a `/-\|` spinner in a {Component::Label}) or
|
|
2432
|
+
# periodic UI refresh from a background task.
|
|
2433
|
+
#
|
|
2434
|
+
# The returned {Ticker} controls the schedule — call {Ticker#cancel} to
|
|
2435
|
+
# stop it.
|
|
2436
|
+
#
|
|
2437
|
+
# **Errors:** if `block` raises, the {Ticker} cancels itself and the
|
|
2438
|
+
# exception flows through the normal event-loop error path — i.e.
|
|
2439
|
+
# {Screen#on_error} for the default Tuile setup. Auto-cancel prevents a
|
|
2440
|
+
# broken block from spamming `on_error` at the tick rate.
|
|
2441
|
+
#
|
|
2442
|
+
# Tickers reuse `concurrent-ruby`'s shared timer thread
|
|
2443
|
+
# ({Concurrent}.global_timer_set) — adding more tickers does not add more
|
|
2444
|
+
# threads, just more work on the shared scheduler.
|
|
2445
|
+
#
|
|
2446
|
+
# _@param_ `fps` — firings per second, must be positive. Fractional values are fine (`fps: 0.5` ⇒ one tick every two seconds).
|
|
2447
|
+
def tick: (Numeric fps) ?{ (Integer tick) -> void } -> Ticker
|
|
2448
|
+
|
|
1761
2449
|
# Runs the event loop and blocks. Must be run from at most one thread at the
|
|
1762
2450
|
# same time. Blocks until some thread calls {#stop}. Calls block for all
|
|
1763
|
-
# events
|
|
1764
|
-
# running this function.
|
|
2451
|
+
# events; the block is always called from the thread running this function.
|
|
1765
2452
|
#
|
|
1766
|
-
# Any exception raised by block is re-thrown, causing this function to
|
|
1767
|
-
# terminate.
|
|
2453
|
+
# Any exception raised by the block is re-thrown, causing this function to
|
|
2454
|
+
# terminate. Wrap the block body in `rescue` if you want to handle errors
|
|
2455
|
+
# without tearing down the loop — see {Screen#event_loop} for an example.
|
|
2456
|
+
#
|
|
2457
|
+
# **Procs are yielded too.** A {#submit}ed block arrives as a `Proc` event;
|
|
2458
|
+
# the consumer is responsible for invoking it (typically `event.call`).
|
|
2459
|
+
# Yielding rather than dispatching inline means a raise inside the
|
|
2460
|
+
# submitted block flows through the consumer's `rescue` like any other
|
|
2461
|
+
# event-handler error, instead of bypassing it.
|
|
1768
2462
|
def run_loop: () ?{ (Object event) -> void } -> void
|
|
1769
2463
|
|
|
1770
2464
|
# _@return_ — true if this thread is running inside an event queue.
|
|
@@ -1835,6 +2529,36 @@ module Tuile
|
|
|
1835
2529
|
class EmptyQueueEvent
|
|
1836
2530
|
include Singleton
|
|
1837
2531
|
end
|
|
2532
|
+
|
|
2533
|
+
# Handle returned by {EventQueue#tick}. Cancel a running ticker via
|
|
2534
|
+
# {#cancel}.
|
|
2535
|
+
#
|
|
2536
|
+
# Internally wraps a `Concurrent::TimerTask` whose firing posts a single
|
|
2537
|
+
# submit-block to the owning {EventQueue}; the user's block therefore
|
|
2538
|
+
# always runs on the event-loop thread and may freely mutate UI. If the
|
|
2539
|
+
# user block raises, the Ticker auto-cancels and the exception is
|
|
2540
|
+
# re-raised so it flows through the loop's normal error handling
|
|
2541
|
+
# ({Screen#on_error} for the default Tuile setup).
|
|
2542
|
+
class Ticker
|
|
2543
|
+
# _@param_ `event_queue` — queue to dispatch tick calls onto.
|
|
2544
|
+
#
|
|
2545
|
+
# _@param_ `fps` — firings per second (positive).
|
|
2546
|
+
#
|
|
2547
|
+
# _@param_ `block` — called as `block.call(tick_count)` on each fire.
|
|
2548
|
+
def initialize: (EventQueue event_queue, Numeric fps, Proc block) -> void
|
|
2549
|
+
|
|
2550
|
+
# _@return_ — true once {#cancel} has been called.
|
|
2551
|
+
def cancelled?: () -> bool
|
|
2552
|
+
|
|
2553
|
+
# Stops the ticker. Idempotent and safe to call from any thread,
|
|
2554
|
+
# including from inside the tick block. Any tick already queued on the
|
|
2555
|
+
# event loop at the moment of cancellation is dropped before the user
|
|
2556
|
+
# block runs.
|
|
2557
|
+
def cancel: () -> void
|
|
2558
|
+
|
|
2559
|
+
# Runs on the event-loop thread.
|
|
2560
|
+
def fire: () -> void
|
|
2561
|
+
end
|
|
1838
2562
|
end
|
|
1839
2563
|
|
|
1840
2564
|
# Testing only — a screen which doesn't paint anything and pretends that the
|
|
@@ -1881,7 +2605,7 @@ module Tuile
|
|
|
1881
2605
|
#
|
|
1882
2606
|
# @!attribute [r] button
|
|
1883
2607
|
# @return [Symbol, nil] one of `:left`, `:middle`, `:right`, `:scroll_up`,
|
|
1884
|
-
# `:scroll_down`; `nil` if not known.
|
|
2608
|
+
# `:scroll_down`, `:scroll_left`, `:scroll_right`; `nil` if not known.
|
|
1885
2609
|
# @!attribute [r] x
|
|
1886
2610
|
# @return [Integer] x coordinate, 0-based.
|
|
1887
2611
|
# @!attribute [r] y
|
|
@@ -1890,14 +2614,29 @@ module Tuile
|
|
|
1890
2614
|
# _@return_ — the event's position.
|
|
1891
2615
|
def point: () -> Point
|
|
1892
2616
|
|
|
1893
|
-
# Checks whether given key is a mouse event key
|
|
2617
|
+
# Checks whether given key is a mouse event key. Returns true on the X10
|
|
2618
|
+
# `\e[M` prefix regardless of length — {.parse} is the place that
|
|
2619
|
+
# validates the full 6-byte shape and raises on malformed input.
|
|
1894
2620
|
#
|
|
1895
2621
|
# _@param_ `key` — key read via {Keys.getkey}
|
|
1896
2622
|
#
|
|
1897
2623
|
# _@return_ — true if it is a mouse event
|
|
1898
2624
|
def self.mouse_event?: (String key) -> bool
|
|
1899
2625
|
|
|
2626
|
+
# Parses an X10 mouse report (`\e[M` + 3 bytes: button, x, y).
|
|
2627
|
+
#
|
|
2628
|
+
# Raises {Tuile::Error} when `key` starts with the mouse prefix but is
|
|
2629
|
+
# not exactly 6 bytes long. Both shorter and longer inputs are bugs in
|
|
2630
|
+
# the upstream key-reader: a shorter prefix means the tail was lost on
|
|
2631
|
+
# the way in, and a longer one means we over-consumed into the next
|
|
2632
|
+
# escape sequence. We refuse to silently truncate either case because
|
|
2633
|
+
# the trailing `\e` of an over-read corrupts the *next* getkey, and the
|
|
2634
|
+
# corruption then surfaces as garbled keystrokes in focused inputs
|
|
2635
|
+
# rather than as a parser failure pointing at the actual cause.
|
|
2636
|
+
#
|
|
1900
2637
|
# _@param_ `key` — key read via {Keys.getkey}
|
|
2638
|
+
#
|
|
2639
|
+
# _@return_ — `nil` if `key` is not a mouse event
|
|
1901
2640
|
def self.parse: (String key) -> MouseEvent?
|
|
1902
2641
|
|
|
1903
2642
|
def self.start_tracking: () -> String
|
|
@@ -1905,7 +2644,7 @@ module Tuile
|
|
|
1905
2644
|
def self.stop_tracking: () -> String
|
|
1906
2645
|
|
|
1907
2646
|
# _@return_ — one of `:left`, `:middle`, `:right`, `:scroll_up`,
|
|
1908
|
-
# `:scroll_down`; `nil` if not known.
|
|
2647
|
+
# `:scroll_down`, `:scroll_left`, `:scroll_right`; `nil` if not known.
|
|
1909
2648
|
attr_reader button: Symbol?
|
|
1910
2649
|
|
|
1911
2650
|
# _@return_ — x coordinate, 0-based.
|
|
@@ -2167,8 +2906,16 @@ module Tuile
|
|
|
2167
2906
|
# `underline`). Useful for row-level highlights — the new bg overlays
|
|
2168
2907
|
# without dropping foreground colors the original styling carried.
|
|
2169
2908
|
#
|
|
2170
|
-
# _@param_ `bg` — background color,
|
|
2171
|
-
def with_bg: ((Symbol | Integer | ::Array[Integer])? bg) -> StyledString
|
|
2909
|
+
# _@param_ `bg` — background color, coerced via {Color.coerce}. `nil` clears bg back to the terminal default.
|
|
2910
|
+
def with_bg: ((Color | Symbol | Integer | ::Array[Integer])? bg) -> StyledString
|
|
2911
|
+
|
|
2912
|
+
# Returns a new {StyledString} with `fg` applied to every span, preserving
|
|
2913
|
+
# each span's text and other style attributes (`bg`, `bold`, `italic`,
|
|
2914
|
+
# `underline`). The new fg overlays without dropping background colors or
|
|
2915
|
+
# text attributes the original styling carried.
|
|
2916
|
+
#
|
|
2917
|
+
# _@param_ `fg` — foreground color, coerced via {Color.coerce}. `nil` clears fg back to the terminal default.
|
|
2918
|
+
def with_fg: ((Color | Symbol | Integer | ::Array[Integer])? fg) -> StyledString
|
|
2172
2919
|
|
|
2173
2920
|
def inspect: () -> String
|
|
2174
2921
|
|
|
@@ -2184,10 +2931,11 @@ module Tuile
|
|
|
2184
2931
|
|
|
2185
2932
|
# _@param_ `color`
|
|
2186
2933
|
#
|
|
2187
|
-
# _@param_ `
|
|
2934
|
+
# _@param_ `target` — `:fg` or `:bg`.
|
|
2188
2935
|
#
|
|
2189
|
-
# _@
|
|
2190
|
-
|
|
2936
|
+
# _@return_ — SGR codes; `[39]` / `[49]` for the "default" reset
|
|
2937
|
+
# when `color` is `nil`, otherwise delegated to {Color#sgr_codes}.
|
|
2938
|
+
def color_codes: (Color? color, target: Symbol) -> ::Array[Integer]
|
|
2191
2939
|
|
|
2192
2940
|
# _@param_ `start_or_range`
|
|
2193
2941
|
#
|
|
@@ -2240,18 +2988,16 @@ module Tuile
|
|
|
2240
2988
|
class ParseError < Tuile::Error
|
|
2241
2989
|
end
|
|
2242
2990
|
|
|
2243
|
-
# A frozen value type describing the visual style of a {Span}.
|
|
2244
|
-
#
|
|
2245
|
-
#
|
|
2246
|
-
#
|
|
2247
|
-
#
|
|
2248
|
-
# - an Integer 0..255 — 256-color palette index (SGR 38;5;N / 48;5;N)
|
|
2249
|
-
# - an `[r, g, b]` Array of three 0..255 Integers — 24-bit RGB
|
|
2991
|
+
# A frozen value type describing the visual style of a {Span}. Colors are
|
|
2992
|
+
# stored as {Color} instances (or `nil` for the terminal default); inputs
|
|
2993
|
+
# to {.new} and {#merge} are coerced via {Color.coerce}, so the four
|
|
2994
|
+
# accepted color forms — `nil`, Symbol, Integer 0..255, RGB Array — work
|
|
2995
|
+
# transparently.
|
|
2250
2996
|
#
|
|
2251
2997
|
# @!attribute [r] fg
|
|
2252
|
-
# @return [
|
|
2998
|
+
# @return [Color, nil]
|
|
2253
2999
|
# @!attribute [r] bg
|
|
2254
|
-
# @return [
|
|
3000
|
+
# @return [Color, nil]
|
|
2255
3001
|
# @!attribute [r] bold
|
|
2256
3002
|
# @return [Boolean]
|
|
2257
3003
|
# @!attribute [r] italic
|
|
@@ -2259,12 +3005,11 @@ module Tuile
|
|
|
2259
3005
|
# @!attribute [r] underline
|
|
2260
3006
|
# @return [Boolean]
|
|
2261
3007
|
class Style
|
|
2262
|
-
COLOR_SYMBOLS: ::Array[Symbol]
|
|
2263
3008
|
DEFAULT: Style
|
|
2264
3009
|
|
|
2265
|
-
# _@param_ `fg`
|
|
3010
|
+
# _@param_ `fg` — coerced via {Color.coerce}.
|
|
2266
3011
|
#
|
|
2267
|
-
# _@param_ `bg`
|
|
3012
|
+
# _@param_ `bg` — coerced via {Color.coerce}.
|
|
2268
3013
|
#
|
|
2269
3014
|
# _@param_ `bold`
|
|
2270
3015
|
#
|
|
@@ -2272,18 +3017,13 @@ module Tuile
|
|
|
2272
3017
|
#
|
|
2273
3018
|
# _@param_ `underline`
|
|
2274
3019
|
def self.new: (
|
|
2275
|
-
?fg: (Symbol | Integer | ::Array[Integer])?,
|
|
2276
|
-
?bg: (Symbol | Integer | ::Array[Integer])?,
|
|
3020
|
+
?fg: (Color | Symbol | Integer | ::Array[Integer])?,
|
|
3021
|
+
?bg: (Color | Symbol | Integer | ::Array[Integer])?,
|
|
2277
3022
|
?bold: bool,
|
|
2278
3023
|
?italic: bool,
|
|
2279
3024
|
?underline: bool
|
|
2280
3025
|
) -> Style
|
|
2281
3026
|
|
|
2282
|
-
# _@param_ `color`
|
|
2283
|
-
#
|
|
2284
|
-
# _@param_ `which`
|
|
2285
|
-
def self.validate_color!: (Object color, Symbol which) -> void
|
|
2286
|
-
|
|
2287
3027
|
def default?: () -> bool
|
|
2288
3028
|
|
|
2289
3029
|
# Returns a new {Style} with the given attributes overridden.
|
|
@@ -2291,9 +3031,9 @@ module Tuile
|
|
|
2291
3031
|
# _@param_ `overrides`
|
|
2292
3032
|
def merge: (**::Hash[Symbol, Object] overrides) -> Style
|
|
2293
3033
|
|
|
2294
|
-
attr_reader fg:
|
|
3034
|
+
attr_reader fg: Color?
|
|
2295
3035
|
|
|
2296
|
-
attr_reader bg:
|
|
3036
|
+
attr_reader bg: Color?
|
|
2297
3037
|
|
|
2298
3038
|
attr_reader bold: bool
|
|
2299
3039
|
|
|
@@ -2359,6 +3099,8 @@ module Tuile
|
|
|
2359
3099
|
# A "synchronous" event queue – no loop is run, submitted blocks are run right
|
|
2360
3100
|
# away and submitted events are thrown away. Intended for testing only.
|
|
2361
3101
|
class FakeEventQueue
|
|
3102
|
+
def initialize: () -> void
|
|
3103
|
+
|
|
2362
3104
|
def locked?: () -> bool
|
|
2363
3105
|
|
|
2364
3106
|
def stop: () -> void
|
|
@@ -2371,6 +3113,42 @@ module Tuile
|
|
|
2371
3113
|
|
|
2372
3114
|
# _@param_ `event`
|
|
2373
3115
|
def post: (Object event) -> void
|
|
3116
|
+
|
|
3117
|
+
# Mirrors {EventQueue#tick} but timeless: returns a {FakeTicker} that
|
|
3118
|
+
# only fires when a test calls {#tick_once}. The `fps` argument is
|
|
3119
|
+
# validated the same way the real queue validates it, then discarded —
|
|
3120
|
+
# the fake has no clock, so frame cadence is up to the test.
|
|
3121
|
+
#
|
|
3122
|
+
# _@param_ `fps` — firings per second, must be positive. Validated for parity with {EventQueue#tick}; otherwise unused.
|
|
3123
|
+
def tick: (Numeric fps) ?{ (Integer tick) -> void } -> FakeTicker
|
|
3124
|
+
|
|
3125
|
+
# Test helper: fires every live ticker's user block once and prunes
|
|
3126
|
+
# cancelled tickers. No-op when no tickers are registered. Pumps once
|
|
3127
|
+
# per call regardless of any ticker's fps — the fake has no clock, so
|
|
3128
|
+
# tests pump N frames by calling this N times.
|
|
3129
|
+
def tick_once: () -> void
|
|
3130
|
+
|
|
3131
|
+
# Handle returned by {FakeEventQueue#tick}. Mirrors the public surface of
|
|
3132
|
+
# {EventQueue::Ticker} (`cancel`, `cancelled?`) but does not auto-fire —
|
|
3133
|
+
# the host {FakeEventQueue} drives firing via {FakeEventQueue#tick_once}.
|
|
3134
|
+
class FakeTicker
|
|
3135
|
+
# _@param_ `block` — called as `block.call(tick_count)` on each {#fire}.
|
|
3136
|
+
def initialize: (Proc block) -> void
|
|
3137
|
+
|
|
3138
|
+
# _@return_ — true once {#cancel} has been called.
|
|
3139
|
+
def cancelled?: () -> bool
|
|
3140
|
+
|
|
3141
|
+
# Marks the ticker cancelled. Idempotent. Subsequent {#fire} calls are
|
|
3142
|
+
# no-ops; {FakeEventQueue#tick_once} also prunes the ticker on its next
|
|
3143
|
+
# pass.
|
|
3144
|
+
def cancel: () -> void
|
|
3145
|
+
|
|
3146
|
+
# Invokes the user block with the current tick counter, then advances.
|
|
3147
|
+
# No-op when {#cancelled?}. Typically driven by
|
|
3148
|
+
# {FakeEventQueue#tick_once}; safe to call directly from a test that
|
|
3149
|
+
# wants to drive a single ticker.
|
|
3150
|
+
def fire: () -> void
|
|
3151
|
+
end
|
|
2374
3152
|
end
|
|
2375
3153
|
|
|
2376
3154
|
# A vertical scrollbar that computes which character to draw at each row.
|