tuile 0.8.0 → 0.9.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 +15 -0
- data/README.md +51 -24
- data/book/01-first-app.md +186 -0
- data/book/02-repaint.md +177 -0
- data/book/03-layout.md +379 -0
- data/book/04-event-loop.md +209 -0
- data/book/05-focus.md +188 -0
- data/book/06-theming.md +250 -0
- data/book/07-components.md +231 -0
- data/book/08-testing.md +186 -0
- data/book/README.md +79 -0
- data/examples/hello_world.rb +1 -2
- data/lib/tuile/buffer.rb +100 -31
- data/lib/tuile/component/button.rb +1 -9
- data/lib/tuile/component/info_window.rb +4 -2
- data/lib/tuile/component/label.rb +7 -15
- data/lib/tuile/component/layout.rb +0 -9
- data/lib/tuile/component/list.rb +0 -26
- data/lib/tuile/component/log_window.rb +0 -14
- data/lib/tuile/component/popup.rb +70 -75
- data/lib/tuile/component/text_view.rb +0 -23
- data/lib/tuile/component/window.rb +57 -75
- data/lib/tuile/component.rb +1 -60
- data/lib/tuile/event_queue.rb +31 -10
- data/lib/tuile/fake_event_queue.rb +21 -5
- data/lib/tuile/fraction.rb +46 -0
- data/lib/tuile/screen.rb +8 -5
- data/lib/tuile/screen_pane.rb +5 -3
- data/lib/tuile/version.rb +1 -1
- data/sig/tuile.rbs +195 -223
- metadata +12 -4
- data/ideas/back-buffer.md +0 -217
- data/lib/tuile/sizing.rb +0 -59
data/sig/tuile.rbs
CHANGED
|
@@ -535,7 +535,7 @@ module Tuile
|
|
|
535
535
|
# string needed to bring a terminal — one that already matches the buffer's
|
|
536
536
|
# state as of the previous flush — up to date. Only cells that actually
|
|
537
537
|
# changed are emitted, so nothing flickers regardless of terminal/multiplexer
|
|
538
|
-
# synchronized-output support.
|
|
538
|
+
# synchronized-output support.
|
|
539
539
|
#
|
|
540
540
|
# Coordinates are 0-based `(x, y)` = `(column, row)`, matching
|
|
541
541
|
# {Component#rect} and `TTY::Cursor.move_to`.
|
|
@@ -567,8 +567,30 @@ module Tuile
|
|
|
567
567
|
# nothing for, since the glyph itself advances the cursor two columns).
|
|
568
568
|
# Overwriting either half of a wide glyph blanks the orphaned half, so the
|
|
569
569
|
# grid never holds a dangling continuation or a headless one.
|
|
570
|
+
#
|
|
571
|
+
# ## Future direction
|
|
572
|
+
#
|
|
573
|
+
# Components paint through this drawing surface ({#set_line} / {#set_char})
|
|
574
|
+
# without knowing whether it is the one global buffer or a private one — that
|
|
575
|
+
# indirection is deliberate, so per-component back buffers plus a z-order
|
|
576
|
+
# compositor could drop in without touching component code. It is not worth
|
|
577
|
+
# doing yet: the diff already drops unchanged cells from the wire, and an
|
|
578
|
+
# occluded component that didn't change is never repainted at all, so a
|
|
579
|
+
# compositor would only save residual `repaint` CPU. It pays off in exactly
|
|
580
|
+
# one regime — high repeat-rate scroll (held arrow / mouse wheel) of a large
|
|
581
|
+
# component on a large screen, where re-rendering the content each repeat is
|
|
582
|
+
# the dominant cost.
|
|
570
583
|
class Buffer
|
|
571
584
|
DEFAULT_STYLE: StyledString::Style
|
|
585
|
+
WIDTH_CACHE: ::Hash[String, Integer]
|
|
586
|
+
|
|
587
|
+
# Memoized {Unicode::DisplayWidth.of}. Use this for every paint-path width
|
|
588
|
+
# lookup instead of calling the gem directly.
|
|
589
|
+
#
|
|
590
|
+
# _@param_ `grapheme` — one grapheme cluster.
|
|
591
|
+
#
|
|
592
|
+
# _@return_ — its display width in columns (0 for combining marks).
|
|
593
|
+
def self.display_width: (String grapheme) -> Integer
|
|
572
594
|
|
|
573
595
|
# _@param_ `size` — grid dimensions in columns × rows.
|
|
574
596
|
def initialize: (Size size) -> void
|
|
@@ -693,6 +715,29 @@ module Tuile
|
|
|
693
715
|
# tests asserting styled output.
|
|
694
716
|
def region_ansi: (Rect rect) -> ::Array[String]
|
|
695
717
|
|
|
718
|
+
# Core of {#set_char} with the grapheme's display width already known.
|
|
719
|
+
# {#set_line} computes each width once while advancing the column and passes
|
|
720
|
+
# it straight through, so the paint hot path measures every grapheme exactly
|
|
721
|
+
# once (and that once is a {.display_width} memo read). See {#set_char} for
|
|
722
|
+
# the wide-glyph / clipping / out-of-bounds contract.
|
|
723
|
+
#
|
|
724
|
+
# _@param_ `x` — column.
|
|
725
|
+
#
|
|
726
|
+
# _@param_ `y` — row.
|
|
727
|
+
#
|
|
728
|
+
# _@param_ `grapheme` — one grapheme cluster.
|
|
729
|
+
#
|
|
730
|
+
# _@param_ `w` — `grapheme`'s display width (0, 1, or 2).
|
|
731
|
+
#
|
|
732
|
+
# _@param_ `style`
|
|
733
|
+
def put_char: (
|
|
734
|
+
Integer x,
|
|
735
|
+
Integer y,
|
|
736
|
+
String grapheme,
|
|
737
|
+
Integer w,
|
|
738
|
+
StyledString::Style style
|
|
739
|
+
) -> void
|
|
740
|
+
|
|
696
741
|
# (Re)allocates a blank grid of `size` with clean dirty state. Callers
|
|
697
742
|
# follow with {#mark_all_dirty} when the terminal doesn't match the new
|
|
698
743
|
# grid — construction and {#resize} both do.
|
|
@@ -748,13 +793,26 @@ module Tuile
|
|
|
748
793
|
StyledString::Style style
|
|
749
794
|
) -> void
|
|
750
795
|
|
|
751
|
-
# If `(x, y)`
|
|
752
|
-
#
|
|
796
|
+
# If `(x, y)` holds the right half (continuation) of a wide glyph, blanks the
|
|
797
|
+
# orphaned left half at `x - 1`. Called before a write lands on `x`, so the
|
|
798
|
+
# wide glyph to the left isn't left headless.
|
|
753
799
|
#
|
|
754
800
|
# _@param_ `x` — column
|
|
755
801
|
#
|
|
756
802
|
# _@param_ `y` — row
|
|
757
|
-
def
|
|
803
|
+
def blank_left_partner: (Integer x, Integer y) -> void
|
|
804
|
+
|
|
805
|
+
# If the cell just right of `(x, y)` is a continuation (the right half of a
|
|
806
|
+
# wide glyph whose origin is `(x, y)`), blanks it. Called before a write
|
|
807
|
+
# lands on `x`, so overwriting a wide origin doesn't strand its continuation.
|
|
808
|
+
# A continuation can only ever belong to the wide glyph immediately to its
|
|
809
|
+
# left, so the empty-grapheme test is exact — and cheaper than re-measuring
|
|
810
|
+
# the origin's width.
|
|
811
|
+
#
|
|
812
|
+
# _@param_ `x` — column
|
|
813
|
+
#
|
|
814
|
+
# _@param_ `y` — row
|
|
815
|
+
def blank_right_partner: (Integer x, Integer y) -> void
|
|
758
816
|
|
|
759
817
|
attr_reader width: Integer
|
|
760
818
|
|
|
@@ -1076,6 +1134,9 @@ module Tuile
|
|
|
1076
1134
|
# _@return_ — the structural root of the component tree.
|
|
1077
1135
|
attr_reader pane: ScreenPane
|
|
1078
1136
|
|
|
1137
|
+
# _@return_ — `:light` or `:dark`
|
|
1138
|
+
attr_reader color_scheme: Symbol
|
|
1139
|
+
|
|
1079
1140
|
# _@return_ — the back buffer components paint into
|
|
1080
1141
|
# ({Buffer#set_line} / {Buffer#fill} / {Buffer#set_char}).
|
|
1081
1142
|
attr_reader buffer: Buffer
|
|
@@ -1141,50 +1202,39 @@ module Tuile
|
|
|
1141
1202
|
end
|
|
1142
1203
|
end
|
|
1143
1204
|
|
|
1144
|
-
# A
|
|
1145
|
-
#
|
|
1146
|
-
#
|
|
1205
|
+
# A width/height ratio, each a float in `0.0..1.0` — the single relational
|
|
1206
|
+
# sizing primitive in Tuile. It exists for exactly one job: sizing a
|
|
1207
|
+
# {Component::Popup} against the screen. A popup has no siblings competing for
|
|
1208
|
+
# space and no rectangle in a tiled layout, so "half the screen, centered" is
|
|
1209
|
+
# the sensible default, and that wants a ratio rather than a hard-coded cell
|
|
1210
|
+
# count that would be wrong on the next terminal size.
|
|
1147
1211
|
#
|
|
1148
|
-
#
|
|
1212
|
+
# Tiled components are *not* sized this way: their parent computes explicit
|
|
1213
|
+
# integer rects in its own `rect=` and hands them down. `Fraction` is
|
|
1214
|
+
# deliberately scoped to {Component::Popup#size=} and is not a general layout
|
|
1215
|
+
# primitive.
|
|
1149
1216
|
#
|
|
1150
|
-
#
|
|
1151
|
-
#
|
|
1152
|
-
# {Component#content_size}), clamped to the slot;
|
|
1153
|
-
# - {.fixed} — take exactly the given number of cells, clamped to the slot.
|
|
1217
|
+
# Resolve it against a reference {Size} (the screen) to get concrete integer
|
|
1218
|
+
# cells:
|
|
1154
1219
|
#
|
|
1155
|
-
#
|
|
1156
|
-
# natural {Component#content_size} ({Component::Label}, {Component::Button},
|
|
1157
|
-
# {Component::List}, …). Input components ({Component::TextField} et al.)
|
|
1158
|
-
# report {Size::ZERO}, so a wrap-content slot collapses to zero width —
|
|
1159
|
-
# i.e. the component becomes invisible. Use {.fixed} or {FILL} for those.
|
|
1220
|
+
# Fraction::HALF.resolve(Size.new(80, 24)) # => 40x12
|
|
1160
1221
|
#
|
|
1161
|
-
#
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
class Sizing
|
|
1166
|
-
FILL: Sizing
|
|
1167
|
-
WRAP_CONTENT: Sizing
|
|
1222
|
+
# Integer arguments are coerced to float, so `Fraction.new(1, 1) == FULL`.
|
|
1223
|
+
class Fraction
|
|
1224
|
+
HALF: Fraction
|
|
1225
|
+
FULL: Fraction
|
|
1168
1226
|
|
|
1169
|
-
# _@param_ `
|
|
1227
|
+
# _@param_ `width` — fraction of the reference width, `0.0..1.0`.
|
|
1170
1228
|
#
|
|
1171
|
-
# _@
|
|
1172
|
-
def
|
|
1229
|
+
# _@param_ `height` — fraction of the reference height, `0.0..1.0`.
|
|
1230
|
+
def initialize: (width: Numeric, height: Numeric) -> void
|
|
1173
1231
|
|
|
1174
|
-
# Resolves
|
|
1175
|
-
#
|
|
1176
|
-
#
|
|
1177
|
-
#
|
|
1178
|
-
# _@param_ `content` — the component's natural extent on this axis (one dimension of its {Component#content_size}).
|
|
1232
|
+
# Resolves this fraction against a reference size, rounding each axis to the
|
|
1233
|
+
# nearest cell and flooring at 1 — so a fraction never yields a zero-size
|
|
1234
|
+
# result on a tiny terminal.
|
|
1179
1235
|
#
|
|
1180
|
-
# _@
|
|
1181
|
-
def resolve: (
|
|
1182
|
-
|
|
1183
|
-
# _@return_ — `:fill`, `:wrap_content` or `:fixed`.
|
|
1184
|
-
attr_reader mode: Symbol
|
|
1185
|
-
|
|
1186
|
-
# _@return_ — the cell count for `:fixed`; `nil` otherwise.
|
|
1187
|
-
attr_reader amount: Integer?
|
|
1236
|
+
# _@param_ `reference` — the size to take a fraction of (usually the screen).
|
|
1237
|
+
def resolve: (Size reference) -> Size
|
|
1188
1238
|
end
|
|
1189
1239
|
|
|
1190
1240
|
# A UI component which is positioned on the screen and draws characters into
|
|
@@ -1324,20 +1374,6 @@ module Tuile
|
|
|
1324
1374
|
# _@param_ `child` — the just-detached child.
|
|
1325
1375
|
def on_child_removed: (Component child) -> void
|
|
1326
1376
|
|
|
1327
|
-
# Called by a child component whose {#content_size} just changed (fired
|
|
1328
|
-
# from the child's {#content_size=}). Does nothing by default — a plain
|
|
1329
|
-
# container is not size-coupled to its children. Containers that derive
|
|
1330
|
-
# their own natural size or child layout from a child's natural size
|
|
1331
|
-
# override this (e.g. {Component::Window} re-lays-out a
|
|
1332
|
-
# {Sizing::WRAP_CONTENT} footer and recomputes its own size from content;
|
|
1333
|
-
# {Component::Popup} re-self-sizes). If the receiver's own
|
|
1334
|
-
# {#content_size} changes as a consequence, its {#content_size=} notifies
|
|
1335
|
-
# *its* parent in turn — so the event bubbles exactly as far as geometry
|
|
1336
|
-
# keeps changing, and stops where it doesn't.
|
|
1337
|
-
#
|
|
1338
|
-
# _@param_ `child` — the resized direct child.
|
|
1339
|
-
def on_child_content_size_changed: (Component child) -> void
|
|
1340
|
-
|
|
1341
1377
|
# Where the hardware terminal cursor should sit when this component is the
|
|
1342
1378
|
# cursor owner. Returns `nil` to indicate the cursor should be hidden. The
|
|
1343
1379
|
# {Screen} positions the hardware cursor after each repaint cycle by
|
|
@@ -1351,18 +1387,6 @@ module Tuile
|
|
|
1351
1387
|
# topmost popup. Empty by default; override to advertise shortcuts.
|
|
1352
1388
|
def keyboard_hint: () -> String
|
|
1353
1389
|
|
|
1354
|
-
# Advice to a wrapping {Component::Popup} on the minimum height this
|
|
1355
|
-
# component prefers to occupy when shown in a popup. `nil` (the default)
|
|
1356
|
-
# means no preference — the popup uses its own {Component::Popup#min_height}.
|
|
1357
|
-
# Override in a content component that should not collapse to a couple of
|
|
1358
|
-
# rows when sparse (e.g. {Component::LogWindow}).
|
|
1359
|
-
def popup_min_height: () -> Integer?
|
|
1360
|
-
|
|
1361
|
-
# Advice to a wrapping {Component::Popup} on the maximum height this
|
|
1362
|
-
# component may grow to when shown in a popup. `nil` (the default) means
|
|
1363
|
-
# no preference — the popup uses its own {Component::Popup#max_height}.
|
|
1364
|
-
def popup_max_height: () -> Integer?
|
|
1365
|
-
|
|
1366
1390
|
# Called whenever the component width changes. Does nothing by default.
|
|
1367
1391
|
def on_width_changed: () -> void
|
|
1368
1392
|
|
|
@@ -1421,15 +1445,6 @@ module Tuile
|
|
|
1421
1445
|
# {#on_theme_changed=} listener keeps firing.
|
|
1422
1446
|
attr_accessor on_theme_changed: Proc?
|
|
1423
1447
|
|
|
1424
|
-
# The {Size} big enough to show the entire component contents without
|
|
1425
|
-
# scrolling. Plain components have no intrinsic content and report
|
|
1426
|
-
# {Size::ZERO}; content-bearing components (e.g. {Label}, {List},
|
|
1427
|
-
# {TextView}, {Window}) maintain it eagerly via {#content_size=} from
|
|
1428
|
-
# their mutators, so reads are O(1). Used by callers like
|
|
1429
|
-
# {Component::Popup} to auto-size to whatever content was assigned,
|
|
1430
|
-
# regardless of its concrete type, and by {Sizing::WRAP_CONTENT} slots.
|
|
1431
|
-
attr_accessor content_size: Size
|
|
1432
|
-
|
|
1433
1448
|
# A scrollable list of items with cursor support.
|
|
1434
1449
|
#
|
|
1435
1450
|
# Items are modeled as {StyledString}s and painted directly into the
|
|
@@ -1540,19 +1555,6 @@ module Tuile
|
|
|
1540
1555
|
# is one, so the list snaps to the bottom on first paint.
|
|
1541
1556
|
def on_width_changed: () -> void
|
|
1542
1557
|
|
|
1543
|
-
# Natural size from scratch: longest line's display width plus the two
|
|
1544
|
-
# single-space gutters {#pad_to_row} adds, × line count. An empty list
|
|
1545
|
-
# is {Size::ZERO} (no gutters for no content).
|
|
1546
|
-
def compute_content_size: () -> Size
|
|
1547
|
-
|
|
1548
|
-
# Incremental {#content_size} update for appends: folds just the
|
|
1549
|
-
# appended lines into the running maximum, keeping {#add_lines}
|
|
1550
|
-
# O(appended) instead of re-scanning the whole list (LogWindow appends
|
|
1551
|
-
# a line per log statement).
|
|
1552
|
-
#
|
|
1553
|
-
# _@param_ `appended` — the just-appended lines (already concatenated onto {@lines}).
|
|
1554
|
-
def grow_content_size: (::Array[StyledString] appended) -> void
|
|
1555
|
-
|
|
1556
1558
|
# Coerces and flattens a list of input entries into trimmed
|
|
1557
1559
|
# {StyledString} lines. Each entry becomes a {StyledString} (String
|
|
1558
1560
|
# via {StyledString.parse}, StyledString passed through, anything else
|
|
@@ -1850,7 +1852,8 @@ module Tuile
|
|
|
1850
1852
|
# embedded ANSI is honored) or a {StyledString} directly. {#text}
|
|
1851
1853
|
# always returns the {StyledString}.
|
|
1852
1854
|
class Label < Component
|
|
1853
|
-
|
|
1855
|
+
# _@param_ `text` — initial text, coerced the same way {#text=} coerces it (a `String` is parsed via {StyledString.parse}; `nil` is an empty label). Equivalent to constructing empty and assigning {#text=}.
|
|
1856
|
+
def initialize: (?(String | StyledString)? text) -> void
|
|
1854
1857
|
|
|
1855
1858
|
# Paints the text into {#rect}.
|
|
1856
1859
|
#
|
|
@@ -1862,11 +1865,6 @@ module Tuile
|
|
|
1862
1865
|
|
|
1863
1866
|
def on_width_changed: () -> void
|
|
1864
1867
|
|
|
1865
|
-
# Natural size: longest hard-line's display width × number of hard
|
|
1866
|
-
# lines. Computed on the *unclipped* text — sizing is intrinsic to the
|
|
1867
|
-
# content, not the viewport. Empty text yields {Size::ZERO}.
|
|
1868
|
-
def compute_content_size: () -> Size
|
|
1869
|
-
|
|
1870
1868
|
# Recomputes {@clipped_lines} for the current text and rect width.
|
|
1871
1869
|
# Each line is ellipsized to fit and padded with trailing spaces out to
|
|
1872
1870
|
# the full width, so {#repaint} is just a lookup + {Buffer#set_line} per
|
|
@@ -1895,18 +1893,30 @@ module Tuile
|
|
|
1895
1893
|
|
|
1896
1894
|
# An overlay that wraps any {Component} as its content. Popup itself
|
|
1897
1895
|
# paints nothing — it's a transparent host that handles its lifecycle
|
|
1898
|
-
# ({#open} / {#close} / {#open?}, ESC/q to close) and
|
|
1899
|
-
#
|
|
1896
|
+
# ({#open} / {#close} / {#open?}, ESC/q to close) and holds a top-down
|
|
1897
|
+
# {#size} the {Screen} applies.
|
|
1898
|
+
#
|
|
1899
|
+
# The popup does *not* size itself to its content. Its box is declared by
|
|
1900
|
+
# {#size} — a {Fraction} (resolved against the screen every layout pass, so
|
|
1901
|
+
# it tracks resize) or an absolute {Size} (clamped to the screen). The
|
|
1902
|
+
# default is {Fraction::HALF}: half the screen, centered. The wrapped
|
|
1903
|
+
# content then fills that box and handles its own overflow by wrapping and
|
|
1904
|
+
# scrolling, so use content that can — a {Component::TextView} or
|
|
1905
|
+
# {Component::TextArea} — for anything longer than fits. A
|
|
1906
|
+
# {Component::Label} only truncates.
|
|
1900
1907
|
#
|
|
1901
1908
|
# Modal by default: it centers on the screen, grabs focus, eats keys, and
|
|
1902
1909
|
# blocks clicks beneath it. Pass `modal: false` for a non-modal overlay
|
|
1903
|
-
# that floats above the content (still painted on top
|
|
1904
|
-
#
|
|
1905
|
-
#
|
|
1906
|
-
#
|
|
1907
|
-
#
|
|
1908
|
-
#
|
|
1909
|
-
#
|
|
1910
|
+
# that floats above the content (still painted on top) without taking focus
|
|
1911
|
+
# or capturing input — the caller positions it (via {#rect=}) and drives it
|
|
1912
|
+
# from app code. That is the building block for an autocomplete/slash-command
|
|
1913
|
+
# list anchored to a {Component::TextField} or {Component::TextArea} caret:
|
|
1914
|
+
# typing keeps focus (and the cursor) in the input, an
|
|
1915
|
+
# {Component::TextInput#on_change} listener refills the list, and an
|
|
1916
|
+
# {Component::TextInput#on_key} interceptor forwards Up/Down/Enter to it.
|
|
1917
|
+
# Such a caller owns the list data, so it sizes the overlay itself
|
|
1918
|
+
# (`overlay.size = Size.new(longest, [items.size, 8].min)`) — still
|
|
1919
|
+
# caller-decides, top-down.
|
|
1910
1920
|
#
|
|
1911
1921
|
# The wrapped content fills the popup's full {#rect}; if you want a frame
|
|
1912
1922
|
# and caption, wrap a {Component::Window} (or any subclass — including
|
|
@@ -1926,10 +1936,12 @@ module Tuile
|
|
|
1926
1936
|
class Popup < Component
|
|
1927
1937
|
include Tuile::Component::HasContent
|
|
1928
1938
|
|
|
1929
|
-
# _@param_ `content` — initial content; can be set later via {#content=}.
|
|
1939
|
+
# _@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.
|
|
1930
1940
|
#
|
|
1931
1941
|
# _@param_ `modal` — true (default) for a centered, focus-grabbing, input-capturing modal; false for a non-modal overlay the caller positions and drives (see the class docs).
|
|
1932
|
-
|
|
1942
|
+
#
|
|
1943
|
+
# _@param_ `size` — the popup's size, applied top-down. A {Fraction} is resolved against the screen each layout pass; a {Size} is clamped to the screen. Defaults to {Fraction::HALF}.
|
|
1944
|
+
def initialize: (?content: Component?, ?modal: bool, ?size: (Size | Fraction)) -> void
|
|
1933
1945
|
|
|
1934
1946
|
# _@return_ — whether this popup is modal. See {#initialize}.
|
|
1935
1947
|
def modal?: () -> bool
|
|
@@ -1947,17 +1959,20 @@ module Tuile
|
|
|
1947
1959
|
# _@param_ `new_rect`
|
|
1948
1960
|
def rect=: (Rect new_rect) -> void
|
|
1949
1961
|
|
|
1950
|
-
# Mounts this popup on the {Screen}
|
|
1951
|
-
#
|
|
1952
|
-
# grown or shrunk while closed picks up the new size.
|
|
1962
|
+
# Mounts this popup on the {Screen}, re-resolving its {#size} against the
|
|
1963
|
+
# current screen first.
|
|
1953
1964
|
def open: () -> void
|
|
1954
1965
|
|
|
1955
1966
|
# Constructs and opens a popup in one call.
|
|
1956
1967
|
#
|
|
1957
1968
|
# _@param_ `content`
|
|
1958
1969
|
#
|
|
1970
|
+
# _@param_ `modal` — see {#initialize}.
|
|
1971
|
+
#
|
|
1972
|
+
# _@param_ `size` — see {#initialize}.
|
|
1973
|
+
#
|
|
1959
1974
|
# _@return_ — the opened popup.
|
|
1960
|
-
def self.open: (?content: Component?) -> Popup
|
|
1975
|
+
def self.open: (?content: Component?, ?modal: bool, ?size: (Size | Fraction)) -> Popup
|
|
1961
1976
|
|
|
1962
1977
|
# Removes this popup from the {Screen}. No-op if not currently open.
|
|
1963
1978
|
def close: () -> void
|
|
@@ -1965,38 +1980,21 @@ module Tuile
|
|
|
1965
1980
|
# _@return_ — true if this popup is currently mounted on the screen.
|
|
1966
1981
|
def open?: () -> bool
|
|
1967
1982
|
|
|
1968
|
-
#
|
|
1969
|
-
#
|
|
1970
|
-
#
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
#
|
|
1974
|
-
# Defers to the content's {Component#popup_max_height} advice when it
|
|
1975
|
-
# gives one, else defaults to 12. Override in a subclass to allow
|
|
1976
|
-
# taller popups regardless of content.
|
|
1977
|
-
def max_height: () -> Integer
|
|
1978
|
-
|
|
1979
|
-
# _@return_ — min height the popup occupies even when its content
|
|
1980
|
-
# is shorter. Defers to the content's {Component#popup_min_height}
|
|
1981
|
-
# advice when it gives one, else defaults to 0 (size purely to
|
|
1982
|
-
# content) — so a {Component::LogWindow} stays readable while only a
|
|
1983
|
-
# few lines are in without callers wiring up a subclass. Override in a
|
|
1984
|
-
# subclass to keep any popup from collapsing to a couple of rows.
|
|
1985
|
-
# Capped at the same 4/5-of-screen ceiling {#update_rect} applies.
|
|
1986
|
-
def min_height: () -> Integer
|
|
1987
|
-
|
|
1988
|
-
# Sets the popup's content and auto-sizes the popup to fit.
|
|
1983
|
+
# Re-resolves {#size} against the current screen and repositions the popup
|
|
1984
|
+
# *itself* (this is not laying out content — the popup's own rect): a
|
|
1985
|
+
# modal popup recenters; a non-modal overlay keeps its caller-assigned
|
|
1986
|
+
# top-left (only its size follows the screen). Called on {#open}, on
|
|
1987
|
+
# {#size=}, and by the screen's layout pass (so a {Fraction} size tracks
|
|
1988
|
+
# SIGWINCH).
|
|
1989
1989
|
#
|
|
1990
|
-
#
|
|
1991
|
-
|
|
1990
|
+
# The final rect is computed and assigned in one step rather than sizing
|
|
1991
|
+
# at the origin and then centering: the intermediate origin rect rarely
|
|
1992
|
+
# covers the previous one, which would make {#rect=}'s shrink/move
|
|
1993
|
+
# detection fire a full repaint on every resize.
|
|
1994
|
+
def reposition: () -> void
|
|
1992
1995
|
|
|
1993
|
-
#
|
|
1994
|
-
|
|
1995
|
-
# `add_line`, or a nested {Window} whose own content grew (the window
|
|
1996
|
-
# recomputes its {Component#content_size} and the change bubbles here).
|
|
1997
|
-
#
|
|
1998
|
-
# _@param_ `_child`
|
|
1999
|
-
def on_child_content_size_changed: (Component _child) -> void
|
|
1996
|
+
# Recenters the popup on the screen, preserving its current width/height.
|
|
1997
|
+
def center: () -> void
|
|
2000
1998
|
|
|
2001
1999
|
# Hint for the status bar: own "q Close" plus the wrapped content's hint.
|
|
2002
2000
|
def keyboard_hint: () -> String
|
|
@@ -2015,21 +2013,15 @@ module Tuile
|
|
|
2015
2013
|
# _@param_ `content`
|
|
2016
2014
|
def layout: (Component content) -> void
|
|
2017
2015
|
|
|
2018
|
-
# Recompute width/height from {#content}'s natural size and recenter
|
|
2019
|
-
# if currently open. Called whenever content is (re)assigned.
|
|
2020
|
-
#
|
|
2021
|
-
# Computes the final (centered) rect and assigns it in one step rather
|
|
2022
|
-
# than positioning at the origin and then centering: the intermediate
|
|
2023
|
-
# origin rect rarely covers the previous one, which would make
|
|
2024
|
-
# {#rect=}'s shrink/move detection fire a full repaint on every resize.
|
|
2025
|
-
def update_rect: () -> void
|
|
2026
|
-
|
|
2027
2016
|
# _@param_ `event`
|
|
2028
2017
|
def handle_mouse: (MouseEvent event) -> void
|
|
2029
2018
|
|
|
2030
2019
|
def children: () -> ::Array[Component]
|
|
2031
2020
|
|
|
2032
2021
|
def on_focus: () -> void
|
|
2022
|
+
|
|
2023
|
+
# _@return_ — the popup's declared size. See {#size=}.
|
|
2024
|
+
attr_accessor size: (Size | Fraction)
|
|
2033
2025
|
end
|
|
2034
2026
|
|
|
2035
2027
|
# A clickable button. Activated by Enter, Space, or a left mouse click;
|
|
@@ -2042,7 +2034,7 @@ module Tuile
|
|
|
2042
2034
|
# {Component#handle_mouse}.
|
|
2043
2035
|
#
|
|
2044
2036
|
# Assign a {#rect} (typically by the surrounding {Layout}) wide enough to
|
|
2045
|
-
# show `[ caption ]
|
|
2037
|
+
# show `[ caption ]` — that natural width is `caption.length + 4`.
|
|
2046
2038
|
class Button < Component
|
|
2047
2039
|
# _@param_ `caption` — the button's label.
|
|
2048
2040
|
def initialize: (?String caption) -> void
|
|
@@ -2059,9 +2051,6 @@ module Tuile
|
|
|
2059
2051
|
|
|
2060
2052
|
def repaint: () -> void
|
|
2061
2053
|
|
|
2062
|
-
# Natural width is `caption.length + 4` to fit `[ caption ]`; height 1.
|
|
2063
|
-
def natural_size: () -> Size
|
|
2064
|
-
|
|
2065
2054
|
# _@return_ — the button's label.
|
|
2066
2055
|
attr_accessor caption: String
|
|
2067
2056
|
|
|
@@ -2103,8 +2092,6 @@ module Tuile
|
|
|
2103
2092
|
# _@param_ `child`
|
|
2104
2093
|
def remove: (Component child) -> void
|
|
2105
2094
|
|
|
2106
|
-
def content_size: () -> Size
|
|
2107
|
-
|
|
2108
2095
|
# Dispatches the event to the child under the mouse cursor.
|
|
2109
2096
|
#
|
|
2110
2097
|
# _@param_ `event`
|
|
@@ -2146,22 +2133,6 @@ module Tuile
|
|
|
2146
2133
|
# _@param_ `value`
|
|
2147
2134
|
def scrollbar=: (bool value) -> void
|
|
2148
2135
|
|
|
2149
|
-
# Sets the new content. Also recomputes the window's natural size.
|
|
2150
|
-
#
|
|
2151
|
-
# _@param_ `new_content`
|
|
2152
|
-
def content=: (Component? new_content) -> void
|
|
2153
|
-
|
|
2154
|
-
# Re-lays-out a {Sizing::WRAP_CONTENT} footer when the footer's natural
|
|
2155
|
-
# size changes, and folds a content resize into the window's own
|
|
2156
|
-
# natural size (whose change then bubbles to the window's parent — e.g.
|
|
2157
|
-
# a {Popup} re-self-sizes). The footer deliberately does *not*
|
|
2158
|
-
# participate in the window's {#content_size}: it is decoration
|
|
2159
|
-
# overlaying the border, and must not drive the window's size — if it
|
|
2160
|
-
# doesn't fit, it is clipped to the inner width.
|
|
2161
|
-
#
|
|
2162
|
-
# _@param_ `child`
|
|
2163
|
-
def on_child_content_size_changed: (Component child) -> void
|
|
2164
|
-
|
|
2165
2136
|
# Fully repaints the window: both frame and contents.
|
|
2166
2137
|
#
|
|
2167
2138
|
# Window deliberately paints over its entire rect (border around the
|
|
@@ -2185,33 +2156,36 @@ module Tuile
|
|
|
2185
2156
|
# active the whole border is drawn in {Theme#active_border_color}.
|
|
2186
2157
|
def repaint_border: () -> void
|
|
2187
2158
|
|
|
2159
|
+
# Builds the bottom border line. The corners take the border color; the
|
|
2160
|
+
# interior is plain dashes when a {#footer} component occupies the row
|
|
2161
|
+
# (it overpaints them) or when there's no chrome, otherwise it carries
|
|
2162
|
+
# {#footer_text} embedded at its own width — keeping the text's own
|
|
2163
|
+
# styling — with dashes filling the remainder up to the inner width.
|
|
2164
|
+
#
|
|
2165
|
+
# _@param_ `inner_w` — the border's interior width.
|
|
2166
|
+
#
|
|
2167
|
+
# _@param_ `fg` — the active-border color, or nil when inactive.
|
|
2168
|
+
def bottom_border: (Integer inner_w, Color? fg) -> StyledString
|
|
2169
|
+
|
|
2188
2170
|
# The caption text as it appears in the rendered border, including the
|
|
2189
2171
|
# shortcut prefix when {#key_shortcut} is set.
|
|
2190
2172
|
def frame_caption: () -> String
|
|
2191
2173
|
|
|
2192
|
-
#
|
|
2193
|
-
#
|
|
2194
|
-
#
|
|
2195
|
-
# window with no content or caption sizes to `Size.new(2, 2)` (bare
|
|
2196
|
-
# border).
|
|
2197
|
-
def update_content_size: () -> void
|
|
2198
|
-
|
|
2199
|
-
# Positions the footer over the bottom border row, with its width
|
|
2200
|
-
# resolved by {#footer_sizing} against the inner width. A
|
|
2201
|
-
# {Sizing::WRAP_CONTENT} footer with zero natural width gets an empty
|
|
2202
|
-
# rect — i.e. it is invisible, as if never assigned.
|
|
2174
|
+
# Positions the footer over the bottom border row, spanning the full
|
|
2175
|
+
# inner width (the only dimension a bottom-row widget needs — the window
|
|
2176
|
+
# already knows it).
|
|
2203
2177
|
def layout_footer: () -> void
|
|
2204
2178
|
|
|
2205
2179
|
def on_focus: () -> void
|
|
2206
2180
|
|
|
2207
|
-
# _@return_ — optional component
|
|
2208
|
-
# row.
|
|
2181
|
+
# _@return_ — optional focusable component occupying the
|
|
2182
|
+
# bottom border row, always spanning the full inner width.
|
|
2209
2183
|
attr_accessor footer: Component?
|
|
2210
2184
|
|
|
2211
|
-
# _@return_ —
|
|
2212
|
-
#
|
|
2213
|
-
#
|
|
2214
|
-
attr_accessor
|
|
2185
|
+
# _@return_ — optional chrome embedded into the bottom border
|
|
2186
|
+
# line, mirroring {#caption} on the top line. Empty by default; hidden
|
|
2187
|
+
# whenever a {#footer} component is present.
|
|
2188
|
+
attr_accessor footer_text: (StyledString | String)?
|
|
2215
2189
|
|
|
2216
2190
|
# _@return_ — the current caption, empty by default.
|
|
2217
2191
|
attr_accessor caption: String
|
|
@@ -2660,9 +2634,6 @@ module Tuile
|
|
|
2660
2634
|
# reader when the cache is cold. Cost is O(total spans).
|
|
2661
2635
|
def build_text: () -> StyledString
|
|
2662
2636
|
|
|
2663
|
-
# _@return_ — {#content_size} computed from {@hard_lines}.
|
|
2664
|
-
def compute_content_size: () -> Size
|
|
2665
|
-
|
|
2666
2637
|
# _@return_ — column width available for wrapped text — viewport
|
|
2667
2638
|
# width minus the scrollbar gutter (when visible). `0` when {#rect}'s
|
|
2668
2639
|
# width is non-positive, which yields a degenerate "no wrap" result.
|
|
@@ -2721,13 +2692,6 @@ module Tuile
|
|
|
2721
2692
|
# bottom and tailing resumes. Default `false`.
|
|
2722
2693
|
attr_accessor auto_scroll: bool
|
|
2723
2694
|
|
|
2724
|
-
# _@return_ — longest hard-line's display width × number of hard
|
|
2725
|
-
# lines. Reported on the *unwrapped* text — wrap-aware sizing would
|
|
2726
|
-
# be circular (width depends on width). Empty text returns
|
|
2727
|
-
# `Size.new(0, 0)`. Maintained incrementally by {#text=} and
|
|
2728
|
-
# {#append}, so reads are O(1).
|
|
2729
|
-
attr_reader content_size: Size
|
|
2730
|
-
|
|
2731
2695
|
# A logical section of a {TextView}'s text — a contiguous run of
|
|
2732
2696
|
# hard lines the app wants to address as a unit (e.g. an LLM's
|
|
2733
2697
|
# "thinking" output vs. its assistant message). The view always
|
|
@@ -2872,18 +2836,6 @@ module Tuile
|
|
|
2872
2836
|
# _@param_ `caption`
|
|
2873
2837
|
def initialize: (?String caption) -> void
|
|
2874
2838
|
|
|
2875
|
-
# Keep the log pane at least half the screen tall even when only a few
|
|
2876
|
-
# lines have been logged: a {Component::Popup} sizes to its content, which
|
|
2877
|
-
# would collapse a near-empty log to two or three rows. Advice consulted
|
|
2878
|
-
# by {Component::Popup#min_height} when this window is a popup's content.
|
|
2879
|
-
def popup_min_height: () -> Integer
|
|
2880
|
-
|
|
2881
|
-
# Let a busy log grow past the popup's base 12-row cap (up to the
|
|
2882
|
-
# 4/5-of-screen ceiling {Component::Popup#update_rect} applies) so the
|
|
2883
|
-
# diagnostic stream stays scrollable in a tall window. Advice consulted
|
|
2884
|
-
# by {Component::Popup#max_height} when this window is a popup's content.
|
|
2885
|
-
def popup_max_height: () -> Integer
|
|
2886
|
-
|
|
2887
2839
|
# Appends given line to the log. Can be called from any thread. Does nothing if nil is passed in.
|
|
2888
2840
|
#
|
|
2889
2841
|
# _@param_ `string` — the line (or multiple lines) to log.
|
|
@@ -3142,8 +3094,10 @@ module Tuile
|
|
|
3142
3094
|
#
|
|
3143
3095
|
# _@param_ `lines` — the content, may contain formatting.
|
|
3144
3096
|
#
|
|
3097
|
+
# _@param_ `size` — the popup's size, applied top-down; the list wraps and scrolls within it. Defaults to {Fraction::HALF}.
|
|
3098
|
+
#
|
|
3145
3099
|
# _@return_ — the opened popup.
|
|
3146
|
-
def self.open: (String caption, ::Array[String] lines) -> Popup
|
|
3100
|
+
def self.open: (String caption, ::Array[String] lines, ?size: (Size | Fraction)) -> Popup
|
|
3147
3101
|
end
|
|
3148
3102
|
|
|
3149
3103
|
# A {Window} that lists options identified by single keyboard keys, asks
|
|
@@ -3296,10 +3250,13 @@ module Tuile
|
|
|
3296
3250
|
# Awaits until the event queue is empty (all events have been processed).
|
|
3297
3251
|
def await_empty: () -> void
|
|
3298
3252
|
|
|
3299
|
-
# Schedules `block` to fire on the event-loop thread
|
|
3300
|
-
#
|
|
3301
|
-
#
|
|
3302
|
-
#
|
|
3253
|
+
# Schedules `block` to fire on the event-loop thread every `seconds`,
|
|
3254
|
+
# passing a 0-based monotonically increasing tick counter. The interval is
|
|
3255
|
+
# in **seconds** — the conventional scheduling unit (`sleep`,
|
|
3256
|
+
# `Concurrent::TimerTask#execution_interval`, …) — so `tick(0.2)` fires five
|
|
3257
|
+
# times a second. Use it for periodic UI refresh from a background task
|
|
3258
|
+
# (poll a status, redraw a clock). For animation, where frames-per-second
|
|
3259
|
+
# is the natural unit, {#tick_fps} reads better.
|
|
3303
3260
|
#
|
|
3304
3261
|
# The returned {Ticker} controls the schedule — call {Ticker#cancel} to
|
|
3305
3262
|
# stop it.
|
|
@@ -3313,8 +3270,15 @@ module Tuile
|
|
|
3313
3270
|
# ({Concurrent}.global_timer_set) — adding more tickers does not add more
|
|
3314
3271
|
# threads, just more work on the shared scheduler.
|
|
3315
3272
|
#
|
|
3316
|
-
# _@param_ `
|
|
3317
|
-
def tick: (Numeric
|
|
3273
|
+
# _@param_ `seconds` — interval between firings, must be positive. Fractional values are fine (`tick(0.05)` ⇒ ~20 firings a second).
|
|
3274
|
+
def tick: (Numeric seconds) ?{ (Integer tick) -> void } -> Ticker
|
|
3275
|
+
|
|
3276
|
+
# Frames-per-second convenience over {#tick}: `tick_fps(15)` is exactly
|
|
3277
|
+
# `tick(1.0 / 15)`. Reads naturally for animation (a `/-\|` spinner, a
|
|
3278
|
+
# progress pulse) where you think in frames, not intervals.
|
|
3279
|
+
#
|
|
3280
|
+
# _@param_ `fps` — firings per second, must be positive. Fractional values are fine (`tick_fps(0.5)` ⇒ one firing every two seconds).
|
|
3281
|
+
def tick_fps: (Numeric fps) ?{ (Integer tick) -> void } -> Ticker
|
|
3318
3282
|
|
|
3319
3283
|
# Runs the event loop and blocks. Must be run from at most one thread at the
|
|
3320
3284
|
# same time. Blocks until some thread calls {#stop}. Calls block for all
|
|
@@ -3435,10 +3399,10 @@ module Tuile
|
|
|
3435
3399
|
class Ticker
|
|
3436
3400
|
# _@param_ `event_queue` — queue to dispatch tick calls onto.
|
|
3437
3401
|
#
|
|
3438
|
-
# _@param_ `
|
|
3402
|
+
# _@param_ `interval` — seconds between firings (positive).
|
|
3439
3403
|
#
|
|
3440
3404
|
# _@param_ `block` — called as `block.call(tick_count)` on each fire.
|
|
3441
|
-
def initialize: (EventQueue event_queue, Numeric
|
|
3405
|
+
def initialize: (EventQueue event_queue, Numeric interval, Proc block) -> void
|
|
3442
3406
|
|
|
3443
3407
|
# _@return_ — true once {#cancel} has been called.
|
|
3444
3408
|
def cancelled?: () -> bool
|
|
@@ -3617,8 +3581,10 @@ module Tuile
|
|
|
3617
3581
|
def rect=: (Rect new_rect) -> void
|
|
3618
3582
|
|
|
3619
3583
|
# Lays out content (full pane minus the bottom row) and the status bar
|
|
3620
|
-
# (bottom row).
|
|
3621
|
-
#
|
|
3584
|
+
# (bottom row). Each popup re-resolves its {Component::Popup#size} against
|
|
3585
|
+
# the new screen via {Component::Popup#reposition} — so a {Fraction} size
|
|
3586
|
+
# tracks resize — repositioning itself (modal popups recenter; non-modal
|
|
3587
|
+
# overlays keep the top-left their owner assigned).
|
|
3622
3588
|
def layout: () -> void
|
|
3623
3589
|
|
|
3624
3590
|
# Pane paints nothing itself; its children paint over the entire rect.
|
|
@@ -4122,12 +4088,18 @@ module Tuile
|
|
|
4122
4088
|
def post: (Object event) -> void
|
|
4123
4089
|
|
|
4124
4090
|
# Mirrors {EventQueue#tick} but timeless: returns a {FakeTicker} that
|
|
4125
|
-
# only fires when a test calls {#tick_once}. The `
|
|
4091
|
+
# only fires when a test calls {#tick_once}. The `seconds` argument is
|
|
4126
4092
|
# validated the same way the real queue validates it, then discarded —
|
|
4127
4093
|
# the fake has no clock, so frame cadence is up to the test.
|
|
4128
4094
|
#
|
|
4129
|
-
# _@param_ `
|
|
4130
|
-
def tick: (Numeric
|
|
4095
|
+
# _@param_ `seconds` — interval between firings, must be positive. Validated for parity with {EventQueue#tick}; otherwise unused.
|
|
4096
|
+
def tick: (Numeric seconds) ?{ (Integer tick) -> void } -> FakeTicker
|
|
4097
|
+
|
|
4098
|
+
# Mirrors {EventQueue#tick_fps}: validates `fps` for parity, then delegates
|
|
4099
|
+
# to {#tick} (the fake discards the interval regardless).
|
|
4100
|
+
#
|
|
4101
|
+
# _@param_ `fps` — firings per second, must be positive.
|
|
4102
|
+
def tick_fps: (Numeric fps) ?{ (Integer tick) -> void } -> FakeTicker
|
|
4131
4103
|
|
|
4132
4104
|
# Test helper: fires every live ticker's user block once and prunes
|
|
4133
4105
|
# cancelled tickers. No-op when no tickers are registered. Pumps once
|