tuile 0.13.0 → 0.14.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 +81 -37
- data/COMPARISON.md +101 -0
- data/DECISIONS.md +1095 -195
- data/README.md +19 -19
- data/TERMINOLOGY.md +6 -5
- data/book/03-layout.md +17 -10
- data/book/05-focus.md +4 -1
- data/book/06-theming.md +98 -0
- data/book/07-components.md +169 -19
- data/book/08-testing.md +16 -0
- data/book/09-styled-text.md +3 -3
- data/examples/file_commander.rb +1 -1
- data/examples/sampler.rb +143 -43
- data/ideas/arrow-key-navigation.md +2 -2
- data/ideas/modal-backdrop.md +24 -0
- data/ideas/new-components.md +24 -24
- data/lib/tuile/buffer.rb +51 -3
- data/lib/tuile/color.rb +143 -0
- data/lib/tuile/color_depth.rb +80 -0
- data/lib/tuile/component/button.rb +3 -3
- data/lib/tuile/component/checkbox.rb +3 -3
- data/lib/tuile/component/combo_box.rb +9 -2
- data/lib/tuile/component/confirm_window.rb +442 -0
- data/lib/tuile/component/has_content.rb +22 -9
- data/lib/tuile/component/has_value.rb +1 -1
- data/lib/tuile/component/info_window.rb +64 -16
- data/lib/tuile/component/layout.rb +0 -10
- data/lib/tuile/component/list_dropdown.rb +18 -10
- data/lib/tuile/component/log_text_view.rb +71 -0
- data/lib/tuile/component/log_window.rb +13 -48
- data/lib/tuile/component/menu_bar/cascade.rb +3 -3
- data/lib/tuile/component/menu_bar.rb +5 -5
- data/lib/tuile/component/notification.rb +16 -34
- data/lib/tuile/component/overlay.rb +192 -0
- data/lib/tuile/component/popup.rb +59 -187
- data/lib/tuile/component/progress_bar.rb +1 -1
- data/lib/tuile/component/select.rb +14 -6
- data/lib/tuile/component/slot.rb +54 -0
- data/lib/tuile/component/tab_sheet.rb +0 -11
- data/lib/tuile/component/tabs.rb +5 -5
- data/lib/tuile/component/window.rb +22 -46
- data/lib/tuile/component.rb +149 -19
- data/lib/tuile/event_queue.rb +21 -1
- data/lib/tuile/fake_screen.rb +26 -2
- data/lib/tuile/keys.rb +7 -0
- data/lib/tuile/screen.rb +120 -38
- data/lib/tuile/screen_pane.rb +37 -35
- data/lib/tuile/styled_string.rb +40 -7
- data/lib/tuile/terminal_background.rb +74 -16
- data/lib/tuile/version.rb +1 -1
- data/sig/tuile.rbs +1157 -368
- metadata +8 -1
data/sig/tuile.rbs
CHANGED
|
@@ -274,6 +274,11 @@ module Tuile
|
|
|
274
274
|
# {Color} instances only.
|
|
275
275
|
class Color
|
|
276
276
|
COLOR_SYMBOLS: ::Array[Symbol]
|
|
277
|
+
CUBE_LEVELS: ::Array[Integer]
|
|
278
|
+
CUBE_INDEX: ::Array[Integer]
|
|
279
|
+
ANSI16_RGB: ::Array[::Array[Integer]]
|
|
280
|
+
ANSI16_COLORS: ::Array[Color]
|
|
281
|
+
PALETTE_COLORS: ::Array[Color]
|
|
277
282
|
PALETTE_NAMES: ::Hash[Symbol, Integer]
|
|
278
283
|
|
|
279
284
|
# Coerces the input to a {Color}. `nil` passes through unchanged (callers
|
|
@@ -322,6 +327,36 @@ module Tuile
|
|
|
322
327
|
# _@param_ `target` — `:fg` or `:bg`.
|
|
323
328
|
def sgr_codes: (?Symbol target) -> ::Array[Integer]
|
|
324
329
|
|
|
330
|
+
# This color as the nearest one `depth` can actually show — the
|
|
331
|
+
# degradation {Buffer#flush} applies to every color on its way to the wire:
|
|
332
|
+
#
|
|
333
|
+
# Color.rgb(100, 100, 100).quantize(:palette256) # => Color.palette(241)
|
|
334
|
+
# Color.rgb(255, 0, 0).quantize(:ansi16) # => Color::BRIGHT_RED
|
|
335
|
+
# Color.rgb(255, 0, 0).quantize(:truecolor) # => itself, unchanged
|
|
336
|
+
#
|
|
337
|
+
# Returns **the same instance** whenever `depth` shows this color as-is —
|
|
338
|
+
# every named color at every depth, a palette index anywhere but
|
|
339
|
+
# `:ansi16`, RGB at `:truecolor` — so `color.quantize(depth).equal?(color)`
|
|
340
|
+
# *is* the "needs no translating" predicate, and the common path
|
|
341
|
+
# allocates nothing.
|
|
342
|
+
#
|
|
343
|
+
# == Implementation details
|
|
344
|
+
#
|
|
345
|
+
# RGB picks whichever is nearer in squared-RGB distance: the 6×6×6 cube
|
|
346
|
+
# (16..231, its per-channel nearest levels being the nearest cell outright
|
|
347
|
+
# — the axes are independent) or the 24-step grey ramp (232..255, whose
|
|
348
|
+
# nearest step is the one nearest the channel mean).
|
|
349
|
+
#
|
|
350
|
+
# Under `:ansi16` a color goes *direct* to the nearest of the 16, never
|
|
351
|
+
# via the 256-palette — two steps would compound the rounding — and the
|
|
352
|
+
# result is a *named* color, which keeps respecting the user's terminal
|
|
353
|
+
# scheme. Matching is against xterm's default RGBs for the 16, which that
|
|
354
|
+
# scheme may itself redefine: the one mapping here that can be honestly
|
|
355
|
+
# wrong.
|
|
356
|
+
#
|
|
357
|
+
# _@param_ `depth` — one of {ColorDepth::DEPTHS}.
|
|
358
|
+
def quantize: (Symbol depth) -> Color
|
|
359
|
+
|
|
325
360
|
# Full SGR escape sequence for this color (e.g. `"\e[31m"`). Useful for
|
|
326
361
|
# `print`-style direct emission; for composing with other attributes use
|
|
327
362
|
# {#sgr_codes} instead.
|
|
@@ -336,6 +371,30 @@ module Tuile
|
|
|
336
371
|
|
|
337
372
|
def inspect: () -> String
|
|
338
373
|
|
|
374
|
+
# This color's RGB — the palette cell's own coordinates when the value is
|
|
375
|
+
# an index. Only ever asked of a non-Symbol value; a named color has no
|
|
376
|
+
# RGB of its own, since the terminal's scheme decides what it looks like.
|
|
377
|
+
#
|
|
378
|
+
# _@return_ — red, green and blue, each 0..255.
|
|
379
|
+
def rgb_triple: () -> ::Array[Integer]
|
|
380
|
+
|
|
381
|
+
# Written flat — destructured rather than splatted, `x * x` rather than
|
|
382
|
+
# `x**2`, no distance helper — because it runs per style transition in
|
|
383
|
+
# {Buffer#flush}, and the tidy shape measures ~2x slower (see
|
|
384
|
+
# `benchmark/quantize.rb`).
|
|
385
|
+
#
|
|
386
|
+
# _@param_ `rgb` — red, green and blue, each 0..255.
|
|
387
|
+
#
|
|
388
|
+
# _@return_ — palette index, 16..255 — the nearer of this color's
|
|
389
|
+
# cube cell and its grey-ramp step. A tie goes to the cube, which spans
|
|
390
|
+
# the whole space where the ramp only covers the diagonal.
|
|
391
|
+
def nearest_palette: (::Array[Integer] rgb) -> Integer
|
|
392
|
+
|
|
393
|
+
# _@param_ `rgb` — red, green and blue, each 0..255.
|
|
394
|
+
#
|
|
395
|
+
# _@return_ — index into {COLOR_SYMBOLS} of the nearest of the 16.
|
|
396
|
+
def nearest_ansi16: (::Array[Integer] rgb) -> Integer
|
|
397
|
+
|
|
339
398
|
# The underlying raw representation — a Symbol, Integer, or frozen
|
|
340
399
|
# Array<Integer>.
|
|
341
400
|
attr_reader value: (Symbol | Integer | ::Array[Integer])
|
|
@@ -635,7 +694,9 @@ module Tuile
|
|
|
635
694
|
def self.display_width: (String grapheme) -> Integer
|
|
636
695
|
|
|
637
696
|
# _@param_ `size` — grid dimensions in columns × rows.
|
|
638
|
-
|
|
697
|
+
#
|
|
698
|
+
# _@param_ `color_depth` — what the terminal can show — one of {ColorDepth::DEPTHS}; {#flush} degrades every emitted color to it. Validated here rather than at paint time: a bad value would otherwise surface as an exception mid-frame, far from the mistake.
|
|
699
|
+
def initialize: (Size size, ?color_depth: Symbol) -> void
|
|
639
700
|
|
|
640
701
|
# _@return_ — grid dimensions.
|
|
641
702
|
def size: () -> Size
|
|
@@ -796,6 +857,26 @@ module Tuile
|
|
|
796
857
|
# _@param_ `style` — style the terminal currently holds.
|
|
797
858
|
def flush_row: (String _out, Integer y, StyledString::Style style) -> StyledString::Style
|
|
798
859
|
|
|
860
|
+
# `style` as {#color_depth} can actually show it, each color through
|
|
861
|
+
# {Color#quantize}. Applied *before* the {StyledString::Style#sgr_to}
|
|
862
|
+
# diff, so two RGBs that quantize onto the same cell emit nothing at all
|
|
863
|
+
# rather than a redundant SGR.
|
|
864
|
+
#
|
|
865
|
+
# The one-slot memo is load-bearing, not a micro-optimization: this runs
|
|
866
|
+
# per dirty *cell*, while a painted run shares one frozen
|
|
867
|
+
# {StyledString::Style} instance, so remembering just the last answer
|
|
868
|
+
# collapses the work onto actual style transitions. Without it a
|
|
869
|
+
# full-screen repaint of RGB-styled content measured 51 ms against 15 ms
|
|
870
|
+
# at `:truecolor` — a keyed cache is still the wrong answer
|
|
871
|
+
# (`D_color_depth`), but paying the arithmetic 8000 times for one span
|
|
872
|
+
# was too.
|
|
873
|
+
#
|
|
874
|
+
# _@param_ `style`
|
|
875
|
+
#
|
|
876
|
+
# _@return_ — `style` itself whenever nothing needed
|
|
877
|
+
# degrading — {Color#quantize}'s identity contract, extended.
|
|
878
|
+
def quantized_style: (StyledString::Style style) -> StyledString::Style
|
|
879
|
+
|
|
799
880
|
# _@param_ `rect`
|
|
800
881
|
#
|
|
801
882
|
# _@return_ — cells within `rect`, row-major, clamped to
|
|
@@ -860,6 +941,11 @@ module Tuile
|
|
|
860
941
|
|
|
861
942
|
attr_reader height: Integer
|
|
862
943
|
|
|
944
|
+
# What the terminal can show ({ColorDepth::DEPTHS}). Cells hold whatever
|
|
945
|
+
# color a component painted — {#region_ansi} and friends report that,
|
|
946
|
+
# unchanged — and only {#flush} degrades it on the way to the wire.
|
|
947
|
+
attr_reader color_depth: Symbol
|
|
948
|
+
|
|
863
949
|
# One screen cell: a single grapheme cluster, the {StyledString::Style} it's
|
|
864
950
|
# drawn in, and a dirty flag. Mutable by design (see {Buffer} "Dirty
|
|
865
951
|
# tracking") — the grid rewrites cells in place. A continuation cell (right
|
|
@@ -923,13 +1009,13 @@ module Tuile
|
|
|
923
1009
|
# out its own children), the modal/overlay {#popups} stack (opened via
|
|
924
1010
|
# {Component::Popup#open}, drawn on top of the content). Popups are *not*
|
|
925
1011
|
# sized from their content — each carries its own top-down
|
|
926
|
-
# {Component::Popup#
|
|
1012
|
+
# {Component::Popup#declared_size} — and they deliberately overdraw the content
|
|
927
1013
|
# without clipping.
|
|
928
1014
|
#
|
|
929
1015
|
# Tuile draws no chrome of its own: there is no status bar and no reserved
|
|
930
1016
|
# row, so {#content} gets the whole terminal. An app that wants a status line
|
|
931
1017
|
# builds one into its own layout and drives it from {#on_focus_changed=}
|
|
932
|
-
# (`
|
|
1018
|
+
# (`D_status_bar`).
|
|
933
1019
|
#
|
|
934
1020
|
# ## Repaint model
|
|
935
1021
|
#
|
|
@@ -976,9 +1062,10 @@ module Tuile
|
|
|
976
1062
|
# _@param_ `content`
|
|
977
1063
|
def content=: (Component content) -> void
|
|
978
1064
|
|
|
979
|
-
# _@return_ —
|
|
980
|
-
#
|
|
981
|
-
|
|
1065
|
+
# _@return_ — the open overlay stack — both
|
|
1066
|
+
# {Component::Popup} modals and bare {Component::Overlay}s — in stacking
|
|
1067
|
+
# order (forwarded to {ScreenPane}). The array must not be modified!
|
|
1068
|
+
def popups: () -> ::Array[Component::Overlay]
|
|
982
1069
|
|
|
983
1070
|
# `:idle` covers *both* ends of the screen's life — before the first
|
|
984
1071
|
# {#run_event_loop} and after it returns — and a screen may cycle
|
|
@@ -1004,11 +1091,11 @@ module Tuile
|
|
|
1004
1091
|
# _@param_ `component`
|
|
1005
1092
|
def invalidate: (Component component) -> void
|
|
1006
1093
|
|
|
1007
|
-
# Internal — use {Component::
|
|
1008
|
-
# {#pane}
|
|
1094
|
+
# Internal — use {Component::Overlay#open} instead. Adds the overlay to
|
|
1095
|
+
# {#pane}; a {Component::Popup} is additionally centered and focused.
|
|
1009
1096
|
#
|
|
1010
|
-
# _@param_ `window`
|
|
1011
|
-
def add_popup: (Component::
|
|
1097
|
+
# _@param_ `window` — any overlay, modal or not.
|
|
1098
|
+
def add_popup: (Component::Overlay window) -> void
|
|
1012
1099
|
|
|
1013
1100
|
# Runs the event loop on the calling thread, taking over stdin (raw mode,
|
|
1014
1101
|
# echo off): keys and mouse events are dispatched via {#handle_key} /
|
|
@@ -1069,13 +1156,13 @@ module Tuile
|
|
|
1069
1156
|
# _@param_ `key`
|
|
1070
1157
|
def unregister_global_shortcut: (String key) -> void
|
|
1071
1158
|
|
|
1072
|
-
# Internal — use {Component::
|
|
1159
|
+
# Internal — use {Component::Overlay#close} instead. Removes the overlay
|
|
1073
1160
|
# from {#pane}, repairs focus, and repaints the scene.
|
|
1074
1161
|
#
|
|
1075
|
-
# Does nothing if the
|
|
1162
|
+
# Does nothing if the overlay is not open on this screen.
|
|
1076
1163
|
#
|
|
1077
|
-
# _@param_ `window`
|
|
1078
|
-
def remove_popup: (Component::
|
|
1164
|
+
# _@param_ `window` — any overlay, modal or not.
|
|
1165
|
+
def remove_popup: (Component::Overlay window) -> void
|
|
1079
1166
|
|
|
1080
1167
|
# Invalidates the entire attached tree, forcing every component to repaint
|
|
1081
1168
|
# on the next cycle. Needed whenever something overdraws the scene without
|
|
@@ -1086,12 +1173,12 @@ module Tuile
|
|
|
1086
1173
|
# its own, so we accept the cost of a full repaint.
|
|
1087
1174
|
def needs_full_repaint: () -> void
|
|
1088
1175
|
|
|
1089
|
-
# Internal — use {Component::
|
|
1176
|
+
# Internal — use {Component::Overlay#open?} instead.
|
|
1090
1177
|
#
|
|
1091
|
-
# _@param_ `window`
|
|
1178
|
+
# _@param_ `window` — any overlay, modal or not.
|
|
1092
1179
|
#
|
|
1093
|
-
# _@return_ — true if this
|
|
1094
|
-
def has_popup?: (Component::
|
|
1180
|
+
# _@return_ — true if this overlay is currently mounted.
|
|
1181
|
+
def has_popup?: (Component::Overlay window) -> bool
|
|
1095
1182
|
|
|
1096
1183
|
# Testing only — creates new screen, locks the UI, and prevents any
|
|
1097
1184
|
# redraws, so that test TTY is not painted over. {FakeScreen#initialize}
|
|
@@ -1107,11 +1194,15 @@ module Tuile
|
|
|
1107
1194
|
def self.close: () -> void
|
|
1108
1195
|
|
|
1109
1196
|
# Writes terminal-housekeeping escapes straight to stdout: {#clear},
|
|
1110
|
-
# mouse-tracking start/stop, the color-scheme notify toggles,
|
|
1111
|
-
# on teardown. Component painting does
|
|
1112
|
-
# writes into {#buffer}, which
|
|
1113
|
-
#
|
|
1114
|
-
# test runner's stdout.
|
|
1197
|
+
# mouse-tracking start/stop, the color-scheme notify toggles, the OSC 11
|
|
1198
|
+
# background re-probe, cursor-show on teardown. Component painting does
|
|
1199
|
+
# *not* go through here anymore — it writes into {#buffer}, which
|
|
1200
|
+
# {#repaint} diffs and {#emit}s. {FakeScreen} overrides this (and
|
|
1201
|
+
# {#emit}) to capture into `@prints` instead of the test runner's stdout.
|
|
1202
|
+
#
|
|
1203
|
+
# Flushed, like {#emit}: none of these escapes ends in a newline, and a
|
|
1204
|
+
# buffered stdout would hold a *query* — one whose reply the app is
|
|
1205
|
+
# waiting on — until the next frame went out.
|
|
1115
1206
|
#
|
|
1116
1207
|
# _@param_ `args` — stuff to print.
|
|
1117
1208
|
def print: (*String args) -> void
|
|
@@ -1145,23 +1236,41 @@ module Tuile
|
|
|
1145
1236
|
# but only one focused.
|
|
1146
1237
|
def cursor_position: () -> Point?
|
|
1147
1238
|
|
|
1148
|
-
#
|
|
1149
|
-
#
|
|
1150
|
-
#
|
|
1151
|
-
#
|
|
1152
|
-
#
|
|
1153
|
-
#
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
#
|
|
1157
|
-
|
|
1239
|
+
# The startup background probe, seeding {#theme} and
|
|
1240
|
+
# {#background_color}. Inconclusive detection means `:dark` with no
|
|
1241
|
+
# color. Runs in the constructor — the OSC 11 reply arrives on stdin,
|
|
1242
|
+
# which is only safe to read before {EventQueue#start_key_thread} owns
|
|
1243
|
+
# it. {FakeScreen} overrides this to pin the result, keeping specs
|
|
1244
|
+
# deterministic and off the test runner's TTY.
|
|
1245
|
+
def detect_background: () -> TerminalBackground::Result
|
|
1246
|
+
|
|
1247
|
+
# The startup color-depth probe, seeding {#color_depth}. Reads the
|
|
1248
|
+
# environment only, so unlike {#detect_background} it has no timing
|
|
1249
|
+
# constraint. {FakeScreen} overrides it to pin the result, keeping specs
|
|
1250
|
+
# off whatever `COLORTERM` the test runner happens to carry.
|
|
1251
|
+
def detect_color_depth: () -> Symbol
|
|
1158
1252
|
|
|
1159
1253
|
# An OS appearance flip arrived (mode-2031 report): remember the
|
|
1160
|
-
# scheme
|
|
1254
|
+
# scheme, apply the matching member of {#theme_def}, and re-probe for
|
|
1255
|
+
# the new background RGB, which the report does not carry.
|
|
1256
|
+
#
|
|
1257
|
+
# The query goes out from *this* thread — the event-loop thread, which
|
|
1258
|
+
# also owns {#emit} — so its bytes can never land inside a frame's
|
|
1259
|
+
# synchronized-output batch. The reply comes back through the key
|
|
1260
|
+
# thread as an {EventQueue::BackgroundColorEvent}.
|
|
1161
1261
|
#
|
|
1162
1262
|
# _@param_ `scheme` — `:dark` or `:light`.
|
|
1163
1263
|
def on_color_scheme: (Symbol scheme) -> void
|
|
1164
1264
|
|
|
1265
|
+
# The re-probe answered: adopt the color and restyle, since an app's
|
|
1266
|
+
# background-derived tints are now a scheme behind. Deliberately keeps
|
|
1267
|
+
# the previous color until the reply lands rather than blanking it on
|
|
1268
|
+
# the flip — a terminal that reports mode-2031 flips but not OSC 11
|
|
1269
|
+
# would otherwise lose the color it gave us at startup, permanently.
|
|
1270
|
+
#
|
|
1271
|
+
# _@param_ `color`
|
|
1272
|
+
def on_background_color: (Color color) -> void
|
|
1273
|
+
|
|
1165
1274
|
# Walks the current modal scope in pre-order, collects tab stops, and
|
|
1166
1275
|
# advances focus by one (wrapping). When the focused component isn't in
|
|
1167
1276
|
# the tab order (e.g. focus is parked on a popup/window chrome with no
|
|
@@ -1236,6 +1345,16 @@ module Tuile
|
|
|
1236
1345
|
# _@return_ — `:light` or `:dark`
|
|
1237
1346
|
attr_reader color_scheme: Symbol
|
|
1238
1347
|
|
|
1348
|
+
# How many colors this terminal can show ({ColorDepth::DEPTHS}), detected
|
|
1349
|
+
# at construction. {Buffer#flush} degrades every color it emits to this,
|
|
1350
|
+
# so an app may compute an RGB tint — say from {#background_color} — and
|
|
1351
|
+
# paint with it, whatever the terminal turns out to understand.
|
|
1352
|
+
#
|
|
1353
|
+
# Deliberately read-only: detection runs once and the answer can't change
|
|
1354
|
+
# mid-session. Override a terminal that reports itself wrong through
|
|
1355
|
+
# {ColorDepth::OVERRIDE_ENV} instead.
|
|
1356
|
+
attr_reader color_depth: Symbol
|
|
1357
|
+
|
|
1239
1358
|
# _@return_ — the back buffer components paint into
|
|
1240
1359
|
# ({Buffer#set_text} / {Buffer#fill} / {Buffer#set_char}).
|
|
1241
1360
|
attr_reader buffer: Buffer
|
|
@@ -1280,6 +1399,25 @@ module Tuile
|
|
|
1280
1399
|
# the user toggling the OS appearance.
|
|
1281
1400
|
attr_accessor theme_def: ThemeDef
|
|
1282
1401
|
|
|
1402
|
+
# The terminal's own background, as it reported it — for deriving a
|
|
1403
|
+
# color *from* the background rather than picking one against it. A pane
|
|
1404
|
+
# tinted a few percent off it sits right on any terminal, where a fixed
|
|
1405
|
+
# near-neutral only sits right near the one it was tuned on:
|
|
1406
|
+
#
|
|
1407
|
+
# bg = screen.background_color
|
|
1408
|
+
# sidebar.bg_color =
|
|
1409
|
+
# bg ? Color.rgb(*bg.value.map { (_1 + 10).clamp(0, 255) }) : FALLBACK_TINT
|
|
1410
|
+
#
|
|
1411
|
+
# **Nil is the normal case, not an edge** — a terminal answering only
|
|
1412
|
+
# `COLORFGBG`, or neither probe, reports no RGB at all. Keep a fallback.
|
|
1413
|
+
#
|
|
1414
|
+
# Kept current across OS appearance flips, a frame behind: the flip
|
|
1415
|
+
# report carries light/dark only, so the screen re-probes and this
|
|
1416
|
+
# updates when the reply lands. A changed color then fires
|
|
1417
|
+
# {Component#on_theme_changed} across the tree exactly as a theme swap
|
|
1418
|
+
# does — a background-derived tint *is* a theme-derived color.
|
|
1419
|
+
attr_reader background_color: Color?
|
|
1420
|
+
|
|
1283
1421
|
# _@return_ — the event queue.
|
|
1284
1422
|
attr_reader event_queue: EventQueue
|
|
1285
1423
|
|
|
@@ -1293,7 +1431,7 @@ module Tuile
|
|
|
1293
1431
|
#
|
|
1294
1432
|
# This is the hook an app drives its own status line from. Tuile owns no
|
|
1295
1433
|
# status bar and reserves no row: build a {Component::Label} into your own
|
|
1296
|
-
# layout and fill it here (`
|
|
1434
|
+
# layout and fill it here (`D_status_bar`).
|
|
1297
1435
|
#
|
|
1298
1436
|
# screen.on_focus_changed = -> { bar.text = hint_for(screen.focused) }
|
|
1299
1437
|
#
|
|
@@ -1362,8 +1500,68 @@ module Tuile
|
|
|
1362
1500
|
# any stale invalidation entries are filtered out at drain time. Subclasses
|
|
1363
1501
|
# can paint freely in {#repaint} without re-asserting attachment.
|
|
1364
1502
|
class Component
|
|
1503
|
+
FINAL_METHODS: ::Array[Symbol]
|
|
1504
|
+
|
|
1505
|
+
# Raises unless `klass` inherits every {FINAL_METHODS} entry from
|
|
1506
|
+
# {Component}; memoized, so it costs one hash lookup per construction.
|
|
1507
|
+
#
|
|
1508
|
+
# Comparing each *resolved* method's `owner` is what makes this catch all
|
|
1509
|
+
# four routes in — `def`, `define_method`, an included module, a `prepend`.
|
|
1510
|
+
# A `method_added` hook would fire earlier but see only the first two.
|
|
1511
|
+
#
|
|
1512
|
+
# _@param_ `klass` — the class being instantiated.
|
|
1513
|
+
def self.verify_final!: (Class klass) -> void
|
|
1514
|
+
|
|
1365
1515
|
def initialize: () -> void
|
|
1366
1516
|
|
|
1517
|
+
# _@return_ — `rect.size`.
|
|
1518
|
+
def size: () -> Size
|
|
1519
|
+
|
|
1520
|
+
# _@return_ — `rect.width`.
|
|
1521
|
+
def width: () -> Integer
|
|
1522
|
+
|
|
1523
|
+
# _@return_ — `rect.height`.
|
|
1524
|
+
def height: () -> Integer
|
|
1525
|
+
|
|
1526
|
+
# The size of the region this component paints, or `nil` (the default) to
|
|
1527
|
+
# declare nothing — in which case the whole {#rect} is treated as fair game
|
|
1528
|
+
# and the default {#repaint} blanks all of it. Override it when you paint
|
|
1529
|
+
# less: a one-row {Component::Checkbox} handed a tall column, or a
|
|
1530
|
+
# {Component::Select} used as a {Component::Popup}'s content and assigned the
|
|
1531
|
+
# whole inner box.
|
|
1532
|
+
#
|
|
1533
|
+
# It always sits at {#rect}'s top-left — which is why this is a {Size} and
|
|
1534
|
+
# not a {Rect}: an offset extent is not merely unsupported, it is
|
|
1535
|
+
# unrepresentable. Use {#extent_rect} where coordinates are wanted.
|
|
1536
|
+
#
|
|
1537
|
+
# **`nil` is not the same as `rect.size`.** `nil` says "I have not declared
|
|
1538
|
+
# what I paint, so clear everything before I do", which is what a
|
|
1539
|
+
# {Component::Label} with short text needs. A declared extent — even one that
|
|
1540
|
+
# happens to equal the rect, as a one-row {Component::Select} in a one-row
|
|
1541
|
+
# rect does — says "I paint this in full, don't blank it", which is what
|
|
1542
|
+
# keeps the default {#repaint} from dirtying cells it is about to redraw
|
|
1543
|
+
# (`D_progress_bar`). The base cannot tell those apart from the value alone;
|
|
1544
|
+
# that is what the `nil` carries.
|
|
1545
|
+
#
|
|
1546
|
+
# It flows **downward only**: no container consults it when dividing space,
|
|
1547
|
+
# so {#rect} still means exactly what the parent assigned (`D_extent`). Three
|
|
1548
|
+
# things read it, all of them this component or the framework painting it:
|
|
1549
|
+
# {#clear_outside_extent} blanks the dead tail, {#handle_mouse} hit-tests
|
|
1550
|
+
# against it so a click on that tail doesn't activate the widget, and a
|
|
1551
|
+
# dropdown anchors under it rather than under unused space.
|
|
1552
|
+
#
|
|
1553
|
+
# **An override promises to paint the extent in full**, so `super` in
|
|
1554
|
+
# {#repaint} blanks only what is outside it. The arithmetic is each widget's
|
|
1555
|
+
# own — caption width, painted strip, one row — and must not vary with
|
|
1556
|
+
# {#bg_color} (`D_boolean_fields`).
|
|
1557
|
+
def extent: () -> Size?
|
|
1558
|
+
|
|
1559
|
+
# {#extent} placed at {#rect}'s top-left, for the consumers that need
|
|
1560
|
+
# coordinates: `extent_rect.contains?(event.point)` in a {#handle_mouse}, and
|
|
1561
|
+
# the anchor a dropdown hangs from. Total — an undeclared {#extent} yields
|
|
1562
|
+
# the whole {#rect}, so a generic caller never sees `nil`.
|
|
1563
|
+
def extent_rect: () -> Rect
|
|
1564
|
+
|
|
1367
1565
|
# _@return_ — the screen which owns this component.
|
|
1368
1566
|
def screen: () -> Screen
|
|
1369
1567
|
|
|
@@ -1388,6 +1586,11 @@ module Tuile
|
|
|
1388
1586
|
# paint the whole {#rect} yourself ({Window}'s border, {Component::List}'s
|
|
1389
1587
|
# row-by-row paint). Never draw outside {#rect}. Only called when attached.
|
|
1390
1588
|
#
|
|
1589
|
+
# **A widget that paints less than its rect declares an {#extent} rather than
|
|
1590
|
+
# skipping `super`.** The clear then covers only what is outside it, so the
|
|
1591
|
+
# cells it is about to repaint are not blanked first — blanking them would
|
|
1592
|
+
# mark them dirty and make {Buffer#flush} re-emit them (`D_progress_bar`).
|
|
1593
|
+
#
|
|
1391
1594
|
# **The children are re-invalidated whether or not they tile.** A container
|
|
1392
1595
|
# that paints nothing of its own can only redraw its area *through* them, so
|
|
1393
1596
|
# a tiling container that skipped this would be a dead end in the cascade: an
|
|
@@ -1430,8 +1633,14 @@ module Tuile
|
|
|
1430
1633
|
# _@return_ — true if the paste was consumed.
|
|
1431
1634
|
def handle_paste: (String _text) -> bool
|
|
1432
1635
|
|
|
1433
|
-
#
|
|
1434
|
-
#
|
|
1636
|
+
# Focuses this component when left-clicked (if {#focusable?}), then hands the
|
|
1637
|
+
# event down to every child whose {#rect} contains the point — which is how a
|
|
1638
|
+
# click descends the tiled tree to a leaf.
|
|
1639
|
+
#
|
|
1640
|
+
# A widget that resolves clicks *inside* its own rect — mapping a point to a
|
|
1641
|
+
# row, or toggling an overlay — overrides this and does not call `super`.
|
|
1642
|
+
# Such an override hit-tests {#extent_rect} rather than {#rect}, so a click
|
|
1643
|
+
# on the tail it doesn't paint never activates it.
|
|
1435
1644
|
#
|
|
1436
1645
|
# _@param_ `event`
|
|
1437
1646
|
def handle_mouse: (MouseEvent event) -> void
|
|
@@ -1609,6 +1818,14 @@ module Tuile
|
|
|
1609
1818
|
# Children with empty rects contribute zero, since they paint nothing.
|
|
1610
1819
|
def children_tile_rect?: () -> bool
|
|
1611
1820
|
|
|
1821
|
+
# Blanks the part of {#rect} outside {#extent} — the dead tail a widget that
|
|
1822
|
+
# paints less than it was given must not leave stale. Up to two regions,
|
|
1823
|
+
# since a narrowed extent leaves an L: the columns right of it, and the rows
|
|
1824
|
+
# below it. A `nil` extent declares nothing, so the whole rect is blanked.
|
|
1825
|
+
# Called by the default {#repaint}; a self-painter that skips `super` calls
|
|
1826
|
+
# it directly.
|
|
1827
|
+
def clear_outside_extent: () -> void
|
|
1828
|
+
|
|
1612
1829
|
# Clears the background: fills every cell with a blank in the
|
|
1613
1830
|
# {#effective_bg_color} (the terminal default when none is inherited).
|
|
1614
1831
|
#
|
|
@@ -1687,6 +1904,10 @@ module Tuile
|
|
|
1687
1904
|
# content (`text=`, `lines=`, …) is safe. Do not assign {Screen#theme=}
|
|
1688
1905
|
# here. Subclasses overriding this must call `super` so an assigned
|
|
1689
1906
|
# {#on_theme_changed=} listener keeps firing.
|
|
1907
|
+
#
|
|
1908
|
+
# Plumbing an app overrides and never calls, hence protected — and
|
|
1909
|
+
# {Screen}, not being a {Component}, fans it out through `__send__`, so an
|
|
1910
|
+
# override is free to declare any visibility (`D_hook_visibility`).
|
|
1690
1911
|
attr_accessor on_theme_changed: Proc?
|
|
1691
1912
|
|
|
1692
1913
|
# A scrollable list of typed items, one row each, with cursor support.
|
|
@@ -2156,6 +2377,51 @@ module Tuile
|
|
|
2156
2377
|
end
|
|
2157
2378
|
end
|
|
2158
2379
|
|
|
2380
|
+
# A one-child region: a place in the tree reserved for content that may be
|
|
2381
|
+
# absent, arrive late, or be swapped. The occupant fills the slot's {#rect}.
|
|
2382
|
+
#
|
|
2383
|
+
# Wire one per region at construction, then swap occupants through
|
|
2384
|
+
# {HasContent#content=} — which is how {Window} holds its footer:
|
|
2385
|
+
#
|
|
2386
|
+
# @footer_slot = Slot.new
|
|
2387
|
+
# add_child(@footer_slot) # the region, wired once
|
|
2388
|
+
# …
|
|
2389
|
+
# @footer_slot.content = new_footer # the occupant, swapped at will
|
|
2390
|
+
#
|
|
2391
|
+
# Because the slot never leaves {Component#children}, a swap's insert index
|
|
2392
|
+
# is always 0 — never a function of which *other* regions happen to be
|
|
2393
|
+
# occupied, which is the whole reason to reach for one (`D_slots`).
|
|
2394
|
+
#
|
|
2395
|
+
# An empty slot does not collapse: it keeps its rect and clears it, so a
|
|
2396
|
+
# dialog with no message shows the hole. Close the gap with a zero extent
|
|
2397
|
+
# from the parent, or assign an empty {Rect} to suppress the clear entirely
|
|
2398
|
+
# (what {Window} does with an absent footer); never detach it.
|
|
2399
|
+
#
|
|
2400
|
+
# Transparent to input: not {Component#focusable?},
|
|
2401
|
+
# {Component#handle_mouse} descends through it, and a departing occupant's
|
|
2402
|
+
# focus repair is handed to the container.
|
|
2403
|
+
class Slot < Component
|
|
2404
|
+
include Tuile::Component::HasContent
|
|
2405
|
+
|
|
2406
|
+
# _@param_ `content` — the initial occupant.
|
|
2407
|
+
def initialize: (?Component? content) -> void
|
|
2408
|
+
|
|
2409
|
+
# Hands the repair to the container rather than doing it here: the default
|
|
2410
|
+
# would move focus to `self`, and a slot is inert — no cursor, no keys,
|
|
2411
|
+
# nothing to bubble from.
|
|
2412
|
+
#
|
|
2413
|
+
# _@param_ `child` — the just-detached occupant.
|
|
2414
|
+
def on_child_removed: (Component child) -> void
|
|
2415
|
+
|
|
2416
|
+
# _@param_ `content`
|
|
2417
|
+
def layout: (Component content) -> void
|
|
2418
|
+
|
|
2419
|
+
# _@param_ `rect`
|
|
2420
|
+
def rect=: (Rect rect) -> void
|
|
2421
|
+
|
|
2422
|
+
def on_focus: () -> void
|
|
2423
|
+
end
|
|
2424
|
+
|
|
2159
2425
|
# A one-row strip of captions with exactly one of them selected — the map of
|
|
2160
2426
|
# where the user is. Knows nothing about content: pair it with
|
|
2161
2427
|
# {Component::TabSheet} to swap panes, or swap views yourself from
|
|
@@ -2183,7 +2449,7 @@ module Tuile
|
|
|
2183
2449
|
#
|
|
2184
2450
|
# {Tab} handles are minted by {#add_tab} and owned by the strip. There is no
|
|
2185
2451
|
# `items=`: a tab is identity plus its own state, so the set grows and
|
|
2186
|
-
# shrinks one tab at a time. See book ch7 and `DECISIONS.md` `
|
|
2452
|
+
# shrinks one tab at a time. See book ch7 and `DECISIONS.md` `D_tabs`.
|
|
2187
2453
|
#
|
|
2188
2454
|
# == Sizing
|
|
2189
2455
|
# Assign a {#rect} (typically from the surrounding {Layout}). One wider than
|
|
@@ -2269,7 +2535,7 @@ module Tuile
|
|
|
2269
2535
|
# blank tail — or on a lower row, when the rect is taller than one —
|
|
2270
2536
|
# selects nothing. It still *focuses*: {Component#handle_mouse}'s
|
|
2271
2537
|
# click-to-focus is ungated by geometry.
|
|
2272
|
-
def extent: () ->
|
|
2538
|
+
def extent: () -> Size
|
|
2273
2539
|
|
|
2274
2540
|
# Switches tabs on LEFT / RIGHT, consuming the key even at the ends of the
|
|
2275
2541
|
# strip (the selection clamps). Every other key is left unhandled so it
|
|
@@ -2517,130 +2783,65 @@ module Tuile
|
|
|
2517
2783
|
attr_accessor bg: (Color | Symbol | Integer | ::Array[Integer])?
|
|
2518
2784
|
end
|
|
2519
2785
|
|
|
2520
|
-
#
|
|
2521
|
-
#
|
|
2522
|
-
#
|
|
2523
|
-
# {#size} the {Screen} applies.
|
|
2524
|
-
#
|
|
2525
|
-
# The popup does *not* size itself to its content. Its box is declared by
|
|
2526
|
-
# {#size} — a {Fraction} (resolved against the screen every layout pass, so
|
|
2527
|
-
# it tracks resize) or an absolute {Size} (clamped to the screen). The
|
|
2528
|
-
# default is {Fraction::HALF}: half the screen, centered. The wrapped
|
|
2529
|
-
# content then fills that box and handles its own overflow by wrapping and
|
|
2530
|
-
# scrolling, so use content that can — a {Component::TextView} or
|
|
2531
|
-
# {Component::TextArea} — for anything longer than fits. A
|
|
2532
|
-
# {Component::Label} only truncates.
|
|
2533
|
-
#
|
|
2534
|
-
# Modal by default: it centers on the screen, grabs focus, eats keys, and
|
|
2535
|
-
# blocks clicks beneath it. Pass `modal: false` for a non-modal overlay
|
|
2536
|
-
# that floats above the content without taking focus or capturing input —
|
|
2537
|
-
# the caller positions it (via {#rect=}), sizes it, and drives it from app
|
|
2538
|
-
# code. That's the building block for an autocomplete/slash-command list
|
|
2539
|
-
# anchored to a text field's caret: typing keeps focus in the input while
|
|
2540
|
-
# the caller refills and drives the overlay.
|
|
2541
|
-
#
|
|
2542
|
-
# The wrapped content fills the popup's full {#rect}; if you want a frame
|
|
2543
|
-
# and caption, wrap a {Component::Window} (or any subclass — including
|
|
2544
|
-
# {Component::LogWindow}) and let it draw its own border:
|
|
2786
|
+
# A modal dialog: an {Component::Overlay} that centers itself on the screen,
|
|
2787
|
+
# grabs focus, scopes keys to its own subtree, blocks clicks beneath it, and
|
|
2788
|
+
# closes on ESC or `q`.
|
|
2545
2789
|
#
|
|
2546
2790
|
# window = Component::Window.new("Help")
|
|
2547
2791
|
# window.content = Component::List.new.tap { _1.lines = lines }
|
|
2548
2792
|
# Component::Popup.new(content: window).open
|
|
2549
2793
|
#
|
|
2550
2794
|
# Bare content also works (a {Component::Label}, a {Component::List}…), in
|
|
2551
|
-
# which case the popup is borderless.
|
|
2795
|
+
# which case the popup is borderless. For a floating layer that does *not*
|
|
2796
|
+
# take focus or capture input — an autocomplete list anchored to a field, a
|
|
2797
|
+
# toast — use {Component::Overlay} directly.
|
|
2798
|
+
#
|
|
2799
|
+
# The popup does *not* size itself to its content. Its box is set by
|
|
2800
|
+
# {#declared_size} — a {Fraction} (resolved against the screen every layout
|
|
2801
|
+
# pass, so it tracks resize) or an absolute {Size} (clamped to the screen).
|
|
2802
|
+
# The default is {Fraction::HALF}: half the screen, centered. The wrapped content
|
|
2803
|
+
# then fills that box and handles its own overflow by wrapping and scrolling,
|
|
2804
|
+
# so use content that can — a {Component::TextView} or
|
|
2805
|
+
# {Component::TextArea} — for anything longer than fits. A
|
|
2806
|
+
# {Component::Label} only truncates.
|
|
2807
|
+
#
|
|
2808
|
+
# == Implementation details
|
|
2552
2809
|
#
|
|
2553
2810
|
# `q` and ESC close the popup — handled here, at the top of the popup's own
|
|
2554
2811
|
# subtree, so the key only arrives after every component on the focus chain
|
|
2555
2812
|
# declined it (see {ScreenPane#handle_key}). That's why typing `q` into a
|
|
2556
|
-
# nested {Component::TextField} doesn't dismiss the popup: the field
|
|
2557
|
-
#
|
|
2813
|
+
# nested {Component::TextField} doesn't dismiss the popup: the field consumes
|
|
2814
|
+
# it first.
|
|
2558
2815
|
#
|
|
2559
|
-
# A left click *outside* the popup closes it too
|
|
2560
|
-
# {#close_on_outside_click?} for the exact contract and
|
|
2561
|
-
# the notice a driver hears when it happens.
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2816
|
+
# A left click *outside* the popup closes it too — see
|
|
2817
|
+
# {Overlay#close_on_outside_click?} for the exact contract and
|
|
2818
|
+
# {Overlay#on_close=} for the notice a driver hears when it happens.
|
|
2819
|
+
#
|
|
2820
|
+
# UI-thread-confined, like every component (see {Screen}).
|
|
2821
|
+
class Popup < Tuile::Component::Overlay
|
|
2565
2822
|
# _@param_ `content` — initial content; can be set later via {#content=}. The content fills the popup's {#rect}; it does not determine the popup's size.
|
|
2566
2823
|
#
|
|
2567
|
-
# _@param_ `
|
|
2824
|
+
# _@param_ `declared_size` — the popup's box, applied top-down. A {Fraction} is resolved against the screen each layout pass; a {Size} is clamped to the screen. Defaults to {Fraction::HALF}.
|
|
2568
2825
|
#
|
|
2569
|
-
# _@param_ `
|
|
2570
|
-
|
|
2571
|
-
# _@param_ `close_on_outside_click` — true (default) to dismiss on a left click that misses this popup. See {#close_on_outside_click?}.
|
|
2572
|
-
def initialize: (
|
|
2573
|
-
?content: Component?,
|
|
2574
|
-
?modal: bool,
|
|
2575
|
-
?size: (Size | Fraction),
|
|
2576
|
-
?close_on_outside_click: bool
|
|
2577
|
-
) -> void
|
|
2826
|
+
# _@param_ `close_on_outside_click` — true (default) to dismiss on a left click that misses this popup. See {Overlay#close_on_outside_click?}.
|
|
2827
|
+
def initialize: (?content: Component?, ?declared_size: (Size | Fraction), ?close_on_outside_click: bool) -> void
|
|
2578
2828
|
|
|
2579
|
-
# _@return_ —
|
|
2829
|
+
# _@return_ — true — a Popup scopes key dispatch, grabs focus on
|
|
2830
|
+
# open, and blocks clicks on the content beneath it.
|
|
2580
2831
|
def modal?: () -> bool
|
|
2581
2832
|
|
|
2582
|
-
#
|
|
2583
|
-
#
|
|
2584
|
-
# the open popups *before* routing the click and closes the dismissable
|
|
2585
|
-
# ones *after*, so a widget that toggles its own overlay from a click on
|
|
2586
|
-
# its face (a {Component::Select}, a {Component::MenuBar} title) still
|
|
2587
|
-
# toggles correctly: the delivered click closes the overlay and the
|
|
2588
|
-
# dismissal then no-ops on it, rather than closing and reopening it. Only
|
|
2589
|
-
# `:left` dismisses; scroll and right clicks never do.
|
|
2590
|
-
#
|
|
2591
|
-
# **"Outside" spans the {#owner} chain, not just this rect.** A click
|
|
2592
|
-
# counts as inside this popup when it lands in its rect *or* in any popup
|
|
2593
|
-
# that belongs to it — so a dialog is not dismissed by a click on a
|
|
2594
|
-
# dropdown its own field opened, and a menu cascade is not dismissed by a
|
|
2595
|
-
# click on one of its deeper panels. Popups with no owner relationship are
|
|
2596
|
-
# independent: clicking one dismisses the other, which is what a
|
|
2597
|
-
# window-like overlay should do. A popup that must survive unrelated
|
|
2598
|
-
# clicks entirely ({Component::Notification}) sets this false.
|
|
2599
|
-
#
|
|
2600
|
-
# Every dismissable popup closes, not just the topmost, and stacking order
|
|
2601
|
-
# plays no part: a {Component::MenuBar} cascade must vanish whole on one
|
|
2602
|
-
# click on the background, not peel one panel per click.
|
|
2603
|
-
def close_on_outside_click?: () -> bool
|
|
2604
|
-
|
|
2833
|
+
# _@return_ — true — {ScreenPane#add_popup} focuses a popup on open,
|
|
2834
|
+
# and focus repair falls back to it when its subtree has no tab stop.
|
|
2605
2835
|
def focusable?: () -> bool
|
|
2606
2836
|
|
|
2607
|
-
#
|
|
2608
|
-
#
|
|
2609
|
-
#
|
|
2610
|
-
#
|
|
2611
|
-
# would repaint into the new rect and leave the vacated cells showing
|
|
2612
|
-
# stale content. When the new rect fully covers the old one (the popup
|
|
2613
|
-
# only grew), the fast path is correct and the full repaint is skipped.
|
|
2614
|
-
#
|
|
2615
|
-
# _@param_ `new_rect`
|
|
2616
|
-
def rect=: (Rect new_rect) -> void
|
|
2617
|
-
|
|
2618
|
-
# Mounts this popup on the {Screen}, re-resolving its {#size} against the
|
|
2619
|
-
# current screen first.
|
|
2620
|
-
#
|
|
2621
|
-
# popup = Component::Popup.new(content: window).open # construct and mount
|
|
2622
|
-
#
|
|
2623
|
-
# There is deliberately no class-level `Popup.open` factory — see
|
|
2624
|
-
# `DECISIONS.md` `D-popup-open`; returning `self` is what keeps the
|
|
2625
|
-
# one-liner above available without one.
|
|
2626
|
-
def open: () -> self
|
|
2627
|
-
|
|
2628
|
-
# Removes this popup from the {Screen}. No-op if not currently open.
|
|
2629
|
-
def close: () -> void
|
|
2630
|
-
|
|
2631
|
-
# _@return_ — true if this popup is currently mounted on the screen.
|
|
2632
|
-
def open?: () -> bool
|
|
2633
|
-
|
|
2634
|
-
# Re-resolves {#size} against the current screen and repositions the popup
|
|
2635
|
-
# *itself* (this is not laying out content — the popup's own rect): a
|
|
2636
|
-
# modal popup recenters; a non-modal overlay keeps its caller-assigned
|
|
2637
|
-
# top-left (only its size follows the screen). Called on {#open}, on
|
|
2638
|
-
# {#size=}, and by the screen's layout pass (so a {Fraction} size tracks
|
|
2639
|
-
# SIGWINCH).
|
|
2837
|
+
# Re-resolves {#declared_size} against the current screen and recenters the popup
|
|
2838
|
+
# *itself* (this is not laying out content — the popup's own rect). Called
|
|
2839
|
+
# on {Overlay#open}, on {#declared_size=}, and by the screen's layout pass,
|
|
2840
|
+
# so a {Fraction} tracks SIGWINCH.
|
|
2640
2841
|
#
|
|
2641
|
-
# The final rect is computed and assigned in one step rather than sizing
|
|
2642
|
-
#
|
|
2643
|
-
#
|
|
2842
|
+
# The final rect is computed and assigned in one step rather than sizing at
|
|
2843
|
+
# the origin and then centering: the intermediate origin rect rarely covers
|
|
2844
|
+
# the previous one, which would make {Overlay#rect=}'s shrink/move
|
|
2644
2845
|
# detection fire a full repaint on every resize.
|
|
2645
2846
|
def reposition: () -> void
|
|
2646
2847
|
|
|
@@ -2656,53 +2857,14 @@ module Tuile
|
|
|
2656
2857
|
# _@return_ — true if the key was handled.
|
|
2657
2858
|
def handle_key: (String key) -> bool
|
|
2658
2859
|
|
|
2659
|
-
#
|
|
2660
|
-
# the
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
#
|
|
2860
|
+
# The box this popup asks for, *as set* — a {Fraction} comes back
|
|
2861
|
+
# unresolved. {#rect} is the resolved answer. Named apart from `size`
|
|
2862
|
+
# because a component's `size` is its `rect.size`, which this is not: it
|
|
2863
|
+
# is an input re-read on every layout pass, not a report of the current
|
|
2864
|
+
# geometry.
|
|
2664
2865
|
#
|
|
2665
|
-
# _@
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
# _@param_ `event`
|
|
2669
|
-
def handle_mouse: (MouseEvent event) -> void
|
|
2670
|
-
|
|
2671
|
-
def on_focus: () -> void
|
|
2672
|
-
|
|
2673
|
-
# _@return_ — the popup's declared size. See {#size=}.
|
|
2674
|
-
attr_accessor size: (Size | Fraction)
|
|
2675
|
-
|
|
2676
|
-
# _@return_ — see {#close_on_outside_click?}.
|
|
2677
|
-
attr_writer close_on_outside_click: bool
|
|
2678
|
-
|
|
2679
|
-
# The component this overlay is *part of*, or `nil` (the default) when it
|
|
2680
|
-
# is an overlay in its own right. It exists for outside-click dismissal:
|
|
2681
|
-
# a click inside this popup also counts as inside whatever popup encloses
|
|
2682
|
-
# its owner, so the host is not dismissed by a click on a panel it put
|
|
2683
|
-
# there. See {#close_on_outside_click?}.
|
|
2684
|
-
#
|
|
2685
|
-
# Set it to the *driver* — {Component::ComboBox} hands its dropdown
|
|
2686
|
-
# `self` — rather than to the enclosing popup: the driver knows what it
|
|
2687
|
-
# is, while the popup above it is a tree relationship the pane resolves
|
|
2688
|
-
# at click time (so it cannot go stale). Any {Component} is accepted, and
|
|
2689
|
-
# a `Popup` resolves to itself, which is how a
|
|
2690
|
-
# {Component::MenuBar::Cascade} chains each panel to the one it dropped
|
|
2691
|
-
# out of.
|
|
2692
|
-
attr_accessor owner: Component?
|
|
2693
|
-
|
|
2694
|
-
# A callback taking no arguments, fired once this popup has left the
|
|
2695
|
-
# screen — **however it left**: {#close}, a direct {Screen#remove_popup},
|
|
2696
|
-
# an outside click, or teardown via {Screen#close}. That unconditionality
|
|
2697
|
-
# is the point, so it hangs off {#on_detached} rather than {#close}; a
|
|
2698
|
-
# driver keeping its own record of open popups reconciles it here and
|
|
2699
|
-
# cannot drift ({Component::MenuBar::Cascade} is the worked example).
|
|
2700
|
-
#
|
|
2701
|
-
# It fires *after* the popup is detached, so {#open?} is already false and
|
|
2702
|
-
# the usual {Component#on_detached} caveats apply: release state, don't
|
|
2703
|
-
# inspect the tree, keep it trivial (it may run while the pane is mid-way
|
|
2704
|
-
# through closing a batch of popups, and a raise propagates).
|
|
2705
|
-
attr_accessor on_close: Proc?
|
|
2866
|
+
# _@return_ — see {#declared_size=}.
|
|
2867
|
+
attr_accessor declared_size: (Size | Fraction)
|
|
2706
2868
|
end
|
|
2707
2869
|
|
|
2708
2870
|
# A clickable button. Activated by Enter, Space, or a left mouse click;
|
|
@@ -2738,7 +2900,7 @@ module Tuile
|
|
|
2738
2900
|
# It still *focuses*: {Component#handle_mouse}'s click-to-focus is ungated
|
|
2739
2901
|
# by geometry. Same rule as {Checkbox#extent}, which documents the two
|
|
2740
2902
|
# traps behind it.
|
|
2741
|
-
def extent: () ->
|
|
2903
|
+
def extent: () -> Size
|
|
2742
2904
|
|
|
2743
2905
|
# Fires {#on_click} on a left click within {#extent}; `super` runs first, so
|
|
2744
2906
|
# a click anywhere in {#rect} still focuses.
|
|
@@ -2801,11 +2963,6 @@ module Tuile
|
|
|
2801
2963
|
# _@param_ `child`
|
|
2802
2964
|
def remove: (Component child) -> void
|
|
2803
2965
|
|
|
2804
|
-
# Dispatches the event to the child under the mouse cursor.
|
|
2805
|
-
#
|
|
2806
|
-
# _@param_ `event`
|
|
2807
|
-
def handle_mouse: (MouseEvent event) -> void
|
|
2808
|
-
|
|
2809
2966
|
def on_focus: () -> void
|
|
2810
2967
|
|
|
2811
2968
|
# How much space a child gets along one axis of a {Box}: exactly {#cells},
|
|
@@ -3237,7 +3394,7 @@ module Tuile
|
|
|
3237
3394
|
# Home/End are declined too, so they stay available app-wide.
|
|
3238
3395
|
#
|
|
3239
3396
|
# There is no type-ahead: a hidden prefix buffer *is* the ComboBox query with
|
|
3240
|
-
# the feedback removed (`DECISIONS.md` `
|
|
3397
|
+
# the feedback removed (`DECISIONS.md` `D_select`). Which is also why labels
|
|
3241
3398
|
# need no prefix-disambiguation.
|
|
3242
3399
|
#
|
|
3243
3400
|
# == Implementation details
|
|
@@ -3284,7 +3441,15 @@ module Tuile
|
|
|
3284
3441
|
# _@param_ `key`
|
|
3285
3442
|
def handle_key: (String key) -> bool
|
|
3286
3443
|
|
|
3287
|
-
#
|
|
3444
|
+
# The one row this Select paints — the full width, at the top of {#rect}.
|
|
3445
|
+
# A single-slot container ({Component::Window}, {Component::Popup}) hands
|
|
3446
|
+
# its content the whole inner rect, so a Select is routinely assigned more
|
|
3447
|
+
# height than it uses; {#repaint} clears that tail, {#handle_mouse} refuses
|
|
3448
|
+
# clicks in it, and the dropdown hangs under this rather than under the
|
|
3449
|
+
# unused space.
|
|
3450
|
+
def extent: () -> Size
|
|
3451
|
+
|
|
3452
|
+
# Toggles the dropdown on a left click anywhere in {#extent} — a field's
|
|
3288
3453
|
# affordance is its whole row, as the well advertises; `super` runs first,
|
|
3289
3454
|
# so the click also focuses.
|
|
3290
3455
|
#
|
|
@@ -3353,7 +3518,7 @@ module Tuile
|
|
|
3353
3518
|
# a read-only display field could override back to `false`. Only
|
|
3354
3519
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
3355
3520
|
# composing wrappers, so it stays per-class (`DECISIONS.md`
|
|
3356
|
-
# `
|
|
3521
|
+
# `D_integer_field`).
|
|
3357
3522
|
def focusable?: () -> bool
|
|
3358
3523
|
|
|
3359
3524
|
# _@return_ — the options.
|
|
@@ -3383,8 +3548,18 @@ module Tuile
|
|
|
3383
3548
|
|
|
3384
3549
|
def focusable?: () -> bool
|
|
3385
3550
|
|
|
3386
|
-
# _@
|
|
3387
|
-
|
|
3551
|
+
# _@return_ — optional focusable component occupying the
|
|
3552
|
+
# bottom border row, always spanning the full inner width.
|
|
3553
|
+
def footer: () -> Component?
|
|
3554
|
+
|
|
3555
|
+
# Mounts a component in the bottom border row, spanning the full inner
|
|
3556
|
+
# width and positioned automatically; `nil` removes it.
|
|
3557
|
+
#
|
|
3558
|
+
# Precedence: a footer component present hides {#footer_text}; absent, the
|
|
3559
|
+
# text embeds into the bottom border. No window needs both at once.
|
|
3560
|
+
#
|
|
3561
|
+
# _@param_ `new_footer`
|
|
3562
|
+
def footer=: (Component? new_footer) -> void
|
|
3388
3563
|
|
|
3389
3564
|
# _@param_ `new_rect`
|
|
3390
3565
|
def rect=: (Rect new_rect) -> void
|
|
@@ -3435,9 +3610,12 @@ module Tuile
|
|
|
3435
3610
|
# _@param_ `fg` — the active-border color, or nil when inactive.
|
|
3436
3611
|
def bottom_border: (Integer inner_w, Color? fg) -> StyledString
|
|
3437
3612
|
|
|
3438
|
-
# Positions the footer over the bottom border row, spanning the full
|
|
3613
|
+
# Positions the footer slot over the bottom border row, spanning the full
|
|
3439
3614
|
# inner width (the only dimension a bottom-row widget needs — the window
|
|
3440
3615
|
# already knows it).
|
|
3616
|
+
#
|
|
3617
|
+
# An unoccupied slot gets an *empty* rect, not the row — a {Slot} clears
|
|
3618
|
+
# whatever it is given, which would blank the border underneath.
|
|
3441
3619
|
def layout_footer: () -> void
|
|
3442
3620
|
|
|
3443
3621
|
# Read through *this* method, never `@caption` — the ivar stays nil until
|
|
@@ -3455,16 +3633,168 @@ module Tuile
|
|
|
3455
3633
|
|
|
3456
3634
|
def on_focus: () -> void
|
|
3457
3635
|
|
|
3458
|
-
# _@return_ — optional focusable component occupying the
|
|
3459
|
-
# bottom border row, always spanning the full inner width.
|
|
3460
|
-
attr_accessor footer: Component?
|
|
3461
|
-
|
|
3462
3636
|
# _@return_ — optional chrome embedded into the bottom border
|
|
3463
3637
|
# line, mirroring {#caption} on the top line. Empty by default; hidden
|
|
3464
3638
|
# whenever a {#footer} component is present.
|
|
3465
3639
|
attr_accessor footer_text: (StyledString | String)?
|
|
3466
3640
|
end
|
|
3467
3641
|
|
|
3642
|
+
# A component mounted on the {Screen}'s overlay stack rather than in the
|
|
3643
|
+
# tiled tree: it floats above the content at a rect the caller assigns, and
|
|
3644
|
+
# has an open/close lifecycle instead of a parent that lays it out.
|
|
3645
|
+
#
|
|
3646
|
+
# overlay = Component::Overlay.new(content: Component::Label.new("saved"))
|
|
3647
|
+
# overlay.rect = Rect.new(10, 4, 20, 1) # you place it — nothing else does
|
|
3648
|
+
# overlay.open # mounts it on the Screen
|
|
3649
|
+
# overlay.close
|
|
3650
|
+
#
|
|
3651
|
+
# That is the whole of it — floating, plus the lifecycle, {#on_close},
|
|
3652
|
+
# outside-click dismissal and {#owner}. {Component::Popup} is the subclass
|
|
3653
|
+
# that adds a declared size, self-centering, focus and key handling.
|
|
3654
|
+
#
|
|
3655
|
+
# The wrapped content fills the overlay's whole {#rect}; for a frame and a
|
|
3656
|
+
# caption, wrap a {Component::Window} and let it draw its own border.
|
|
3657
|
+
#
|
|
3658
|
+
# == Implementation details
|
|
3659
|
+
#
|
|
3660
|
+
# **{#focusable?} and {#modal?} move together — flip both or neither.** The
|
|
3661
|
+
# defaults here are inert (`false`, `false`): a bare overlay floats without
|
|
3662
|
+
# disturbing focus or key dispatch. {Component::Popup} flips both. What must
|
|
3663
|
+
# not appear is a *focusable non-modal* overlay: {ScreenPane#handle_key}
|
|
3664
|
+
# scopes delivery to the topmost modal popup or else the tiled content, so
|
|
3665
|
+
# such an overlay would hold focus outside the key scope, where delivery
|
|
3666
|
+
# reaches nobody and *every* keystroke goes dead until Tab recovers. A
|
|
3667
|
+
# non-modal overlay is therefore driven from its owner's key handler
|
|
3668
|
+
# ({Component::Select} forwarding to its dropdown) instead of claiming focus.
|
|
3669
|
+
#
|
|
3670
|
+
# **A derived position needs a {#reposition} override.** The default is a
|
|
3671
|
+
# no-op: the rect is whatever the caller last assigned. An overlay whose
|
|
3672
|
+
# position is computed — from the screen, or from an anchor — must recompute
|
|
3673
|
+
# it there, or it keeps a stale rect after a SIGWINCH and sits off-screen
|
|
3674
|
+
# entirely if the terminal narrowed. Closing on resize is equally legal
|
|
3675
|
+
# ({Component::MenuBar} drops its cascade from `rect=` rather than walking
|
|
3676
|
+
# every level to re-anchor it).
|
|
3677
|
+
#
|
|
3678
|
+
# UI-thread-confined, like every component (see {Screen}).
|
|
3679
|
+
class Overlay < Component
|
|
3680
|
+
include Tuile::Component::HasContent
|
|
3681
|
+
|
|
3682
|
+
# _@param_ `content` — initial content; can be set later via {#content=}. It fills the overlay's {#rect} and does not determine it.
|
|
3683
|
+
#
|
|
3684
|
+
# _@param_ `close_on_outside_click` — true (default) to dismiss on a left click that misses this overlay. See {#close_on_outside_click?}.
|
|
3685
|
+
def initialize: (?content: Component?, ?close_on_outside_click: bool) -> void
|
|
3686
|
+
|
|
3687
|
+
# _@return_ — false — a bare overlay scopes no keys and blocks no
|
|
3688
|
+
# clicks. {Component::Popup} overrides it, together with {#focusable?}.
|
|
3689
|
+
def modal?: () -> bool
|
|
3690
|
+
|
|
3691
|
+
# _@return_ — false — a bare overlay leaves focus where it was. See
|
|
3692
|
+
# the class docs: override it only together with {#modal?}.
|
|
3693
|
+
def focusable?: () -> bool
|
|
3694
|
+
|
|
3695
|
+
# _@return_ — false — Tab never lands on the overlay wrapper itself
|
|
3696
|
+
# (its content may still carry stops).
|
|
3697
|
+
def tab_stop?: () -> bool
|
|
3698
|
+
|
|
3699
|
+
# Whether a left click outside this overlay closes it (default true). The
|
|
3700
|
+
# pane does the closing — {ScreenPane#handle_mouse} snapshots the open
|
|
3701
|
+
# overlays *before* routing the click and closes the dismissable ones
|
|
3702
|
+
# *after*, so a widget that toggles its own overlay from a click on its
|
|
3703
|
+
# face (a {Component::Select}, a {Component::MenuBar} title) still toggles
|
|
3704
|
+
# correctly: the delivered click closes the overlay and the dismissal then
|
|
3705
|
+
# no-ops on it, rather than closing and reopening it. Only `:left`
|
|
3706
|
+
# dismisses; scroll and right clicks never do.
|
|
3707
|
+
#
|
|
3708
|
+
# **"Outside" spans the {#owner} chain, not just this rect.** A click
|
|
3709
|
+
# counts as inside this overlay when it lands in its rect *or* in any
|
|
3710
|
+
# overlay that belongs to it — so a dialog is not dismissed by a click on a
|
|
3711
|
+
# dropdown its own field opened, and a menu cascade is not dismissed by a
|
|
3712
|
+
# click on one of its deeper panels. Overlays with no owner relationship
|
|
3713
|
+
# are independent: clicking one dismisses the other, which is what a
|
|
3714
|
+
# window-like overlay should do. One that must survive unrelated clicks
|
|
3715
|
+
# entirely ({Component::Notification}) sets this false.
|
|
3716
|
+
#
|
|
3717
|
+
# Every dismissable overlay closes, not just the topmost, and stacking
|
|
3718
|
+
# order plays no part: a {Component::MenuBar} cascade must vanish whole on
|
|
3719
|
+
# one click on the background, not peel one panel per click.
|
|
3720
|
+
def close_on_outside_click?: () -> bool
|
|
3721
|
+
|
|
3722
|
+
# Reassigns the overlay's rect, escalating to a full scene repaint when an
|
|
3723
|
+
# open overlay shrinks or moves so its new rect no longer covers the cells
|
|
3724
|
+
# it previously painted. An overlay overdraws the scene without clipping
|
|
3725
|
+
# and nothing clears underneath it, so {Screen#repaint}'s overlay-only fast
|
|
3726
|
+
# path would repaint into the new rect and leave the vacated cells showing
|
|
3727
|
+
# stale content. When the new rect fully covers the old one (the overlay
|
|
3728
|
+
# only grew), the fast path is correct and the full repaint is skipped.
|
|
3729
|
+
#
|
|
3730
|
+
# _@param_ `new_rect`
|
|
3731
|
+
def rect=: (Rect new_rect) -> void
|
|
3732
|
+
|
|
3733
|
+
# Mounts this overlay on the {Screen}.
|
|
3734
|
+
#
|
|
3735
|
+
# overlay = Component::Overlay.new(content: label).open # construct and mount
|
|
3736
|
+
#
|
|
3737
|
+
# There is deliberately no class-level `Overlay.open` factory — see
|
|
3738
|
+
# `DECISIONS.md` `D_popup_open`; returning `self` is what keeps the
|
|
3739
|
+
# one-liner above available without one.
|
|
3740
|
+
def open: () -> self
|
|
3741
|
+
|
|
3742
|
+
# Removes this overlay from the {Screen}. No-op if not currently open.
|
|
3743
|
+
def close: () -> void
|
|
3744
|
+
|
|
3745
|
+
# _@return_ — true if this overlay is currently mounted on the screen.
|
|
3746
|
+
def open?: () -> bool
|
|
3747
|
+
|
|
3748
|
+
# Recomputes this overlay's own rect (not its content's layout). A no-op
|
|
3749
|
+
# here — the rect is whatever the caller assigned — and the hook a subclass
|
|
3750
|
+
# with a *derived* position overrides; see the class docs. Called on
|
|
3751
|
+
# {#open} and by the screen's layout pass, so an override tracks SIGWINCH.
|
|
3752
|
+
def reposition: () -> void
|
|
3753
|
+
|
|
3754
|
+
# Fires {#on_close}. A subclass overriding this **must** call `super`, or
|
|
3755
|
+
# the overlay's driver never hears that it closed.
|
|
3756
|
+
def on_detached: () -> void
|
|
3757
|
+
|
|
3758
|
+
# Content fills the overlay's full rect — an Overlay has no border to
|
|
3759
|
+
# subtract.
|
|
3760
|
+
#
|
|
3761
|
+
# _@param_ `content`
|
|
3762
|
+
def layout: (Component content) -> void
|
|
3763
|
+
|
|
3764
|
+
def on_focus: () -> void
|
|
3765
|
+
|
|
3766
|
+
# _@return_ — see {#close_on_outside_click?}.
|
|
3767
|
+
attr_writer close_on_outside_click: bool
|
|
3768
|
+
|
|
3769
|
+
# The component this overlay is *part of*, or `nil` (the default) when it
|
|
3770
|
+
# is an overlay in its own right. It exists for outside-click dismissal: a
|
|
3771
|
+
# click inside this overlay also counts as inside whatever overlay encloses
|
|
3772
|
+
# its owner, so the host is not dismissed by a click on a panel it put
|
|
3773
|
+
# there. See {#close_on_outside_click?}.
|
|
3774
|
+
#
|
|
3775
|
+
# Set it to the *driver* — {Component::ComboBox} hands its dropdown `self`
|
|
3776
|
+
# — rather than to the enclosing overlay: the driver knows what it is,
|
|
3777
|
+
# while the overlay above it is a tree relationship the pane resolves at
|
|
3778
|
+
# click time (so it cannot go stale). Any {Component} is accepted, and an
|
|
3779
|
+
# `Overlay` resolves to itself, which is how a
|
|
3780
|
+
# {Component::MenuBar::Cascade} chains each panel to the one it dropped out
|
|
3781
|
+
# of.
|
|
3782
|
+
attr_accessor owner: Component?
|
|
3783
|
+
|
|
3784
|
+
# A callback taking no arguments, fired once this overlay has left the
|
|
3785
|
+
# screen — **however it left**: {#close}, a direct {Screen#remove_popup},
|
|
3786
|
+
# an outside click, or teardown via {Screen#close}. That unconditionality
|
|
3787
|
+
# is the point, so it hangs off {#on_detached} rather than {#close}; a
|
|
3788
|
+
# driver keeping its own record of open overlays reconciles it here and
|
|
3789
|
+
# cannot drift ({Component::MenuBar::Cascade} is the worked example).
|
|
3790
|
+
#
|
|
3791
|
+
# It fires *after* the overlay is detached, so {#open?} is already false
|
|
3792
|
+
# and the usual {Component#on_detached} caveats apply: release state, don't
|
|
3793
|
+
# inspect the tree, keep it trivial (it may run while the pane is mid-way
|
|
3794
|
+
# through closing a batch of overlays, and a raise propagates).
|
|
3795
|
+
attr_accessor on_close: Proc?
|
|
3796
|
+
end
|
|
3797
|
+
|
|
3468
3798
|
# A boolean input on one row. Space, Enter or a left click toggles it:
|
|
3469
3799
|
#
|
|
3470
3800
|
# [x] Enable syslog forwarding
|
|
@@ -3547,7 +3877,7 @@ module Tuile
|
|
|
3547
3877
|
# The extent ignores {Component#bg_color}: an inherited tint paints the dead
|
|
3548
3878
|
# tail, but a hit test that silently widened with a background would be a
|
|
3549
3879
|
# mode switch invisible in the code and untestable by inspection.
|
|
3550
|
-
def extent: () ->
|
|
3880
|
+
def extent: () -> Size
|
|
3551
3881
|
|
|
3552
3882
|
# Toggles on Space or Enter. Every other key is left unhandled so it bubbles
|
|
3553
3883
|
# to an ancestor.
|
|
@@ -3589,7 +3919,7 @@ module Tuile
|
|
|
3589
3919
|
# a read-only display field could override back to `false`. Only
|
|
3590
3920
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
3591
3921
|
# composing wrappers, so it stays per-class (`DECISIONS.md`
|
|
3592
|
-
# `
|
|
3922
|
+
# `D_integer_field`).
|
|
3593
3923
|
def focusable?: () -> bool
|
|
3594
3924
|
end
|
|
3595
3925
|
|
|
@@ -3634,7 +3964,7 @@ module Tuile
|
|
|
3634
3964
|
# {Item} handles are minted by {#add_item} and nest via the *same* method, so
|
|
3635
3965
|
# depth is unlimited. There is no removal, no reordering and no dynamic
|
|
3636
3966
|
# rebuilding: a menu is built once, at construction. See `DECISIONS.md`
|
|
3637
|
-
# `
|
|
3967
|
+
# `D_menu_bar`.
|
|
3638
3968
|
#
|
|
3639
3969
|
# == Sizing
|
|
3640
3970
|
# Assign a {#rect} (typically one {Layout::Fixed}`[1]` row at the top of a
|
|
@@ -3697,7 +4027,7 @@ module Tuile
|
|
|
3697
4027
|
# tail — or on a lower row, when the rect is taller than one — opens
|
|
3698
4028
|
# nothing. It still *focuses*: {Component#handle_mouse}'s click-to-focus is
|
|
3699
4029
|
# ungated by geometry.
|
|
3700
|
-
def extent: () ->
|
|
4030
|
+
def extent: () -> Size
|
|
3701
4031
|
|
|
3702
4032
|
# _@param_ `new_rect`
|
|
3703
4033
|
def rect=: (Rect new_rect) -> void
|
|
@@ -3970,7 +4300,7 @@ module Tuile
|
|
|
3970
4300
|
#
|
|
3971
4301
|
# Widths are measured here, per level — the panel is as wide as the level's
|
|
3972
4302
|
# widest label — because {ListDropdown} deliberately measures nothing
|
|
3973
|
-
# itself (`DECISIONS.md` `
|
|
4303
|
+
# itself (`DECISIONS.md` `D_select`).
|
|
3974
4304
|
#
|
|
3975
4305
|
# == Implementation details
|
|
3976
4306
|
# While open it consumes **everything** except the two keys that mean
|
|
@@ -4094,7 +4424,7 @@ module Tuile
|
|
|
4094
4424
|
# combo.value = some_user # selects it; field shows its label
|
|
4095
4425
|
#
|
|
4096
4426
|
# It's the assembly you'd otherwise wire by hand — a {TextField} plus a
|
|
4097
|
-
#
|
|
4427
|
+
# an {Overlay} over a {List} — promoted to one component. Give it a
|
|
4098
4428
|
# single-row {#rect}; it paints the field across that row with a `▾` in the
|
|
4099
4429
|
# last column and floats the dropdown above or below.
|
|
4100
4430
|
#
|
|
@@ -4149,6 +4479,12 @@ module Tuile
|
|
|
4149
4479
|
|
|
4150
4480
|
def repaint: () -> void
|
|
4151
4481
|
|
|
4482
|
+
# The one row this combo paints — the full width, at the top of {#rect}.
|
|
4483
|
+
# A single-slot container hands its content the whole inner rect, so a
|
|
4484
|
+
# ComboBox is routinely assigned more height than it uses; the dropdown
|
|
4485
|
+
# hangs under this rather than under the unused space below it.
|
|
4486
|
+
def extent: () -> Size
|
|
4487
|
+
|
|
4152
4488
|
# Field spans the row bar the last column, which the `▾` occupies
|
|
4153
4489
|
# ({HasContent} layout hook). One row, or none at all when the combo itself
|
|
4154
4490
|
# was given none — a starved parent must not hand out a rect it doesn't own.
|
|
@@ -4231,7 +4567,7 @@ module Tuile
|
|
|
4231
4567
|
# a read-only display field could override back to `false`. Only
|
|
4232
4568
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
4233
4569
|
# composing wrappers, so it stays per-class (`DECISIONS.md`
|
|
4234
|
-
# `
|
|
4570
|
+
# `D_integer_field`).
|
|
4235
4571
|
def focusable?: () -> bool
|
|
4236
4572
|
|
|
4237
4573
|
def on_focus: () -> void
|
|
@@ -4287,7 +4623,7 @@ module Tuile
|
|
|
4287
4623
|
# a read-only display field could override back to `false`. Only
|
|
4288
4624
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
4289
4625
|
# composing wrappers, so it stays per-class (`DECISIONS.md`
|
|
4290
|
-
# `
|
|
4626
|
+
# `D_integer_field`).
|
|
4291
4627
|
def focusable?: () -> bool
|
|
4292
4628
|
|
|
4293
4629
|
# _@return_ — one-arg callable fired with the new value
|
|
@@ -4402,12 +4738,6 @@ module Tuile
|
|
|
4402
4738
|
# _@param_ `new_rect`
|
|
4403
4739
|
def rect=: (Rect new_rect) -> void
|
|
4404
4740
|
|
|
4405
|
-
# Forwards to whichever child the click landed on — the strip's row, or
|
|
4406
|
-
# the pane below it.
|
|
4407
|
-
#
|
|
4408
|
-
# _@param_ `event`
|
|
4409
|
-
def handle_mouse: (MouseEvent event) -> void
|
|
4410
|
-
|
|
4411
4741
|
# Sends focus to the strip: a sheet is a container, and the strip is where
|
|
4412
4742
|
# a tab switch is driven from. The pane is a Tab press away.
|
|
4413
4743
|
def on_focus: () -> void
|
|
@@ -5281,42 +5611,23 @@ module Tuile
|
|
|
5281
5611
|
end
|
|
5282
5612
|
end
|
|
5283
5613
|
|
|
5284
|
-
#
|
|
5285
|
-
#
|
|
5614
|
+
# A {Window} framing a {LogTextView} — the framed log pane. All the log
|
|
5615
|
+
# behavior lives on the view (see {LogTextView} for the adapter and the
|
|
5616
|
+
# wrap rationale); the window adds the border, caption and delegation:
|
|
5286
5617
|
#
|
|
5287
5618
|
# log_window = Tuile::Component::LogWindow.new
|
|
5288
5619
|
# logger = Logger.new(Tuile::Component::LogWindow::IO.new(log_window))
|
|
5289
|
-
#
|
|
5290
|
-
# Any logger that writes formatted lines to an IO works the same way —
|
|
5291
|
-
# for example `TTY::Logger` configured with the `:console` handler and
|
|
5292
|
-
# `output: LogWindow::IO.new(window)`.
|
|
5293
5620
|
class LogWindow < Tuile::Component::Window
|
|
5621
|
+
IO: Class
|
|
5622
|
+
|
|
5294
5623
|
# _@param_ `caption`
|
|
5295
5624
|
def initialize: (?String caption) -> void
|
|
5296
5625
|
|
|
5297
|
-
# Appends given line to the log
|
|
5626
|
+
# Appends the given line to the log, from any thread — delegates to
|
|
5627
|
+
# {LogTextView#log}.
|
|
5298
5628
|
#
|
|
5299
|
-
# _@param_ `string` — the line (or multiple lines) to log.
|
|
5629
|
+
# _@param_ `string` — the line (or multiple lines) to log; `nil` is a no-op.
|
|
5300
5630
|
def log: (String? string) -> void
|
|
5301
|
-
|
|
5302
|
-
# IO-shaped adapter that forwards each log line to the owning {LogWindow}.
|
|
5303
|
-
# Implements both {#write} (stdlib `Logger`) and {#puts} (loggers that
|
|
5304
|
-
# call `output.puts`, e.g. `TTY::Logger`).
|
|
5305
|
-
class IO
|
|
5306
|
-
# _@param_ `window`
|
|
5307
|
-
def initialize: (LogWindow window) -> void
|
|
5308
|
-
|
|
5309
|
-
# _@param_ `string`
|
|
5310
|
-
def write: (String string) -> void
|
|
5311
|
-
|
|
5312
|
-
# _@param_ `string`
|
|
5313
|
-
def puts: (String string) -> void
|
|
5314
|
-
|
|
5315
|
-
# Stdlib `Logger` only treats an object as an IO target when it
|
|
5316
|
-
# responds to both {#write} and {#close}; otherwise it tries to
|
|
5317
|
-
# interpret it as a filename. This is a no-op.
|
|
5318
|
-
def close: () -> void
|
|
5319
|
-
end
|
|
5320
5631
|
end
|
|
5321
5632
|
|
|
5322
5633
|
# A single-line text input with a real hardware caret, scrolling
|
|
@@ -5588,12 +5899,9 @@ module Tuile
|
|
|
5588
5899
|
# a read-only display field could override back to `false`. Only
|
|
5589
5900
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
5590
5901
|
# composing wrappers, so it stays per-class (`DECISIONS.md`
|
|
5591
|
-
# `
|
|
5902
|
+
# `D_integer_field`).
|
|
5592
5903
|
def focusable?: () -> bool
|
|
5593
5904
|
|
|
5594
|
-
# _@param_ `event`
|
|
5595
|
-
def handle_mouse: (MouseEvent event) -> void
|
|
5596
|
-
|
|
5597
5905
|
# _@param_ `rect`
|
|
5598
5906
|
def rect=: (Rect rect) -> void
|
|
5599
5907
|
|
|
@@ -5634,13 +5942,29 @@ module Tuile
|
|
|
5634
5942
|
def caption=: ((String | StyledString)? new_caption) -> void
|
|
5635
5943
|
end
|
|
5636
5944
|
|
|
5637
|
-
# A
|
|
5638
|
-
#
|
|
5639
|
-
# content
|
|
5945
|
+
# A component that owns exactly one child *directly*, under the name
|
|
5946
|
+
# `content`. The includer initializes `@content` to nil and provides a
|
|
5947
|
+
# protected `layout(content)` positioning the child; the mixin owns the swap:
|
|
5948
|
+
#
|
|
5949
|
+
# class Slot < Component
|
|
5950
|
+
# include Component::HasContent
|
|
5951
|
+
#
|
|
5952
|
+
# def initialize = (super; @content = nil)
|
|
5953
|
+
#
|
|
5954
|
+
# protected
|
|
5955
|
+
#
|
|
5956
|
+
# def layout(content) = content.rect = rect
|
|
5957
|
+
# end
|
|
5958
|
+
#
|
|
5959
|
+
# Include it when the child is **permanent and integral** — a typed field's
|
|
5960
|
+
# inner {TextField}, an {Overlay}'s body. It does *not* mean "a component
|
|
5961
|
+
# with one child": an includer may hold others alongside, as {Window} does
|
|
5962
|
+
# with its footer. For a region an app swaps, hold a {Slot} instead.
|
|
5963
|
+
#
|
|
5964
|
+
# A tree walk finds content generically through `is_a?(HasContent)` plus a
|
|
5965
|
+
# `content` compare, which is why this is a mixin rather than a per-class
|
|
5966
|
+
# accessor — the same reason {HasCaption} is one.
|
|
5640
5967
|
module HasContent
|
|
5641
|
-
# _@param_ `event`
|
|
5642
|
-
def handle_mouse: (MouseEvent event) -> void
|
|
5643
|
-
|
|
5644
5968
|
# _@param_ `rect`
|
|
5645
5969
|
def rect=: (Rect rect) -> void
|
|
5646
5970
|
|
|
@@ -5650,27 +5974,46 @@ module Tuile
|
|
|
5650
5974
|
attr_accessor content: Component?
|
|
5651
5975
|
end
|
|
5652
5976
|
|
|
5653
|
-
# A {Window}
|
|
5654
|
-
#
|
|
5977
|
+
# A {Window} with a read-only body, in one of two presentations:
|
|
5978
|
+
# **prose** ({#message=}) wraps in a scrollable {TextView}, **rows**
|
|
5979
|
+
# ({#lines=}) stay one per row in a {List}, truncating instead of
|
|
5980
|
+
# wrapping — the presentation for columnar output, where a wrap would
|
|
5981
|
+
# destroy the alignment. Usable tiled (add it to a {Layout}) or as a
|
|
5982
|
+
# popup via {.open}; the constructor and {.open} pick the presentation
|
|
5983
|
+
# from the body's type:
|
|
5655
5984
|
#
|
|
5656
|
-
#
|
|
5657
|
-
#
|
|
5985
|
+
# Component::InfoWindow.open("Help", "A long explanation, wrapped to fit.")
|
|
5986
|
+
# Component::InfoWindow.open("Files", ["drwx src/", "-rw- README.md"])
|
|
5987
|
+
#
|
|
5988
|
+
# Both write the same body slot — the last writer wins.
|
|
5658
5989
|
class InfoWindow < Tuile::Component::Window
|
|
5659
|
-
# _@param_ `caption`
|
|
5990
|
+
# _@param_ `caption` — the border title.
|
|
5660
5991
|
#
|
|
5661
|
-
# _@param_ `
|
|
5662
|
-
def initialize: (?String caption,
|
|
5992
|
+
# _@param_ `body` — an `Array` is assigned through {#lines=}, anything else through {#message=}.
|
|
5993
|
+
def initialize: (?(String | StyledString)? caption, ?(String | StyledString | Component | ::Array[untyped])? body) -> void
|
|
5994
|
+
|
|
5995
|
+
# Sets the rows body: a {List} populated via {List#lines=} (so entries
|
|
5996
|
+
# are coerced, `\n`-split and rstripped exactly as there), one item per
|
|
5997
|
+
# row, long rows truncated — never wrapped. For prose use {#message=}.
|
|
5998
|
+
#
|
|
5999
|
+
# _@param_ `lines` — entries are `String`, `StyledString`, or anything that responds to `#to_s`.
|
|
6000
|
+
def lines=: (::Array[untyped] lines) -> void
|
|
5663
6001
|
|
|
5664
6002
|
# Opens the info window as a popup.
|
|
5665
6003
|
#
|
|
5666
|
-
# _@param_ `caption`
|
|
6004
|
+
# _@param_ `caption` — the border title.
|
|
5667
6005
|
#
|
|
5668
|
-
# _@param_ `
|
|
6006
|
+
# _@param_ `body` — dispatched by type as in {#initialize}.
|
|
5669
6007
|
#
|
|
5670
|
-
# _@param_ `
|
|
6008
|
+
# _@param_ `declared_size` — the popup's box, applied top-down; the body wraps or scrolls within it. Defaults to {Fraction::HALF}.
|
|
5671
6009
|
#
|
|
5672
6010
|
# _@return_ — the opened popup.
|
|
5673
|
-
def self.open: (String caption, ::Array[
|
|
6011
|
+
def self.open: ((String | StyledString)? caption, ?(String | StyledString | Component | ::Array[untyped])? body, ?declared_size: (Size | Fraction)) -> Popup
|
|
6012
|
+
|
|
6013
|
+
# _@return_ — whatever {#message=}
|
|
6014
|
+
# was given — set a `String`, read that `String` back. After
|
|
6015
|
+
# {#lines=} it is the {List} that setter mounted.
|
|
6016
|
+
attr_accessor message: (String | StyledString | Component)?
|
|
5674
6017
|
end
|
|
5675
6018
|
|
|
5676
6019
|
# Single-select from a set of typed items, one row each. Arrows move a
|
|
@@ -5811,12 +6154,9 @@ module Tuile
|
|
|
5811
6154
|
# a read-only display field could override back to `false`. Only
|
|
5812
6155
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
5813
6156
|
# composing wrappers, so it stays per-class (`DECISIONS.md`
|
|
5814
|
-
# `
|
|
6157
|
+
# `D_integer_field`).
|
|
5815
6158
|
def focusable?: () -> bool
|
|
5816
6159
|
|
|
5817
|
-
# _@param_ `event`
|
|
5818
|
-
def handle_mouse: (MouseEvent event) -> void
|
|
5819
|
-
|
|
5820
6160
|
# _@param_ `rect`
|
|
5821
6161
|
def rect=: (Rect rect) -> void
|
|
5822
6162
|
|
|
@@ -5853,22 +6193,23 @@ module Tuile
|
|
|
5853
6193
|
# (floor {MIN_CAP_WIDTH}) and {HEIGHT_FRACTION} tall, and **grows but never
|
|
5854
6194
|
# shrinks** while it lives; a long message wraps to {MAX_ROWS_PER_MESSAGE}
|
|
5855
6195
|
# rows and is then ellipsized, and entries past the height cap wait unpainted.
|
|
5856
|
-
# `DECISIONS.md` `
|
|
6196
|
+
# `DECISIONS.md` `D_notification` has why each of those is what it is.
|
|
5857
6197
|
#
|
|
5858
6198
|
# Three things it deliberately doesn't do:
|
|
5859
6199
|
#
|
|
5860
|
-
# - **Take focus, or receive keys.**
|
|
5861
|
-
# key-dispatch scope ({ScreenPane#handle_key}), so
|
|
5862
|
-
#
|
|
5863
|
-
#
|
|
5864
|
-
#
|
|
5865
|
-
#
|
|
5866
|
-
#
|
|
6200
|
+
# - **Take focus, or receive keys.** An {Overlay} sits off the
|
|
6201
|
+
# key-dispatch scope ({ScreenPane#handle_key}), so no key arrives here at
|
|
6202
|
+
# all. A left click *on the box* dismisses ({#handle_mouse}); an app
|
|
6203
|
+
# wanting a key registers a global shortcut and calls {Overlay#close}. A
|
|
6204
|
+
# click *elsewhere* does not — this is the one overlay with
|
|
6205
|
+
# {Overlay#close_on_outside_click?} false, since a toast is timed and an
|
|
6206
|
+
# unrelated click is not about it.
|
|
5867
6207
|
# - **Follow a theme flip.** A `Theme::Ref` `color:` is resolved once, when
|
|
5868
6208
|
# the message is added — a toast lives seconds, so there is no
|
|
5869
6209
|
# {Component#on_theme_changed} rebuild.
|
|
5870
|
-
# - **Take a size.** {
|
|
5871
|
-
|
|
6210
|
+
# - **Take a size.** An {Overlay} has no declared box; the messages decide
|
|
6211
|
+
# this one's, in {#reposition}.
|
|
6212
|
+
class Notification < Tuile::Component::Overlay
|
|
5872
6213
|
MAX_MESSAGES: Integer
|
|
5873
6214
|
MAX_ROWS_PER_MESSAGE: Integer
|
|
5874
6215
|
DISPLAY_SECONDS: Float
|
|
@@ -5891,16 +6232,6 @@ module Tuile
|
|
|
5891
6232
|
|
|
5892
6233
|
def initialize: () -> void
|
|
5893
6234
|
|
|
5894
|
-
# Load-bearing, not cosmetic: focus landing inside a non-modal popup sits
|
|
5895
|
-
# outside the key-dispatch scope, where {ScreenPane#handle_key} delivers to
|
|
5896
|
-
# nobody — every keystroke would go dead until the user pressed Tab.
|
|
5897
|
-
#
|
|
5898
|
-
# _@return_ — false.
|
|
5899
|
-
def focusable?: () -> bool
|
|
5900
|
-
|
|
5901
|
-
# _@return_ — false — see {#focusable?}.
|
|
5902
|
-
def tab_stop?: () -> bool
|
|
5903
|
-
|
|
5904
6235
|
# Appends a message, dropping it (with a {Tuile.logger} warning) once
|
|
5905
6236
|
# {MAX_MESSAGES} are held. Public so a caller holding the instance can
|
|
5906
6237
|
# append without repeating {show}'s lookup.
|
|
@@ -5912,7 +6243,7 @@ module Tuile
|
|
|
5912
6243
|
|
|
5913
6244
|
# Recomputes the box from its messages and re-anchors it to the screen's
|
|
5914
6245
|
# top-right corner — so a SIGWINCH re-wraps and re-anchors, where
|
|
5915
|
-
# {
|
|
6246
|
+
# {Overlay#reposition} would have kept the stale left column of a *derived*
|
|
5916
6247
|
# position (off-screen entirely if the terminal narrowed).
|
|
5917
6248
|
#
|
|
5918
6249
|
# Rebuilds the {TextView}'s text too, and every mutation routes through
|
|
@@ -5921,19 +6252,13 @@ module Tuile
|
|
|
5921
6252
|
# from the width.
|
|
5922
6253
|
def reposition: () -> void
|
|
5923
6254
|
|
|
5924
|
-
# A notification is sized by its messages, so this always raises. Failing
|
|
5925
|
-
# loudly beats accepting a size the next {#reposition} would discard.
|
|
5926
|
-
#
|
|
5927
|
-
# _@param_ `_new_size`
|
|
5928
|
-
def size=: ((Size | Fraction) _new_size) -> void
|
|
5929
|
-
|
|
5930
6255
|
# A left click dismisses the whole box, every message with it. Other buttons
|
|
5931
6256
|
# are consumed and inert — including the scroll wheel, which would otherwise
|
|
5932
6257
|
# nuke the box on a stray spin.
|
|
5933
6258
|
#
|
|
5934
6259
|
# Deliberately *replaces* rather than augments: neither `super` nor
|
|
5935
6260
|
# {HasContent#handle_mouse} may run, since both end at a
|
|
5936
|
-
# `screen.focused = …` inside this subtree (see {#focusable?}).
|
|
6261
|
+
# `screen.focused = …` inside this subtree (see {Overlay#focusable?}).
|
|
5937
6262
|
#
|
|
5938
6263
|
# _@param_ `event`
|
|
5939
6264
|
def handle_mouse: (MouseEvent event) -> void
|
|
@@ -6034,7 +6359,7 @@ module Tuile
|
|
|
6034
6359
|
# The `█`/`░` pair is the same one {VerticalScrollBar} uses — East-Asian
|
|
6035
6360
|
# Ambiguous and Neutral respectively, so under an ambiguous-as-wide terminal
|
|
6036
6361
|
# the rendered length would vary with the fill level. Shipped anyway, per
|
|
6037
|
-
# `DECISIONS.md` `
|
|
6362
|
+
# `DECISIONS.md` `D_ambiguous_width`: a bar that rhymes with the scrollbar
|
|
6038
6363
|
# beats a third convention, and if that bet is ever reversed both swap
|
|
6039
6364
|
# together.
|
|
6040
6365
|
class ProgressBar < Component
|
|
@@ -6240,12 +6565,9 @@ module Tuile
|
|
|
6240
6565
|
# a read-only display field could override back to `false`. Only
|
|
6241
6566
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
6242
6567
|
# composing wrappers, so it stays per-class (`DECISIONS.md`
|
|
6243
|
-
# `
|
|
6568
|
+
# `D_integer_field`).
|
|
6244
6569
|
def focusable?: () -> bool
|
|
6245
6570
|
|
|
6246
|
-
# _@param_ `event`
|
|
6247
|
-
def handle_mouse: (MouseEvent event) -> void
|
|
6248
|
-
|
|
6249
6571
|
# _@param_ `rect`
|
|
6250
6572
|
def rect=: (Rect rect) -> void
|
|
6251
6573
|
|
|
@@ -6254,7 +6576,7 @@ module Tuile
|
|
|
6254
6576
|
|
|
6255
6577
|
# A borderless, tinted, non-focusable floating selection list — the dropdown
|
|
6256
6578
|
# a *driver* drops open, drives by forwarding movement keys, and commits a
|
|
6257
|
-
# pick from:
|
|
6579
|
+
# pick from: an {Overlay} wrapping a {List} that never takes focus, so
|
|
6258
6580
|
# focus stays on the driver while the caller refills the rows, moves the
|
|
6259
6581
|
# highlight, and reads the pick.
|
|
6260
6582
|
#
|
|
@@ -6284,7 +6606,7 @@ module Tuile
|
|
|
6284
6606
|
# different tint (a `Theme.ref(:token)` keeps the flip-tracking).
|
|
6285
6607
|
#
|
|
6286
6608
|
# UI-thread-confined, like every component (see {Screen}).
|
|
6287
|
-
class ListDropdown < Tuile::Component::
|
|
6609
|
+
class ListDropdown < Tuile::Component::Overlay
|
|
6288
6610
|
MOVE_KEYS: ::Array[String]
|
|
6289
6611
|
MAX_VISIBLE_ROWS: Integer
|
|
6290
6612
|
|
|
@@ -6331,7 +6653,15 @@ module Tuile
|
|
|
6331
6653
|
# Vertical flips but horizontal slides because covering the driver would
|
|
6332
6654
|
# hide what is being chosen, while sharing its columns is the point.
|
|
6333
6655
|
#
|
|
6334
|
-
#
|
|
6656
|
+
# **`anchor` is the region actually occupied, and may be taller than one
|
|
6657
|
+
# row** — "beneath" means the row *after* it, so a multi-row driver (a
|
|
6658
|
+
# {Component::TextArea} carrying an autocomplete menu) is cleared entirely
|
|
6659
|
+
# rather than overdrawn from its second row down. A widget that paints one
|
|
6660
|
+
# row but may be *assigned* more height passes its face, not its rect:
|
|
6661
|
+
# {ComboBox} and {Select} both do, since a {Window} content slot hands them
|
|
6662
|
+
# the full inner height.
|
|
6663
|
+
#
|
|
6664
|
+
# _@param_ `anchor` — the region the driver occupies, of any height; the dropdown never covers it.
|
|
6335
6665
|
#
|
|
6336
6666
|
# _@param_ `rows` — how many rows there are to show — the content count, not the height: more than fits turns the scrollbar on. `0` collapses the dropdown to an empty rect (drivers close instead).
|
|
6337
6667
|
#
|
|
@@ -6366,7 +6696,7 @@ module Tuile
|
|
|
6366
6696
|
#
|
|
6367
6697
|
# _@param_ `rows` — how many rows there are to show — the content count, not the height; more than fits turns the scrollbar on. `0` collapses the dropdown to an empty rect (drivers close instead).
|
|
6368
6698
|
#
|
|
6369
|
-
# _@param_ `width` — the panel's width in columns, clamped to the screen. **Required, with no default:** `anchor.width` is the *parent's* width and would be meaningless here, so the caller measures (see `DECISIONS.md` `
|
|
6699
|
+
# _@param_ `width` — the panel's width in columns, clamped to the screen. **Required, with no default:** `anchor.width` is the *parent's* width and would be meaningless here, so the caller measures (see `DECISIONS.md` `D_select` on why the width policy stays with the driver).
|
|
6370
6700
|
#
|
|
6371
6701
|
# _@param_ `max_rows` — rows shown before the list scrolls.
|
|
6372
6702
|
def anchor_beside: (
|
|
@@ -6419,6 +6749,54 @@ module Tuile
|
|
|
6419
6749
|
end
|
|
6420
6750
|
end
|
|
6421
6751
|
|
|
6752
|
+
# A {TextView} purpose-built for log output: auto-scroll is on, the
|
|
6753
|
+
# scrollbar is visible, and lines arrive via {#log} — from any thread.
|
|
6754
|
+
# Point a logger at a {LogTextView::IO} to route its lines here:
|
|
6755
|
+
#
|
|
6756
|
+
# view = Tuile::Component::LogTextView.new
|
|
6757
|
+
# logger = Logger.new(Tuile::Component::LogTextView::IO.new(view))
|
|
6758
|
+
# logger.info("started") # appears in the view, from any thread
|
|
6759
|
+
#
|
|
6760
|
+
# Any logger that writes formatted lines to an IO works the same way —
|
|
6761
|
+
# for example `TTY::Logger` configured with the `:console` handler and
|
|
6762
|
+
# `output: LogTextView::IO.new(view)`.
|
|
6763
|
+
#
|
|
6764
|
+
# Add it to a layout as-is for a frameless log pane, or use {LogWindow}
|
|
6765
|
+
# for the framed assembly. A {TextView} rather than a {List}: long lines
|
|
6766
|
+
# (stacktraces, wide log records) word-wrap rather than ellipsize — a
|
|
6767
|
+
# truncated log line hides the very detail you opened the log to read.
|
|
6768
|
+
class LogTextView < Tuile::Component::TextView
|
|
6769
|
+
def initialize: () -> void
|
|
6770
|
+
|
|
6771
|
+
# Appends the given line to the log. Safe to call from any thread —
|
|
6772
|
+
# the one exception to the UI-thread confinement every component
|
|
6773
|
+
# carries: the append is marshalled through `event_queue.submit`, so
|
|
6774
|
+
# it lands once a loop drains the queue (deferred before the first
|
|
6775
|
+
# loop; silently dropped after the loop has returned).
|
|
6776
|
+
#
|
|
6777
|
+
# _@param_ `string` — the line (or multiple lines) to log; `nil` is a no-op.
|
|
6778
|
+
def log: (String? string) -> void
|
|
6779
|
+
|
|
6780
|
+
# IO-shaped adapter that forwards each log line to its sink's `#log`.
|
|
6781
|
+
# Implements both {#write} (stdlib `Logger`) and {#puts} (loggers that
|
|
6782
|
+
# call `output.puts`, e.g. `TTY::Logger`).
|
|
6783
|
+
class IO
|
|
6784
|
+
# _@param_ `sink` — anything responding to `#log`.
|
|
6785
|
+
def initialize: ((LogTextView | LogWindow) sink) -> void
|
|
6786
|
+
|
|
6787
|
+
# _@param_ `string`
|
|
6788
|
+
def write: (String string) -> void
|
|
6789
|
+
|
|
6790
|
+
# _@param_ `string`
|
|
6791
|
+
def puts: (String string) -> void
|
|
6792
|
+
|
|
6793
|
+
# Stdlib `Logger` only treats an object as an IO target when it
|
|
6794
|
+
# responds to both {#write} and {#close}; otherwise it tries to
|
|
6795
|
+
# interpret it as a filename. This is a no-op.
|
|
6796
|
+
def close: () -> void
|
|
6797
|
+
end
|
|
6798
|
+
end
|
|
6799
|
+
|
|
6422
6800
|
# A {Window} that lists options identified by single keyboard keys, asks
|
|
6423
6801
|
# the user to pick one, and fires a callback with the picked key.
|
|
6424
6802
|
#
|
|
@@ -6609,12 +6987,9 @@ module Tuile
|
|
|
6609
6987
|
# a read-only display field could override back to `false`. Only
|
|
6610
6988
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
6611
6989
|
# composing wrappers, so it stays per-class (`DECISIONS.md`
|
|
6612
|
-
# `
|
|
6990
|
+
# `D_integer_field`).
|
|
6613
6991
|
def focusable?: () -> bool
|
|
6614
6992
|
|
|
6615
|
-
# _@param_ `event`
|
|
6616
|
-
def handle_mouse: (MouseEvent event) -> void
|
|
6617
|
-
|
|
6618
6993
|
# _@param_ `rect`
|
|
6619
6994
|
def rect=: (Rect rect) -> void
|
|
6620
6995
|
|
|
@@ -6625,6 +7000,254 @@ module Tuile
|
|
|
6625
7000
|
attr_accessor item_label: (Proc | Method)
|
|
6626
7001
|
end
|
|
6627
7002
|
|
|
7003
|
+
# The confirm dialog: a {Window} asking a question — a caption, a prose
|
|
7004
|
+
# {#message}, a centered row of buttons — plus the one-button degenerate
|
|
7005
|
+
# case, the alert. Three factories cover the common shapes and open the
|
|
7006
|
+
# dialog as a centered, content-sized {Popup}:
|
|
7007
|
+
#
|
|
7008
|
+
# Component::ConfirmWindow.alert("Export failed", "Contact support@example.com.")
|
|
7009
|
+
# Component::ConfirmWindow.confirm("Delete Report Q4?", "This cannot be undone.",
|
|
7010
|
+
# confirm: "Delete") { delete! }
|
|
7011
|
+
# Component::ConfirmWindow.yes_no("Overwrite file?", "target.txt already exists.") { overwrite! }
|
|
7012
|
+
#
|
|
7013
|
+
# The component itself is the builder — declare any button set through
|
|
7014
|
+
# {#button}, then {#open}:
|
|
7015
|
+
#
|
|
7016
|
+
# dialog = Component::ConfirmWindow.new("Unsaved changes")
|
|
7017
|
+
# dialog.message = "Save your changes before leaving?"
|
|
7018
|
+
# dialog.button("Save") { save! }
|
|
7019
|
+
# dialog.button("Discard") { discard! }
|
|
7020
|
+
# dialog.button("Cancel") # no action: pressing it dismisses
|
|
7021
|
+
# dialog.on_dismiss = -> { stay_put }
|
|
7022
|
+
# dialog.open
|
|
7023
|
+
#
|
|
7024
|
+
# **Every button closes the dialog.** A button with a block then fires it; a
|
|
7025
|
+
# button without one is a Cancel. ESC, `q`, an outside click and a Cancel
|
|
7026
|
+
# button are all one outcome — {#on_dismiss}, fired exactly once, and only
|
|
7027
|
+
# when no action button was chosen. There is deliberately no keep-open knob:
|
|
7028
|
+
# a dialog that leads somewhere opens the next window from its callback.
|
|
7029
|
+
#
|
|
7030
|
+
# Keys: Left/Right (and Tab) move between the buttons, Enter/Space press the
|
|
7031
|
+
# focused one, and each button answers to its underlined mnemonic letter
|
|
7032
|
+
# (see {#button}). The message scrolls without taking focus —
|
|
7033
|
+
# {BODY_SCROLL_KEYS} are handed to it from anywhere in the dialog. Focus
|
|
7034
|
+
# opens on the first button; Shift+Tab reaches the message, a tab stop of
|
|
7035
|
+
# its own.
|
|
7036
|
+
#
|
|
7037
|
+
# The popup sizes itself from what the dialog owns — caption, message,
|
|
7038
|
+
# buttons — capped at half the screen ({#measured_size}), re-measured when
|
|
7039
|
+
# any of them changes. Usable tiled too (add it to a {Layout}): buttons then
|
|
7040
|
+
# fire their callbacks with nothing to close.
|
|
7041
|
+
#
|
|
7042
|
+
# There is deliberately no content slot: the body is prose ({#message=}
|
|
7043
|
+
# takes a component for the rare rich body, but the dialog then cannot
|
|
7044
|
+
# measure it). A dialog collecting *input* is not a confirm dialog — build a
|
|
7045
|
+
# `Popup.new(content: your_layout)`. See `DECISIONS.md` `D_confirm_window`
|
|
7046
|
+
# for the API rationale.
|
|
7047
|
+
class ConfirmWindow < Tuile::Component::Window
|
|
7048
|
+
BODY_SCROLL_KEYS: ::Array[String]
|
|
7049
|
+
RESERVED_MNEMONICS: ::Array[String]
|
|
7050
|
+
HEIGHT_CHROME: Integer
|
|
7051
|
+
WIDTH_CHROME: Integer
|
|
7052
|
+
|
|
7053
|
+
# _@param_ `caption` — the border title, coerced the same way {HasCaption#caption=} coerces it.
|
|
7054
|
+
def initialize: (?(String | StyledString)? caption) -> void
|
|
7055
|
+
|
|
7056
|
+
# Also re-measures the popup: the caption participates in the width.
|
|
7057
|
+
#
|
|
7058
|
+
# _@param_ `new_caption`
|
|
7059
|
+
def caption=: ((String | StyledString)? new_caption) -> void
|
|
7060
|
+
|
|
7061
|
+
# Appends a button and returns it. A button with a block is an action
|
|
7062
|
+
# button: pressing it closes the dialog, then fires the block. A button
|
|
7063
|
+
# without one is a Cancel: pressing it closes the dialog, then fires
|
|
7064
|
+
# {#on_dismiss}.
|
|
7065
|
+
#
|
|
7066
|
+
# dialog.button("Delete") { delete! } # mnemonic d, underlined
|
|
7067
|
+
# dialog.button("Keep", mnemonic: "e") # explicit letter
|
|
7068
|
+
# dialog.button("Cancel", mnemonic: nil) # no mnemonic
|
|
7069
|
+
#
|
|
7070
|
+
# The mnemonic — a printable letter activating the button from anywhere in
|
|
7071
|
+
# the dialog, case-insensitively — is underlined in the caption. Any case
|
|
7072
|
+
# is accepted, and the underline prefers the exact case given, so the case
|
|
7073
|
+
# picks which occurrence is cued (`"Save As"` with `"A"` underlines the
|
|
7074
|
+
# *As*), exactly as on {MenuBar#add_item}. The default `:auto` derives the
|
|
7075
|
+
# caption's first letter (cueing it as displayed) and is best-effort:
|
|
7076
|
+
# silently skipped when that letter is reserved, taken, or unusable. An
|
|
7077
|
+
# explicit letter is a promise and raises when it cannot be kept.
|
|
7078
|
+
#
|
|
7079
|
+
# Declare buttons before {#open}: adding one to an open dialog re-measures
|
|
7080
|
+
# the popup, but momentarily bounces focus off the button row.
|
|
7081
|
+
#
|
|
7082
|
+
# _@param_ `caption` — the button label.
|
|
7083
|
+
#
|
|
7084
|
+
# _@param_ `mnemonic` — `:auto` (default), a printable one-column character, or `nil` for none.
|
|
7085
|
+
#
|
|
7086
|
+
# _@return_ — the appended button.
|
|
7087
|
+
def button: ((String | StyledString) caption, ?mnemonic: (Symbol | String)?) -> Button
|
|
7088
|
+
|
|
7089
|
+
# Opens the dialog as a centered modal {Popup} sized by {#measured_size}
|
|
7090
|
+
# and returns that popup. ESC, `q` and an outside click dismiss it
|
|
7091
|
+
# (firing {#on_dismiss}); every button closes it too.
|
|
7092
|
+
#
|
|
7093
|
+
# _@return_ — the mounted popup; `popup.close` closes programmatically,
|
|
7094
|
+
# as {#close} also does.
|
|
7095
|
+
def open: () -> Popup
|
|
7096
|
+
|
|
7097
|
+
# Closes the popup {#open} mounted, which counts as a dismissal
|
|
7098
|
+
# ({#on_dismiss} fires). No-op when not open, or when used tiled.
|
|
7099
|
+
def close: () -> void
|
|
7100
|
+
|
|
7101
|
+
# The popup box this dialog wants: wide enough for the caption, the widest
|
|
7102
|
+
# message line and the button row, tall enough for the wrapped message
|
|
7103
|
+
# plus the button row — each capped at half of `reference`. The re-grow
|
|
7104
|
+
# rule's caller-side measure query: the dialog measures only content it
|
|
7105
|
+
# *owns*, so a {Component} assigned to {#message=} yields the full
|
|
7106
|
+
# half-screen box.
|
|
7107
|
+
#
|
|
7108
|
+
# _@param_ `reference` — the screen size to cap against.
|
|
7109
|
+
def measured_size: (?Size reference) -> Size
|
|
7110
|
+
|
|
7111
|
+
# Focus lands on the first button rather than cascading into the message
|
|
7112
|
+
# body, which sits before the button row in the tree.
|
|
7113
|
+
def on_focus: () -> void
|
|
7114
|
+
|
|
7115
|
+
# Handles the dialog-wide keys: Left/Right move between the buttons,
|
|
7116
|
+
# {BODY_SCROLL_KEYS} are hand-fed to the message body, and a mnemonic
|
|
7117
|
+
# letter presses its button. Reached by bubbling — the focused button or
|
|
7118
|
+
# body sees the key first, so a focused body consumes its own scroll keys
|
|
7119
|
+
# before this runs.
|
|
7120
|
+
#
|
|
7121
|
+
# _@param_ `key`
|
|
7122
|
+
#
|
|
7123
|
+
# _@return_ — true if the key was handled.
|
|
7124
|
+
def handle_key: (String key) -> bool
|
|
7125
|
+
|
|
7126
|
+
# Opens an acknowledgement dialog: a message and one button, whose press
|
|
7127
|
+
# is the dismissal. The button exists for discoverability — ESC and `q`
|
|
7128
|
+
# close too, but Tuile advertises no keys anywhere, and the button is
|
|
7129
|
+
# clickable.
|
|
7130
|
+
#
|
|
7131
|
+
# _@param_ `caption` — the border title.
|
|
7132
|
+
#
|
|
7133
|
+
# _@param_ `message` — see {#message=}.
|
|
7134
|
+
#
|
|
7135
|
+
# _@param_ `button` — the button label.
|
|
7136
|
+
#
|
|
7137
|
+
# _@return_ — the mounted popup.
|
|
7138
|
+
def self.alert: ((String | StyledString)? caption, (String | StyledString | Component)? message, ?button: (String | StyledString)) -> Popup
|
|
7139
|
+
|
|
7140
|
+
# Opens a two-button question: the block is the action, the cancel button
|
|
7141
|
+
# (with ESC, `q` and an outside click) is the dismissal.
|
|
7142
|
+
#
|
|
7143
|
+
# Component::ConfirmWindow.confirm("Delete Report Q4?", "This cannot be undone.",
|
|
7144
|
+
# confirm: "Delete") { delete! }
|
|
7145
|
+
#
|
|
7146
|
+
# _@param_ `caption` — the border title.
|
|
7147
|
+
#
|
|
7148
|
+
# _@param_ `message` — see {#message=}.
|
|
7149
|
+
#
|
|
7150
|
+
# _@param_ `confirm` — the action button's label.
|
|
7151
|
+
#
|
|
7152
|
+
# _@param_ `cancel` — the dismissal button's label.
|
|
7153
|
+
#
|
|
7154
|
+
# _@param_ `on_dismiss` — see {#on_dismiss}.
|
|
7155
|
+
#
|
|
7156
|
+
# _@return_ — the mounted popup.
|
|
7157
|
+
def self.confirm: (
|
|
7158
|
+
(String | StyledString)? caption,
|
|
7159
|
+
(String | StyledString | Component)? message,
|
|
7160
|
+
?confirm: (String | StyledString),
|
|
7161
|
+
?cancel: (String | StyledString),
|
|
7162
|
+
?on_dismiss: Proc?
|
|
7163
|
+
) -> Popup
|
|
7164
|
+
|
|
7165
|
+
# {.confirm} with Yes/No labels — the other canonical phrasing.
|
|
7166
|
+
#
|
|
7167
|
+
# _@param_ `caption` — the border title.
|
|
7168
|
+
#
|
|
7169
|
+
# _@param_ `message` — see {#message=}.
|
|
7170
|
+
#
|
|
7171
|
+
# _@param_ `on_dismiss` — see {#on_dismiss}.
|
|
7172
|
+
#
|
|
7173
|
+
# _@return_ — the mounted popup.
|
|
7174
|
+
def self.yes_no: ((String | StyledString)? caption, (String | StyledString | Component)? message, ?on_dismiss: Proc?) -> Popup
|
|
7175
|
+
|
|
7176
|
+
# The chosen-button path, in the settled order: mark chosen, close the
|
|
7177
|
+
# popup (whose on_close then skips the dismissal), fire the callback last
|
|
7178
|
+
# so it sees the dialog already gone — a callback opening a follow-up
|
|
7179
|
+
# popup gets clean focus-repair state.
|
|
7180
|
+
#
|
|
7181
|
+
# _@param_ `btn`
|
|
7182
|
+
def activate: (Button btn) -> void
|
|
7183
|
+
|
|
7184
|
+
# The popup's on_close: fires {#on_dismiss} unless a button was chosen —
|
|
7185
|
+
# which makes ESC, `q`, an outside click, {#close}, a direct
|
|
7186
|
+
# {Screen#remove_popup} and teardown all one event, fired exactly once.
|
|
7187
|
+
def notice_dismissed: () -> void
|
|
7188
|
+
|
|
7189
|
+
# _@param_ `delta` — -1 or 1.
|
|
7190
|
+
#
|
|
7191
|
+
# _@return_ — false when focus is not on a button (the key bubbles on).
|
|
7192
|
+
def focus_button_step: (Integer delta) -> bool
|
|
7193
|
+
|
|
7194
|
+
# Validates an explicit mnemonic (raising, as {MenuBar#add_item} does —
|
|
7195
|
+
# none of these has a sane answer at keypress time) or best-effort derives
|
|
7196
|
+
# one from the caption's first letter (returning nil where the explicit
|
|
7197
|
+
# path would raise).
|
|
7198
|
+
#
|
|
7199
|
+
# _@param_ `caption`
|
|
7200
|
+
#
|
|
7201
|
+
# _@param_ `mnemonic`
|
|
7202
|
+
#
|
|
7203
|
+
# _@return_ — the downcased letter, or nil for none.
|
|
7204
|
+
def resolve_mnemonic: (StyledString caption, (Symbol | String)? mnemonic) -> String?
|
|
7205
|
+
|
|
7206
|
+
# _@param_ `caption`
|
|
7207
|
+
def derive_mnemonic: (StyledString caption) -> String?
|
|
7208
|
+
|
|
7209
|
+
# {StyledString#slice} counts **columns** while a caption search yields a
|
|
7210
|
+
# **character** index, so the prefix is measured, never counted (the
|
|
7211
|
+
# {MenuBar::Item} cue, duplicated per `D_float_field`'s shallow-shell rule).
|
|
7212
|
+
#
|
|
7213
|
+
# _@param_ `caption`
|
|
7214
|
+
#
|
|
7215
|
+
# _@param_ `cue` — the letter in the case it was given in — exact case first, so the case picks which occurrence is underlined.
|
|
7216
|
+
def underline_mnemonic: (StyledString caption, String cue) -> StyledString
|
|
7217
|
+
|
|
7218
|
+
# Re-declares the button row's cross extent — the buttons' summed natural
|
|
7219
|
+
# width — so the {Layout::Vertical} centers it. Remove-and-re-add is the
|
|
7220
|
+
# only way to change a {Layout::Box} constraint; the row stays after the
|
|
7221
|
+
# body because both `add`s append.
|
|
7222
|
+
def recenter_button_row: () -> void
|
|
7223
|
+
|
|
7224
|
+
# _@return_ — columns of the button row at its natural width.
|
|
7225
|
+
def button_row_width: () -> Integer
|
|
7226
|
+
|
|
7227
|
+
# Re-measures the popup while open; a no-op tiled or before {#open}.
|
|
7228
|
+
def resize_popup: () -> void
|
|
7229
|
+
|
|
7230
|
+
# Callback taking no arguments, fired when the dialog is dismissed — ESC,
|
|
7231
|
+
# `q`, an outside click, or a {#button} declared without a block. Fires
|
|
7232
|
+
# exactly once per {#open}, and never when an action button was chosen.
|
|
7233
|
+
attr_accessor on_dismiss: Proc?
|
|
7234
|
+
|
|
7235
|
+
# _@return_ — whatever {#message=} was
|
|
7236
|
+
# given — set a `String`, read that `String` back. The component
|
|
7237
|
+
# rendering it is derived, never returned.
|
|
7238
|
+
attr_accessor message: (String | StyledString | Component)?
|
|
7239
|
+
|
|
7240
|
+
# The {Popup} that {#open} wraps the dialog in: its declared size is
|
|
7241
|
+
# derived from the dialog on every {#reposition}, so a message change and
|
|
7242
|
+
# a SIGWINCH both re-measure against the current screen.
|
|
7243
|
+
class MeasuredPopup < Tuile::Component::Popup
|
|
7244
|
+
# _@param_ `window`
|
|
7245
|
+
def initialize: (ConfirmWindow window) -> void
|
|
7246
|
+
|
|
7247
|
+
def reposition: () -> void
|
|
7248
|
+
end
|
|
7249
|
+
end
|
|
7250
|
+
|
|
6628
7251
|
# A {TextField} that paints one mask glyph per character instead of the
|
|
6629
7252
|
# text. Editing, caret, clicks and horizontal scrolling are the field's,
|
|
6630
7253
|
# unchanged:
|
|
@@ -6799,12 +7422,9 @@ module Tuile
|
|
|
6799
7422
|
# a read-only display field could override back to `false`. Only
|
|
6800
7423
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
6801
7424
|
# composing wrappers, so it stays per-class (`DECISIONS.md`
|
|
6802
|
-
# `
|
|
7425
|
+
# `D_integer_field`).
|
|
6803
7426
|
def focusable?: () -> bool
|
|
6804
7427
|
|
|
6805
|
-
# _@param_ `event`
|
|
6806
|
-
def handle_mouse: (MouseEvent event) -> void
|
|
6807
|
-
|
|
6808
7428
|
# _@param_ `rect`
|
|
6809
7429
|
def rect=: (Rect rect) -> void
|
|
6810
7430
|
|
|
@@ -7013,7 +7633,7 @@ module Tuile
|
|
|
7013
7633
|
# a read-only display field could override back to `false`. Only
|
|
7014
7634
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
7015
7635
|
# composing wrappers, so it stays per-class (`DECISIONS.md`
|
|
7016
|
-
# `
|
|
7636
|
+
# `D_integer_field`).
|
|
7017
7637
|
def focusable?: () -> bool
|
|
7018
7638
|
|
|
7019
7639
|
# _@return_ — current text contents.
|
|
@@ -7121,6 +7741,48 @@ module Tuile
|
|
|
7121
7741
|
attr_reader light: Theme
|
|
7122
7742
|
end
|
|
7123
7743
|
|
|
7744
|
+
# How many colors the terminal on the other end can actually show — the
|
|
7745
|
+
# depth {Color#quantize} degrades a color to:
|
|
7746
|
+
#
|
|
7747
|
+
# ColorDepth.detect # => :truecolor
|
|
7748
|
+
# ColorDepth.detect(env: { "TERM" => "xterm-256color" }) # => :palette256
|
|
7749
|
+
# ColorDepth.detect(env: {}) # => :ansi16
|
|
7750
|
+
#
|
|
7751
|
+
# Env-only: no terminal round-trip, so unlike {TerminalBackground.detect}
|
|
7752
|
+
# there is no stdin timing to respect, and the answer cannot go stale
|
|
7753
|
+
# mid-session the way a background color can.
|
|
7754
|
+
#
|
|
7755
|
+
# Terminals lie in both directions — `COLORTERM` frequently doesn't survive
|
|
7756
|
+
# ssh (it isn't in the default `SendEnv` set) or tmux — so {OVERRIDE_ENV}
|
|
7757
|
+
# beats every other signal, the escape hatch for a terminal detected wrong.
|
|
7758
|
+
# Misdetection otherwise lands *conservatively*: a truecolor tmux
|
|
7759
|
+
# advertising only `tmux-256color` reads as `:palette256`, which renders
|
|
7760
|
+
# coarser but never mangled.
|
|
7761
|
+
#
|
|
7762
|
+
# == Implementation details
|
|
7763
|
+
#
|
|
7764
|
+
# Terminfo is deliberately not consulted — its `RGB` boolean and
|
|
7765
|
+
# `colors#0x1000000` would mean shelling out to `tput`/`infocmp` at every
|
|
7766
|
+
# startup, and the env ladder plus the override already covers the real
|
|
7767
|
+
# terminal matrix.
|
|
7768
|
+
module ColorDepth
|
|
7769
|
+
DEPTHS: ::Array[Symbol]
|
|
7770
|
+
OVERRIDE_ENV: String
|
|
7771
|
+
TRUECOLOR_COLORTERM: ::Array[String]
|
|
7772
|
+
|
|
7773
|
+
# The terminal's color depth, from {OVERRIDE_ENV}, else `COLORTERM`,
|
|
7774
|
+
# else `TERM` (a `-direct` entry means 24-bit, a `256color` one the
|
|
7775
|
+
# palette), else the 16-color floor.
|
|
7776
|
+
#
|
|
7777
|
+
# _@param_ `env` — environment to read; defaults to `ENV` (which duck-types the `[]` lookup).
|
|
7778
|
+
#
|
|
7779
|
+
# _@return_ — one of {DEPTHS}.
|
|
7780
|
+
def self.detect: (?env: ::Hash[String, String]) -> Symbol
|
|
7781
|
+
|
|
7782
|
+
# _@param_ `value` — the raw {OVERRIDE_ENV} value.
|
|
7783
|
+
def self.parse_override: (String value) -> Symbol
|
|
7784
|
+
end
|
|
7785
|
+
|
|
7124
7786
|
# An event queue. The idea is that all UI-related updates run from the thread
|
|
7125
7787
|
# which runs the event queue only; this removes any need for locking and/or
|
|
7126
7788
|
# need for thread-safety mechanisms.
|
|
@@ -7298,6 +7960,26 @@ module Tuile
|
|
|
7298
7960
|
attr_reader scheme: Symbol
|
|
7299
7961
|
end
|
|
7300
7962
|
|
|
7963
|
+
# The terminal reported its background color — an OSC 11 reply, read
|
|
7964
|
+
# back as a whole "key" by {Keys.getkey}.
|
|
7965
|
+
#
|
|
7966
|
+
# Only ever an answer to a {TerminalBackground::QUERY} someone wrote,
|
|
7967
|
+
# and only the color: a {ColorSchemeEvent} is what prompts the re-probe
|
|
7968
|
+
# and what carries the light/dark half. See {Screen#background_color}.
|
|
7969
|
+
#
|
|
7970
|
+
# @!attribute [r] color
|
|
7971
|
+
# @return [Color] the reported background, 24-bit RGB.
|
|
7972
|
+
class BackgroundColorEvent
|
|
7973
|
+
# _@param_ `key` — key read via {Keys.getkey}.
|
|
7974
|
+
#
|
|
7975
|
+
# _@return_ — nil when `key` is not an OSC 11
|
|
7976
|
+
# background reply.
|
|
7977
|
+
def self.parse: (String key) -> BackgroundColorEvent?
|
|
7978
|
+
|
|
7979
|
+
# _@return_ — the reported background, 24-bit RGB.
|
|
7980
|
+
attr_reader color: Color
|
|
7981
|
+
end
|
|
7982
|
+
|
|
7301
7983
|
# Emitted once when the queue is cleared, all messages are processed and the
|
|
7302
7984
|
# event loop will block waiting for more messages. Perfect time for
|
|
7303
7985
|
# repainting windows.
|
|
@@ -7397,10 +8079,31 @@ module Tuile
|
|
|
7397
8079
|
|
|
7398
8080
|
def invalidated_clear: () -> void
|
|
7399
8081
|
|
|
8082
|
+
# Plays the terminal answering the OSC 11 re-probe, so a spec can
|
|
8083
|
+
# exercise app code that derives colors from {#background_color}:
|
|
8084
|
+
#
|
|
8085
|
+
# Screen.instance.background_color = Color.rgb(30, 30, 46)
|
|
8086
|
+
#
|
|
8087
|
+
# Takes the same path a real reply does — a changed color fires
|
|
8088
|
+
# {Component#on_theme_changed} across the tree and invalidates it.
|
|
8089
|
+
# There is no such writer on {Screen}: the value is a report from the
|
|
8090
|
+
# terminal, not a setting.
|
|
8091
|
+
#
|
|
8092
|
+
# _@param_ `color`
|
|
8093
|
+
def background_color=: (Color color) -> void
|
|
8094
|
+
|
|
7400
8095
|
# No terminal probing in tests: skip {TerminalBackground.detect}
|
|
7401
8096
|
# (which would write an OSC 11 query to the test runner's TTY and
|
|
7402
|
-
# steal its input) and pin the deterministic default.
|
|
7403
|
-
|
|
8097
|
+
# steal its input) and pin the deterministic default. The color is nil
|
|
8098
|
+
# — the case every app must handle anyway — until a spec assigns one
|
|
8099
|
+
# through {#background_color=}.
|
|
8100
|
+
def detect_background: () -> TerminalBackground::Result
|
|
8101
|
+
|
|
8102
|
+
# Pins the depth rather than reading the test runner's environment, so a
|
|
8103
|
+
# spec asserting flushed bytes gets the same answer on a truecolor
|
|
8104
|
+
# terminal, under `TERM=dumb` in CI, and inside tmux. A spec exercising
|
|
8105
|
+
# degradation builds its own {Buffer} with the depth it wants.
|
|
8106
|
+
def detect_color_depth: () -> Symbol
|
|
7404
8107
|
|
|
7405
8108
|
# _@return_ — whatever {#print} / {#emit} produced so far.
|
|
7406
8109
|
# Component painting lands in {#buffer}, not here — assert on
|
|
@@ -7474,30 +8177,32 @@ module Tuile
|
|
|
7474
8177
|
# The pane owns no chrome of its own — no status bar, no reserved row.
|
|
7475
8178
|
# {#content} gets the full pane rect, and an app that wants a status line
|
|
7476
8179
|
# builds one into its own layout and drives it from
|
|
7477
|
-
# {Screen#on_focus_changed=} (`
|
|
8180
|
+
# {Screen#on_focus_changed=} (`D_status_bar`).
|
|
7478
8181
|
#
|
|
7479
8182
|
# The pane is not a {Component::Layout}: popups deliberately overlap content
|
|
7480
8183
|
# (Z-ordered, full overdraw, no clipping) and key/mouse dispatch follows
|
|
7481
8184
|
# modal-popup rules rather than active-child dispatch.
|
|
7482
8185
|
class ScreenPane < Tuile::Component
|
|
8186
|
+
FINAL_METHODS: ::Array[Symbol]
|
|
8187
|
+
|
|
7483
8188
|
def initialize: () -> void
|
|
7484
8189
|
|
|
7485
8190
|
def focusable?: () -> bool
|
|
7486
8191
|
|
|
7487
|
-
# Adds
|
|
7488
|
-
# and grabs focus; a
|
|
7489
|
-
#
|
|
7490
|
-
#
|
|
7491
|
-
#
|
|
8192
|
+
# Adds an overlay and invalidates it for repaint. A {Component::Popup} is
|
|
8193
|
+
# centered and grabs focus; a bare {Component::Overlay} is left wherever
|
|
8194
|
+
# the caller positioned it and does *not* take focus, so the component that
|
|
8195
|
+
# was focused keeps the cursor and keeps receiving keys — the overlay
|
|
8196
|
+
# floats above the content, driven from app code.
|
|
7492
8197
|
#
|
|
7493
|
-
# The *whole subtree* is invalidated, not just the
|
|
8198
|
+
# The *whole subtree* is invalidated, not just the overlay wrapper (which
|
|
7494
8199
|
# paints nothing on its own): a reopened popup may land on cells that the
|
|
7495
8200
|
# tiled content has since overpainted, and if its rect is unchanged from
|
|
7496
8201
|
# last time its content components won't re-invalidate themselves — so
|
|
7497
|
-
# without this the
|
|
8202
|
+
# without this the overlay's contents would stay blank on reopen.
|
|
7498
8203
|
#
|
|
7499
|
-
# _@param_ `window`
|
|
7500
|
-
def add_popup: (Component::
|
|
8204
|
+
# _@param_ `window` — any overlay, modal or not.
|
|
8205
|
+
def add_popup: (Component::Overlay window) -> void
|
|
7501
8206
|
|
|
7502
8207
|
# Removes a popup. If the popup held focus, focus shifts to the now-topmost
|
|
7503
8208
|
# remaining popup, falling back to the focus snapshotted when the popup
|
|
@@ -7522,11 +8227,11 @@ module Tuile
|
|
|
7522
8227
|
# _@return_ — true if this pane currently hosts the popup.
|
|
7523
8228
|
def has_popup?: (Component window) -> bool
|
|
7524
8229
|
|
|
7525
|
-
# _@return_ — the topmost
|
|
7526
|
-
# only
|
|
8230
|
+
# _@return_ — the topmost modal overlay, or nil when
|
|
8231
|
+
# only bare {Component::Overlay}s (or none) are open. This is the "modal
|
|
7527
8232
|
# owner": the popup that scopes key dispatch, blocks mouse clicks, and
|
|
7528
|
-
# confines Tab cycling.
|
|
7529
|
-
#
|
|
8233
|
+
# confines Tab cycling. Bare overlays are excluded — they float above the
|
|
8234
|
+
# content without capturing input.
|
|
7530
8235
|
def modal_popup: () -> Component::Popup?
|
|
7531
8236
|
|
|
7532
8237
|
# Re-lays out children whenever the pane's own rect changes.
|
|
@@ -7535,7 +8240,7 @@ module Tuile
|
|
|
7535
8240
|
def rect=: (Rect new_rect) -> void
|
|
7536
8241
|
|
|
7537
8242
|
# Gives {#content} the whole pane rect — the pane reserves nothing for
|
|
7538
|
-
# itself. Each popup re-resolves its {Component::Popup#
|
|
8243
|
+
# itself. Each popup re-resolves its {Component::Popup#declared_size} against the new
|
|
7539
8244
|
# screen via {Component::Popup#reposition} — so a {Fraction} size tracks
|
|
7540
8245
|
# resize — repositioning itself (modal popups recenter; non-modal overlays
|
|
7541
8246
|
# keep the top-left their owner assigned).
|
|
@@ -7582,12 +8287,12 @@ module Tuile
|
|
|
7582
8287
|
# content beneath.
|
|
7583
8288
|
#
|
|
7584
8289
|
# A left click also *dismisses* the open popups it landed outside of that
|
|
7585
|
-
# asked for it ({Component::
|
|
8290
|
+
# asked for it ({Component::Overlay#close_on_outside_click?}). That is a
|
|
7586
8291
|
# second thing happening on a click, but not a second dispatch: the click is
|
|
7587
8292
|
# still delivered exactly once, down one chain, and a dismissed popup is
|
|
7588
8293
|
# closed rather than told.
|
|
7589
8294
|
#
|
|
7590
|
-
# "Outside" is measured against the {Component::
|
|
8295
|
+
# "Outside" is measured against the {Component::Overlay#owner} chain, not
|
|
7591
8296
|
# against one rect and not against stacking order: the popup the click hit
|
|
7592
8297
|
# is kept, and so is every popup that one *belongs to*, transitively. That
|
|
7593
8298
|
# is what stops a dialog being dismissed by a click on a dropdown its own
|
|
@@ -7628,21 +8333,22 @@ module Tuile
|
|
|
7628
8333
|
# _@param_ `child`
|
|
7629
8334
|
def on_child_removed: (Component child) -> void
|
|
7630
8335
|
|
|
7631
|
-
# The
|
|
7632
|
-
#
|
|
7633
|
-
# is any component, so it is resolved to the
|
|
7634
|
-
# resolves to itself) — which keeps the
|
|
7635
|
-
# rather than one frozen when the overlay
|
|
7636
|
-
# a mis-wired cycle terminate instead of
|
|
8336
|
+
# The overlays a click counts as landing *inside*: the one it hit, plus
|
|
8337
|
+
# every overlay that one belongs to, up the {Component::Overlay#owner}
|
|
8338
|
+
# chain. An owner is any component, so it is resolved to the overlay
|
|
8339
|
+
# enclosing it (an overlay resolves to itself) — which keeps the
|
|
8340
|
+
# relationship a live tree question rather than one frozen when the overlay
|
|
8341
|
+
# opened. The `include?` guard makes a mis-wired cycle terminate instead of
|
|
8342
|
+
# hanging the UI thread.
|
|
7637
8343
|
#
|
|
7638
|
-
# _@param_ `hit` — the
|
|
7639
|
-
def kept_by: (Component::
|
|
8344
|
+
# _@param_ `hit` — the overlay the click landed in, if any.
|
|
8345
|
+
def kept_by: (Component::Overlay? hit) -> ::Array[Component::Overlay]
|
|
7640
8346
|
|
|
7641
8347
|
# _@param_ `component`
|
|
7642
8348
|
#
|
|
7643
|
-
# _@return_ — `component` itself when it is
|
|
7644
|
-
# else the nearest
|
|
7645
|
-
def enclosing_popup: (Component? component) -> Component::
|
|
8349
|
+
# _@return_ — `component` itself when it is an
|
|
8350
|
+
# overlay, else the nearest overlay above it, else nil.
|
|
8351
|
+
def enclosing_popup: (Component? component) -> Component::Overlay?
|
|
7646
8352
|
|
|
7647
8353
|
# Delivers `key` to {Screen#focused} and bubbles it up the ancestor chain,
|
|
7648
8354
|
# stopping at (and including) `scope`. Delivers to no one — returning false
|
|
@@ -7675,15 +8381,16 @@ module Tuile
|
|
|
7675
8381
|
# _@return_ — the tiled content component.
|
|
7676
8382
|
attr_accessor content: Component?
|
|
7677
8383
|
|
|
7678
|
-
# _@return_ —
|
|
7679
|
-
# topmost. Holds both
|
|
7680
|
-
# ({Component::
|
|
7681
|
-
|
|
8384
|
+
# _@return_ — the open overlays in stacking order;
|
|
8385
|
+
# last is topmost. Holds both {Component::Popup} modals and bare
|
|
8386
|
+
# {Component::Overlay}s ({Component::Overlay#modal?}). The array must not
|
|
8387
|
+
# be mutated by callers.
|
|
8388
|
+
attr_reader popups: ::Array[Component::Overlay]
|
|
7682
8389
|
end
|
|
7683
8390
|
|
|
7684
8391
|
# An immutable string-with-styling, modeled as a sequence of {Span}s where
|
|
7685
8392
|
# each span carries a complete {Style} (`fg`, `bg`, `bold`, `italic`,
|
|
7686
|
-
# `underline`, `strikethrough`). Spans are non-overlapping and fully tile
|
|
8393
|
+
# `underline`, `strikethrough`, `inverse`). Spans are non-overlapping and fully tile
|
|
7687
8394
|
# the string — every character has exactly one resolved style, no overlay
|
|
7688
8395
|
# layers to merge, so the style at any column is just its span's `style`
|
|
7689
8396
|
# rather than a replay of the SGR state machine. The book's chapter 9 is
|
|
@@ -7714,7 +8421,7 @@ module Tuile
|
|
|
7714
8421
|
# ## Parser
|
|
7715
8422
|
#
|
|
7716
8423
|
# {.parse} is strict by default — it recognizes only the SGR codes for
|
|
7717
|
-
# {Style}'s attributes (fg/bg/bold/italic/underline/strikethrough) and
|
|
8424
|
+
# {Style}'s attributes (fg/bg/bold/italic/underline/strikethrough/inverse) and
|
|
7718
8425
|
# raises {ParseError} on anything else, keeping the `parse(to_ansi(x)) == x`
|
|
7719
8426
|
# round-trip honest. Pass `lenient: true` to instead discard everything it
|
|
7720
8427
|
# can't model (unmodeled SGR, cursor moves, OSC/DCS, stray escapes) and keep
|
|
@@ -7849,6 +8556,10 @@ module Tuile
|
|
|
7849
8556
|
# so a log line keeps its red error-level bg while its plain text picks up an
|
|
7850
8557
|
# inherited panel tint.
|
|
7851
8558
|
#
|
|
8559
|
+
# An `inverse` span counts as backgrounded and is skipped even when its `bg`
|
|
8560
|
+
# member is nil: SGR 7 swaps the pair in effect, so a bg filled under it
|
|
8561
|
+
# would recolor the span's *glyphs*, not the ground behind them.
|
|
8562
|
+
#
|
|
7852
8563
|
# _@param_ `bg` — background color, coerced via {Color.coerce}. `nil` returns `self` unchanged.
|
|
7853
8564
|
def under_bg: ((Color | Symbol | Integer | ::Array[Integer])? bg) -> StyledString
|
|
7854
8565
|
|
|
@@ -7894,6 +8605,20 @@ module Tuile
|
|
|
7894
8605
|
# _@param_ `underline` — whether the spans should be underlined.
|
|
7895
8606
|
def with_underline: (?underline: bool) -> StyledString
|
|
7896
8607
|
|
|
8608
|
+
# Returns a new {StyledString} with `inverse` applied to every span,
|
|
8609
|
+
# preserving each span's text and other style attributes. Inverse swaps
|
|
8610
|
+
# whatever fg/bg are actually in effect at each cell — terminal defaults
|
|
8611
|
+
# included — so a focus chip built with it reads as "backgrounded" on any
|
|
8612
|
+
# terminal palette without picking a single color:
|
|
8613
|
+
#
|
|
8614
|
+
# StyledString.plain(" 1 VMs ").with_inverse # the inverted-chip idiom
|
|
8615
|
+
#
|
|
8616
|
+
# There is deliberately no `under_inverse`, for the reason {#with_bold}
|
|
8617
|
+
# spells out. Pass `inverse: false` to clear it.
|
|
8618
|
+
#
|
|
8619
|
+
# _@param_ `inverse` — whether the spans should be inverted.
|
|
8620
|
+
def with_inverse: (?inverse: bool) -> StyledString
|
|
8621
|
+
|
|
7897
8622
|
def inspect: () -> String
|
|
7898
8623
|
|
|
7899
8624
|
def build_ansi: () -> String
|
|
@@ -7987,6 +8712,9 @@ module Tuile
|
|
|
7987
8712
|
# @return [Boolean]
|
|
7988
8713
|
# @!attribute [r] strikethrough
|
|
7989
8714
|
# @return [Boolean]
|
|
8715
|
+
# @!attribute [r] inverse
|
|
8716
|
+
# @return [Boolean] swap the fg/bg *in effect* at the cell (SGR 7) —
|
|
8717
|
+
# including terminal defaults, which `fg`/`bg` can't name.
|
|
7990
8718
|
class Style
|
|
7991
8719
|
DEFAULT: Style
|
|
7992
8720
|
|
|
@@ -8001,13 +8729,16 @@ module Tuile
|
|
|
8001
8729
|
# _@param_ `underline`
|
|
8002
8730
|
#
|
|
8003
8731
|
# _@param_ `strikethrough`
|
|
8732
|
+
#
|
|
8733
|
+
# _@param_ `inverse`
|
|
8004
8734
|
def self.new: (
|
|
8005
8735
|
?fg: (Color | Symbol | Integer | ::Array[Integer])?,
|
|
8006
8736
|
?bg: (Color | Symbol | Integer | ::Array[Integer])?,
|
|
8007
8737
|
?bold: bool,
|
|
8008
8738
|
?italic: bool,
|
|
8009
8739
|
?underline: bool,
|
|
8010
|
-
?strikethrough: bool
|
|
8740
|
+
?strikethrough: bool,
|
|
8741
|
+
?inverse: bool
|
|
8011
8742
|
) -> Style
|
|
8012
8743
|
|
|
8013
8744
|
def default?: () -> bool
|
|
@@ -8045,6 +8776,10 @@ module Tuile
|
|
|
8045
8776
|
attr_reader underline: bool
|
|
8046
8777
|
|
|
8047
8778
|
attr_reader strikethrough: bool
|
|
8779
|
+
|
|
8780
|
+
# _@return_ — swap the fg/bg *in effect* at the cell (SGR 7) —
|
|
8781
|
+
# including terminal defaults, which `fg`/`bg` can't name.
|
|
8782
|
+
attr_reader inverse: bool
|
|
8048
8783
|
end
|
|
8049
8784
|
|
|
8050
8785
|
# A maximal run of text sharing a single {Style}. `text` is plain — it
|
|
@@ -8203,8 +8938,9 @@ module Tuile
|
|
|
8203
8938
|
end
|
|
8204
8939
|
end
|
|
8205
8940
|
|
|
8206
|
-
# Detects
|
|
8207
|
-
#
|
|
8941
|
+
# Detects the terminal's background color — both the light/dark scheme
|
|
8942
|
+
# {Screen} picks {Theme::LIGHT} or {Theme::DARK} from, and, when the
|
|
8943
|
+
# terminal answers the query, the actual RGB behind it.
|
|
8208
8944
|
#
|
|
8209
8945
|
# Two mechanisms, in order of reliability:
|
|
8210
8946
|
#
|
|
@@ -8217,13 +8953,19 @@ module Tuile
|
|
|
8217
8953
|
# bounded by a short timeout.
|
|
8218
8954
|
# 2. **`COLORFGBG` env var** — rxvt/konsole export `"fg;bg"` ANSI palette
|
|
8219
8955
|
# indices. Less reliable (stale across SSH/tmux, often unset); used
|
|
8220
|
-
# only when OSC 11 yields nothing.
|
|
8956
|
+
# only when OSC 11 yields nothing. Palette indices carry no RGB, so
|
|
8957
|
+
# this path fills in {Result#scheme} and leaves {Result#color} nil.
|
|
8221
8958
|
#
|
|
8222
8959
|
# **Timing matters**: the OSC 11 reply arrives on stdin, so the query
|
|
8223
8960
|
# must complete before {EventQueue#start_key_thread} owns stdin —
|
|
8224
8961
|
# otherwise the reply bytes get consumed as garbage keystrokes. {Screen}
|
|
8225
8962
|
# calls {.detect} from its constructor, which apps run before
|
|
8226
8963
|
# {Screen#run_event_loop}; don't call this after the event loop started.
|
|
8964
|
+
#
|
|
8965
|
+
# Once the loop *is* running the query is still available, from the other
|
|
8966
|
+
# side: {Screen} writes {QUERY} on every OS appearance flip and the key
|
|
8967
|
+
# thread — which owns stdin by then — reads the reply back through
|
|
8968
|
+
# {Keys.getkey} and {.parse}. See {Screen#background_color}.
|
|
8227
8969
|
module TerminalBackground
|
|
8228
8970
|
QUERY_TIMEOUT: Float
|
|
8229
8971
|
QUERY: String
|
|
@@ -8234,6 +8976,9 @@ module Tuile
|
|
|
8234
8976
|
# Detects the terminal background. Queries OSC 11 when both `input`
|
|
8235
8977
|
# and `output` are TTYs, falling back to `COLORFGBG`.
|
|
8236
8978
|
#
|
|
8979
|
+
# TerminalBackground.detect
|
|
8980
|
+
# # => #<data Result scheme=:dark, color=#<Tuile::Color [30, 30, 46]>>
|
|
8981
|
+
#
|
|
8237
8982
|
# _@param_ `input` — where the OSC 11 reply arrives (the TTY input).
|
|
8238
8983
|
#
|
|
8239
8984
|
# _@param_ `output` — where the query is written (the TTY output).
|
|
@@ -8242,25 +8987,41 @@ module Tuile
|
|
|
8242
8987
|
#
|
|
8243
8988
|
# _@param_ `timeout` — max seconds to wait for the OSC 11 reply.
|
|
8244
8989
|
#
|
|
8245
|
-
# _@return_ —
|
|
8990
|
+
# _@return_ — nil when the background is undetectable —
|
|
8991
|
+
# neither mechanism answered.
|
|
8246
8992
|
def self.detect: (
|
|
8247
8993
|
?input: IO,
|
|
8248
8994
|
?output: IO,
|
|
8249
8995
|
?env: ::Hash[String, String],
|
|
8250
8996
|
?timeout: Numeric
|
|
8251
|
-
) ->
|
|
8997
|
+
) -> Result?
|
|
8998
|
+
|
|
8999
|
+
# Parses an OSC 11 reply — the terminal's answer to {QUERY}, matched
|
|
9000
|
+
# anywhere in `reply`.
|
|
9001
|
+
#
|
|
9002
|
+
# TerminalBackground.parse("\e]11;rgb:1e1e/1e1e/2e2e\a").color
|
|
9003
|
+
# # => #<Tuile::Color [30, 30, 46]>
|
|
9004
|
+
#
|
|
9005
|
+
# Public because a reply also arrives *mid-session*, long after
|
|
9006
|
+
# {.detect}'s own bounded read: once the key thread owns stdin, a
|
|
9007
|
+
# whole reply surfaces as one "key" from {Keys.getkey}.
|
|
9008
|
+
#
|
|
9009
|
+
# _@param_ `reply` — raw terminal output that may contain a reply.
|
|
9010
|
+
#
|
|
9011
|
+
# _@return_ — nil when `reply` holds no OSC 11 reply.
|
|
9012
|
+
def self.parse: (String reply) -> Result?
|
|
8252
9013
|
|
|
8253
|
-
# Writes the OSC 11 query and
|
|
8254
|
-
#
|
|
8255
|
-
#
|
|
8256
|
-
#
|
|
9014
|
+
# Writes the OSC 11 query and parses the reply. The whole exchange
|
|
9015
|
+
# runs with `input` in raw mode: the reply has no trailing newline,
|
|
9016
|
+
# so a canonical-mode read would block past the timeout, and echo
|
|
9017
|
+
# would smear the reply bytes onto the screen.
|
|
8257
9018
|
#
|
|
8258
9019
|
# _@param_ `input`
|
|
8259
9020
|
#
|
|
8260
9021
|
# _@param_ `output`
|
|
8261
9022
|
#
|
|
8262
9023
|
# _@param_ `timeout`
|
|
8263
|
-
def self.query_osc11: (IO input, IO output, Numeric timeout) ->
|
|
9024
|
+
def self.query_osc11: (IO input, IO output, Numeric timeout) -> Result?
|
|
8264
9025
|
|
|
8265
9026
|
# Accumulates reply bytes until a BEL/ST terminator or the deadline.
|
|
8266
9027
|
# Terminals that don't support OSC 11 never reply — returning
|
|
@@ -8271,13 +9032,20 @@ module Tuile
|
|
|
8271
9032
|
# _@param_ `timeout`
|
|
8272
9033
|
def self.read_reply: (IO input, Numeric timeout) -> String
|
|
8273
9034
|
|
|
8274
|
-
#
|
|
8275
|
-
# component hex width (xterm replies 4 digits per channel, others 2).
|
|
9035
|
+
# Light or dark, by relative luminance against a 0.5 threshold.
|
|
8276
9036
|
#
|
|
8277
|
-
# _@param_ `components` —
|
|
9037
|
+
# _@param_ `components` — red, green and blue, each 0.0..1.0.
|
|
8278
9038
|
#
|
|
8279
9039
|
# _@return_ — `:light` or `:dark`.
|
|
8280
|
-
def self.classify: (::Array[
|
|
9040
|
+
def self.classify: (::Array[Float] components) -> Symbol
|
|
9041
|
+
|
|
9042
|
+
# The reported background as a 24-bit {Color}. Quantizing xterm's
|
|
9043
|
+
# 16-bit-per-channel reply down to 8 loses nothing a terminal can
|
|
9044
|
+
# display, and keeps the exposed value in the type the rest of Tuile
|
|
9045
|
+
# paints with.
|
|
9046
|
+
#
|
|
9047
|
+
# _@param_ `components` — red, green and blue, each 0.0..1.0.
|
|
9048
|
+
def self.to_color: (::Array[Float] components) -> Color
|
|
8281
9049
|
|
|
8282
9050
|
# `COLORFGBG` is `"fg;bg"` (rxvt sometimes `"fg;default;bg"`) with
|
|
8283
9051
|
# ANSI palette indices. White-ish backgrounds — 7 (white) and the
|
|
@@ -8286,6 +9054,27 @@ module Tuile
|
|
|
8286
9054
|
#
|
|
8287
9055
|
# _@param_ `value`
|
|
8288
9056
|
def self.from_colorfgbg: (String? value) -> Symbol?
|
|
9057
|
+
|
|
9058
|
+
# What a detection found: the light/dark `scheme`, and the background
|
|
9059
|
+
# `color` when a terminal actually reported one.
|
|
9060
|
+
#
|
|
9061
|
+
# `color` is nil whenever the scheme came from the `COLORFGBG`
|
|
9062
|
+
# fallback, so a consumer deriving a tint from the background must
|
|
9063
|
+
# handle nil — plenty of terminals answer neither query.
|
|
9064
|
+
#
|
|
9065
|
+
# @!attribute [r] scheme
|
|
9066
|
+
# @return [Symbol] `:light` or `:dark`.
|
|
9067
|
+
# @!attribute [r] color
|
|
9068
|
+
# @return [Color, nil] the reported background as 24-bit RGB, or nil
|
|
9069
|
+
# when only `COLORFGBG` answered.
|
|
9070
|
+
class Result < Data
|
|
9071
|
+
# _@return_ — `:light` or `:dark`.
|
|
9072
|
+
attr_reader scheme: Symbol
|
|
9073
|
+
|
|
9074
|
+
# _@return_ — the reported background as 24-bit RGB, or nil
|
|
9075
|
+
# when only `COLORFGBG` answered.
|
|
9076
|
+
attr_reader color: Color?
|
|
9077
|
+
end
|
|
8289
9078
|
end
|
|
8290
9079
|
|
|
8291
9080
|
# A vertical scrollbar that computes which character to draw at each row.
|