tuile 0.7.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 +26 -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/examples/sampler.rb +109 -0
- data/lib/tuile/ansi.rb +16 -0
- data/lib/tuile/buffer.rb +481 -0
- data/lib/tuile/component/button.rb +3 -14
- data/lib/tuile/component/has_content.rb +0 -6
- data/lib/tuile/component/info_window.rb +4 -2
- data/lib/tuile/component/label.rb +15 -23
- data/lib/tuile/component/layout.rb +0 -21
- data/lib/tuile/component/list.rb +10 -37
- data/lib/tuile/component/log_window.rb +6 -5
- data/lib/tuile/component/picker_window.rb +4 -2
- data/lib/tuile/component/popup.rb +85 -55
- 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 -30
- data/lib/tuile/component/window.rb +77 -112
- data/lib/tuile/component.rb +16 -71
- data/lib/tuile/event_queue.rb +31 -10
- data/lib/tuile/fake_event_queue.rb +21 -5
- data/lib/tuile/fake_screen.rb +14 -1
- data/lib/tuile/fraction.rb +46 -0
- data/lib/tuile/screen.rb +98 -105
- data/lib/tuile/screen_pane.rb +82 -19
- data/lib/tuile/styled_string.rb +40 -30
- data/lib/tuile/version.rb +1 -1
- data/sig/tuile.rbs +653 -282
- metadata +13 -3
- data/lib/tuile/sizing.rb +0 -59
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,344 @@ 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.
|
|
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
|
+
#
|
|
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.
|
|
583
|
+
class Buffer
|
|
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
|
|
594
|
+
|
|
595
|
+
# _@param_ `size` — grid dimensions in columns × rows.
|
|
596
|
+
def initialize: (Size size) -> void
|
|
597
|
+
|
|
598
|
+
# _@return_ — grid dimensions.
|
|
599
|
+
def size: () -> Size
|
|
600
|
+
|
|
601
|
+
# _@param_ `x` — column.
|
|
602
|
+
#
|
|
603
|
+
# _@param_ `y` — row.
|
|
604
|
+
#
|
|
605
|
+
# _@return_ — the live cell at `(x, y)` (do not mutate — paint via
|
|
606
|
+
# {#set_char} / {#set_line} so dirty tracking stays correct), or nil when
|
|
607
|
+
# out of bounds.
|
|
608
|
+
def cell: (Integer x, Integer y) -> Cell?
|
|
609
|
+
|
|
610
|
+
# _@return_ — true if any cell has changed since the last {#flush}.
|
|
611
|
+
def dirty?: () -> bool
|
|
612
|
+
|
|
613
|
+
# Writes one grapheme cluster at `(x, y)`. A 2-column glyph also writes a
|
|
614
|
+
# continuation cell at `(x + 1, y)`; a wide glyph that would overflow the
|
|
615
|
+
# last column is replaced by a blank (terminals can't render a half-clipped
|
|
616
|
+
# wide glyph). Zero-width input (a lone combining mark) is ignored — it has
|
|
617
|
+
# no cell of its own. Out-of-bounds writes are dropped.
|
|
618
|
+
#
|
|
619
|
+
# _@param_ `x` — column.
|
|
620
|
+
#
|
|
621
|
+
# _@param_ `y` — row.
|
|
622
|
+
#
|
|
623
|
+
# _@param_ `grapheme` — one grapheme cluster.
|
|
624
|
+
#
|
|
625
|
+
# _@param_ `style`
|
|
626
|
+
def set_char: (
|
|
627
|
+
Integer x,
|
|
628
|
+
Integer y,
|
|
629
|
+
String grapheme,
|
|
630
|
+
?StyledString::Style style
|
|
631
|
+
) -> void
|
|
632
|
+
|
|
633
|
+
# Writes a {StyledString} starting at `(x, y)`, advancing by each grapheme's
|
|
634
|
+
# display width and clipping at the right edge. The workhorse that replaces
|
|
635
|
+
# the old `screen.print(TTY::Cursor.move_to(x, y), styled.to_ansi)` per-row
|
|
636
|
+
# paint. Newlines in the string are not handled — pass one physical line.
|
|
637
|
+
#
|
|
638
|
+
# _@param_ `x` — starting column.
|
|
639
|
+
#
|
|
640
|
+
# _@param_ `y` — row.
|
|
641
|
+
#
|
|
642
|
+
# _@param_ `styled`
|
|
643
|
+
def set_line: (Integer x, Integer y, StyledString styled) -> void
|
|
644
|
+
|
|
645
|
+
# Fills the intersection of `rect` and the buffer with blank cells in
|
|
646
|
+
# `style` — the cell-grid equivalent of clearing a background. Only `bg`
|
|
647
|
+
# shows; the grapheme is a space.
|
|
648
|
+
#
|
|
649
|
+
# _@param_ `rect`
|
|
650
|
+
#
|
|
651
|
+
# _@param_ `style`
|
|
652
|
+
def fill: (Rect rect, ?StyledString::Style style) -> void
|
|
653
|
+
|
|
654
|
+
# Blanks the entire buffer in `style`. A flat pass over every cell — no
|
|
655
|
+
# rect math or nested loops, since it covers the whole grid. Only cells
|
|
656
|
+
# that actually change are marked dirty (and their rows), so a {#flush}
|
|
657
|
+
# after clearing an already-blank buffer emits nothing.
|
|
658
|
+
#
|
|
659
|
+
# _@param_ `style`
|
|
660
|
+
def clear: (?StyledString::Style style) -> void
|
|
661
|
+
|
|
662
|
+
# Marks every cell dirty, so the next {#flush} re-emits the whole grid.
|
|
663
|
+
# Used after a resize and whenever the terminal contents become unknown
|
|
664
|
+
# (e.g. the screen was cleared underneath us).
|
|
665
|
+
def mark_all_dirty: () -> void
|
|
666
|
+
|
|
667
|
+
# Resizes the grid to `size`, reallocating blank cells and marking the
|
|
668
|
+
# whole buffer dirty — after a resize the terminal contents are undefined,
|
|
669
|
+
# so the next flush redraws from scratch.
|
|
670
|
+
#
|
|
671
|
+
# _@param_ `size`
|
|
672
|
+
def resize: (Size size) -> void
|
|
673
|
+
|
|
674
|
+
# Emits the minimal escape sequence that updates a terminal — already
|
|
675
|
+
# matching this buffer as of the previous flush — to the current contents,
|
|
676
|
+
# then clears the dirty flags. Returns `""` when nothing changed.
|
|
677
|
+
#
|
|
678
|
+
# Scans only dirty rows; within a row, consecutive dirty cells form one run
|
|
679
|
+
# (one `TTY::Cursor.move_to` followed by their graphemes), with a running
|
|
680
|
+
# {StyledString::Style#sgr_to} diff so only changed attributes are sent
|
|
681
|
+
# (continuation cells emit nothing). The sequence always ends in the default
|
|
682
|
+
# style ({Ansi::RESET} when needed), the invariant the next flush relies on:
|
|
683
|
+
# the terminal's SGR state is default at flush boundaries.
|
|
684
|
+
#
|
|
685
|
+
# _@return_ — the escape sequence to write to the terminal.
|
|
686
|
+
def flush: () -> String
|
|
687
|
+
|
|
688
|
+
# _@param_ `y` — row.
|
|
689
|
+
#
|
|
690
|
+
# _@return_ — the plain text of row `y` (continuation cells contribute
|
|
691
|
+
# nothing, so wide glyphs read as their single cluster). Intended for
|
|
692
|
+
# tests; see {FakeScreen}.
|
|
693
|
+
def row_text: (Integer y) -> String
|
|
694
|
+
|
|
695
|
+
# _@param_ `y` — row.
|
|
696
|
+
#
|
|
697
|
+
# _@return_ — row `y` rendered to ANSI across its full width — the
|
|
698
|
+
# minimal-SGR encoding of its cells, equivalent to what a component's
|
|
699
|
+
# `set_line` of the whole row would have printed. Intended for tests that
|
|
700
|
+
# assert on styled output (see {FakeScreen}); empty for an out-of-range row.
|
|
701
|
+
def row_ansi: (Integer y) -> String
|
|
702
|
+
|
|
703
|
+
# _@param_ `rect`
|
|
704
|
+
#
|
|
705
|
+
# _@return_ — the plain text of each row within `rect`'s column
|
|
706
|
+
# range, top to bottom. The region equivalent of {#row_text}, for asserting
|
|
707
|
+
# what a component painted into its own rect. Intended for tests.
|
|
708
|
+
def region_text: (Rect rect) -> ::Array[String]
|
|
709
|
+
|
|
710
|
+
# _@param_ `rect`
|
|
711
|
+
#
|
|
712
|
+
# _@return_ — each row within `rect` rendered to ANSI, top to
|
|
713
|
+
# bottom — byte-identical to what a component's per-row `set_line` over
|
|
714
|
+
# that rect emitted. The region equivalent of {#row_ansi}. Intended for
|
|
715
|
+
# tests asserting styled output.
|
|
716
|
+
def region_ansi: (Rect rect) -> ::Array[String]
|
|
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
|
+
|
|
741
|
+
# (Re)allocates a blank grid of `size` with clean dirty state. Callers
|
|
742
|
+
# follow with {#mark_all_dirty} when the terminal doesn't match the new
|
|
743
|
+
# grid — construction and {#resize} both do.
|
|
744
|
+
#
|
|
745
|
+
# _@param_ `size`
|
|
746
|
+
def allocate_grid: (Size size) -> void
|
|
747
|
+
|
|
748
|
+
# Emits the dirty cells of row `y` into `out`, breaking a run at each clean
|
|
749
|
+
# cell, and returns the running style at the end of the row.
|
|
750
|
+
#
|
|
751
|
+
# _@param_ `out` — accumulator.
|
|
752
|
+
#
|
|
753
|
+
# _@param_ `y`
|
|
754
|
+
#
|
|
755
|
+
# _@param_ `style` — style the terminal currently holds.
|
|
756
|
+
def flush_row: (String _out, Integer y, StyledString::Style style) -> StyledString::Style
|
|
757
|
+
|
|
758
|
+
# _@param_ `rect`
|
|
759
|
+
#
|
|
760
|
+
# _@return_ — cells within `rect`, row-major, clamped to
|
|
761
|
+
# the grid (out-of-bounds positions yield a blank cell).
|
|
762
|
+
def region_cells: (Rect rect) -> ::Array[::Array[Cell]]
|
|
763
|
+
|
|
764
|
+
# _@param_ `x` — column
|
|
765
|
+
#
|
|
766
|
+
# _@param_ `y` — row
|
|
767
|
+
#
|
|
768
|
+
# _@return_ — flat-array index for `(x, y)`.
|
|
769
|
+
def index: (Integer x, Integer y) -> Integer
|
|
770
|
+
|
|
771
|
+
# _@param_ `x` — column
|
|
772
|
+
#
|
|
773
|
+
# _@param_ `y` — row
|
|
774
|
+
#
|
|
775
|
+
# _@return_ — true when `(x, y)` falls within the grid.
|
|
776
|
+
def in_bounds?: (Integer x, Integer y) -> bool
|
|
777
|
+
|
|
778
|
+
# Rewrites the cell at `(x, y)` in place, marking it (and its row) dirty
|
|
779
|
+
# only when grapheme or style actually changes. Caller guarantees `(x, y)`
|
|
780
|
+
# is in bounds.
|
|
781
|
+
#
|
|
782
|
+
# _@param_ `x` — column
|
|
783
|
+
#
|
|
784
|
+
# _@param_ `y` — row
|
|
785
|
+
#
|
|
786
|
+
# _@param_ `grapheme` — the new grapheme cluster
|
|
787
|
+
#
|
|
788
|
+
# _@param_ `style` — the new style
|
|
789
|
+
def write_cell: (
|
|
790
|
+
Integer x,
|
|
791
|
+
Integer y,
|
|
792
|
+
String grapheme,
|
|
793
|
+
StyledString::Style style
|
|
794
|
+
) -> void
|
|
795
|
+
|
|
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.
|
|
799
|
+
#
|
|
800
|
+
# _@param_ `x` — column
|
|
801
|
+
#
|
|
802
|
+
# _@param_ `y` — row
|
|
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
|
|
816
|
+
|
|
817
|
+
attr_reader width: Integer
|
|
818
|
+
|
|
819
|
+
attr_reader height: Integer
|
|
820
|
+
|
|
821
|
+
# One screen cell: a single grapheme cluster, the {StyledString::Style} it's
|
|
822
|
+
# drawn in, and a dirty flag. Mutable by design (see {Buffer} "Dirty
|
|
823
|
+
# tracking") — the grid rewrites cells in place. A continuation cell (right
|
|
824
|
+
# half of a wide glyph) carries an empty grapheme — see {#continuation?}.
|
|
825
|
+
class Cell
|
|
826
|
+
# _@param_ `grapheme`
|
|
827
|
+
#
|
|
828
|
+
# _@param_ `style`
|
|
829
|
+
def initialize: (String grapheme, StyledString::Style style) -> void
|
|
830
|
+
|
|
831
|
+
# _@return_ — true if this is the right half of a wide glyph, which
|
|
832
|
+
# {Buffer#flush} skips (the glyph to the left already moved the cursor
|
|
833
|
+
# past it).
|
|
834
|
+
def continuation?: () -> bool
|
|
835
|
+
|
|
836
|
+
# Sets the cell's content, flipping {#dirty} on when grapheme or style
|
|
837
|
+
# actually changes (an already-dirty cell stays dirty). Returns the
|
|
838
|
+
# resulting dirty flag, so callers can aggregate row/buffer dirty state in
|
|
839
|
+
# one step. The single mutation path behind {Buffer#set_char} / {#fill} /
|
|
840
|
+
# {#clear}.
|
|
841
|
+
#
|
|
842
|
+
# _@param_ `grapheme`
|
|
843
|
+
#
|
|
844
|
+
# _@param_ `style`
|
|
845
|
+
#
|
|
846
|
+
# _@return_ — {#dirty} after the write.
|
|
847
|
+
def set: (String grapheme, StyledString::Style style) -> bool
|
|
848
|
+
|
|
849
|
+
# Content equality (grapheme + style); the dirty flag is bookkeeping and
|
|
850
|
+
# is deliberately excluded.
|
|
851
|
+
#
|
|
852
|
+
# _@param_ `other`
|
|
853
|
+
def ==: (Object other) -> bool
|
|
854
|
+
|
|
855
|
+
# Read-only: mutate content through {#set} so dirty tracking stays correct.
|
|
856
|
+
#
|
|
857
|
+
# _@return_ — one grapheme cluster, `" "` for blank, or `""` for a
|
|
858
|
+
# wide-glyph continuation.
|
|
859
|
+
attr_reader grapheme: String
|
|
860
|
+
|
|
861
|
+
attr_reader style: StyledString::Style
|
|
862
|
+
|
|
863
|
+
# _@return_ — true if this cell changed since the last {Buffer#flush}.
|
|
864
|
+
# {Buffer} flips it (off as it flushes, on via {Buffer#mark_all_dirty}).
|
|
865
|
+
attr_accessor dirty: bool
|
|
866
|
+
end
|
|
867
|
+
end
|
|
868
|
+
|
|
529
869
|
# The TTY screen. There is exactly one screen per app.
|
|
530
870
|
#
|
|
531
871
|
# A screen runs the event loop; call {#run_event_loop} to do that.
|
|
@@ -698,23 +1038,12 @@ module Tuile
|
|
|
698
1038
|
|
|
699
1039
|
def self.close: () -> void
|
|
700
1040
|
|
|
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.
|
|
1041
|
+
# Writes terminal-housekeeping escapes straight to stdout: {#clear},
|
|
1042
|
+
# mouse-tracking start/stop, the color-scheme notify toggles, cursor-show
|
|
1043
|
+
# on teardown. Component painting does *not* go through here anymore — it
|
|
1044
|
+
# writes into {#buffer}, which {#repaint} diffs and {#emit}s. {FakeScreen}
|
|
1045
|
+
# overrides this (and {#emit}) to capture into `@prints` instead of the
|
|
1046
|
+
# test runner's stdout.
|
|
718
1047
|
#
|
|
719
1048
|
# _@param_ `args` — stuff to print.
|
|
720
1049
|
def print: (*String args) -> void
|
|
@@ -757,14 +1086,16 @@ module Tuile
|
|
|
757
1086
|
# _@return_ — true if focus moved.
|
|
758
1087
|
def cycle_focus: (forward: bool) -> bool
|
|
759
1088
|
|
|
760
|
-
#
|
|
761
|
-
#
|
|
762
|
-
#
|
|
763
|
-
|
|
764
|
-
def collect_subtree: (Component component) -> ::Array[Component]
|
|
1089
|
+
# The escape sequence positioning the hardware cursor for the current focus
|
|
1090
|
+
# state: hidden when nothing owns it, else moved to the focused component's
|
|
1091
|
+
# {Component#cursor_position} and shown. Appended to each frame's flush.
|
|
1092
|
+
def cursor_sequence: () -> String
|
|
765
1093
|
|
|
766
|
-
#
|
|
767
|
-
|
|
1094
|
+
# Writes an assembled frame (escape string) to the terminal. The single
|
|
1095
|
+
# sink for repaint output; {FakeScreen} overrides it to capture instead.
|
|
1096
|
+
#
|
|
1097
|
+
# _@param_ `str`
|
|
1098
|
+
def emit: (String str) -> void
|
|
768
1099
|
|
|
769
1100
|
# Recalculates positions of all windows, and repaints the scene.
|
|
770
1101
|
# Automatically called whenever terminal size changes. Call when the app
|
|
@@ -781,10 +1112,12 @@ module Tuile
|
|
|
781
1112
|
# doesn't trap them.
|
|
782
1113
|
# 2. App-level shortcuts from {#register_global_shortcut}. An entry
|
|
783
1114
|
# 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
|
-
#
|
|
1115
|
+
# default `over_popups: false` fires only when no modal popup is open
|
|
1116
|
+
# (otherwise the modal popup receives the key normally). A non-modal
|
|
1117
|
+
# overlay doesn't suppress global shortcuts.
|
|
1118
|
+
# 3. {ScreenPane#handle_key}, which captures a matching {#key_shortcut}
|
|
1119
|
+
# in the active scope, then delivers the key to {#focused} and bubbles
|
|
1120
|
+
# it up the focus chain.
|
|
788
1121
|
#
|
|
789
1122
|
# _@param_ `key`
|
|
790
1123
|
#
|
|
@@ -801,6 +1134,13 @@ module Tuile
|
|
|
801
1134
|
# _@return_ — the structural root of the component tree.
|
|
802
1135
|
attr_reader pane: ScreenPane
|
|
803
1136
|
|
|
1137
|
+
# _@return_ — `:light` or `:dark`
|
|
1138
|
+
attr_reader color_scheme: Symbol
|
|
1139
|
+
|
|
1140
|
+
# _@return_ — the back buffer components paint into
|
|
1141
|
+
# ({Buffer#set_line} / {Buffer#fill} / {Buffer#set_char}).
|
|
1142
|
+
attr_reader buffer: Buffer
|
|
1143
|
+
|
|
804
1144
|
# Handler invoked when a {StandardError} escapes an event handler inside
|
|
805
1145
|
# the event loop (e.g. a {Component::TextField}'s `on_change` raises).
|
|
806
1146
|
#
|
|
@@ -862,50 +1202,39 @@ module Tuile
|
|
|
862
1202
|
end
|
|
863
1203
|
end
|
|
864
1204
|
|
|
865
|
-
# A
|
|
866
|
-
#
|
|
867
|
-
#
|
|
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.
|
|
868
1211
|
#
|
|
869
|
-
#
|
|
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.
|
|
870
1216
|
#
|
|
871
|
-
#
|
|
872
|
-
#
|
|
873
|
-
# {Component#content_size}), clamped to the slot;
|
|
874
|
-
# - {.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:
|
|
875
1219
|
#
|
|
876
|
-
#
|
|
877
|
-
# natural {Component#content_size} ({Component::Label}, {Component::Button},
|
|
878
|
-
# {Component::List}, …). Input components ({Component::TextField} et al.)
|
|
879
|
-
# report {Size::ZERO}, so a wrap-content slot collapses to zero width —
|
|
880
|
-
# i.e. the component becomes invisible. Use {.fixed} or {FILL} for those.
|
|
1220
|
+
# Fraction::HALF.resolve(Size.new(80, 24)) # => 40x12
|
|
881
1221
|
#
|
|
882
|
-
#
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
class Sizing
|
|
887
|
-
FILL: Sizing
|
|
888
|
-
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
|
|
889
1226
|
|
|
890
|
-
# _@param_ `
|
|
1227
|
+
# _@param_ `width` — fraction of the reference width, `0.0..1.0`.
|
|
891
1228
|
#
|
|
892
|
-
# _@
|
|
893
|
-
def
|
|
1229
|
+
# _@param_ `height` — fraction of the reference height, `0.0..1.0`.
|
|
1230
|
+
def initialize: (width: Numeric, height: Numeric) -> void
|
|
894
1231
|
|
|
895
|
-
# Resolves
|
|
896
|
-
#
|
|
897
|
-
#
|
|
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.
|
|
898
1235
|
#
|
|
899
|
-
# _@param_ `
|
|
900
|
-
|
|
901
|
-
# _@return_ — the resolved extent, always in `0..available`.
|
|
902
|
-
def resolve: (Integer available, Integer content) -> Integer
|
|
903
|
-
|
|
904
|
-
# _@return_ — `:fill`, `:wrap_content` or `:fixed`.
|
|
905
|
-
attr_reader mode: Symbol
|
|
906
|
-
|
|
907
|
-
# _@return_ — the cell count for `:fixed`; `nil` otherwise.
|
|
908
|
-
attr_reader amount: Integer?
|
|
1236
|
+
# _@param_ `reference` — the size to take a fraction of (usually the screen).
|
|
1237
|
+
def resolve: (Size reference) -> Size
|
|
909
1238
|
end
|
|
910
1239
|
|
|
911
1240
|
# A UI component which is positioned on the screen and draws characters into
|
|
@@ -950,20 +1279,23 @@ module Tuile
|
|
|
950
1279
|
# Only called when the component is attached.
|
|
951
1280
|
def repaint: () -> void
|
|
952
1281
|
|
|
953
|
-
# Called when a character is pressed on the keyboard.
|
|
1282
|
+
# Called when a character is pressed on the keyboard. The default does
|
|
1283
|
+
# nothing and reports the key as unhandled; input components
|
|
1284
|
+
# ({Component::TextField}, {Component::List}, {Component::Button}, …)
|
|
1285
|
+
# override it to act on keys they care about.
|
|
954
1286
|
#
|
|
955
|
-
#
|
|
956
|
-
#
|
|
1287
|
+
# Dispatch is owned by {ScreenPane#handle_key}: a {#key_shortcut} match
|
|
1288
|
+
# anywhere in the active scope is captured first (suppressed while a
|
|
1289
|
+
# cursor-owner is mid-edit), then the key is delivered to {Screen#focused}
|
|
1290
|
+
# and bubbles up its ancestor chain until some component handles it. A
|
|
1291
|
+
# component therefore only ever receives keys when it is on the focus chain
|
|
1292
|
+
# — or when app code hands it a key directly — so it acts on the key alone
|
|
1293
|
+
# and must never gate on its own {#active?} state.
|
|
957
1294
|
#
|
|
958
|
-
#
|
|
959
|
-
# focuses it. The shortcut search is suppressed while the focused component
|
|
960
|
-
# owns the hardware cursor (e.g. a {Component::TextField} the user is
|
|
961
|
-
# typing into) so that hotkeys don't steal printable keys from the editor.
|
|
962
|
-
#
|
|
963
|
-
# _@param_ `key` — a key.
|
|
1295
|
+
# _@param_ `_key` — a key.
|
|
964
1296
|
#
|
|
965
1297
|
# _@return_ — true if the key was handled, false if not.
|
|
966
|
-
def handle_key: (String
|
|
1298
|
+
def handle_key: (String _key) -> bool
|
|
967
1299
|
|
|
968
1300
|
# _@param_ `key` — keyboard key to look up.
|
|
969
1301
|
#
|
|
@@ -1042,20 +1374,6 @@ module Tuile
|
|
|
1042
1374
|
# _@param_ `child` — the just-detached child.
|
|
1043
1375
|
def on_child_removed: (Component child) -> void
|
|
1044
1376
|
|
|
1045
|
-
# Called by a child component whose {#content_size} just changed (fired
|
|
1046
|
-
# from the child's {#content_size=}). Does nothing by default — a plain
|
|
1047
|
-
# container is not size-coupled to its children. Containers that derive
|
|
1048
|
-
# their own natural size or child layout from a child's natural size
|
|
1049
|
-
# override this (e.g. {Component::Window} re-lays-out a
|
|
1050
|
-
# {Sizing::WRAP_CONTENT} footer and recomputes its own size from content;
|
|
1051
|
-
# {Component::Popup} re-self-sizes). If the receiver's own
|
|
1052
|
-
# {#content_size} changes as a consequence, its {#content_size=} notifies
|
|
1053
|
-
# *its* parent in turn — so the event bubbles exactly as far as geometry
|
|
1054
|
-
# keeps changing, and stops where it doesn't.
|
|
1055
|
-
#
|
|
1056
|
-
# _@param_ `child` — the resized direct child.
|
|
1057
|
-
def on_child_content_size_changed: (Component child) -> void
|
|
1058
|
-
|
|
1059
1377
|
# Where the hardware terminal cursor should sit when this component is the
|
|
1060
1378
|
# cursor owner. Returns `nil` to indicate the cursor should be hidden. The
|
|
1061
1379
|
# {Screen} positions the hardware cursor after each repaint cycle by
|
|
@@ -1127,15 +1445,6 @@ module Tuile
|
|
|
1127
1445
|
# {#on_theme_changed=} listener keeps firing.
|
|
1128
1446
|
attr_accessor on_theme_changed: Proc?
|
|
1129
1447
|
|
|
1130
|
-
# The {Size} big enough to show the entire component contents without
|
|
1131
|
-
# scrolling. Plain components have no intrinsic content and report
|
|
1132
|
-
# {Size::ZERO}; content-bearing components (e.g. {Label}, {List},
|
|
1133
|
-
# {TextView}, {Window}) maintain it eagerly via {#content_size=} from
|
|
1134
|
-
# their mutators, so reads are O(1). Used by callers like
|
|
1135
|
-
# {Component::Popup} to auto-size to whatever content was assigned,
|
|
1136
|
-
# regardless of its concrete type, and by {Sizing::WRAP_CONTENT} slots.
|
|
1137
|
-
attr_accessor content_size: Size
|
|
1138
|
-
|
|
1139
1448
|
# A scrollable list of items with cursor support.
|
|
1140
1449
|
#
|
|
1141
1450
|
# Items are modeled as {StyledString}s and painted directly into the
|
|
@@ -1246,19 +1555,6 @@ module Tuile
|
|
|
1246
1555
|
# is one, so the list snaps to the bottom on first paint.
|
|
1247
1556
|
def on_width_changed: () -> void
|
|
1248
1557
|
|
|
1249
|
-
# Natural size from scratch: longest line's display width plus the two
|
|
1250
|
-
# single-space gutters {#pad_to_row} adds, × line count. An empty list
|
|
1251
|
-
# is {Size::ZERO} (no gutters for no content).
|
|
1252
|
-
def compute_content_size: () -> Size
|
|
1253
|
-
|
|
1254
|
-
# Incremental {#content_size} update for appends: folds just the
|
|
1255
|
-
# appended lines into the running maximum, keeping {#add_lines}
|
|
1256
|
-
# O(appended) instead of re-scanning the whole list (LogWindow appends
|
|
1257
|
-
# a line per log statement).
|
|
1258
|
-
#
|
|
1259
|
-
# _@param_ `appended` — the just-appended lines (already concatenated onto {@lines}).
|
|
1260
|
-
def grow_content_size: (::Array[StyledString] appended) -> void
|
|
1261
|
-
|
|
1262
1558
|
# Coerces and flattens a list of input entries into trimmed
|
|
1263
1559
|
# {StyledString} lines. Each entry becomes a {StyledString} (String
|
|
1264
1560
|
# via {StyledString.parse}, StyledString passed through, anything else
|
|
@@ -1385,9 +1681,9 @@ module Tuile
|
|
|
1385
1681
|
#
|
|
1386
1682
|
# _@param_ `scrollbar` — scrollbar instance, or nil if not shown.
|
|
1387
1683
|
#
|
|
1388
|
-
# _@return_ — paintable
|
|
1389
|
-
#
|
|
1390
|
-
def paintable_line: (Integer index, Integer row_in_viewport, VerticalScrollBar? scrollbar) ->
|
|
1684
|
+
# _@return_ — paintable line exactly `rect.width` columns wide;
|
|
1685
|
+
# highlighted if cursor is here.
|
|
1686
|
+
def paintable_line: (Integer index, Integer row_in_viewport, VerticalScrollBar? scrollbar) -> StyledString
|
|
1391
1687
|
|
|
1392
1688
|
# _@return_ — callback fired when an item is chosen — by pressing
|
|
1393
1689
|
# Enter on the cursor's item, or by left-clicking an item. Called as
|
|
@@ -1556,7 +1852,8 @@ module Tuile
|
|
|
1556
1852
|
# embedded ANSI is honored) or a {StyledString} directly. {#text}
|
|
1557
1853
|
# always returns the {StyledString}.
|
|
1558
1854
|
class Label < Component
|
|
1559
|
-
|
|
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
|
|
1560
1857
|
|
|
1561
1858
|
# Paints the text into {#rect}.
|
|
1562
1859
|
#
|
|
@@ -1568,17 +1865,12 @@ module Tuile
|
|
|
1568
1865
|
|
|
1569
1866
|
def on_width_changed: () -> void
|
|
1570
1867
|
|
|
1571
|
-
# Natural size: longest hard-line's display width × number of hard
|
|
1572
|
-
# lines. Computed on the *unclipped* text — sizing is intrinsic to the
|
|
1573
|
-
# content, not the viewport. Empty text yields {Size::ZERO}.
|
|
1574
|
-
def compute_content_size: () -> Size
|
|
1575
|
-
|
|
1576
1868
|
# Recomputes {@clipped_lines} for the current text and rect width.
|
|
1577
|
-
# Each line is ellipsized to fit
|
|
1578
|
-
# the full width,
|
|
1579
|
-
#
|
|
1580
|
-
#
|
|
1581
|
-
#
|
|
1869
|
+
# Each line is ellipsized to fit and padded with trailing spaces out to
|
|
1870
|
+
# the full width, so {#repaint} is just a lookup + {Buffer#set_line} per
|
|
1871
|
+
# row. {@blank_line} covers rows past the last text line. When {#bg} is
|
|
1872
|
+
# set, every produced line (and the blank row) has the bg applied
|
|
1873
|
+
# uniformly.
|
|
1582
1874
|
def update_clipped_lines: () -> void
|
|
1583
1875
|
|
|
1584
1876
|
# _@param_ `line`
|
|
@@ -1599,10 +1891,32 @@ module Tuile
|
|
|
1599
1891
|
attr_accessor bg: (Color | Symbol | Integer | ::Array[Integer])?
|
|
1600
1892
|
end
|
|
1601
1893
|
|
|
1602
|
-
#
|
|
1603
|
-
# paints nothing — it's a transparent host that handles
|
|
1604
|
-
# ({#open} / {#close} / {#open?}, ESC/q to close)
|
|
1605
|
-
#
|
|
1894
|
+
# An overlay that wraps any {Component} as its content. Popup itself
|
|
1895
|
+
# paints nothing — it's a transparent host that handles its lifecycle
|
|
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.
|
|
1907
|
+
#
|
|
1908
|
+
# Modal by default: it centers on the screen, grabs focus, eats keys, and
|
|
1909
|
+
# blocks clicks beneath it. Pass `modal: false` for a non-modal overlay
|
|
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.
|
|
1606
1920
|
#
|
|
1607
1921
|
# The wrapped content fills the popup's full {#rect}; if you want a frame
|
|
1608
1922
|
# and caption, wrap a {Component::Window} (or any subclass — including
|
|
@@ -1622,8 +1936,15 @@ module Tuile
|
|
|
1622
1936
|
class Popup < Component
|
|
1623
1937
|
include Tuile::Component::HasContent
|
|
1624
1938
|
|
|
1625
|
-
# _@param_ `content` — initial content; can be set later via {#content=}.
|
|
1626
|
-
|
|
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.
|
|
1940
|
+
#
|
|
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).
|
|
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
|
|
1945
|
+
|
|
1946
|
+
# _@return_ — whether this popup is modal. See {#initialize}.
|
|
1947
|
+
def modal?: () -> bool
|
|
1627
1948
|
|
|
1628
1949
|
def focusable?: () -> bool
|
|
1629
1950
|
|
|
@@ -1638,17 +1959,20 @@ module Tuile
|
|
|
1638
1959
|
# _@param_ `new_rect`
|
|
1639
1960
|
def rect=: (Rect new_rect) -> void
|
|
1640
1961
|
|
|
1641
|
-
# Mounts this popup on the {Screen}
|
|
1642
|
-
#
|
|
1643
|
-
# 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.
|
|
1644
1964
|
def open: () -> void
|
|
1645
1965
|
|
|
1646
1966
|
# Constructs and opens a popup in one call.
|
|
1647
1967
|
#
|
|
1648
1968
|
# _@param_ `content`
|
|
1649
1969
|
#
|
|
1970
|
+
# _@param_ `modal` — see {#initialize}.
|
|
1971
|
+
#
|
|
1972
|
+
# _@param_ `size` — see {#initialize}.
|
|
1973
|
+
#
|
|
1650
1974
|
# _@return_ — the opened popup.
|
|
1651
|
-
def self.open: (?content: Component?) -> Popup
|
|
1975
|
+
def self.open: (?content: Component?, ?modal: bool, ?size: (Size | Fraction)) -> Popup
|
|
1652
1976
|
|
|
1653
1977
|
# Removes this popup from the {Screen}. No-op if not currently open.
|
|
1654
1978
|
def close: () -> void
|
|
@@ -1656,31 +1980,29 @@ module Tuile
|
|
|
1656
1980
|
# _@return_ — true if this popup is currently mounted on the screen.
|
|
1657
1981
|
def open?: () -> bool
|
|
1658
1982
|
|
|
1659
|
-
#
|
|
1660
|
-
#
|
|
1661
|
-
#
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
#
|
|
1665
|
-
# defaults to 12. Override in a subclass to allow taller popups.
|
|
1666
|
-
def max_height: () -> Integer
|
|
1667
|
-
|
|
1668
|
-
# 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).
|
|
1669
1989
|
#
|
|
1670
|
-
#
|
|
1671
|
-
|
|
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
|
|
1672
1995
|
|
|
1673
|
-
#
|
|
1674
|
-
|
|
1675
|
-
# `add_line`, or a nested {Window} whose own content grew (the window
|
|
1676
|
-
# recomputes its {Component#content_size} and the change bubbles here).
|
|
1677
|
-
#
|
|
1678
|
-
# _@param_ `_child`
|
|
1679
|
-
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
|
|
1680
1998
|
|
|
1681
1999
|
# Hint for the status bar: own "q Close" plus the wrapped content's hint.
|
|
1682
2000
|
def keyboard_hint: () -> String
|
|
1683
2001
|
|
|
2002
|
+
# `q` and ESC close the popup. The popup sits on the focus chain of
|
|
2003
|
+
# whatever it wraps, so the key reaches here by bubbling up from the
|
|
2004
|
+
# focused content after that content declined to handle it.
|
|
2005
|
+
#
|
|
1684
2006
|
# _@param_ `key`
|
|
1685
2007
|
#
|
|
1686
2008
|
# _@return_ — true if the key was handled.
|
|
@@ -1691,21 +2013,15 @@ module Tuile
|
|
|
1691
2013
|
# _@param_ `content`
|
|
1692
2014
|
def layout: (Component content) -> void
|
|
1693
2015
|
|
|
1694
|
-
# Recompute width/height from {#content}'s natural size and recenter
|
|
1695
|
-
# if currently open. Called whenever content is (re)assigned.
|
|
1696
|
-
#
|
|
1697
|
-
# Computes the final (centered) rect and assigns it in one step rather
|
|
1698
|
-
# than positioning at the origin and then centering: the intermediate
|
|
1699
|
-
# origin rect rarely covers the previous one, which would make
|
|
1700
|
-
# {#rect=}'s shrink/move detection fire a full repaint on every resize.
|
|
1701
|
-
def update_rect: () -> void
|
|
1702
|
-
|
|
1703
2016
|
# _@param_ `event`
|
|
1704
2017
|
def handle_mouse: (MouseEvent event) -> void
|
|
1705
2018
|
|
|
1706
2019
|
def children: () -> ::Array[Component]
|
|
1707
2020
|
|
|
1708
2021
|
def on_focus: () -> void
|
|
2022
|
+
|
|
2023
|
+
# _@return_ — the popup's declared size. See {#size=}.
|
|
2024
|
+
attr_accessor size: (Size | Fraction)
|
|
1709
2025
|
end
|
|
1710
2026
|
|
|
1711
2027
|
# A clickable button. Activated by Enter, Space, or a left mouse click;
|
|
@@ -1718,7 +2034,7 @@ module Tuile
|
|
|
1718
2034
|
# {Component#handle_mouse}.
|
|
1719
2035
|
#
|
|
1720
2036
|
# Assign a {#rect} (typically by the surrounding {Layout}) wide enough to
|
|
1721
|
-
# show `[ caption ]
|
|
2037
|
+
# show `[ caption ]` — that natural width is `caption.length + 4`.
|
|
1722
2038
|
class Button < Component
|
|
1723
2039
|
# _@param_ `caption` — the button's label.
|
|
1724
2040
|
def initialize: (?String caption) -> void
|
|
@@ -1735,9 +2051,6 @@ module Tuile
|
|
|
1735
2051
|
|
|
1736
2052
|
def repaint: () -> void
|
|
1737
2053
|
|
|
1738
|
-
# Natural width is `caption.length + 4` to fit `[ caption ]`; height 1.
|
|
1739
|
-
def natural_size: () -> Size
|
|
1740
|
-
|
|
1741
2054
|
# _@return_ — the button's label.
|
|
1742
2055
|
attr_accessor caption: String
|
|
1743
2056
|
|
|
@@ -1779,20 +2092,11 @@ module Tuile
|
|
|
1779
2092
|
# _@param_ `child`
|
|
1780
2093
|
def remove: (Component child) -> void
|
|
1781
2094
|
|
|
1782
|
-
def content_size: () -> Size
|
|
1783
|
-
|
|
1784
2095
|
# Dispatches the event to the child under the mouse cursor.
|
|
1785
2096
|
#
|
|
1786
2097
|
# _@param_ `event`
|
|
1787
2098
|
def handle_mouse: (MouseEvent event) -> void
|
|
1788
2099
|
|
|
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
2100
|
def on_focus: () -> void
|
|
1797
2101
|
|
|
1798
2102
|
# Absolute layout. Extend this class, register any children, and
|
|
@@ -1820,9 +2124,6 @@ module Tuile
|
|
|
1820
2124
|
|
|
1821
2125
|
def children: () -> ::Array[Component]
|
|
1822
2126
|
|
|
1823
|
-
# _@param_ `key`
|
|
1824
|
-
def handle_key: (String key) -> bool
|
|
1825
|
-
|
|
1826
2127
|
# _@param_ `event`
|
|
1827
2128
|
def handle_mouse: (MouseEvent event) -> void
|
|
1828
2129
|
|
|
@@ -1832,22 +2133,6 @@ module Tuile
|
|
|
1832
2133
|
# _@param_ `value`
|
|
1833
2134
|
def scrollbar=: (bool value) -> void
|
|
1834
2135
|
|
|
1835
|
-
# Sets the new content. Also recomputes the window's natural size.
|
|
1836
|
-
#
|
|
1837
|
-
# _@param_ `new_content`
|
|
1838
|
-
def content=: (Component? new_content) -> void
|
|
1839
|
-
|
|
1840
|
-
# Re-lays-out a {Sizing::WRAP_CONTENT} footer when the footer's natural
|
|
1841
|
-
# size changes, and folds a content resize into the window's own
|
|
1842
|
-
# natural size (whose change then bubbles to the window's parent — e.g.
|
|
1843
|
-
# a {Popup} re-self-sizes). The footer deliberately does *not*
|
|
1844
|
-
# participate in the window's {#content_size}: it is decoration
|
|
1845
|
-
# overlaying the border, and must not drive the window's size — if it
|
|
1846
|
-
# doesn't fit, it is clipped to the inner width.
|
|
1847
|
-
#
|
|
1848
|
-
# _@param_ `child`
|
|
1849
|
-
def on_child_content_size_changed: (Component child) -> void
|
|
1850
|
-
|
|
1851
2136
|
# Fully repaints the window: both frame and contents.
|
|
1852
2137
|
#
|
|
1853
2138
|
# Window deliberately paints over its entire rect (border around the
|
|
@@ -1866,43 +2151,41 @@ module Tuile
|
|
|
1866
2151
|
# _@param_ `content`
|
|
1867
2152
|
def layout: (Component content) -> void
|
|
1868
2153
|
|
|
1869
|
-
# Paints the window border.
|
|
2154
|
+
# Paints the window border into the {Screen#buffer}. Title is clipped to
|
|
2155
|
+
# the inner width so the box never overflows {#rect}; when the window is
|
|
2156
|
+
# active the whole border is drawn in {Theme#active_border_color}.
|
|
1870
2157
|
def repaint_border: () -> void
|
|
1871
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
|
+
|
|
1872
2170
|
# The caption text as it appears in the rendered border, including the
|
|
1873
2171
|
# shortcut prefix when {#key_shortcut} is set.
|
|
1874
2172
|
def frame_caption: () -> String
|
|
1875
2173
|
|
|
1876
|
-
#
|
|
1877
|
-
#
|
|
1878
|
-
#
|
|
1879
|
-
#
|
|
1880
|
-
# _@param_ `caption`
|
|
1881
|
-
def build_frame: (String caption) -> String
|
|
1882
|
-
|
|
1883
|
-
# Recomputes the window's natural size: content's natural size (or the
|
|
1884
|
-
# caption, whichever is wider) plus the 2-character border. The footer
|
|
1885
|
-
# is deliberately excluded — see {#on_child_content_size_changed}. A
|
|
1886
|
-
# window with no content or caption sizes to `Size.new(2, 2)` (bare
|
|
1887
|
-
# border).
|
|
1888
|
-
def update_content_size: () -> void
|
|
1889
|
-
|
|
1890
|
-
# Positions the footer over the bottom border row, with its width
|
|
1891
|
-
# resolved by {#footer_sizing} against the inner width. A
|
|
1892
|
-
# {Sizing::WRAP_CONTENT} footer with zero natural width gets an empty
|
|
1893
|
-
# 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).
|
|
1894
2177
|
def layout_footer: () -> void
|
|
1895
2178
|
|
|
1896
2179
|
def on_focus: () -> void
|
|
1897
2180
|
|
|
1898
|
-
# _@return_ — optional component
|
|
1899
|
-
# row.
|
|
2181
|
+
# _@return_ — optional focusable component occupying the
|
|
2182
|
+
# bottom border row, always spanning the full inner width.
|
|
1900
2183
|
attr_accessor footer: Component?
|
|
1901
2184
|
|
|
1902
|
-
# _@return_ —
|
|
1903
|
-
#
|
|
1904
|
-
#
|
|
1905
|
-
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)?
|
|
1906
2189
|
|
|
1907
2190
|
# _@return_ — the current caption, empty by default.
|
|
1908
2191
|
attr_accessor caption: String
|
|
@@ -2351,9 +2634,6 @@ module Tuile
|
|
|
2351
2634
|
# reader when the cache is cold. Cost is O(total spans).
|
|
2352
2635
|
def build_text: () -> StyledString
|
|
2353
2636
|
|
|
2354
|
-
# _@return_ — {#content_size} computed from {@hard_lines}.
|
|
2355
|
-
def compute_content_size: () -> Size
|
|
2356
|
-
|
|
2357
2637
|
# _@return_ — column width available for wrapped text — viewport
|
|
2358
2638
|
# width minus the scrollbar gutter (when visible). `0` when {#rect}'s
|
|
2359
2639
|
# width is non-positive, which yields a degenerate "no wrap" result.
|
|
@@ -2394,11 +2674,10 @@ module Tuile
|
|
|
2394
2674
|
#
|
|
2395
2675
|
# _@param_ `scrollbar`
|
|
2396
2676
|
#
|
|
2397
|
-
# _@return_ — paintable
|
|
2398
|
-
#
|
|
2399
|
-
#
|
|
2400
|
-
|
|
2401
|
-
def paintable_line: (Integer index, Integer row_in_viewport, VerticalScrollBar? scrollbar) -> String
|
|
2677
|
+
# _@return_ — paintable line exactly `rect.width` columns wide.
|
|
2678
|
+
# Body lines come pre-padded from {#rewrap}, so this reduces to a lookup
|
|
2679
|
+
# plus a concat of the scrollbar glyph when one is present.
|
|
2680
|
+
def paintable_line: (Integer index, Integer row_in_viewport, VerticalScrollBar? scrollbar) -> StyledString
|
|
2402
2681
|
|
|
2403
2682
|
# _@return_ — index of the first visible physical line.
|
|
2404
2683
|
attr_accessor top_line: Integer
|
|
@@ -2413,13 +2692,6 @@ module Tuile
|
|
|
2413
2692
|
# bottom and tailing resumes. Default `false`.
|
|
2414
2693
|
attr_accessor auto_scroll: bool
|
|
2415
2694
|
|
|
2416
|
-
# _@return_ — longest hard-line's display width × number of hard
|
|
2417
|
-
# lines. Reported on the *unwrapped* text — wrap-aware sizing would
|
|
2418
|
-
# be circular (width depends on width). Empty text returns
|
|
2419
|
-
# `Size.new(0, 0)`. Maintained incrementally by {#text=} and
|
|
2420
|
-
# {#append}, so reads are O(1).
|
|
2421
|
-
attr_reader content_size: Size
|
|
2422
|
-
|
|
2423
2695
|
# A logical section of a {TextView}'s text — a contiguous run of
|
|
2424
2696
|
# hard lines the app wants to address as a unit (e.g. an LLM's
|
|
2425
2697
|
# "thinking" output vs. its assistant message). The view always
|
|
@@ -2682,22 +2954,24 @@ module Tuile
|
|
|
2682
2954
|
|
|
2683
2955
|
def tab_stop?: () -> bool
|
|
2684
2956
|
|
|
2685
|
-
# Handles a key.
|
|
2686
|
-
#
|
|
2687
|
-
#
|
|
2957
|
+
# Handles a key. An {#on_key} interceptor (if set) gets first refusal —
|
|
2958
|
+
# a truthy return consumes the key — otherwise it delegates to
|
|
2959
|
+
# {#handle_text_input_key}. Dispatch ({ScreenPane#handle_key}) only routes
|
|
2960
|
+
# keys here when this input is on the focus chain, so there is no
|
|
2961
|
+
# {#active?} gate.
|
|
2688
2962
|
#
|
|
2689
2963
|
# _@param_ `key`
|
|
2690
2964
|
def handle_key: (String key) -> bool
|
|
2691
2965
|
|
|
2692
2966
|
# 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#
|
|
2967
|
+
# current {Screen#theme} at paint time: {Theme#active_bg_color} when this
|
|
2968
|
+
# input is on the active (focus) chain, {Theme#input_bg_color} otherwise —
|
|
2695
2969
|
# visibly a field either way, distinctly highlighted when active.
|
|
2696
2970
|
#
|
|
2697
2971
|
# _@param_ `text`
|
|
2698
2972
|
#
|
|
2699
|
-
# _@return_ —
|
|
2700
|
-
def background: (String text) ->
|
|
2973
|
+
# _@return_ — text on the field's background well.
|
|
2974
|
+
def background: (String text) -> StyledString
|
|
2701
2975
|
|
|
2702
2976
|
# Input filter for {#text=}. Subclasses override to truncate or reject
|
|
2703
2977
|
# invalid input. Default coerces to String.
|
|
@@ -2760,6 +3034,21 @@ module Tuile
|
|
|
2760
3034
|
# _@return_ — one-arg callable, or nil.
|
|
2761
3035
|
attr_accessor on_change: (Proc | Method)?
|
|
2762
3036
|
|
|
3037
|
+
# Optional interceptor consulted before the input's own key handling.
|
|
3038
|
+
# Receives the pressed key; return a truthy value to consume it (the
|
|
3039
|
+
# input then ignores that key), falsy to let normal editing proceed.
|
|
3040
|
+
#
|
|
3041
|
+
# The keyboard analog of {#on_change}: it lets app code layer behavior
|
|
3042
|
+
# onto an input without subclassing. The motivating case is an
|
|
3043
|
+
# autocomplete / slash-command overlay (a non-modal {Component::Popup}):
|
|
3044
|
+
# while it is open the interceptor claims Up/Down/Enter/ESC and forwards
|
|
3045
|
+
# them to the overlay's list, but lets ordinary characters fall through
|
|
3046
|
+
# so typing keeps editing the field (and {#on_change} keeps refilling the
|
|
3047
|
+
# list).
|
|
3048
|
+
#
|
|
3049
|
+
# _@return_ — one-arg callable, or nil.
|
|
3050
|
+
attr_accessor on_key: (Proc | Method)?
|
|
3051
|
+
|
|
2763
3052
|
# Callback fired when ESC is pressed. Defaults to a closure that clears
|
|
2764
3053
|
# focus (`screen.focused = nil`) so ESC visibly cancels text entry instead
|
|
2765
3054
|
# of bubbling to the parent — and, in particular, instead of reaching the
|
|
@@ -2774,11 +3063,6 @@ module Tuile
|
|
|
2774
3063
|
# provide a protected `layout(content)` method which repositions the
|
|
2775
3064
|
# content component; the mixin manages `@content` itself.
|
|
2776
3065
|
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
3066
|
# _@param_ `event`
|
|
2783
3067
|
def handle_mouse: (MouseEvent event) -> void
|
|
2784
3068
|
|
|
@@ -2810,8 +3094,10 @@ module Tuile
|
|
|
2810
3094
|
#
|
|
2811
3095
|
# _@param_ `lines` — the content, may contain formatting.
|
|
2812
3096
|
#
|
|
3097
|
+
# _@param_ `size` — the popup's size, applied top-down; the list wraps and scrolls within it. Defaults to {Fraction::HALF}.
|
|
3098
|
+
#
|
|
2813
3099
|
# _@return_ — the opened popup.
|
|
2814
|
-
def self.open: (String caption, ::Array[String] lines) -> Popup
|
|
3100
|
+
def self.open: (String caption, ::Array[String] lines, ?size: (Size | Fraction)) -> Popup
|
|
2815
3101
|
end
|
|
2816
3102
|
|
|
2817
3103
|
# A {Window} that lists options identified by single keyboard keys, asks
|
|
@@ -2828,6 +3114,11 @@ module Tuile
|
|
|
2828
3114
|
# _@param_ `options` — pairs of keyboard key and option caption. No Rainbow formatting must be used.
|
|
2829
3115
|
def initialize: (String caption, ::Array[[String, String]] options) ?{ (String key) -> void } -> void
|
|
2830
3116
|
|
|
3117
|
+
# Handles an option-key press. Reached by bubbling: the inner {List}
|
|
3118
|
+
# (the focused component) sees the key first and handles cursor/Enter
|
|
3119
|
+
# picks; anything it declines bubbles up here, where a key matching an
|
|
3120
|
+
# option's `key` picks that option.
|
|
3121
|
+
#
|
|
2831
3122
|
# _@param_ `key`
|
|
2832
3123
|
def handle_key: (String key) -> bool
|
|
2833
3124
|
|
|
@@ -2959,10 +3250,13 @@ module Tuile
|
|
|
2959
3250
|
# Awaits until the event queue is empty (all events have been processed).
|
|
2960
3251
|
def await_empty: () -> void
|
|
2961
3252
|
|
|
2962
|
-
# Schedules `block` to fire on the event-loop thread
|
|
2963
|
-
#
|
|
2964
|
-
#
|
|
2965
|
-
#
|
|
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.
|
|
2966
3260
|
#
|
|
2967
3261
|
# The returned {Ticker} controls the schedule — call {Ticker#cancel} to
|
|
2968
3262
|
# stop it.
|
|
@@ -2976,8 +3270,15 @@ module Tuile
|
|
|
2976
3270
|
# ({Concurrent}.global_timer_set) — adding more tickers does not add more
|
|
2977
3271
|
# threads, just more work on the shared scheduler.
|
|
2978
3272
|
#
|
|
2979
|
-
# _@param_ `
|
|
2980
|
-
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
|
|
2981
3282
|
|
|
2982
3283
|
# Runs the event loop and blocks. Must be run from at most one thread at the
|
|
2983
3284
|
# same time. Blocks until some thread calls {#stop}. Calls block for all
|
|
@@ -3098,10 +3399,10 @@ module Tuile
|
|
|
3098
3399
|
class Ticker
|
|
3099
3400
|
# _@param_ `event_queue` — queue to dispatch tick calls onto.
|
|
3100
3401
|
#
|
|
3101
|
-
# _@param_ `
|
|
3402
|
+
# _@param_ `interval` — seconds between firings (positive).
|
|
3102
3403
|
#
|
|
3103
3404
|
# _@param_ `block` — called as `block.call(tick_count)` on each fire.
|
|
3104
|
-
def initialize: (EventQueue event_queue, Numeric
|
|
3405
|
+
def initialize: (EventQueue event_queue, Numeric interval, Proc block) -> void
|
|
3105
3406
|
|
|
3106
3407
|
# _@return_ — true once {#cancel} has been called.
|
|
3107
3408
|
def cancelled?: () -> bool
|
|
@@ -3148,6 +3449,13 @@ module Tuile
|
|
|
3148
3449
|
# _@param_ `args`
|
|
3149
3450
|
def print: (*String args) -> void
|
|
3150
3451
|
|
|
3452
|
+
# Captures the assembled repaint frame instead of writing to the test
|
|
3453
|
+
# runner's TTY. Lands in {#prints} so cursor/sync escapes can be asserted;
|
|
3454
|
+
# painted content is read from {#buffer}.
|
|
3455
|
+
#
|
|
3456
|
+
# _@param_ `str`
|
|
3457
|
+
def emit: (String str) -> void
|
|
3458
|
+
|
|
3151
3459
|
# _@param_ `component` — the component to check.
|
|
3152
3460
|
def invalidated?: (Component component) -> bool
|
|
3153
3461
|
|
|
@@ -3158,7 +3466,10 @@ module Tuile
|
|
|
3158
3466
|
# steal its input) and pin the deterministic default.
|
|
3159
3467
|
def detect_scheme: () -> Symbol
|
|
3160
3468
|
|
|
3161
|
-
# _@return_ — whatever {#print}
|
|
3469
|
+
# _@return_ — whatever {#print} / {#emit} produced so far.
|
|
3470
|
+
# Component painting lands in {#buffer}, not here — assert on
|
|
3471
|
+
# {Buffer#row_text} / {Buffer#row_ansi} / {Buffer#cell} for content, and
|
|
3472
|
+
# on `prints` for cursor and housekeeping escapes.
|
|
3162
3473
|
attr_reader prints: ::Array[String]
|
|
3163
3474
|
end
|
|
3164
3475
|
|
|
@@ -3236,7 +3547,11 @@ module Tuile
|
|
|
3236
3547
|
# status bar last.
|
|
3237
3548
|
def children: () -> ::Array[Component]
|
|
3238
3549
|
|
|
3239
|
-
# Adds a popup
|
|
3550
|
+
# Adds a popup and invalidates it for repaint. A modal popup is centered
|
|
3551
|
+
# and grabs focus; a non-modal overlay ({Component::Popup#modal?} false) is
|
|
3552
|
+
# left wherever the caller positions it and does *not* take focus, so the
|
|
3553
|
+
# component that was focused keeps the cursor and keeps receiving keys —
|
|
3554
|
+
# the overlay floats above the content, driven from app code.
|
|
3240
3555
|
#
|
|
3241
3556
|
# _@param_ `window`
|
|
3242
3557
|
def add_popup: (Component::Popup window) -> void
|
|
@@ -3253,26 +3568,54 @@ module Tuile
|
|
|
3253
3568
|
# _@return_ — true if this pane currently hosts the popup.
|
|
3254
3569
|
def has_popup?: (Component window) -> bool
|
|
3255
3570
|
|
|
3571
|
+
# _@return_ — the topmost *modal* popup, or nil when
|
|
3572
|
+
# only non-modal overlays (or no popups) are open. This is the "modal
|
|
3573
|
+
# owner": the popup that scopes key dispatch, blocks mouse clicks, owns
|
|
3574
|
+
# the status bar, and confines Tab cycling. Non-modal overlays are
|
|
3575
|
+
# excluded — they float above the content without capturing input.
|
|
3576
|
+
def modal_popup: () -> Component::Popup?
|
|
3577
|
+
|
|
3256
3578
|
# Re-lays out children whenever the pane's own rect changes.
|
|
3257
3579
|
#
|
|
3258
3580
|
# _@param_ `new_rect`
|
|
3259
3581
|
def rect=: (Rect new_rect) -> void
|
|
3260
3582
|
|
|
3261
3583
|
# Lays out content (full pane minus the bottom row) and the status bar
|
|
3262
|
-
# (bottom row).
|
|
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).
|
|
3263
3588
|
def layout: () -> void
|
|
3264
3589
|
|
|
3265
3590
|
# Pane paints nothing itself; its children paint over the entire rect.
|
|
3266
3591
|
def repaint: () -> void
|
|
3267
3592
|
|
|
3268
|
-
#
|
|
3269
|
-
# when
|
|
3593
|
+
# Dispatches a key in two phases, both scoped to the topmost *modal* popup
|
|
3594
|
+
# (when one is open) or else the tiled {#content}. Non-modal overlays are
|
|
3595
|
+
# never the scope: focus stays in the content beneath them, and the overlay
|
|
3596
|
+
# is driven by app code (which forwards keys to it explicitly), so it
|
|
3597
|
+
# doesn't appear in this path at all.
|
|
3598
|
+
#
|
|
3599
|
+
# 1. *Capture* — a {Component#key_shortcut} match anywhere in the scope
|
|
3600
|
+
# focuses that component and consumes the key. Suppressed while a
|
|
3601
|
+
# cursor-owner ({Screen#cursor_position}) is mid-edit, so typing into a
|
|
3602
|
+
# {Component::TextField} isn't hijacked by a sibling's shortcut.
|
|
3603
|
+
# 2. *Delivery* — the key is handed to {Screen#focused} and bubbles up its
|
|
3604
|
+
# ancestor chain to the scope root; the first component to return true
|
|
3605
|
+
# wins. Focus that is nil or sits outside the scope receives nothing,
|
|
3606
|
+
# which is what keeps an open modal popup modal.
|
|
3607
|
+
#
|
|
3608
|
+
# _@param_ `key`
|
|
3609
|
+
#
|
|
3610
|
+
# _@return_ — true if the key was handled.
|
|
3270
3611
|
def handle_key: (String key) -> bool
|
|
3271
3612
|
|
|
3272
3613
|
# 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
|
-
#
|
|
3614
|
+
# fall through to content only when no popup is hit *and* no modal popup is
|
|
3615
|
+
# open. This preserves modal click-blocking — an open modal eats clicks
|
|
3616
|
+
# even outside its rect — while a non-modal overlay blocks nothing: clicks
|
|
3617
|
+
# inside it route to it (e.g. click-to-select), clicks elsewhere reach the
|
|
3618
|
+
# content beneath.
|
|
3276
3619
|
#
|
|
3277
3620
|
# _@param_ `event`
|
|
3278
3621
|
def handle_mouse: (MouseEvent event) -> void
|
|
@@ -3280,7 +3623,7 @@ module Tuile
|
|
|
3280
3623
|
# Focus repair when a child detaches. Default {Component#on_child_removed}
|
|
3281
3624
|
# would refocus to `self` (the pane), which isn't a useful focus target.
|
|
3282
3625
|
# 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
|
|
3626
|
+
# modal popup; falling back to the focus snapshotted when this popup was opened
|
|
3284
3627
|
# (if still attached and still focusable); then to the first interactable
|
|
3285
3628
|
# widget in {#content}; then to {#content} itself; then nil.
|
|
3286
3629
|
#
|
|
@@ -3292,6 +3635,19 @@ module Tuile
|
|
|
3292
3635
|
# _@param_ `child`
|
|
3293
3636
|
def on_child_removed: (Component child) -> void
|
|
3294
3637
|
|
|
3638
|
+
# Delivers `key` to {Screen#focused} and bubbles it up the ancestor chain,
|
|
3639
|
+
# stopping at (and including) `scope`. Delivers to no one — returning false
|
|
3640
|
+
# — when focus is nil or sits outside `scope`; the latter is what makes an
|
|
3641
|
+
# open popup modal, since focus is always inside it and content beneath
|
|
3642
|
+
# never receives keys.
|
|
3643
|
+
#
|
|
3644
|
+
# _@param_ `key`
|
|
3645
|
+
#
|
|
3646
|
+
# _@param_ `scope` — the modal scope root (topmost popup or content).
|
|
3647
|
+
#
|
|
3648
|
+
# _@return_ — true if some component on the chain handled the key.
|
|
3649
|
+
def bubble_key: (String key, Component scope) -> bool
|
|
3650
|
+
|
|
3295
3651
|
# First {Component#tab_stop?} in `root`'s subtree (pre-order), falling
|
|
3296
3652
|
# back to `root` itself when the subtree has no tab stops. Returns `nil`
|
|
3297
3653
|
# if `root` is `nil`.
|
|
@@ -3302,8 +3658,9 @@ module Tuile
|
|
|
3302
3658
|
# _@return_ — the tiled content component.
|
|
3303
3659
|
attr_accessor content: Component?
|
|
3304
3660
|
|
|
3305
|
-
# _@return_ —
|
|
3306
|
-
# topmost.
|
|
3661
|
+
# _@return_ — overlay popups in stacking order; last is
|
|
3662
|
+
# topmost. Holds both modal popups and non-modal overlays
|
|
3663
|
+
# ({Component::Popup#modal?}). The array must not be mutated by callers.
|
|
3307
3664
|
attr_reader popups: ::Array[Component]
|
|
3308
3665
|
|
|
3309
3666
|
# _@return_ — the bottom status bar.
|
|
@@ -3496,19 +3853,6 @@ module Tuile
|
|
|
3496
3853
|
# _@param_ `spans`
|
|
3497
3854
|
def normalize: (::Array[Span] spans) -> ::Array[Span]
|
|
3498
3855
|
|
|
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
3856
|
# _@param_ `start_or_range`
|
|
3513
3857
|
#
|
|
3514
3858
|
# _@param_ `len`
|
|
@@ -3608,6 +3952,27 @@ module Tuile
|
|
|
3608
3952
|
# _@param_ `overrides`
|
|
3609
3953
|
def merge: (**::Hash[Symbol, Object] overrides) -> Style
|
|
3610
3954
|
|
|
3955
|
+
# Minimal SGR escape that transitions a terminal already showing `self`
|
|
3956
|
+
# into `other`: only the attributes that differ are emitted. Returns
|
|
3957
|
+
# `""` when the styles are identical (nothing to do), and {Ansi::RESET}
|
|
3958
|
+
# (`\e[0m`, one code) when `other` is the default style — shorter than
|
|
3959
|
+
# turning each attribute off individually.
|
|
3960
|
+
#
|
|
3961
|
+
# Shared by {StyledString#to_ansi} (diffing span-to-span from the default
|
|
3962
|
+
# style) and {Buffer}'s flush (diffing cell-to-cell against the style the
|
|
3963
|
+
# terminal currently holds), so both emit identical minimal sequences.
|
|
3964
|
+
#
|
|
3965
|
+
# _@param_ `other` — the style to transition to.
|
|
3966
|
+
def sgr_to: (Style other) -> String
|
|
3967
|
+
|
|
3968
|
+
# _@param_ `color`
|
|
3969
|
+
#
|
|
3970
|
+
# _@param_ `target` — either `:fg` or `:bg`.
|
|
3971
|
+
#
|
|
3972
|
+
# _@return_ — SGR codes; `[39]` / `[49]` for the "default"
|
|
3973
|
+
# reset when `color` is `nil`, otherwise delegated to {Color#sgr_codes}.
|
|
3974
|
+
def color_codes: (Color? color, target: Symbol) -> ::Array[Integer]
|
|
3975
|
+
|
|
3611
3976
|
attr_reader fg: Color?
|
|
3612
3977
|
|
|
3613
3978
|
attr_reader bg: Color?
|
|
@@ -3723,12 +4088,18 @@ module Tuile
|
|
|
3723
4088
|
def post: (Object event) -> void
|
|
3724
4089
|
|
|
3725
4090
|
# Mirrors {EventQueue#tick} but timeless: returns a {FakeTicker} that
|
|
3726
|
-
# only fires when a test calls {#tick_once}. The `
|
|
4091
|
+
# only fires when a test calls {#tick_once}. The `seconds` argument is
|
|
3727
4092
|
# validated the same way the real queue validates it, then discarded —
|
|
3728
4093
|
# the fake has no clock, so frame cadence is up to the test.
|
|
3729
4094
|
#
|
|
3730
|
-
# _@param_ `
|
|
3731
|
-
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
|
|
3732
4103
|
|
|
3733
4104
|
# Test helper: fires every live ticker's user block once and prunes
|
|
3734
4105
|
# cancelled tickers. No-op when no tickers are registered. Pumps once
|