tuile 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/examples/sampler.rb +109 -0
- data/ideas/back-buffer.md +217 -0
- data/lib/tuile/ansi.rb +16 -0
- data/lib/tuile/buffer.rb +412 -0
- data/lib/tuile/component/button.rb +2 -5
- data/lib/tuile/component/has_content.rb +0 -6
- data/lib/tuile/component/label.rb +8 -8
- data/lib/tuile/component/layout.rb +0 -12
- data/lib/tuile/component/list.rb +10 -11
- data/lib/tuile/component/log_window.rb +20 -5
- data/lib/tuile/component/picker_window.rb +4 -2
- data/lib/tuile/component/popup.rb +48 -13
- data/lib/tuile/component/text_area.rb +1 -1
- data/lib/tuile/component/text_field.rb +1 -1
- data/lib/tuile/component/text_input.rb +25 -9
- data/lib/tuile/component/text_view.rb +6 -7
- data/lib/tuile/component/window.rb +21 -38
- data/lib/tuile/component.rb +29 -25
- data/lib/tuile/fake_screen.rb +14 -1
- data/lib/tuile/screen.rb +90 -100
- data/lib/tuile/screen_pane.rb +80 -19
- data/lib/tuile/styled_string.rb +40 -30
- data/lib/tuile/version.rb +1 -1
- data/sig/tuile.rbs +511 -112
- metadata +4 -2
data/sig/tuile.rbs
CHANGED
|
@@ -23,6 +23,8 @@ module Tuile
|
|
|
23
23
|
# same form.
|
|
24
24
|
module Ansi
|
|
25
25
|
RESET: String
|
|
26
|
+
SYNC_BEGIN: String
|
|
27
|
+
SYNC_END: String
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
# Constants for keys returned by {.getkey} and helpers for reading them from
|
|
@@ -526,6 +528,286 @@ module Tuile
|
|
|
526
528
|
attr_reader custom: ::Hash[Symbol, Color]
|
|
527
529
|
end
|
|
528
530
|
|
|
531
|
+
# An in-memory grid of styled cells mirroring the terminal screen. This is
|
|
532
|
+
# the back buffer behind flicker-free rendering: components paint into it
|
|
533
|
+
# (via {#set_line} / {#set_char} / {#fill}) instead of writing escape
|
|
534
|
+
# sequences straight to the terminal, and {#flush} emits the minimal escape
|
|
535
|
+
# string needed to bring a terminal — one that already matches the buffer's
|
|
536
|
+
# state as of the previous flush — up to date. Only cells that actually
|
|
537
|
+
# changed are emitted, so nothing flickers regardless of terminal/multiplexer
|
|
538
|
+
# synchronized-output support. See `ideas/back-buffer.md`.
|
|
539
|
+
#
|
|
540
|
+
# Coordinates are 0-based `(x, y)` = `(column, row)`, matching
|
|
541
|
+
# {Component#rect} and `TTY::Cursor.move_to`.
|
|
542
|
+
#
|
|
543
|
+
# ## Dirty tracking
|
|
544
|
+
#
|
|
545
|
+
# Every mutator compares the incoming grapheme+style against what's already
|
|
546
|
+
# there and records the cell dirty only when it differs — so both mutation
|
|
547
|
+
# and {#flush} cost scale with what actually changed, never with the buffer
|
|
548
|
+
# size. There is deliberately no per-frame whole-buffer clear or copy;
|
|
549
|
+
# un-touched cells retain the previous frame's value.
|
|
550
|
+
#
|
|
551
|
+
# The bookkeeping avoids hashing and full-grid scans: a dirty flag **on each
|
|
552
|
+
# cell** (O(1) set, no `Set` bucket math, no separate array), a per-row
|
|
553
|
+
# boolean so {#flush} scans only the rows that changed, and one global flag
|
|
554
|
+
# so {#dirty?} and the "nothing changed" early-out are O(1). {#flush} clears
|
|
555
|
+
# every flag it consumes.
|
|
556
|
+
#
|
|
557
|
+
# Cells are **mutable and pre-allocated**: the grid builds its {Cell}s once
|
|
558
|
+
# (at construction and {#resize}) and rewrites them in place, so a normal
|
|
559
|
+
# paint allocates nothing per cell. That is why {Cell} is a plain mutable
|
|
560
|
+
# object rather than a frozen value type. The empty state of a cell is a
|
|
561
|
+
# space in the default style.
|
|
562
|
+
#
|
|
563
|
+
# ## Wide characters
|
|
564
|
+
#
|
|
565
|
+
# A 2-column glyph (fullwidth CJK, most emoji) occupies its origin cell plus a
|
|
566
|
+
# **continuation** cell to its right (an empty-grapheme {Cell} the flush emits
|
|
567
|
+
# nothing for, since the glyph itself advances the cursor two columns).
|
|
568
|
+
# Overwriting either half of a wide glyph blanks the orphaned half, so the
|
|
569
|
+
# grid never holds a dangling continuation or a headless one.
|
|
570
|
+
class Buffer
|
|
571
|
+
DEFAULT_STYLE: StyledString::Style
|
|
572
|
+
|
|
573
|
+
# _@param_ `size` — grid dimensions in columns × rows.
|
|
574
|
+
def initialize: (Size size) -> void
|
|
575
|
+
|
|
576
|
+
# _@return_ — grid dimensions.
|
|
577
|
+
def size: () -> Size
|
|
578
|
+
|
|
579
|
+
# _@param_ `x` — column.
|
|
580
|
+
#
|
|
581
|
+
# _@param_ `y` — row.
|
|
582
|
+
#
|
|
583
|
+
# _@return_ — the live cell at `(x, y)` (do not mutate — paint via
|
|
584
|
+
# {#set_char} / {#set_line} so dirty tracking stays correct), or nil when
|
|
585
|
+
# out of bounds.
|
|
586
|
+
def cell: (Integer x, Integer y) -> Cell?
|
|
587
|
+
|
|
588
|
+
# _@return_ — true if any cell has changed since the last {#flush}.
|
|
589
|
+
def dirty?: () -> bool
|
|
590
|
+
|
|
591
|
+
# Writes one grapheme cluster at `(x, y)`. A 2-column glyph also writes a
|
|
592
|
+
# continuation cell at `(x + 1, y)`; a wide glyph that would overflow the
|
|
593
|
+
# last column is replaced by a blank (terminals can't render a half-clipped
|
|
594
|
+
# wide glyph). Zero-width input (a lone combining mark) is ignored — it has
|
|
595
|
+
# no cell of its own. Out-of-bounds writes are dropped.
|
|
596
|
+
#
|
|
597
|
+
# _@param_ `x` — column.
|
|
598
|
+
#
|
|
599
|
+
# _@param_ `y` — row.
|
|
600
|
+
#
|
|
601
|
+
# _@param_ `grapheme` — one grapheme cluster.
|
|
602
|
+
#
|
|
603
|
+
# _@param_ `style`
|
|
604
|
+
def set_char: (
|
|
605
|
+
Integer x,
|
|
606
|
+
Integer y,
|
|
607
|
+
String grapheme,
|
|
608
|
+
?StyledString::Style style
|
|
609
|
+
) -> void
|
|
610
|
+
|
|
611
|
+
# Writes a {StyledString} starting at `(x, y)`, advancing by each grapheme's
|
|
612
|
+
# display width and clipping at the right edge. The workhorse that replaces
|
|
613
|
+
# the old `screen.print(TTY::Cursor.move_to(x, y), styled.to_ansi)` per-row
|
|
614
|
+
# paint. Newlines in the string are not handled — pass one physical line.
|
|
615
|
+
#
|
|
616
|
+
# _@param_ `x` — starting column.
|
|
617
|
+
#
|
|
618
|
+
# _@param_ `y` — row.
|
|
619
|
+
#
|
|
620
|
+
# _@param_ `styled`
|
|
621
|
+
def set_line: (Integer x, Integer y, StyledString styled) -> void
|
|
622
|
+
|
|
623
|
+
# Fills the intersection of `rect` and the buffer with blank cells in
|
|
624
|
+
# `style` — the cell-grid equivalent of clearing a background. Only `bg`
|
|
625
|
+
# shows; the grapheme is a space.
|
|
626
|
+
#
|
|
627
|
+
# _@param_ `rect`
|
|
628
|
+
#
|
|
629
|
+
# _@param_ `style`
|
|
630
|
+
def fill: (Rect rect, ?StyledString::Style style) -> void
|
|
631
|
+
|
|
632
|
+
# Blanks the entire buffer in `style`. A flat pass over every cell — no
|
|
633
|
+
# rect math or nested loops, since it covers the whole grid. Only cells
|
|
634
|
+
# that actually change are marked dirty (and their rows), so a {#flush}
|
|
635
|
+
# after clearing an already-blank buffer emits nothing.
|
|
636
|
+
#
|
|
637
|
+
# _@param_ `style`
|
|
638
|
+
def clear: (?StyledString::Style style) -> void
|
|
639
|
+
|
|
640
|
+
# Marks every cell dirty, so the next {#flush} re-emits the whole grid.
|
|
641
|
+
# Used after a resize and whenever the terminal contents become unknown
|
|
642
|
+
# (e.g. the screen was cleared underneath us).
|
|
643
|
+
def mark_all_dirty: () -> void
|
|
644
|
+
|
|
645
|
+
# Resizes the grid to `size`, reallocating blank cells and marking the
|
|
646
|
+
# whole buffer dirty — after a resize the terminal contents are undefined,
|
|
647
|
+
# so the next flush redraws from scratch.
|
|
648
|
+
#
|
|
649
|
+
# _@param_ `size`
|
|
650
|
+
def resize: (Size size) -> void
|
|
651
|
+
|
|
652
|
+
# Emits the minimal escape sequence that updates a terminal — already
|
|
653
|
+
# matching this buffer as of the previous flush — to the current contents,
|
|
654
|
+
# then clears the dirty flags. Returns `""` when nothing changed.
|
|
655
|
+
#
|
|
656
|
+
# Scans only dirty rows; within a row, consecutive dirty cells form one run
|
|
657
|
+
# (one `TTY::Cursor.move_to` followed by their graphemes), with a running
|
|
658
|
+
# {StyledString::Style#sgr_to} diff so only changed attributes are sent
|
|
659
|
+
# (continuation cells emit nothing). The sequence always ends in the default
|
|
660
|
+
# style ({Ansi::RESET} when needed), the invariant the next flush relies on:
|
|
661
|
+
# the terminal's SGR state is default at flush boundaries.
|
|
662
|
+
#
|
|
663
|
+
# _@return_ — the escape sequence to write to the terminal.
|
|
664
|
+
def flush: () -> String
|
|
665
|
+
|
|
666
|
+
# _@param_ `y` — row.
|
|
667
|
+
#
|
|
668
|
+
# _@return_ — the plain text of row `y` (continuation cells contribute
|
|
669
|
+
# nothing, so wide glyphs read as their single cluster). Intended for
|
|
670
|
+
# tests; see {FakeScreen}.
|
|
671
|
+
def row_text: (Integer y) -> String
|
|
672
|
+
|
|
673
|
+
# _@param_ `y` — row.
|
|
674
|
+
#
|
|
675
|
+
# _@return_ — row `y` rendered to ANSI across its full width — the
|
|
676
|
+
# minimal-SGR encoding of its cells, equivalent to what a component's
|
|
677
|
+
# `set_line` of the whole row would have printed. Intended for tests that
|
|
678
|
+
# assert on styled output (see {FakeScreen}); empty for an out-of-range row.
|
|
679
|
+
def row_ansi: (Integer y) -> String
|
|
680
|
+
|
|
681
|
+
# _@param_ `rect`
|
|
682
|
+
#
|
|
683
|
+
# _@return_ — the plain text of each row within `rect`'s column
|
|
684
|
+
# range, top to bottom. The region equivalent of {#row_text}, for asserting
|
|
685
|
+
# what a component painted into its own rect. Intended for tests.
|
|
686
|
+
def region_text: (Rect rect) -> ::Array[String]
|
|
687
|
+
|
|
688
|
+
# _@param_ `rect`
|
|
689
|
+
#
|
|
690
|
+
# _@return_ — each row within `rect` rendered to ANSI, top to
|
|
691
|
+
# bottom — byte-identical to what a component's per-row `set_line` over
|
|
692
|
+
# that rect emitted. The region equivalent of {#row_ansi}. Intended for
|
|
693
|
+
# tests asserting styled output.
|
|
694
|
+
def region_ansi: (Rect rect) -> ::Array[String]
|
|
695
|
+
|
|
696
|
+
# (Re)allocates a blank grid of `size` with clean dirty state. Callers
|
|
697
|
+
# follow with {#mark_all_dirty} when the terminal doesn't match the new
|
|
698
|
+
# grid — construction and {#resize} both do.
|
|
699
|
+
#
|
|
700
|
+
# _@param_ `size`
|
|
701
|
+
def allocate_grid: (Size size) -> void
|
|
702
|
+
|
|
703
|
+
# Emits the dirty cells of row `y` into `out`, breaking a run at each clean
|
|
704
|
+
# cell, and returns the running style at the end of the row.
|
|
705
|
+
#
|
|
706
|
+
# _@param_ `out` — accumulator.
|
|
707
|
+
#
|
|
708
|
+
# _@param_ `y`
|
|
709
|
+
#
|
|
710
|
+
# _@param_ `style` — style the terminal currently holds.
|
|
711
|
+
def flush_row: (String _out, Integer y, StyledString::Style style) -> StyledString::Style
|
|
712
|
+
|
|
713
|
+
# _@param_ `rect`
|
|
714
|
+
#
|
|
715
|
+
# _@return_ — cells within `rect`, row-major, clamped to
|
|
716
|
+
# the grid (out-of-bounds positions yield a blank cell).
|
|
717
|
+
def region_cells: (Rect rect) -> ::Array[::Array[Cell]]
|
|
718
|
+
|
|
719
|
+
# _@param_ `x` — column
|
|
720
|
+
#
|
|
721
|
+
# _@param_ `y` — row
|
|
722
|
+
#
|
|
723
|
+
# _@return_ — flat-array index for `(x, y)`.
|
|
724
|
+
def index: (Integer x, Integer y) -> Integer
|
|
725
|
+
|
|
726
|
+
# _@param_ `x` — column
|
|
727
|
+
#
|
|
728
|
+
# _@param_ `y` — row
|
|
729
|
+
#
|
|
730
|
+
# _@return_ — true when `(x, y)` falls within the grid.
|
|
731
|
+
def in_bounds?: (Integer x, Integer y) -> bool
|
|
732
|
+
|
|
733
|
+
# Rewrites the cell at `(x, y)` in place, marking it (and its row) dirty
|
|
734
|
+
# only when grapheme or style actually changes. Caller guarantees `(x, y)`
|
|
735
|
+
# is in bounds.
|
|
736
|
+
#
|
|
737
|
+
# _@param_ `x` — column
|
|
738
|
+
#
|
|
739
|
+
# _@param_ `y` — row
|
|
740
|
+
#
|
|
741
|
+
# _@param_ `grapheme` — the new grapheme cluster
|
|
742
|
+
#
|
|
743
|
+
# _@param_ `style` — the new style
|
|
744
|
+
def write_cell: (
|
|
745
|
+
Integer x,
|
|
746
|
+
Integer y,
|
|
747
|
+
String grapheme,
|
|
748
|
+
StyledString::Style style
|
|
749
|
+
) -> void
|
|
750
|
+
|
|
751
|
+
# If `(x, y)` is half of a wide glyph, blanks the *other* half, so a write
|
|
752
|
+
# that lands on either half doesn't strand the remaining one.
|
|
753
|
+
#
|
|
754
|
+
# _@param_ `x` — column
|
|
755
|
+
#
|
|
756
|
+
# _@param_ `y` — row
|
|
757
|
+
def repair_orphans: (Integer x, Integer y) -> void
|
|
758
|
+
|
|
759
|
+
attr_reader width: Integer
|
|
760
|
+
|
|
761
|
+
attr_reader height: Integer
|
|
762
|
+
|
|
763
|
+
# One screen cell: a single grapheme cluster, the {StyledString::Style} it's
|
|
764
|
+
# drawn in, and a dirty flag. Mutable by design (see {Buffer} "Dirty
|
|
765
|
+
# tracking") — the grid rewrites cells in place. A continuation cell (right
|
|
766
|
+
# half of a wide glyph) carries an empty grapheme — see {#continuation?}.
|
|
767
|
+
class Cell
|
|
768
|
+
# _@param_ `grapheme`
|
|
769
|
+
#
|
|
770
|
+
# _@param_ `style`
|
|
771
|
+
def initialize: (String grapheme, StyledString::Style style) -> void
|
|
772
|
+
|
|
773
|
+
# _@return_ — true if this is the right half of a wide glyph, which
|
|
774
|
+
# {Buffer#flush} skips (the glyph to the left already moved the cursor
|
|
775
|
+
# past it).
|
|
776
|
+
def continuation?: () -> bool
|
|
777
|
+
|
|
778
|
+
# Sets the cell's content, flipping {#dirty} on when grapheme or style
|
|
779
|
+
# actually changes (an already-dirty cell stays dirty). Returns the
|
|
780
|
+
# resulting dirty flag, so callers can aggregate row/buffer dirty state in
|
|
781
|
+
# one step. The single mutation path behind {Buffer#set_char} / {#fill} /
|
|
782
|
+
# {#clear}.
|
|
783
|
+
#
|
|
784
|
+
# _@param_ `grapheme`
|
|
785
|
+
#
|
|
786
|
+
# _@param_ `style`
|
|
787
|
+
#
|
|
788
|
+
# _@return_ — {#dirty} after the write.
|
|
789
|
+
def set: (String grapheme, StyledString::Style style) -> bool
|
|
790
|
+
|
|
791
|
+
# Content equality (grapheme + style); the dirty flag is bookkeeping and
|
|
792
|
+
# is deliberately excluded.
|
|
793
|
+
#
|
|
794
|
+
# _@param_ `other`
|
|
795
|
+
def ==: (Object other) -> bool
|
|
796
|
+
|
|
797
|
+
# Read-only: mutate content through {#set} so dirty tracking stays correct.
|
|
798
|
+
#
|
|
799
|
+
# _@return_ — one grapheme cluster, `" "` for blank, or `""` for a
|
|
800
|
+
# wide-glyph continuation.
|
|
801
|
+
attr_reader grapheme: String
|
|
802
|
+
|
|
803
|
+
attr_reader style: StyledString::Style
|
|
804
|
+
|
|
805
|
+
# _@return_ — true if this cell changed since the last {Buffer#flush}.
|
|
806
|
+
# {Buffer} flips it (off as it flushes, on via {Buffer#mark_all_dirty}).
|
|
807
|
+
attr_accessor dirty: bool
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
|
|
529
811
|
# The TTY screen. There is exactly one screen per app.
|
|
530
812
|
#
|
|
531
813
|
# A screen runs the event loop; call {#run_event_loop} to do that.
|
|
@@ -698,23 +980,12 @@ module Tuile
|
|
|
698
980
|
|
|
699
981
|
def self.close: () -> void
|
|
700
982
|
|
|
701
|
-
#
|
|
702
|
-
#
|
|
703
|
-
#
|
|
704
|
-
#
|
|
705
|
-
#
|
|
706
|
-
#
|
|
707
|
-
#
|
|
708
|
-
# Outside repaint, writes go straight to stdout. We deliberately
|
|
709
|
-
# don't raise on a "print outside repaint" — that would be a useful
|
|
710
|
-
# guardrail against components painting outside the repaint loop,
|
|
711
|
-
# but it'd force terminal-housekeeping writes (`Screen#clear`,
|
|
712
|
-
# mouse-tracking start/stop, cursor-show on teardown) to bypass
|
|
713
|
-
# this method entirely and write directly to `$stdout`. {FakeScreen}
|
|
714
|
-
# overrides `print` to capture every byte into its `@prints` array,
|
|
715
|
-
# and tests that exercise `run_event_loop` against a real {Screen}
|
|
716
|
-
# would otherwise leak escape sequences to the test runner's stdout.
|
|
717
|
-
# Keeping `print` as the single sink preserves that override seam.
|
|
983
|
+
# Writes terminal-housekeeping escapes straight to stdout: {#clear},
|
|
984
|
+
# mouse-tracking start/stop, the color-scheme notify toggles, cursor-show
|
|
985
|
+
# on teardown. Component painting does *not* go through here anymore — it
|
|
986
|
+
# writes into {#buffer}, which {#repaint} diffs and {#emit}s. {FakeScreen}
|
|
987
|
+
# overrides this (and {#emit}) to capture into `@prints` instead of the
|
|
988
|
+
# test runner's stdout.
|
|
718
989
|
#
|
|
719
990
|
# _@param_ `args` — stuff to print.
|
|
720
991
|
def print: (*String args) -> void
|
|
@@ -757,14 +1028,16 @@ module Tuile
|
|
|
757
1028
|
# _@return_ — true if focus moved.
|
|
758
1029
|
def cycle_focus: (forward: bool) -> bool
|
|
759
1030
|
|
|
760
|
-
#
|
|
761
|
-
#
|
|
762
|
-
#
|
|
763
|
-
|
|
764
|
-
def collect_subtree: (Component component) -> ::Array[Component]
|
|
1031
|
+
# The escape sequence positioning the hardware cursor for the current focus
|
|
1032
|
+
# state: hidden when nothing owns it, else moved to the focused component's
|
|
1033
|
+
# {Component#cursor_position} and shown. Appended to each frame's flush.
|
|
1034
|
+
def cursor_sequence: () -> String
|
|
765
1035
|
|
|
766
|
-
#
|
|
767
|
-
|
|
1036
|
+
# Writes an assembled frame (escape string) to the terminal. The single
|
|
1037
|
+
# sink for repaint output; {FakeScreen} overrides it to capture instead.
|
|
1038
|
+
#
|
|
1039
|
+
# _@param_ `str`
|
|
1040
|
+
def emit: (String str) -> void
|
|
768
1041
|
|
|
769
1042
|
# Recalculates positions of all windows, and repaints the scene.
|
|
770
1043
|
# Automatically called whenever terminal size changes. Call when the app
|
|
@@ -781,10 +1054,12 @@ module Tuile
|
|
|
781
1054
|
# doesn't trap them.
|
|
782
1055
|
# 2. App-level shortcuts from {#register_global_shortcut}. An entry
|
|
783
1056
|
# registered with `over_popups: true` always fires; one with the
|
|
784
|
-
# default `over_popups: false` fires only when no popup is open
|
|
785
|
-
# (otherwise the popup receives the key normally).
|
|
786
|
-
#
|
|
787
|
-
#
|
|
1057
|
+
# default `over_popups: false` fires only when no modal popup is open
|
|
1058
|
+
# (otherwise the modal popup receives the key normally). A non-modal
|
|
1059
|
+
# overlay doesn't suppress global shortcuts.
|
|
1060
|
+
# 3. {ScreenPane#handle_key}, which captures a matching {#key_shortcut}
|
|
1061
|
+
# in the active scope, then delivers the key to {#focused} and bubbles
|
|
1062
|
+
# it up the focus chain.
|
|
788
1063
|
#
|
|
789
1064
|
# _@param_ `key`
|
|
790
1065
|
#
|
|
@@ -801,6 +1076,10 @@ module Tuile
|
|
|
801
1076
|
# _@return_ — the structural root of the component tree.
|
|
802
1077
|
attr_reader pane: ScreenPane
|
|
803
1078
|
|
|
1079
|
+
# _@return_ — the back buffer components paint into
|
|
1080
|
+
# ({Buffer#set_line} / {Buffer#fill} / {Buffer#set_char}).
|
|
1081
|
+
attr_reader buffer: Buffer
|
|
1082
|
+
|
|
804
1083
|
# Handler invoked when a {StandardError} escapes an event handler inside
|
|
805
1084
|
# the event loop (e.g. a {Component::TextField}'s `on_change` raises).
|
|
806
1085
|
#
|
|
@@ -950,20 +1229,23 @@ module Tuile
|
|
|
950
1229
|
# Only called when the component is attached.
|
|
951
1230
|
def repaint: () -> void
|
|
952
1231
|
|
|
953
|
-
# Called when a character is pressed on the keyboard.
|
|
954
|
-
#
|
|
955
|
-
#
|
|
956
|
-
#
|
|
1232
|
+
# Called when a character is pressed on the keyboard. The default does
|
|
1233
|
+
# nothing and reports the key as unhandled; input components
|
|
1234
|
+
# ({Component::TextField}, {Component::List}, {Component::Button}, …)
|
|
1235
|
+
# override it to act on keys they care about.
|
|
957
1236
|
#
|
|
958
|
-
#
|
|
959
|
-
#
|
|
960
|
-
#
|
|
961
|
-
#
|
|
1237
|
+
# Dispatch is owned by {ScreenPane#handle_key}: a {#key_shortcut} match
|
|
1238
|
+
# anywhere in the active scope is captured first (suppressed while a
|
|
1239
|
+
# cursor-owner is mid-edit), then the key is delivered to {Screen#focused}
|
|
1240
|
+
# and bubbles up its ancestor chain until some component handles it. A
|
|
1241
|
+
# component therefore only ever receives keys when it is on the focus chain
|
|
1242
|
+
# — or when app code hands it a key directly — so it acts on the key alone
|
|
1243
|
+
# and must never gate on its own {#active?} state.
|
|
962
1244
|
#
|
|
963
|
-
# _@param_ `
|
|
1245
|
+
# _@param_ `_key` — a key.
|
|
964
1246
|
#
|
|
965
1247
|
# _@return_ — true if the key was handled, false if not.
|
|
966
|
-
def handle_key: (String
|
|
1248
|
+
def handle_key: (String _key) -> bool
|
|
967
1249
|
|
|
968
1250
|
# _@param_ `key` — keyboard key to look up.
|
|
969
1251
|
#
|
|
@@ -1069,6 +1351,18 @@ module Tuile
|
|
|
1069
1351
|
# topmost popup. Empty by default; override to advertise shortcuts.
|
|
1070
1352
|
def keyboard_hint: () -> String
|
|
1071
1353
|
|
|
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
|
+
|
|
1072
1366
|
# Called whenever the component width changes. Does nothing by default.
|
|
1073
1367
|
def on_width_changed: () -> void
|
|
1074
1368
|
|
|
@@ -1385,9 +1679,9 @@ module Tuile
|
|
|
1385
1679
|
#
|
|
1386
1680
|
# _@param_ `scrollbar` — scrollbar instance, or nil if not shown.
|
|
1387
1681
|
#
|
|
1388
|
-
# _@return_ — paintable
|
|
1389
|
-
#
|
|
1390
|
-
def paintable_line: (Integer index, Integer row_in_viewport, VerticalScrollBar? scrollbar) ->
|
|
1682
|
+
# _@return_ — paintable line exactly `rect.width` columns wide;
|
|
1683
|
+
# highlighted if cursor is here.
|
|
1684
|
+
def paintable_line: (Integer index, Integer row_in_viewport, VerticalScrollBar? scrollbar) -> StyledString
|
|
1391
1685
|
|
|
1392
1686
|
# _@return_ — callback fired when an item is chosen — by pressing
|
|
1393
1687
|
# Enter on the cursor's item, or by left-clicking an item. Called as
|
|
@@ -1574,11 +1868,11 @@ module Tuile
|
|
|
1574
1868
|
def compute_content_size: () -> Size
|
|
1575
1869
|
|
|
1576
1870
|
# Recomputes {@clipped_lines} for the current text and rect width.
|
|
1577
|
-
# Each line is ellipsized to fit
|
|
1578
|
-
# the full width,
|
|
1579
|
-
#
|
|
1580
|
-
#
|
|
1581
|
-
#
|
|
1871
|
+
# Each line is ellipsized to fit and padded with trailing spaces out to
|
|
1872
|
+
# the full width, so {#repaint} is just a lookup + {Buffer#set_line} per
|
|
1873
|
+
# row. {@blank_line} covers rows past the last text line. When {#bg} is
|
|
1874
|
+
# set, every produced line (and the blank row) has the bg applied
|
|
1875
|
+
# uniformly.
|
|
1582
1876
|
def update_clipped_lines: () -> void
|
|
1583
1877
|
|
|
1584
1878
|
# _@param_ `line`
|
|
@@ -1599,10 +1893,20 @@ module Tuile
|
|
|
1599
1893
|
attr_accessor bg: (Color | Symbol | Integer | ::Array[Integer])?
|
|
1600
1894
|
end
|
|
1601
1895
|
|
|
1602
|
-
#
|
|
1603
|
-
# paints nothing — it's a transparent host that handles
|
|
1604
|
-
# ({#open} / {#close} / {#open?}, ESC/q to close)
|
|
1605
|
-
#
|
|
1896
|
+
# An overlay that wraps any {Component} as its content. Popup itself
|
|
1897
|
+
# paints nothing — it's a transparent host that handles its lifecycle
|
|
1898
|
+
# ({#open} / {#close} / {#open?}, ESC/q to close) and auto-sizes to the
|
|
1899
|
+
# wrapped content.
|
|
1900
|
+
#
|
|
1901
|
+
# Modal by default: it centers on the screen, grabs focus, eats keys, and
|
|
1902
|
+
# blocks clicks beneath it. Pass `modal: false` for a non-modal overlay
|
|
1903
|
+
# that floats above the content (still painted on top, still auto-sized)
|
|
1904
|
+
# without taking focus or capturing input — the caller positions it (via
|
|
1905
|
+
# {#rect=}) and drives it from app code. That is the building block for an
|
|
1906
|
+
# autocomplete/slash-command list anchored to a {Component::TextField} or
|
|
1907
|
+
# {Component::TextArea} caret: typing keeps focus (and the cursor) in the
|
|
1908
|
+
# input, an {Component::TextInput#on_change} listener refills the list, and
|
|
1909
|
+
# an {Component::TextInput#on_key} interceptor forwards Up/Down/Enter to it.
|
|
1606
1910
|
#
|
|
1607
1911
|
# The wrapped content fills the popup's full {#rect}; if you want a frame
|
|
1608
1912
|
# and caption, wrap a {Component::Window} (or any subclass — including
|
|
@@ -1623,7 +1927,12 @@ module Tuile
|
|
|
1623
1927
|
include Tuile::Component::HasContent
|
|
1624
1928
|
|
|
1625
1929
|
# _@param_ `content` — initial content; can be set later via {#content=}. When provided here, the popup auto-sizes to fit.
|
|
1626
|
-
|
|
1930
|
+
#
|
|
1931
|
+
# _@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
|
+
def initialize: (?content: Component?, ?modal: bool) -> void
|
|
1933
|
+
|
|
1934
|
+
# _@return_ — whether this popup is modal. See {#initialize}.
|
|
1935
|
+
def modal?: () -> bool
|
|
1627
1936
|
|
|
1628
1937
|
def focusable?: () -> bool
|
|
1629
1938
|
|
|
@@ -1661,10 +1970,21 @@ module Tuile
|
|
|
1661
1970
|
# when the popup is open.
|
|
1662
1971
|
def center: () -> void
|
|
1663
1972
|
|
|
1664
|
-
# _@return_ — max height the popup will grow to fit its content
|
|
1665
|
-
#
|
|
1973
|
+
# _@return_ — max height the popup will grow to fit its content.
|
|
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.
|
|
1666
1977
|
def max_height: () -> Integer
|
|
1667
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
|
+
|
|
1668
1988
|
# Sets the popup's content and auto-sizes the popup to fit.
|
|
1669
1989
|
#
|
|
1670
1990
|
# _@param_ `new_content`
|
|
@@ -1681,6 +2001,10 @@ module Tuile
|
|
|
1681
2001
|
# Hint for the status bar: own "q Close" plus the wrapped content's hint.
|
|
1682
2002
|
def keyboard_hint: () -> String
|
|
1683
2003
|
|
|
2004
|
+
# `q` and ESC close the popup. The popup sits on the focus chain of
|
|
2005
|
+
# whatever it wraps, so the key reaches here by bubbling up from the
|
|
2006
|
+
# focused content after that content declined to handle it.
|
|
2007
|
+
#
|
|
1684
2008
|
# _@param_ `key`
|
|
1685
2009
|
#
|
|
1686
2010
|
# _@return_ — true if the key was handled.
|
|
@@ -1786,13 +2110,6 @@ module Tuile
|
|
|
1786
2110
|
# _@param_ `event`
|
|
1787
2111
|
def handle_mouse: (MouseEvent event) -> void
|
|
1788
2112
|
|
|
1789
|
-
# Called when a character is pressed on the keyboard.
|
|
1790
|
-
#
|
|
1791
|
-
# _@param_ `key` — a key.
|
|
1792
|
-
#
|
|
1793
|
-
# _@return_ — true if the key was handled, false if not.
|
|
1794
|
-
def handle_key: (String key) -> bool
|
|
1795
|
-
|
|
1796
2113
|
def on_focus: () -> void
|
|
1797
2114
|
|
|
1798
2115
|
# Absolute layout. Extend this class, register any children, and
|
|
@@ -1820,9 +2137,6 @@ module Tuile
|
|
|
1820
2137
|
|
|
1821
2138
|
def children: () -> ::Array[Component]
|
|
1822
2139
|
|
|
1823
|
-
# _@param_ `key`
|
|
1824
|
-
def handle_key: (String key) -> bool
|
|
1825
|
-
|
|
1826
2140
|
# _@param_ `event`
|
|
1827
2141
|
def handle_mouse: (MouseEvent event) -> void
|
|
1828
2142
|
|
|
@@ -1866,20 +2180,15 @@ module Tuile
|
|
|
1866
2180
|
# _@param_ `content`
|
|
1867
2181
|
def layout: (Component content) -> void
|
|
1868
2182
|
|
|
1869
|
-
# Paints the window border.
|
|
2183
|
+
# Paints the window border into the {Screen#buffer}. Title is clipped to
|
|
2184
|
+
# the inner width so the box never overflows {#rect}; when the window is
|
|
2185
|
+
# active the whole border is drawn in {Theme#active_border_color}.
|
|
1870
2186
|
def repaint_border: () -> void
|
|
1871
2187
|
|
|
1872
2188
|
# The caption text as it appears in the rendered border, including the
|
|
1873
2189
|
# shortcut prefix when {#key_shortcut} is set.
|
|
1874
2190
|
def frame_caption: () -> String
|
|
1875
2191
|
|
|
1876
|
-
# Builds the border as a single string with embedded cursor-positioning
|
|
1877
|
-
# escapes, mirroring the layout {TTY::Box.frame} used to produce. Title
|
|
1878
|
-
# is clipped to fit the inner width so the box never overflows {#rect}.
|
|
1879
|
-
#
|
|
1880
|
-
# _@param_ `caption`
|
|
1881
|
-
def build_frame: (String caption) -> String
|
|
1882
|
-
|
|
1883
2192
|
# Recomputes the window's natural size: content's natural size (or the
|
|
1884
2193
|
# caption, whichever is wider) plus the 2-character border. The footer
|
|
1885
2194
|
# is deliberately excluded — see {#on_child_content_size_changed}. A
|
|
@@ -2394,11 +2703,10 @@ module Tuile
|
|
|
2394
2703
|
#
|
|
2395
2704
|
# _@param_ `scrollbar`
|
|
2396
2705
|
#
|
|
2397
|
-
# _@return_ — paintable
|
|
2398
|
-
#
|
|
2399
|
-
#
|
|
2400
|
-
|
|
2401
|
-
def paintable_line: (Integer index, Integer row_in_viewport, VerticalScrollBar? scrollbar) -> String
|
|
2706
|
+
# _@return_ — paintable line exactly `rect.width` columns wide.
|
|
2707
|
+
# Body lines come pre-padded from {#rewrap}, so this reduces to a lookup
|
|
2708
|
+
# plus a concat of the scrollbar glyph when one is present.
|
|
2709
|
+
def paintable_line: (Integer index, Integer row_in_viewport, VerticalScrollBar? scrollbar) -> StyledString
|
|
2402
2710
|
|
|
2403
2711
|
# _@return_ — index of the first visible physical line.
|
|
2404
2712
|
attr_accessor top_line: Integer
|
|
@@ -2564,6 +2872,18 @@ module Tuile
|
|
|
2564
2872
|
# _@param_ `caption`
|
|
2565
2873
|
def initialize: (?String caption) -> void
|
|
2566
2874
|
|
|
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
|
+
|
|
2567
2887
|
# Appends given line to the log. Can be called from any thread. Does nothing if nil is passed in.
|
|
2568
2888
|
#
|
|
2569
2889
|
# _@param_ `string` — the line (or multiple lines) to log.
|
|
@@ -2682,22 +3002,24 @@ module Tuile
|
|
|
2682
3002
|
|
|
2683
3003
|
def tab_stop?: () -> bool
|
|
2684
3004
|
|
|
2685
|
-
# Handles a key.
|
|
2686
|
-
#
|
|
2687
|
-
#
|
|
3005
|
+
# Handles a key. An {#on_key} interceptor (if set) gets first refusal —
|
|
3006
|
+
# a truthy return consumes the key — otherwise it delegates to
|
|
3007
|
+
# {#handle_text_input_key}. Dispatch ({ScreenPane#handle_key}) only routes
|
|
3008
|
+
# keys here when this input is on the focus chain, so there is no
|
|
3009
|
+
# {#active?} gate.
|
|
2688
3010
|
#
|
|
2689
3011
|
# _@param_ `key`
|
|
2690
3012
|
def handle_key: (String key) -> bool
|
|
2691
3013
|
|
|
2692
3014
|
# Renders `text` on the field's background well, looked up from the
|
|
2693
|
-
# current {Screen#theme} at paint time: {Theme#
|
|
2694
|
-
# input is on the active (focus) chain, {Theme#
|
|
3015
|
+
# current {Screen#theme} at paint time: {Theme#active_bg_color} when this
|
|
3016
|
+
# input is on the active (focus) chain, {Theme#input_bg_color} otherwise —
|
|
2695
3017
|
# visibly a field either way, distinctly highlighted when active.
|
|
2696
3018
|
#
|
|
2697
3019
|
# _@param_ `text`
|
|
2698
3020
|
#
|
|
2699
|
-
# _@return_ —
|
|
2700
|
-
def background: (String text) ->
|
|
3021
|
+
# _@return_ — text on the field's background well.
|
|
3022
|
+
def background: (String text) -> StyledString
|
|
2701
3023
|
|
|
2702
3024
|
# Input filter for {#text=}. Subclasses override to truncate or reject
|
|
2703
3025
|
# invalid input. Default coerces to String.
|
|
@@ -2760,6 +3082,21 @@ module Tuile
|
|
|
2760
3082
|
# _@return_ — one-arg callable, or nil.
|
|
2761
3083
|
attr_accessor on_change: (Proc | Method)?
|
|
2762
3084
|
|
|
3085
|
+
# Optional interceptor consulted before the input's own key handling.
|
|
3086
|
+
# Receives the pressed key; return a truthy value to consume it (the
|
|
3087
|
+
# input then ignores that key), falsy to let normal editing proceed.
|
|
3088
|
+
#
|
|
3089
|
+
# The keyboard analog of {#on_change}: it lets app code layer behavior
|
|
3090
|
+
# onto an input without subclassing. The motivating case is an
|
|
3091
|
+
# autocomplete / slash-command overlay (a non-modal {Component::Popup}):
|
|
3092
|
+
# while it is open the interceptor claims Up/Down/Enter/ESC and forwards
|
|
3093
|
+
# them to the overlay's list, but lets ordinary characters fall through
|
|
3094
|
+
# so typing keeps editing the field (and {#on_change} keeps refilling the
|
|
3095
|
+
# list).
|
|
3096
|
+
#
|
|
3097
|
+
# _@return_ — one-arg callable, or nil.
|
|
3098
|
+
attr_accessor on_key: (Proc | Method)?
|
|
3099
|
+
|
|
2763
3100
|
# Callback fired when ESC is pressed. Defaults to a closure that clears
|
|
2764
3101
|
# focus (`screen.focused = nil`) so ESC visibly cancels text entry instead
|
|
2765
3102
|
# of bubbling to the parent — and, in particular, instead of reaching the
|
|
@@ -2774,11 +3111,6 @@ module Tuile
|
|
|
2774
3111
|
# provide a protected `layout(content)` method which repositions the
|
|
2775
3112
|
# content component; the mixin manages `@content` itself.
|
|
2776
3113
|
module HasContent
|
|
2777
|
-
# _@param_ `key` — a key.
|
|
2778
|
-
#
|
|
2779
|
-
# _@return_ — true if the key was handled, false if not.
|
|
2780
|
-
def handle_key: (String key) -> bool
|
|
2781
|
-
|
|
2782
3114
|
# _@param_ `event`
|
|
2783
3115
|
def handle_mouse: (MouseEvent event) -> void
|
|
2784
3116
|
|
|
@@ -2828,6 +3160,11 @@ module Tuile
|
|
|
2828
3160
|
# _@param_ `options` — pairs of keyboard key and option caption. No Rainbow formatting must be used.
|
|
2829
3161
|
def initialize: (String caption, ::Array[[String, String]] options) ?{ (String key) -> void } -> void
|
|
2830
3162
|
|
|
3163
|
+
# Handles an option-key press. Reached by bubbling: the inner {List}
|
|
3164
|
+
# (the focused component) sees the key first and handles cursor/Enter
|
|
3165
|
+
# picks; anything it declines bubbles up here, where a key matching an
|
|
3166
|
+
# option's `key` picks that option.
|
|
3167
|
+
#
|
|
2831
3168
|
# _@param_ `key`
|
|
2832
3169
|
def handle_key: (String key) -> bool
|
|
2833
3170
|
|
|
@@ -3148,6 +3485,13 @@ module Tuile
|
|
|
3148
3485
|
# _@param_ `args`
|
|
3149
3486
|
def print: (*String args) -> void
|
|
3150
3487
|
|
|
3488
|
+
# Captures the assembled repaint frame instead of writing to the test
|
|
3489
|
+
# runner's TTY. Lands in {#prints} so cursor/sync escapes can be asserted;
|
|
3490
|
+
# painted content is read from {#buffer}.
|
|
3491
|
+
#
|
|
3492
|
+
# _@param_ `str`
|
|
3493
|
+
def emit: (String str) -> void
|
|
3494
|
+
|
|
3151
3495
|
# _@param_ `component` — the component to check.
|
|
3152
3496
|
def invalidated?: (Component component) -> bool
|
|
3153
3497
|
|
|
@@ -3158,7 +3502,10 @@ module Tuile
|
|
|
3158
3502
|
# steal its input) and pin the deterministic default.
|
|
3159
3503
|
def detect_scheme: () -> Symbol
|
|
3160
3504
|
|
|
3161
|
-
# _@return_ — whatever {#print}
|
|
3505
|
+
# _@return_ — whatever {#print} / {#emit} produced so far.
|
|
3506
|
+
# Component painting lands in {#buffer}, not here — assert on
|
|
3507
|
+
# {Buffer#row_text} / {Buffer#row_ansi} / {Buffer#cell} for content, and
|
|
3508
|
+
# on `prints` for cursor and housekeeping escapes.
|
|
3162
3509
|
attr_reader prints: ::Array[String]
|
|
3163
3510
|
end
|
|
3164
3511
|
|
|
@@ -3236,7 +3583,11 @@ module Tuile
|
|
|
3236
3583
|
# status bar last.
|
|
3237
3584
|
def children: () -> ::Array[Component]
|
|
3238
3585
|
|
|
3239
|
-
# Adds a popup
|
|
3586
|
+
# Adds a popup and invalidates it for repaint. A modal popup is centered
|
|
3587
|
+
# and grabs focus; a non-modal overlay ({Component::Popup#modal?} false) is
|
|
3588
|
+
# left wherever the caller positions it and does *not* take focus, so the
|
|
3589
|
+
# component that was focused keeps the cursor and keeps receiving keys —
|
|
3590
|
+
# the overlay floats above the content, driven from app code.
|
|
3240
3591
|
#
|
|
3241
3592
|
# _@param_ `window`
|
|
3242
3593
|
def add_popup: (Component::Popup window) -> void
|
|
@@ -3253,26 +3604,52 @@ module Tuile
|
|
|
3253
3604
|
# _@return_ — true if this pane currently hosts the popup.
|
|
3254
3605
|
def has_popup?: (Component window) -> bool
|
|
3255
3606
|
|
|
3607
|
+
# _@return_ — the topmost *modal* popup, or nil when
|
|
3608
|
+
# only non-modal overlays (or no popups) are open. This is the "modal
|
|
3609
|
+
# owner": the popup that scopes key dispatch, blocks mouse clicks, owns
|
|
3610
|
+
# the status bar, and confines Tab cycling. Non-modal overlays are
|
|
3611
|
+
# excluded — they float above the content without capturing input.
|
|
3612
|
+
def modal_popup: () -> Component::Popup?
|
|
3613
|
+
|
|
3256
3614
|
# Re-lays out children whenever the pane's own rect changes.
|
|
3257
3615
|
#
|
|
3258
3616
|
# _@param_ `new_rect`
|
|
3259
3617
|
def rect=: (Rect new_rect) -> void
|
|
3260
3618
|
|
|
3261
3619
|
# Lays out content (full pane minus the bottom row) and the status bar
|
|
3262
|
-
# (bottom row).
|
|
3620
|
+
# (bottom row). Modal popups self-recenter via {Component::Popup#center};
|
|
3621
|
+
# non-modal overlays keep the position their owner assigned.
|
|
3263
3622
|
def layout: () -> void
|
|
3264
3623
|
|
|
3265
3624
|
# Pane paints nothing itself; its children paint over the entire rect.
|
|
3266
3625
|
def repaint: () -> void
|
|
3267
3626
|
|
|
3268
|
-
#
|
|
3269
|
-
# when
|
|
3627
|
+
# Dispatches a key in two phases, both scoped to the topmost *modal* popup
|
|
3628
|
+
# (when one is open) or else the tiled {#content}. Non-modal overlays are
|
|
3629
|
+
# never the scope: focus stays in the content beneath them, and the overlay
|
|
3630
|
+
# is driven by app code (which forwards keys to it explicitly), so it
|
|
3631
|
+
# doesn't appear in this path at all.
|
|
3632
|
+
#
|
|
3633
|
+
# 1. *Capture* — a {Component#key_shortcut} match anywhere in the scope
|
|
3634
|
+
# focuses that component and consumes the key. Suppressed while a
|
|
3635
|
+
# cursor-owner ({Screen#cursor_position}) is mid-edit, so typing into a
|
|
3636
|
+
# {Component::TextField} isn't hijacked by a sibling's shortcut.
|
|
3637
|
+
# 2. *Delivery* — the key is handed to {Screen#focused} and bubbles up its
|
|
3638
|
+
# ancestor chain to the scope root; the first component to return true
|
|
3639
|
+
# wins. Focus that is nil or sits outside the scope receives nothing,
|
|
3640
|
+
# which is what keeps an open modal popup modal.
|
|
3641
|
+
#
|
|
3642
|
+
# _@param_ `key`
|
|
3643
|
+
#
|
|
3644
|
+
# _@return_ — true if the key was handled.
|
|
3270
3645
|
def handle_key: (String key) -> bool
|
|
3271
3646
|
|
|
3272
3647
|
# Mouse events check popups in reverse stacking order (topmost first), and
|
|
3273
|
-
# fall through to content only when no popup is hit *and*
|
|
3274
|
-
#
|
|
3275
|
-
#
|
|
3648
|
+
# fall through to content only when no popup is hit *and* no modal popup is
|
|
3649
|
+
# open. This preserves modal click-blocking — an open modal eats clicks
|
|
3650
|
+
# even outside its rect — while a non-modal overlay blocks nothing: clicks
|
|
3651
|
+
# inside it route to it (e.g. click-to-select), clicks elsewhere reach the
|
|
3652
|
+
# content beneath.
|
|
3276
3653
|
#
|
|
3277
3654
|
# _@param_ `event`
|
|
3278
3655
|
def handle_mouse: (MouseEvent event) -> void
|
|
@@ -3280,7 +3657,7 @@ module Tuile
|
|
|
3280
3657
|
# Focus repair when a child detaches. Default {Component#on_child_removed}
|
|
3281
3658
|
# would refocus to `self` (the pane), which isn't a useful focus target.
|
|
3282
3659
|
# Instead, route focus to the first interactable widget in the now-topmost
|
|
3283
|
-
# popup; falling back to the focus snapshotted when this popup was opened
|
|
3660
|
+
# modal popup; falling back to the focus snapshotted when this popup was opened
|
|
3284
3661
|
# (if still attached and still focusable); then to the first interactable
|
|
3285
3662
|
# widget in {#content}; then to {#content} itself; then nil.
|
|
3286
3663
|
#
|
|
@@ -3292,6 +3669,19 @@ module Tuile
|
|
|
3292
3669
|
# _@param_ `child`
|
|
3293
3670
|
def on_child_removed: (Component child) -> void
|
|
3294
3671
|
|
|
3672
|
+
# Delivers `key` to {Screen#focused} and bubbles it up the ancestor chain,
|
|
3673
|
+
# stopping at (and including) `scope`. Delivers to no one — returning false
|
|
3674
|
+
# — when focus is nil or sits outside `scope`; the latter is what makes an
|
|
3675
|
+
# open popup modal, since focus is always inside it and content beneath
|
|
3676
|
+
# never receives keys.
|
|
3677
|
+
#
|
|
3678
|
+
# _@param_ `key`
|
|
3679
|
+
#
|
|
3680
|
+
# _@param_ `scope` — the modal scope root (topmost popup or content).
|
|
3681
|
+
#
|
|
3682
|
+
# _@return_ — true if some component on the chain handled the key.
|
|
3683
|
+
def bubble_key: (String key, Component scope) -> bool
|
|
3684
|
+
|
|
3295
3685
|
# First {Component#tab_stop?} in `root`'s subtree (pre-order), falling
|
|
3296
3686
|
# back to `root` itself when the subtree has no tab stops. Returns `nil`
|
|
3297
3687
|
# if `root` is `nil`.
|
|
@@ -3302,8 +3692,9 @@ module Tuile
|
|
|
3302
3692
|
# _@return_ — the tiled content component.
|
|
3303
3693
|
attr_accessor content: Component?
|
|
3304
3694
|
|
|
3305
|
-
# _@return_ —
|
|
3306
|
-
# topmost.
|
|
3695
|
+
# _@return_ — overlay popups in stacking order; last is
|
|
3696
|
+
# topmost. Holds both modal popups and non-modal overlays
|
|
3697
|
+
# ({Component::Popup#modal?}). The array must not be mutated by callers.
|
|
3307
3698
|
attr_reader popups: ::Array[Component]
|
|
3308
3699
|
|
|
3309
3700
|
# _@return_ — the bottom status bar.
|
|
@@ -3496,19 +3887,6 @@ module Tuile
|
|
|
3496
3887
|
# _@param_ `spans`
|
|
3497
3888
|
def normalize: (::Array[Span] spans) -> ::Array[Span]
|
|
3498
3889
|
|
|
3499
|
-
# _@param_ `from`
|
|
3500
|
-
#
|
|
3501
|
-
# _@param_ `to`
|
|
3502
|
-
def sgr_diff: (Style from, Style to) -> String
|
|
3503
|
-
|
|
3504
|
-
# _@param_ `color`
|
|
3505
|
-
#
|
|
3506
|
-
# _@param_ `target` — `:fg` or `:bg`.
|
|
3507
|
-
#
|
|
3508
|
-
# _@return_ — SGR codes; `[39]` / `[49]` for the "default" reset
|
|
3509
|
-
# when `color` is `nil`, otherwise delegated to {Color#sgr_codes}.
|
|
3510
|
-
def color_codes: (Color? color, target: Symbol) -> ::Array[Integer]
|
|
3511
|
-
|
|
3512
3890
|
# _@param_ `start_or_range`
|
|
3513
3891
|
#
|
|
3514
3892
|
# _@param_ `len`
|
|
@@ -3608,6 +3986,27 @@ module Tuile
|
|
|
3608
3986
|
# _@param_ `overrides`
|
|
3609
3987
|
def merge: (**::Hash[Symbol, Object] overrides) -> Style
|
|
3610
3988
|
|
|
3989
|
+
# Minimal SGR escape that transitions a terminal already showing `self`
|
|
3990
|
+
# into `other`: only the attributes that differ are emitted. Returns
|
|
3991
|
+
# `""` when the styles are identical (nothing to do), and {Ansi::RESET}
|
|
3992
|
+
# (`\e[0m`, one code) when `other` is the default style — shorter than
|
|
3993
|
+
# turning each attribute off individually.
|
|
3994
|
+
#
|
|
3995
|
+
# Shared by {StyledString#to_ansi} (diffing span-to-span from the default
|
|
3996
|
+
# style) and {Buffer}'s flush (diffing cell-to-cell against the style the
|
|
3997
|
+
# terminal currently holds), so both emit identical minimal sequences.
|
|
3998
|
+
#
|
|
3999
|
+
# _@param_ `other` — the style to transition to.
|
|
4000
|
+
def sgr_to: (Style other) -> String
|
|
4001
|
+
|
|
4002
|
+
# _@param_ `color`
|
|
4003
|
+
#
|
|
4004
|
+
# _@param_ `target` — either `:fg` or `:bg`.
|
|
4005
|
+
#
|
|
4006
|
+
# _@return_ — SGR codes; `[39]` / `[49]` for the "default"
|
|
4007
|
+
# reset when `color` is `nil`, otherwise delegated to {Color#sgr_codes}.
|
|
4008
|
+
def color_codes: (Color? color, target: Symbol) -> ::Array[Integer]
|
|
4009
|
+
|
|
3611
4010
|
attr_reader fg: Color?
|
|
3612
4011
|
|
|
3613
4012
|
attr_reader bg: Color?
|