tuile 0.15.0 → 0.16.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 +121 -80
- data/README.md +28 -12
- data/book/04-event-loop.md +12 -12
- data/book/05-focus.md +95 -26
- data/book/06-theming.md +58 -26
- data/book/07-components.md +61 -6
- data/book/08-testing.md +24 -22
- data/book/10-locale.md +2 -2
- data/book/README.md +6 -5
- data/examples/file_commander.rb +14 -5
- data/examples/hello_world.rb +17 -4
- data/examples/sampler.rb +392 -18
- data/lib/tuile/component/abstract_string_field.rb +16 -18
- data/lib/tuile/component/abstract_wrapping_field.rb +47 -11
- data/lib/tuile/component/button.rb +8 -8
- data/lib/tuile/component/checkbox.rb +9 -9
- data/lib/tuile/component/checkbox_group.rb +6 -5
- data/lib/tuile/component/combo_box.rb +50 -35
- data/lib/tuile/component/confirm_window.rb +7 -5
- data/lib/tuile/component/date_field.rb +28 -3
- data/lib/tuile/component/date_time_field.rb +275 -0
- data/lib/tuile/component/has_bad_input.rb +2 -2
- data/lib/tuile/component/has_content.rb +3 -3
- data/lib/tuile/component/has_placeholder.rb +1 -1
- data/lib/tuile/component/has_validation.rb +2 -2
- data/lib/tuile/component/has_value.rb +1 -1
- data/lib/tuile/component/label.rb +1 -1
- data/lib/tuile/component/layout/box.rb +4 -1
- data/lib/tuile/component/layout.rb +3 -3
- data/lib/tuile/component/list.rb +42 -32
- data/lib/tuile/component/list_dropdown.rb +3 -3
- data/lib/tuile/component/menu_bar/cascade.rb +5 -5
- data/lib/tuile/component/menu_bar.rb +18 -18
- data/lib/tuile/component/notification.rb +32 -18
- data/lib/tuile/component/overlay.rb +9 -8
- data/lib/tuile/component/picker_window.rb +27 -8
- data/lib/tuile/component/popup.rb +2 -2
- data/lib/tuile/component/progress_bar.rb +10 -4
- data/lib/tuile/component/radio_group.rb +6 -5
- data/lib/tuile/component/select.rb +11 -12
- data/lib/tuile/component/slot.rb +3 -3
- data/lib/tuile/component/tab_sheet.rb +6 -6
- data/lib/tuile/component/tabs.rb +11 -11
- data/lib/tuile/component/text_area.rb +12 -10
- data/lib/tuile/component/text_field.rb +14 -12
- data/lib/tuile/component/text_view.rb +15 -11
- data/lib/tuile/component/time_field.rb +29 -4
- data/lib/tuile/component.rb +201 -93
- data/lib/tuile/event_queue.rb +4 -4
- data/lib/tuile/fake_event_queue.rb +1 -1
- data/lib/tuile/fake_screen.rb +84 -2
- data/lib/tuile/mouse/router.rb +217 -0
- data/lib/tuile/mouse.rb +177 -0
- data/lib/tuile/screen.rb +98 -61
- data/lib/tuile/screen_pane.rb +41 -36
- data/lib/tuile/styled_string.rb +5 -5
- data/lib/tuile/testing.rb +8 -8
- data/lib/tuile/theme.rb +22 -34
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile/vertical_scroll_bar.rb +1 -1
- data/sig/tuile.rbs +1211 -427
- metadata +4 -16
- data/COMPARISON.md +0 -101
- data/DECISIONS.md +0 -8562
- data/TERMINOLOGY.md +0 -85
- data/ideas/arrow-key-navigation.md +0 -221
- data/ideas/binder.md +0 -177
- data/ideas/composite-field.md +0 -77
- data/ideas/focus-accent.md +0 -116
- data/ideas/form-layout.md +0 -151
- data/ideas/hover/probe.rb +0 -241
- data/ideas/hover/probe_spec.rb +0 -82
- data/ideas/hover.md +0 -909
- data/ideas/modal-backdrop.md +0 -24
- data/ideas/new-components.md +0 -144
- data/ideas/per-component-buffers.md +0 -55
- data/lib/tuile/mouse_event.rb +0 -68
data/sig/tuile.rbs
CHANGED
|
@@ -454,6 +454,321 @@ module Tuile
|
|
|
454
454
|
def verify_final!: (Class klass) -> void
|
|
455
455
|
end
|
|
456
456
|
|
|
457
|
+
# The mouse: the events, their parser, and the tracking levels
|
|
458
|
+
# {Screen#run_event_loop} asks the terminal for. Who gets one is
|
|
459
|
+
# {Mouse::Router}'s story.
|
|
460
|
+
#
|
|
461
|
+
# Four of the five classes are what {.parse} reads off the wire; {DragEvent} is
|
|
462
|
+
# the router's own, a move while something holds the grab. Each is a
|
|
463
|
+
# `Data.define` including the {Event} marker — no inheritance, so a `case`
|
|
464
|
+
# matches either one class or `Mouse::Event` for all of them. Enter and exit
|
|
465
|
+
# have no class at all: nothing is parsed, they are the difference between two
|
|
466
|
+
# hovered chains.
|
|
467
|
+
#
|
|
468
|
+
# Coordinates are screen-absolute and 0-based everywhere.
|
|
469
|
+
module Mouse
|
|
470
|
+
LEVELS: ::Array[Symbol]
|
|
471
|
+
MODES: ::Hash[Symbol, Integer]
|
|
472
|
+
MODIFIER_BITS: Integer
|
|
473
|
+
|
|
474
|
+
# Normalizes a `capture_mouse:` argument to a level.
|
|
475
|
+
#
|
|
476
|
+
# _@param_ `capture_mouse` — `false`, `true` (== `:clicks`), or one of {LEVELS}.
|
|
477
|
+
#
|
|
478
|
+
# _@return_ — the level; nil when tracking is off.
|
|
479
|
+
def self.level: ((bool | Symbol) capture_mouse) -> Symbol?
|
|
480
|
+
|
|
481
|
+
# _@param_ `level` — one of {LEVELS}.
|
|
482
|
+
#
|
|
483
|
+
# _@return_ — the escape enabling that level.
|
|
484
|
+
def self.start_tracking: (Symbol level) -> String
|
|
485
|
+
|
|
486
|
+
# _@param_ `level` — one of {LEVELS}.
|
|
487
|
+
#
|
|
488
|
+
# _@return_ — the escape disabling that level.
|
|
489
|
+
def self.stop_tracking: (Symbol level) -> String
|
|
490
|
+
|
|
491
|
+
# Whether `key` is a mouse report. True on the X10 `\e[M` prefix
|
|
492
|
+
# regardless of length — {.parse} is the place that validates the full
|
|
493
|
+
# 6-byte shape and raises on malformed input.
|
|
494
|
+
#
|
|
495
|
+
# _@param_ `key` — key read via {Keys.getkey}
|
|
496
|
+
def self.report?: (String key) -> bool
|
|
497
|
+
|
|
498
|
+
# Parses an X10 mouse report (`\e[M` + 3 bytes: button, x, y) into one of
|
|
499
|
+
# {DownEvent}, {UpEvent}, {ScrollEvent} or {MoveEvent}. Modifier bits are
|
|
500
|
+
# ignored.
|
|
501
|
+
#
|
|
502
|
+
# Raises {Tuile::Error} when `key` starts with the mouse prefix but is
|
|
503
|
+
# not exactly 6 bytes long. Both shorter and longer inputs are bugs in
|
|
504
|
+
# the upstream key-reader: a shorter prefix means the tail was lost on
|
|
505
|
+
# the way in, and a longer one means we over-consumed into the next
|
|
506
|
+
# escape sequence. We refuse to silently truncate either case because
|
|
507
|
+
# the trailing `\e` of an over-read corrupts the *next* getkey, and the
|
|
508
|
+
# corruption then surfaces as garbled keystrokes in focused inputs
|
|
509
|
+
# rather than as a parser failure pointing at the actual cause.
|
|
510
|
+
#
|
|
511
|
+
# _@param_ `key` — key read via {Keys.getkey}
|
|
512
|
+
#
|
|
513
|
+
# _@return_ — `nil` if `key` is not a mouse report, or reports
|
|
514
|
+
# a wheel button beyond the four directions.
|
|
515
|
+
def self.parse: (String key) -> Event?
|
|
516
|
+
|
|
517
|
+
# _@param_ `low` — the code's two low bits.
|
|
518
|
+
def self.button: (Integer low) -> Symbol?
|
|
519
|
+
|
|
520
|
+
# Included by every mouse event class: a marker for `case`/`is_a?`, plus the
|
|
521
|
+
# {#point} they share.
|
|
522
|
+
module Event
|
|
523
|
+
# _@return_ — the event's position.
|
|
524
|
+
def point: () -> Point
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# A button went down.
|
|
528
|
+
#
|
|
529
|
+
# @!attribute [r] button
|
|
530
|
+
# @return [Symbol] `:left`, `:middle` or `:right`.
|
|
531
|
+
# @!attribute [r] x
|
|
532
|
+
# @return [Integer] 0-based column.
|
|
533
|
+
# @!attribute [r] y
|
|
534
|
+
# @return [Integer] 0-based row.
|
|
535
|
+
class DownEvent < Data
|
|
536
|
+
include Tuile::Mouse::Event
|
|
537
|
+
|
|
538
|
+
# _@return_ — the event's position.
|
|
539
|
+
def point: () -> Point
|
|
540
|
+
|
|
541
|
+
# _@return_ — `:left`, `:middle` or `:right`.
|
|
542
|
+
attr_reader button: Symbol
|
|
543
|
+
|
|
544
|
+
# _@return_ — 0-based column.
|
|
545
|
+
attr_reader x: Integer
|
|
546
|
+
|
|
547
|
+
# _@return_ — 0-based row.
|
|
548
|
+
attr_reader y: Integer
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# A button came up. **Deliberately no `button`**: an up reaches only the
|
|
552
|
+
# component that claimed the press, and that grab already knows which button
|
|
553
|
+
# it holds (`D_mouse_dispatch`).
|
|
554
|
+
#
|
|
555
|
+
# @!attribute [r] x
|
|
556
|
+
# @return [Integer] 0-based column.
|
|
557
|
+
# @!attribute [r] y
|
|
558
|
+
# @return [Integer] 0-based row.
|
|
559
|
+
class UpEvent < Data
|
|
560
|
+
include Tuile::Mouse::Event
|
|
561
|
+
|
|
562
|
+
# _@return_ — the event's position.
|
|
563
|
+
def point: () -> Point
|
|
564
|
+
|
|
565
|
+
# _@return_ — 0-based column.
|
|
566
|
+
attr_reader x: Integer
|
|
567
|
+
|
|
568
|
+
# _@return_ — 0-based row.
|
|
569
|
+
attr_reader y: Integer
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# One wheel notch. A notch is not a button, which is why this is not a
|
|
573
|
+
# {DownEvent}.
|
|
574
|
+
#
|
|
575
|
+
# @!attribute [r] direction
|
|
576
|
+
# @return [Symbol] `:up`, `:down`, `:left` or `:right`.
|
|
577
|
+
# @!attribute [r] x
|
|
578
|
+
# @return [Integer] 0-based column.
|
|
579
|
+
# @!attribute [r] y
|
|
580
|
+
# @return [Integer] 0-based row.
|
|
581
|
+
class ScrollEvent < Data
|
|
582
|
+
include Tuile::Mouse::Event
|
|
583
|
+
|
|
584
|
+
# _@return_ — the event's position.
|
|
585
|
+
def point: () -> Point
|
|
586
|
+
|
|
587
|
+
# _@return_ — `:up`, `:down`, `:left` or `:right`.
|
|
588
|
+
attr_reader direction: Symbol
|
|
589
|
+
|
|
590
|
+
# _@return_ — 0-based column.
|
|
591
|
+
attr_reader x: Integer
|
|
592
|
+
|
|
593
|
+
# _@return_ — 0-based row.
|
|
594
|
+
attr_reader y: Integer
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# The pointer moved and nothing holds the grab.
|
|
598
|
+
#
|
|
599
|
+
# @!attribute [r] button
|
|
600
|
+
# @return [Symbol, nil] the button held while moving, or nil.
|
|
601
|
+
# @!attribute [r] x
|
|
602
|
+
# @return [Integer] 0-based column.
|
|
603
|
+
# @!attribute [r] y
|
|
604
|
+
# @return [Integer] 0-based row.
|
|
605
|
+
class MoveEvent < Data
|
|
606
|
+
include Tuile::Mouse::Event
|
|
607
|
+
|
|
608
|
+
# _@return_ — the event's position.
|
|
609
|
+
def point: () -> Point
|
|
610
|
+
|
|
611
|
+
# _@return_ — the button held while moving, or nil.
|
|
612
|
+
attr_reader button: Symbol?
|
|
613
|
+
|
|
614
|
+
# _@return_ — 0-based column.
|
|
615
|
+
attr_reader x: Integer
|
|
616
|
+
|
|
617
|
+
# _@return_ — 0-based row.
|
|
618
|
+
attr_reader y: Integer
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
# The pointer moved while a component holds the grab.
|
|
622
|
+
#
|
|
623
|
+
# The one event {Router} *makes* rather than routes, so posting one to
|
|
624
|
+
# {Screen#handle_mouse} raises: drive a grabbed component with a
|
|
625
|
+
# {MoveEvent} carrying the held button, or with {FakeScreen#drag}.
|
|
626
|
+
#
|
|
627
|
+
# @!attribute [r] button
|
|
628
|
+
# @return [Symbol] the button whose press took the grab.
|
|
629
|
+
# @!attribute [r] x
|
|
630
|
+
# @return [Integer] 0-based column — may lie outside the grabbed
|
|
631
|
+
# component's rect, and even outside the screen's last known size.
|
|
632
|
+
# @!attribute [r] y
|
|
633
|
+
# @return [Integer] 0-based row.
|
|
634
|
+
class DragEvent < Data
|
|
635
|
+
include Tuile::Mouse::Event
|
|
636
|
+
|
|
637
|
+
# _@return_ — the event's position.
|
|
638
|
+
def point: () -> Point
|
|
639
|
+
|
|
640
|
+
# _@return_ — the button whose press took the grab.
|
|
641
|
+
attr_reader button: Symbol
|
|
642
|
+
|
|
643
|
+
# _@return_ — 0-based column — may lie outside the grabbed
|
|
644
|
+
# component's rect, and even outside the screen's last known size.
|
|
645
|
+
attr_reader x: Integer
|
|
646
|
+
|
|
647
|
+
# _@return_ — 0-based row.
|
|
648
|
+
attr_reader y: Integer
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# Delivers {Mouse::Event}s to the component tree. {Screen} owns one and hands
|
|
652
|
+
# it every event the terminal reports; a component only answers the handlers
|
|
653
|
+
# it calls, and walks nothing itself.
|
|
654
|
+
#
|
|
655
|
+
# screen.handle_mouse(Mouse::DownEvent.new(:left, 5, 2)) # what the loop does
|
|
656
|
+
#
|
|
657
|
+
# Every event resolves against one **path**: the topmost popup containing the
|
|
658
|
+
# point, else the tiled content unless a modal popup is open
|
|
659
|
+
# ({ScreenPane#mouse_root_at}), then down through the shown children whose
|
|
660
|
+
# {Component#rect} contains it.
|
|
661
|
+
#
|
|
662
|
+
# - **{DownEvent}** — focuses the innermost {Component#focusable?} on that
|
|
663
|
+
# path, then offers {Component#handle_mouse_down?} innermost-first until one
|
|
664
|
+
# component answers `true`. That claimant becomes {#grabbed}.
|
|
665
|
+
# - **{ScrollEvent}**, **{MoveEvent}** — bubble the same way through
|
|
666
|
+
# {Component#handle_mouse_scroll?} / {Component#handle_mouse_move?}, and grab
|
|
667
|
+
# nothing. A move fires the enter/exit hooks first, and arrives only at
|
|
668
|
+
# `:hover`.
|
|
669
|
+
# - **{DragEvent}**, **{UpEvent}** — skip the path entirely: they go to
|
|
670
|
+
# {#grabbed}, wherever the pointer is, and an up ends the grab.
|
|
671
|
+
#
|
|
672
|
+
# The two geometries are deliberately different. Focus follows `rect`, so a
|
|
673
|
+
# press on the dead tail a widget does not paint still focuses it; the
|
|
674
|
+
# handlers bubble only along the prefix whose {Component#extent_rect} contains
|
|
675
|
+
# the point, so that same press activates nothing (`D_extent`).
|
|
676
|
+
#
|
|
677
|
+
# UI-thread-confined.
|
|
678
|
+
#
|
|
679
|
+
# == Implementation details
|
|
680
|
+
#
|
|
681
|
+
# **The grab has three releases and no relinquishing**: the up, the next
|
|
682
|
+
# down (the up was lost — ssh and tmux do lose them), and any key
|
|
683
|
+
# ({#release_grab}). None of the last two tells the grabbed component. Hiding
|
|
684
|
+
# or detaching it does not end the grab either; the router just stops
|
|
685
|
+
# delivering to it until the release (`D_mouse_dispatch`).
|
|
686
|
+
#
|
|
687
|
+
# **The hovered chain is synced, not toggled**: a move re-resolves it and
|
|
688
|
+
# diffs, and {#sync_hover} — run by {Screen#repaint} — drops members that
|
|
689
|
+
# were detached, hidden or reparented since, firing their exits.
|
|
690
|
+
class Router
|
|
691
|
+
# _@param_ `screen`
|
|
692
|
+
def initialize: (Screen screen) -> void
|
|
693
|
+
|
|
694
|
+
# _@return_ — the innermost component under the pointer, as of
|
|
695
|
+
# the last move under `:hover`.
|
|
696
|
+
def hovered: () -> Component?
|
|
697
|
+
|
|
698
|
+
# _@param_ `event`
|
|
699
|
+
def dispatch: (Mouse::Event event) -> void
|
|
700
|
+
|
|
701
|
+
# Ends the grab without telling the grabbed component.
|
|
702
|
+
def release_grab: () -> void
|
|
703
|
+
|
|
704
|
+
# Drops the hovered-chain members that are no longer attached, shown, or
|
|
705
|
+
# children of the member before them, firing
|
|
706
|
+
# {Component#handle_mouse_exit} innermost first. Idempotent.
|
|
707
|
+
def sync_hover: () -> void
|
|
708
|
+
|
|
709
|
+
# _@param_ `event`
|
|
710
|
+
def press: (DownEvent event) -> void
|
|
711
|
+
|
|
712
|
+
# _@param_ `event`
|
|
713
|
+
def release: (UpEvent event) -> void
|
|
714
|
+
|
|
715
|
+
# _@param_ `event`
|
|
716
|
+
def move: (MoveEvent event) -> void
|
|
717
|
+
|
|
718
|
+
# A non-modal overlay is never focused into: it sits outside the key scope,
|
|
719
|
+
# so focus there would make every keystroke go dead (`D_overlay`).
|
|
720
|
+
#
|
|
721
|
+
# _@param_ `root`
|
|
722
|
+
#
|
|
723
|
+
# _@param_ `path`
|
|
724
|
+
def focus_innermost: (Component? root, ::Array[Component] path) -> void
|
|
725
|
+
|
|
726
|
+
# _@param_ `path` — root first.
|
|
727
|
+
#
|
|
728
|
+
# _@param_ `handler` — a routed `handle_mouse_…?`.
|
|
729
|
+
#
|
|
730
|
+
# _@param_ `event`
|
|
731
|
+
#
|
|
732
|
+
# _@return_ — the component that answered true.
|
|
733
|
+
def bubble: (::Array[Component] path, Symbol handler, Mouse::Event event) -> Component?
|
|
734
|
+
|
|
735
|
+
# _@param_ `point`
|
|
736
|
+
#
|
|
737
|
+
# _@return_ — the shown components under `point` whose
|
|
738
|
+
# extent contains it, root first.
|
|
739
|
+
def extent_path: (Point point) -> ::Array[Component]
|
|
740
|
+
|
|
741
|
+
# _@param_ `root`
|
|
742
|
+
#
|
|
743
|
+
# _@param_ `point`
|
|
744
|
+
#
|
|
745
|
+
# _@return_ — the shown components whose rect contains
|
|
746
|
+
# `point`, root first. Tiled siblings never overlap, so at most one child
|
|
747
|
+
# qualifies at each level.
|
|
748
|
+
def rect_path: (Component? root, Point point) -> ::Array[Component]
|
|
749
|
+
|
|
750
|
+
# _@param_ `chain` — the new hovered chain, root first.
|
|
751
|
+
def rehover: (::Array[Component] chain) -> void
|
|
752
|
+
|
|
753
|
+
# _@param_ `component`
|
|
754
|
+
#
|
|
755
|
+
# _@return_ — whether `component` is attached and it and every
|
|
756
|
+
# ancestor are shown.
|
|
757
|
+
def reachable?: (Component? component) -> bool
|
|
758
|
+
|
|
759
|
+
# The tracking level {Screen#run_event_loop} asked the terminal for, one of
|
|
760
|
+
# {Mouse::LEVELS}; `:hover` while no loop is running, so a spec may post
|
|
761
|
+
# anything. An ungrabbed move is dropped below `:hover` — under `:drag` the
|
|
762
|
+
# terminal reports one only while a button nobody claimed is held, and
|
|
763
|
+
# enter/exit that fire *sometimes* are worse than none.
|
|
764
|
+
attr_accessor level: Symbol?
|
|
765
|
+
|
|
766
|
+
# _@return_ — the component whose {Component#handle_mouse_down?}
|
|
767
|
+
# claimed the press currently held.
|
|
768
|
+
attr_reader grabbed: Component?
|
|
769
|
+
end
|
|
770
|
+
end
|
|
771
|
+
|
|
457
772
|
# A point with `x` and `y` integer coordinates, both 0-based.
|
|
458
773
|
#
|
|
459
774
|
# @!attribute [r] x
|
|
@@ -476,12 +791,12 @@ module Tuile
|
|
|
476
791
|
# restyles everything via one invalidate-everything pass. Book ch6 is the
|
|
477
792
|
# concept in full (why accents-only, dark/light, live OS flips).
|
|
478
793
|
#
|
|
479
|
-
# The rendering helpers — {#active_bg}, {#active_border}, {#input_bg}
|
|
480
|
-
#
|
|
481
|
-
#
|
|
794
|
+
# The rendering helpers — {#active_bg}, {#active_border}, {#input_bg} — wrap
|
|
795
|
+
# a plain string in the token's SGR color (on the channel appropriate for the
|
|
796
|
+
# token's role) and reset:
|
|
482
797
|
#
|
|
483
|
-
# screen.theme.active_bg("[ Ok ]")
|
|
484
|
-
# screen.theme.
|
|
798
|
+
# screen.theme.active_bg("[ Ok ]") # => "\e[48;5;59m[ Ok ]\e[0m"
|
|
799
|
+
# screen.theme.active_border("┌────┐") # => "\e[32m┌────┐\e[0m"
|
|
485
800
|
#
|
|
486
801
|
# Content passes through verbatim (so it may carry other escapes). For
|
|
487
802
|
# span-aware styling — a token applied to a {StyledString} without flattening
|
|
@@ -513,7 +828,7 @@ module Tuile
|
|
|
513
828
|
#
|
|
514
829
|
# For a color slot resolved *live* at paint — currently
|
|
515
830
|
# {Component#bg_color=} — assign a {Ref} instead of reading + rebuilding
|
|
516
|
-
# the token in {Component#
|
|
831
|
+
# the token in {Component#handle_theme_changed}; it tracks theme swaps on its
|
|
517
832
|
# own. Baked content colors ({Component::Label} text and friends) can't:
|
|
518
833
|
# they live in a frozen {StyledString} and still need the hook.
|
|
519
834
|
#
|
|
@@ -533,12 +848,6 @@ module Tuile
|
|
|
533
848
|
# {Component::TextArea} when *not* active — visibly a field, but
|
|
534
849
|
# distinctly subtler than {#active_bg_color}.
|
|
535
850
|
# @return [Color]
|
|
536
|
-
# @!attribute [r] hint_color
|
|
537
|
-
# Foreground of subdued *accent* text an app wants noticed — a
|
|
538
|
-
# keyboard-shortcut caption, a {Component::PickerWindow} option. An accent,
|
|
539
|
-
# not a grey: it pulls the eye, which is why a placeholder uses
|
|
540
|
-
# {#placeholder_color} instead. See {#hint}.
|
|
541
|
-
# @return [Color]
|
|
542
851
|
# @!attribute [r] placeholder_color
|
|
543
852
|
# Foreground of the hint a field paints into its own empty well
|
|
544
853
|
# ({Component::HasPlaceholder}) — the one token tuned to be *barely*
|
|
@@ -583,8 +892,6 @@ module Tuile
|
|
|
583
892
|
#
|
|
584
893
|
# _@param_ `input_bg_color`
|
|
585
894
|
#
|
|
586
|
-
# _@param_ `hint_color`
|
|
587
|
-
#
|
|
588
895
|
# _@param_ `placeholder_color`
|
|
589
896
|
#
|
|
590
897
|
# _@param_ `error_color`
|
|
@@ -600,7 +907,6 @@ module Tuile
|
|
|
600
907
|
active_bg_color: Color,
|
|
601
908
|
active_border_color: Color,
|
|
602
909
|
input_bg_color: Color,
|
|
603
|
-
hint_color: Color,
|
|
604
910
|
placeholder_color: Color,
|
|
605
911
|
error_color: Color,
|
|
606
912
|
error_bg_color: Color,
|
|
@@ -627,7 +933,14 @@ module Tuile
|
|
|
627
933
|
def self.ref: (Symbol name) -> Ref
|
|
628
934
|
|
|
629
935
|
# Renders `text` in the foreground color of the app-specific `token`
|
|
630
|
-
# — the generic counterpart of {#
|
|
936
|
+
# — the generic counterpart of {#active_border} for {#custom} tokens, and
|
|
937
|
+
# the route for an app's own status-line chrome:
|
|
938
|
+
#
|
|
939
|
+
# "q #{screen.theme.fg(:hint, "quit")}" # => "q \e[38;5;245mquit\e[0m"
|
|
940
|
+
#
|
|
941
|
+
# The color is baked into the returned String, so text built this way does
|
|
942
|
+
# *not* restyle on a {Screen#theme=} — rebuild it from
|
|
943
|
+
# {Component#handle_theme_changed} instead.
|
|
631
944
|
#
|
|
632
945
|
# _@param_ `token`
|
|
633
946
|
#
|
|
@@ -669,17 +982,6 @@ module Tuile
|
|
|
669
982
|
# _@return_ — ANSI-rendered text, ending with an SGR reset.
|
|
670
983
|
def input_bg: (String text) -> String
|
|
671
984
|
|
|
672
|
-
# Renders `text` in the {#hint_color} foreground, for status-bar hints,
|
|
673
|
-
# e.g. `"q #{screen.theme.hint("quit")}"`. The color is baked into the
|
|
674
|
-
# returned String, so strings built this way do *not* restyle when the
|
|
675
|
-
# theme changes — rebuild them instead (the framework's own call sites
|
|
676
|
-
# rebuild on every status-bar refresh).
|
|
677
|
-
#
|
|
678
|
-
# _@param_ `text`
|
|
679
|
-
#
|
|
680
|
-
# _@return_ — ANSI-rendered text, ending with an SGR reset.
|
|
681
|
-
def hint: (String text) -> String
|
|
682
|
-
|
|
683
985
|
# The single sanctioned place for verbatim SGR wrapping: `text` is not
|
|
684
986
|
# parsed or validated, so callers may embed non-SGR escapes. Emits the
|
|
685
987
|
# same bytes `StyledString.styled(text, ...).to_ansi` would for plain
|
|
@@ -708,12 +1010,6 @@ module Tuile
|
|
|
708
1010
|
# distinctly subtler than {#active_bg_color}.
|
|
709
1011
|
attr_reader input_bg_color: Color
|
|
710
1012
|
|
|
711
|
-
# Foreground of subdued *accent* text an app wants noticed — a
|
|
712
|
-
# keyboard-shortcut caption, a {Component::PickerWindow} option. An accent,
|
|
713
|
-
# not a grey: it pulls the eye, which is why a placeholder uses
|
|
714
|
-
# {#placeholder_color} instead. See {#hint}.
|
|
715
|
-
attr_reader hint_color: Color
|
|
716
|
-
|
|
717
1013
|
# Foreground of the hint a field paints into its own empty well
|
|
718
1014
|
# ({Component::HasPlaceholder}) — the one token tuned to be *barely*
|
|
719
1015
|
# visible, since a placeholder the user misses costs nothing.
|
|
@@ -750,7 +1046,7 @@ module Tuile
|
|
|
750
1046
|
# A live reference to a theme token, resolved against the current theme at
|
|
751
1047
|
# paint time rather than baked to a concrete {Color}. Assign one where a
|
|
752
1048
|
# slot is resolved late — currently {Component#bg_color=} — and it follows
|
|
753
|
-
# light/dark flips with no {Component#
|
|
1049
|
+
# light/dark flips with no {Component#handle_theme_changed} hook:
|
|
754
1050
|
#
|
|
755
1051
|
# panel.bg_color = Tuile::Theme.ref(:panel_bg) # a #custom token
|
|
756
1052
|
# dropdown.bg_color = Tuile::Theme.ref(:input_bg_color) # built-in chrome
|
|
@@ -1973,17 +2269,17 @@ module Tuile
|
|
|
1973
2269
|
def add_popup: (Component::Overlay window) -> void
|
|
1974
2270
|
|
|
1975
2271
|
# Runs the event loop on the calling thread, taking over stdin (raw mode,
|
|
1976
|
-
# echo off): keys and mouse events are dispatched via {#handle_key} /
|
|
2272
|
+
# echo off): keys and mouse events are dispatched via {#handle_key?} /
|
|
1977
2273
|
# {#handle_mouse}, and the loop repaints once per drained tick. Returns
|
|
1978
2274
|
# when `q` or ESC is pressed unhandled. Restores terminal state on exit.
|
|
1979
2275
|
#
|
|
1980
2276
|
# For the duration this thread owns the UI ({#state} is `:running`);
|
|
1981
2277
|
# ownership reverts to the creating thread once it returns.
|
|
1982
2278
|
#
|
|
1983
|
-
# _@param_ `capture_mouse` —
|
|
2279
|
+
# _@param_ `capture_mouse` — how much mouse tracking to ask the terminal for — each level unlocking one tier of {Mouse}'s events, and costing what that tier reports: - `false` — none. The terminal keeps its native click handling, which is what you want if the app benefits more from select-to-copy. - `true` (default), `:clicks` — presses, releases and the wheel. - `:drag` — plus motion while a button is held, as {Component#handle_mouse_drag}. - `:hover` — plus motion with no button, as {Component#handle_mouse_move?} and the enter/exit hooks. ~84 reports a second (`R_mouse_reporting`), so ask for it only if something uses it.
|
|
1984
2280
|
#
|
|
1985
2281
|
# _@param_ `bracketed_paste` — when true (default), enables DEC private mode 2004 so pasted text arrives whole, as {Component#handle_paste}, instead of as one keystroke per character — which is the only way a pasted line break can be told from a typed Enter. When false, a paste streams in as keys again and a component that gives ENTER a meaning fires it once per pasted line. Turn it off only for a terminal that mishandles the mode.
|
|
1986
|
-
def run_event_loop: (?capture_mouse: bool, ?bracketed_paste: bool) -> void
|
|
2282
|
+
def run_event_loop: (?capture_mouse: (bool | Symbol), ?bracketed_paste: bool) -> void
|
|
1987
2283
|
|
|
1988
2284
|
# Advances focus to the next {Component#tab_stop?} in tree order, wrapping
|
|
1989
2285
|
# around. Scope is the topmost popup if one is open, otherwise {#content}
|
|
@@ -2008,9 +2304,9 @@ module Tuile
|
|
|
2008
2304
|
#
|
|
2009
2305
|
# - **Printable keys** — they'd hijack typing into a
|
|
2010
2306
|
# {Component::TextField}. A scope-wide one-key binding belongs on the
|
|
2011
|
-
# scope root's own `handle_key
|
|
2012
|
-
# (see {ScreenPane#handle_key}).
|
|
2013
|
-
# - **TAB / SHIFT_TAB** — {#handle_key} intercepts them for focus
|
|
2307
|
+
# scope root's own `handle_key?`, where a focused field consumes it first
|
|
2308
|
+
# (see {ScreenPane#handle_key?}).
|
|
2309
|
+
# - **TAB / SHIFT_TAB** — {#handle_key?} intercepts them for focus
|
|
2014
2310
|
# navigation before the registry is consulted, so a binding would never
|
|
2015
2311
|
# fire.
|
|
2016
2312
|
# - **{EDITING_KEYS}** — `ENTER`, `BACKSPACE`, `DELETE` and the arrows,
|
|
@@ -2063,7 +2359,7 @@ module Tuile
|
|
|
2063
2359
|
|
|
2064
2360
|
# Tears the screen down and vacates the singleton slot, moving {#state} to
|
|
2065
2361
|
# the terminal `:closed`. Unmounts the tree first, so every component gets
|
|
2066
|
-
# its {Component#
|
|
2362
|
+
# its {Component#handle_detached}. Idempotent.
|
|
2067
2363
|
def close: () -> void
|
|
2068
2364
|
|
|
2069
2365
|
def self.close: () -> void
|
|
@@ -2111,13 +2407,31 @@ module Tuile
|
|
|
2111
2407
|
# but only one focused.
|
|
2112
2408
|
def cursor_position: () -> Point?
|
|
2113
2409
|
|
|
2410
|
+
# Routes one mouse event into the tree ({Mouse::Router}) — what the event
|
|
2411
|
+
# loop does with every report the terminal sends, and how a spec drives the
|
|
2412
|
+
# mouse:
|
|
2413
|
+
#
|
|
2414
|
+
# screen.handle_mouse(Mouse::DownEvent.new(:left, 5, 2))
|
|
2415
|
+
#
|
|
2416
|
+
# _@param_ `event`
|
|
2417
|
+
def handle_mouse: (Mouse::Event event) -> void
|
|
2418
|
+
|
|
2419
|
+
# _@return_ — the innermost component the pointer is over, as
|
|
2420
|
+
# of the last move. Always nil below `capture_mouse: :hover`, and frozen
|
|
2421
|
+
# at its last value while a press is grabbed.
|
|
2422
|
+
def hovered: () -> Component?
|
|
2423
|
+
|
|
2424
|
+
# _@return_ — the component holding the mouse grab — the one
|
|
2425
|
+
# whose {Component#handle_mouse_down?} claimed the press still held.
|
|
2426
|
+
def grabbed: () -> Component?
|
|
2427
|
+
|
|
2114
2428
|
# Whether `component` is out of the user's reach because it or an ancestor
|
|
2115
2429
|
# is {Component#visible? hidden}.
|
|
2116
2430
|
#
|
|
2117
2431
|
# Private, not a `Component#shown?`, for the reason `D_empty_ancestor`
|
|
2118
2432
|
# declined a `Component#paintable?`: it reads as a component-level concept
|
|
2119
2433
|
# and is really this class's question. A *walk* needs no such predicate —
|
|
2120
|
-
# it prunes at the hidden subtree's root ({Component#
|
|
2434
|
+
# it prunes at the hidden subtree's root ({Component#walk_shown_tree}).
|
|
2121
2435
|
#
|
|
2122
2436
|
# _@param_ `component`
|
|
2123
2437
|
def hidden?: (Component component) -> bool
|
|
@@ -2164,7 +2478,7 @@ module Tuile
|
|
|
2164
2478
|
# thread as an {EventQueue::BackgroundColorEvent}.
|
|
2165
2479
|
#
|
|
2166
2480
|
# _@param_ `scheme` — `:dark` or `:light`.
|
|
2167
|
-
def
|
|
2481
|
+
def handle_color_scheme: (Symbol scheme) -> void
|
|
2168
2482
|
|
|
2169
2483
|
# The re-probe answered: adopt the color and restyle, since an app's
|
|
2170
2484
|
# background-derived tints are now a scheme behind. Deliberately keeps
|
|
@@ -2173,7 +2487,7 @@ module Tuile
|
|
|
2173
2487
|
# would otherwise lose the color it gave us at startup, permanently.
|
|
2174
2488
|
#
|
|
2175
2489
|
# _@param_ `color`
|
|
2176
|
-
def
|
|
2490
|
+
def handle_background_color: (Color color) -> void
|
|
2177
2491
|
|
|
2178
2492
|
# Walks the current modal scope in pre-order, collects tab stops, and
|
|
2179
2493
|
# advances focus by one (wrapping). When the focused component isn't in
|
|
@@ -2215,18 +2529,13 @@ module Tuile
|
|
|
2215
2529
|
# default `over_popups: false` fires only when no modal popup is open
|
|
2216
2530
|
# (otherwise the modal popup receives the key normally). A non-modal
|
|
2217
2531
|
# overlay doesn't suppress global shortcuts.
|
|
2218
|
-
# 3. {ScreenPane#handle_key} — delivery to {#focused}, bubbling up the
|
|
2532
|
+
# 3. {ScreenPane#handle_key?} — delivery to {#focused}, bubbling up the
|
|
2219
2533
|
# focus chain to the scope root.
|
|
2220
2534
|
#
|
|
2221
2535
|
# _@param_ `key`
|
|
2222
2536
|
#
|
|
2223
2537
|
# _@return_ — true if the key was handled by some window.
|
|
2224
|
-
def handle_key
|
|
2225
|
-
|
|
2226
|
-
# Finds target window and calls {Component::Window#handle_mouse}.
|
|
2227
|
-
#
|
|
2228
|
-
# _@param_ `event`
|
|
2229
|
-
def handle_mouse: (MouseEvent event) -> void
|
|
2538
|
+
def handle_key?: (String key) -> bool
|
|
2230
2539
|
|
|
2231
2540
|
# Delivers pasted text to {#focused} ({ScreenPane#handle_paste}).
|
|
2232
2541
|
#
|
|
@@ -2234,12 +2543,11 @@ module Tuile
|
|
|
2234
2543
|
# Tab traversal and the global-shortcut registry entirely, goes straight to
|
|
2235
2544
|
# delivery, and does not bubble to ancestors the way a key does. Unhandled
|
|
2236
2545
|
# text is dropped — there is no fallback that replays it as keys, which
|
|
2237
|
-
# would put back the very ambiguity mode 2004 exists to remove
|
|
2546
|
+
# would put back the very ambiguity mode 2004 exists to remove — and with no
|
|
2547
|
+
# alternative delivery, no verdict to carry either.
|
|
2238
2548
|
#
|
|
2239
2549
|
# _@param_ `text`
|
|
2240
|
-
|
|
2241
|
-
# _@return_ — true if the focused component consumed it.
|
|
2242
|
-
def handle_paste: (String text) -> bool
|
|
2550
|
+
def handle_paste: (String text) -> void
|
|
2243
2551
|
|
|
2244
2552
|
def event_loop: () -> void
|
|
2245
2553
|
|
|
@@ -2318,7 +2626,7 @@ module Tuile
|
|
|
2318
2626
|
# Kept current across OS appearance flips, a frame behind: the flip
|
|
2319
2627
|
# report carries light/dark only, so the screen re-probes and this
|
|
2320
2628
|
# updates when the reply lands. A changed color then fires
|
|
2321
|
-
# {Component#
|
|
2629
|
+
# {Component#handle_theme_changed} across the tree exactly as a theme swap
|
|
2322
2630
|
# does — a background-derived tint *is* a theme-derived color.
|
|
2323
2631
|
attr_reader background_color: Color?
|
|
2324
2632
|
|
|
@@ -2348,17 +2656,17 @@ module Tuile
|
|
|
2348
2656
|
#
|
|
2349
2657
|
# screen.on_focus_changed = -> { bar.text = hint_for(screen.focused) }
|
|
2350
2658
|
#
|
|
2351
|
-
# **Edge-triggered**, like {Component#
|
|
2659
|
+
# **Edge-triggered**, like {Component#handle_attached}: re-assigning the
|
|
2352
2660
|
# component that already has focus fires nothing, so a callback can be as
|
|
2353
2661
|
# expensive as rebuilding a hint string without a `did it really change?`
|
|
2354
2662
|
# guard of its own. That matters more than it looks — `ScreenPane#content=`
|
|
2355
2663
|
# clears focus on every content swap, which on a level-triggered hook would
|
|
2356
2664
|
# fire a nil→nil notification during assembly.
|
|
2357
2665
|
#
|
|
2358
|
-
# It runs *after* the active-flag cascade and `
|
|
2666
|
+
# It runs *after* the active-flag cascade and `handle_focus`, so the tree is
|
|
2359
2667
|
# settled. Two things a callback must tolerate: {#focused} being `nil`, and
|
|
2360
2668
|
# firing during {#close} — teardown clears focus, exactly as it fires
|
|
2361
|
-
# {Component#
|
|
2669
|
+
# {Component#handle_detached}. A raising callback propagates out of {#focused=}
|
|
2362
2670
|
# and leaves focus assigned; keep it trivial, as with the attach hooks.
|
|
2363
2671
|
attr_accessor on_focus_changed: Proc?
|
|
2364
2672
|
|
|
@@ -2377,7 +2685,7 @@ module Tuile
|
|
|
2377
2685
|
# Finds a component in the tree, so a spec can drive the UI it built four
|
|
2378
2686
|
# layers down:
|
|
2379
2687
|
#
|
|
2380
|
-
# Testing.get(Component::Button, caption: "Save").handle_key(Keys::ENTER)
|
|
2688
|
+
# Testing.get(Component::Button, caption: "Save").handle_key?(Keys::ENTER)
|
|
2381
2689
|
# Testing.get(id: :name).value = "Zaphod"
|
|
2382
2690
|
# Testing.find(Component::Checkbox, in: pane, count: 3)
|
|
2383
2691
|
#
|
|
@@ -2393,7 +2701,7 @@ module Tuile
|
|
|
2393
2701
|
#
|
|
2394
2702
|
# **A hidden component is never found**: these simulate a user, and a spec
|
|
2395
2703
|
# that drove a hidden {Component::Button} would pass against a form nobody
|
|
2396
|
-
# can operate. Both walk {Component#
|
|
2704
|
+
# can operate. Both walk {Component#walk_shown_tree}; a failed lookup says how
|
|
2397
2705
|
# many hidden components *would* have matched, and {.dump} shows them.
|
|
2398
2706
|
#
|
|
2399
2707
|
# Testing.find(Component::TextField, count: 0) # the user can't reach it
|
|
@@ -2404,7 +2712,7 @@ module Tuile
|
|
|
2404
2712
|
#
|
|
2405
2713
|
# For *what a component shows*, assert on {Screen#buffer} instead — this
|
|
2406
2714
|
# locates and drives, it does not replace that channel. See book ch8 for the
|
|
2407
|
-
# worked usage and `
|
|
2715
|
+
# worked usage and `design/decisions.md` `D_component_lookup` for the design.
|
|
2408
2716
|
module Testing
|
|
2409
2717
|
# Every component in the searched tree matching the spec, in pre-order.
|
|
2410
2718
|
#
|
|
@@ -2579,6 +2887,29 @@ module Tuile
|
|
|
2579
2887
|
# isn't {Screen#pane}) is never enqueued for repaint via {#invalidate}, and
|
|
2580
2888
|
# any stale invalidation entries are filtered out at drain time. Subclasses
|
|
2581
2889
|
# can paint freely in {#repaint} without re-asserting attachment.
|
|
2890
|
+
#
|
|
2891
|
+
# == Handlers and listener slots
|
|
2892
|
+
#
|
|
2893
|
+
# Two families, told apart by the `=`:
|
|
2894
|
+
#
|
|
2895
|
+
# class Trimmed < Component::TextField
|
|
2896
|
+
# def handle_blur # handle_ — the override point
|
|
2897
|
+
# super
|
|
2898
|
+
# self.text = text.strip
|
|
2899
|
+
# end
|
|
2900
|
+
# end
|
|
2901
|
+
#
|
|
2902
|
+
# label.on_theme_changed = -> { … } # on_…= — the listener slot
|
|
2903
|
+
#
|
|
2904
|
+
# **What a handler returns is per hook**, declared in its own rdoc. Only the
|
|
2905
|
+
# ones a dispatcher routes answer at all — {#handle_key?},
|
|
2906
|
+
# {#handle_text_input_key?}, `MenuBar#handle_mnemonic?` — where `true` means "I
|
|
2907
|
+
# took this, stop bubbling". The rest, {#handle_paste} included, return `void`.
|
|
2908
|
+
#
|
|
2909
|
+
# An override calls `super`, even where the base body is empty: that is what
|
|
2910
|
+
# lets a hook grow an `on_foo=` slot without breaking you. The one carve-out is
|
|
2911
|
+
# {#handle_child_removed}, whose base does real work and whose overrides
|
|
2912
|
+
# replace it. `D_handler_naming` carries the argument.
|
|
2582
2913
|
class Component
|
|
2583
2914
|
extend Tuile::Final
|
|
2584
2915
|
BG_STATES: ::Array[Symbol]
|
|
@@ -2618,7 +2949,7 @@ module Tuile
|
|
|
2618
2949
|
# It flows **downward only**: no container consults it when dividing space,
|
|
2619
2950
|
# so {#rect} still means exactly what the parent assigned (`D_extent`). Three
|
|
2620
2951
|
# things read it, all of them this component or the framework painting it:
|
|
2621
|
-
# {#clear_outside_extent} blanks the dead tail, {
|
|
2952
|
+
# {#clear_outside_extent} blanks the dead tail, {Mouse::Router} hit-tests
|
|
2622
2953
|
# against it so a click on that tail doesn't activate the widget, and a
|
|
2623
2954
|
# dropdown anchors under it rather than under unused space.
|
|
2624
2955
|
#
|
|
@@ -2629,7 +2960,7 @@ module Tuile
|
|
|
2629
2960
|
def extent: () -> Size?
|
|
2630
2961
|
|
|
2631
2962
|
# {#extent} placed at {#rect}'s top-left, for the consumers that need
|
|
2632
|
-
# coordinates: `extent_rect.contains?(
|
|
2963
|
+
# coordinates: `extent_rect.contains?(point)` in {Mouse::Router}, and
|
|
2633
2964
|
# the anchor a dropdown hangs from. Total — an undeclared {#extent} yields
|
|
2634
2965
|
# the whole {#rect}, so a generic caller never sees `nil`.
|
|
2635
2966
|
def extent_rect: () -> Rect
|
|
@@ -2659,7 +2990,7 @@ module Tuile
|
|
|
2659
2990
|
# showing it restores exactly the subtree that was showing before.
|
|
2660
2991
|
# - **Focus never stays on what the user cannot see.** Hiding the subtree
|
|
2661
2992
|
# holding focus repairs it exactly as removing that subtree would (see
|
|
2662
|
-
# {#
|
|
2993
|
+
# {#handle_child_removed}), and does not hand it back on the way in.
|
|
2663
2994
|
#
|
|
2664
2995
|
# For "invisible but still occupying its space", use a
|
|
2665
2996
|
# {Component::Slot} with no content (`D_slots`).
|
|
@@ -2688,6 +3019,9 @@ module Tuile
|
|
|
2688
3019
|
# skipping `super`.** The clear then covers only what is outside it, so the
|
|
2689
3020
|
# cells it is about to repaint are not blanked first — blanking them would
|
|
2690
3021
|
# mark them dirty and make {Buffer#flush} re-emit them (`D_progress_bar`).
|
|
3022
|
+
# That saving is a *leaf*'s: a container's children paint its extent for it,
|
|
3023
|
+
# so a cell among them that none covers still gets blanked — an extent
|
|
3024
|
+
# narrows which cells are yours, never whether your gaps are wiped.
|
|
2691
3025
|
#
|
|
2692
3026
|
# **The children are re-invalidated whether or not they tile.** A container
|
|
2693
3027
|
# that paints nothing of its own can only redraw its area *through* them, so
|
|
@@ -2709,48 +3043,108 @@ module Tuile
|
|
|
2709
3043
|
# on the key alone and never gate on your own {#active?} state. See book ch5
|
|
2710
3044
|
# for how a keystroke is routed to reach here.
|
|
2711
3045
|
#
|
|
3046
|
+
# The `?` reads like `Set#add?`: calling it *delivers* the key and reports
|
|
3047
|
+
# whether it was taken, so it is never a "would you handle this?" probe.
|
|
3048
|
+
#
|
|
2712
3049
|
# _@param_ `_key` — a key.
|
|
2713
3050
|
#
|
|
2714
3051
|
# _@return_ — true if the key was handled, false if not.
|
|
2715
|
-
def handle_key
|
|
3052
|
+
def handle_key?: (String _key) -> bool
|
|
2716
3053
|
|
|
2717
3054
|
# Called when text is pasted while this component is {Screen#focused};
|
|
2718
|
-
# override to accept it
|
|
2719
|
-
#
|
|
2720
|
-
#
|
|
2721
|
-
# its descendant declined. The text arrives whole and `\n`-normalized, so
|
|
2722
|
-
# `text.lines.size` is the paste's line count and a single mutation can
|
|
2723
|
-
# absorb it:
|
|
3055
|
+
# override to accept it. The default drops the text. It arrives whole and
|
|
3056
|
+
# `\n`-normalized, so `text.lines.size` is the paste's line count and a
|
|
3057
|
+
# single mutation can absorb it:
|
|
2724
3058
|
#
|
|
2725
3059
|
# def handle_paste(text)
|
|
2726
3060
|
# self.caption = "[Pasted #{text.lines.size} lines]"
|
|
2727
|
-
# true
|
|
2728
3061
|
# end
|
|
2729
3062
|
#
|
|
3063
|
+
# **No verdict, unlike {#handle_key?}**: a paste reaches the focused component
|
|
3064
|
+
# and stops, so one that declines has nowhere to hand it on to
|
|
3065
|
+
# (`D_bracketed_paste`).
|
|
3066
|
+
#
|
|
2730
3067
|
# Reaching here means the terminal said "this came from the clipboard" —
|
|
2731
3068
|
# {Component::AbstractStringField} inserts it at the caret, which is why a
|
|
2732
3069
|
# subclass that rebinds ENTER to submit needs no paste handling of its own
|
|
2733
3070
|
# to stop firing once per pasted line.
|
|
2734
3071
|
#
|
|
2735
3072
|
# _@param_ `_text` — the pasted text.
|
|
3073
|
+
def handle_paste: (String _text) -> void
|
|
3074
|
+
|
|
3075
|
+
# Called when a mouse button goes down over this component; answer `true` to
|
|
3076
|
+
# claim the press. The default claims nothing.
|
|
3077
|
+
#
|
|
3078
|
+
# def handle_mouse_down?(event)
|
|
3079
|
+
# return false unless event.button == :left
|
|
2736
3080
|
#
|
|
2737
|
-
#
|
|
2738
|
-
|
|
3081
|
+
# @on_click&.call
|
|
3082
|
+
# true
|
|
3083
|
+
# end
|
|
3084
|
+
#
|
|
3085
|
+
# {Mouse::Router} delivers it to the innermost component under the pointer
|
|
3086
|
+
# and bubbles it up the ancestors until one answers `true`; the claimant
|
|
3087
|
+
# then holds the *grab*, and the button's {#handle_mouse_drag} and
|
|
3088
|
+
# {#handle_mouse_up} go to it alone. The press has already moved focus by
|
|
3089
|
+
# the time it arrives, and it only arrives where {#extent_rect} contains the
|
|
3090
|
+
# point, so an override needs neither `super` nor a hit test of its own.
|
|
3091
|
+
#
|
|
3092
|
+
# Activate here, on the press: Tuile synthesizes no click, because a release
|
|
3093
|
+
# is losable over ssh and tmux (`D_mouse_dispatch`).
|
|
3094
|
+
#
|
|
3095
|
+
# _@param_ `_event`
|
|
3096
|
+
#
|
|
3097
|
+
# _@return_ — whether this component claimed the press.
|
|
3098
|
+
def handle_mouse_down?: (Mouse::DownEvent _event) -> bool
|
|
3099
|
+
|
|
3100
|
+
# Called when the wheel turns over this component; answer `true` to consume
|
|
3101
|
+
# the notch. Bubbles exactly as {#handle_mouse_down?} does, but grabs
|
|
3102
|
+
# nothing — so a scroller already at its limit answers `false` and its
|
|
3103
|
+
# ancestor scrolls instead.
|
|
3104
|
+
#
|
|
3105
|
+
# _@param_ `_event`
|
|
3106
|
+
#
|
|
3107
|
+
# _@return_ — whether this component consumed the notch.
|
|
3108
|
+
def handle_mouse_scroll?: (Mouse::ScrollEvent _event) -> bool
|
|
2739
3109
|
|
|
2740
|
-
#
|
|
2741
|
-
#
|
|
2742
|
-
#
|
|
3110
|
+
# Called when the pointer moves over this component with nothing grabbed;
|
|
3111
|
+
# answer `true` to consume the move. Bubbles as {#handle_mouse_down?} does.
|
|
3112
|
+
# Arrives only under `run_event_loop(capture_mouse: :hover)`, at up to ~84
|
|
3113
|
+
# events a second, which is why the default passes it on untouched.
|
|
3114
|
+
#
|
|
3115
|
+
# _@param_ `_event`
|
|
2743
3116
|
#
|
|
2744
|
-
#
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
# on the
|
|
3117
|
+
# _@return_ — whether this component consumed the move.
|
|
3118
|
+
def handle_mouse_move?: (Mouse::MoveEvent _event) -> bool
|
|
3119
|
+
|
|
3120
|
+
# Called on the component that claimed a press when the button comes up,
|
|
3121
|
+
# ending the grab. For press feedback and for ending a drag — never for
|
|
3122
|
+
# activation: a release may never arrive, and any key or the next press
|
|
3123
|
+
# ends the grab without it.
|
|
2748
3124
|
#
|
|
2749
|
-
#
|
|
2750
|
-
|
|
3125
|
+
# _@param_ `_event`
|
|
3126
|
+
def handle_mouse_up: (Mouse::UpEvent _event) -> void
|
|
3127
|
+
|
|
3128
|
+
# Called on the component that claimed a press whenever the pointer moves
|
|
3129
|
+
# while the button is held, wherever the pointer is. Needs
|
|
3130
|
+
# `capture_mouse: :drag` or `:hover`.
|
|
2751
3131
|
#
|
|
2752
|
-
# _@param_ `
|
|
2753
|
-
def
|
|
3132
|
+
# _@param_ `_event` — its point may lie outside {#rect}.
|
|
3133
|
+
def handle_mouse_drag: (Mouse::DragEvent _event) -> void
|
|
3134
|
+
|
|
3135
|
+
# Called when the pointer comes over this component or any of its
|
|
3136
|
+
# descendants — down the chain, root first, and after every
|
|
3137
|
+
# {#handle_mouse_exit} the same move fires. Needs `capture_mouse: :hover`,
|
|
3138
|
+
# and is suspended while a press is grabbed.
|
|
3139
|
+
#
|
|
3140
|
+
# **Never a commit point**: no terminal reports the pointer leaving the
|
|
3141
|
+
# window, so the matching {#handle_mouse_exit} may arrive late or not at
|
|
3142
|
+
# all. Anything done here must be cosmetic and survive that.
|
|
3143
|
+
def handle_mouse_enter: () -> void
|
|
3144
|
+
|
|
3145
|
+
# The other half of {#handle_mouse_enter}; also fires when this component is
|
|
3146
|
+
# detached or hidden while hovered, innermost first.
|
|
3147
|
+
def handle_mouse_exit: () -> void
|
|
2754
3148
|
|
|
2755
3149
|
# _@return_ — true if the component is on the active chain — i.e. it
|
|
2756
3150
|
# is the focused component or an ancestor of it. Set by {Screen#focused=}.
|
|
@@ -2787,30 +3181,30 @@ module Tuile
|
|
|
2787
3181
|
def root: () -> Component
|
|
2788
3182
|
|
|
2789
3183
|
# Calls block for this component and for every descendant component.
|
|
2790
|
-
def
|
|
3184
|
+
def walk_tree: () ?{ (Component component) -> void } -> void
|
|
2791
3185
|
|
|
2792
|
-
# {#
|
|
3186
|
+
# {#walk_tree}, pruned: a hidden subtree is skipped whole, this component
|
|
2793
3187
|
# included when it is itself hidden (in which case nothing is yielded).
|
|
2794
3188
|
#
|
|
2795
3189
|
# stops = []
|
|
2796
|
-
# scope.
|
|
3190
|
+
# scope.walk_shown_tree { |c| stops << c if c.tab_stop? }
|
|
2797
3191
|
#
|
|
2798
|
-
# **Use this for anything asking "can the user reach it"**, {#
|
|
3192
|
+
# **Use this for anything asking "can the user reach it"**, {#walk_tree} for
|
|
2799
3193
|
# what the framework does *to* a component regardless — lifecycle, theme
|
|
2800
3194
|
# fan-out, invalidation — which a hidden component still gets. Writing the
|
|
2801
|
-
# first as `
|
|
3195
|
+
# first as `walk_tree` plus a `visible?` test is the trap: that is this walk
|
|
2802
3196
|
# with the ancestor case missing, so a field under a hidden panel is back
|
|
2803
3197
|
# in the Tab cycle (`D_visibility`).
|
|
2804
|
-
def
|
|
3198
|
+
def walk_shown_tree: () ?{ (Component component) -> void } -> void
|
|
2805
3199
|
|
|
2806
3200
|
# Called when the component receives focus — on this component alone, never
|
|
2807
|
-
# on the ancestors that light up with it. {#
|
|
3201
|
+
# on the ancestors that light up with it. {#handle_blur} is the other half.
|
|
2808
3202
|
#
|
|
2809
|
-
# Unlike `
|
|
3203
|
+
# Unlike `handle_blur` it is **not** edge-triggered: it fires on every
|
|
2810
3204
|
# {Screen#focused=}, re-assigning the component that already has focus
|
|
2811
3205
|
# included, which is what lets a container forward focus into its content
|
|
2812
3206
|
# from here.
|
|
2813
|
-
def
|
|
3207
|
+
def handle_focus: () -> void
|
|
2814
3208
|
|
|
2815
3209
|
# Whether this component's tree is mounted on a UI, {ScreenPane} being the
|
|
2816
3210
|
# root of every displayed tree.
|
|
@@ -2838,8 +3232,11 @@ module Tuile
|
|
|
2838
3232
|
# its parent: an override may repair focus however it likes, but must not
|
|
2839
3233
|
# assume the child is gone. Removal bookkeeping belongs in the remover.
|
|
2840
3234
|
#
|
|
3235
|
+
# The one hook whose base body does real work, so an override *replaces* it
|
|
3236
|
+
# (as {Component::Slot} and {ScreenPane} do) instead of calling `super`.
|
|
3237
|
+
#
|
|
2841
3238
|
# _@param_ `child` — the just-detached, or just-hidden, child.
|
|
2842
|
-
def
|
|
3239
|
+
def handle_child_removed: (Component child) -> void
|
|
2843
3240
|
|
|
2844
3241
|
# Where the hardware terminal cursor should sit when this component is the
|
|
2845
3242
|
# cursor owner. Returns `nil` to indicate the cursor should be hidden. The
|
|
@@ -2885,7 +3282,7 @@ module Tuile
|
|
|
2885
3282
|
# _@param_ `at` — index to insert at; appends when nil.
|
|
2886
3283
|
def add_child: (Component child, ?at: Integer?) -> void
|
|
2887
3284
|
|
|
2888
|
-
# Drops `child` and notifies {#
|
|
3285
|
+
# Drops `child` and notifies {#handle_child_removed}.
|
|
2889
3286
|
# Final: one of the three mutators that write {#children} and the parent
|
|
2890
3287
|
# pointer in the same call, which is what keeps them in agreement.
|
|
2891
3288
|
#
|
|
@@ -2893,12 +3290,12 @@ module Tuile
|
|
|
2893
3290
|
def remove_child: (Component child) -> void
|
|
2894
3291
|
|
|
2895
3292
|
# Drops `child` *without* notifying — for a container swapping a named slot,
|
|
2896
|
-
# which owes the {#
|
|
3293
|
+
# which owes the {#handle_child_removed} call once the new occupant is wired:
|
|
2897
3294
|
#
|
|
2898
3295
|
# detach_child(old)
|
|
2899
3296
|
# @content = new
|
|
2900
3297
|
# add_child(new, at: 0)
|
|
2901
|
-
#
|
|
3298
|
+
# handle_child_removed(old) # focus repair cascades into the *new* content
|
|
2902
3299
|
#
|
|
2903
3300
|
# The child leaves {#children} before its pointer is cleared, so nothing
|
|
2904
3301
|
# observes a child whose parent has disowned it while still listing it.
|
|
@@ -2912,33 +3309,33 @@ module Tuile
|
|
|
2912
3309
|
# i.e. when {#attached?} flips to true — the place to acquire whatever is
|
|
2913
3310
|
# supposed to live for exactly as long as the component is on screen:
|
|
2914
3311
|
#
|
|
2915
|
-
# def
|
|
3312
|
+
# def handle_attached
|
|
2916
3313
|
# @ticker = screen.event_queue.tick_fps(10) { advance }
|
|
2917
3314
|
# end
|
|
2918
3315
|
#
|
|
2919
|
-
# def
|
|
3316
|
+
# def handle_detached
|
|
2920
3317
|
# @ticker&.cancel
|
|
2921
3318
|
# @ticker = nil
|
|
2922
3319
|
# end
|
|
2923
3320
|
#
|
|
2924
|
-
# `
|
|
3321
|
+
# `handle_attached` starts what `handle_detached` stops; both must be cheap and
|
|
2925
3322
|
# idempotent, since a component moved between parents is genuinely detached
|
|
2926
3323
|
# in between and gets both, in that order. Whatever you acquire here you
|
|
2927
|
-
# must release in {#
|
|
2928
|
-
# process teardown does *not* fire {#
|
|
3324
|
+
# must release in {#handle_detached} — nothing else will. Not a destructor:
|
|
3325
|
+
# process teardown does *not* fire {#handle_detached}.
|
|
2929
3326
|
#
|
|
2930
3327
|
# {#invalidate} needs no guard: {#attached?} is already true here (and
|
|
2931
|
-
# already false in {#
|
|
3328
|
+
# already false in {#handle_detached}, where it no-ops). Do not read {#rect} —
|
|
2932
3329
|
# a parent assigns it *after* wiring, so it is still stale. Runs on the
|
|
2933
3330
|
# thread that owns the UI.
|
|
2934
|
-
def
|
|
3331
|
+
def handle_attached: () -> void
|
|
2935
3332
|
|
|
2936
|
-
# Mirror of {#
|
|
3333
|
+
# Mirror of {#handle_attached}, called once the tree has been unmounted — see
|
|
2937
3334
|
# there for the contract. Two things are still mid-flight when it runs, both
|
|
2938
3335
|
# deliberate: {Screen#focused} may still point into this subtree (repair
|
|
2939
3336
|
# happens after), and the ex-parent's own bookkeeping may not be finished.
|
|
2940
3337
|
# So release resources here and don't inspect the tree around you.
|
|
2941
|
-
def
|
|
3338
|
+
def handle_detached: () -> void
|
|
2942
3339
|
|
|
2943
3340
|
# Walks self-then-children calling one lifecycle hook, delivering at most one
|
|
2944
3341
|
# call per component per transition however the hooks mutate the tree. Two
|
|
@@ -2950,22 +3347,25 @@ module Tuile
|
|
|
2950
3347
|
# current attachedness rather than on `parent.equal?(self)`: a child pulled
|
|
2951
3348
|
# out during a detach walk is *already* detached, so its own `parent=` saw
|
|
2952
3349
|
# no transition and stayed silent — a parentage check would skip it too and
|
|
2953
|
-
# it would never hear `
|
|
2954
|
-
# during an *attach* walk) gets `
|
|
2955
|
-
# `
|
|
2956
|
-
# unpaired detach releases nothing, whereas firing `
|
|
3350
|
+
# it would never hear `handle_detached` at all. The reverse case (pulled out
|
|
3351
|
+
# during an *attach* walk) gets `handle_detached` from its own `parent=` and no
|
|
3352
|
+
# `handle_attached`, which is why the hooks are required to be idempotent: an
|
|
3353
|
+
# unpaired detach releases nothing, whereas firing `handle_attached` at a
|
|
2957
3354
|
# component that is no longer attached would start a ticker nothing stops.
|
|
2958
3355
|
#
|
|
2959
|
-
# _@param_ `attached` — true to fire {#
|
|
3356
|
+
# _@param_ `attached` — true to fire {#handle_attached}, false for {#handle_detached}.
|
|
2960
3357
|
def fire_lifecycle: (bool attached) -> void
|
|
2961
3358
|
|
|
2962
3359
|
# Called whenever the component width changes. Does nothing by default.
|
|
2963
|
-
def
|
|
3360
|
+
def handle_width_changed: () -> void
|
|
2964
3361
|
|
|
2965
3362
|
# Called on the parent after a direct child's {#visible=} flipped, so a
|
|
2966
3363
|
# container that divides space can re-divide it:
|
|
2967
3364
|
#
|
|
2968
|
-
# def
|
|
3365
|
+
# def handle_child_visibility_changed(_child)
|
|
3366
|
+
# super
|
|
3367
|
+
# relayout
|
|
3368
|
+
# end
|
|
2969
3369
|
#
|
|
2970
3370
|
# **A container with layout arithmetic owes this override**, or a hidden
|
|
2971
3371
|
# child keeps its slot and its gap — the hole the flag exists to close.
|
|
@@ -2973,27 +3373,31 @@ module Tuile
|
|
|
2973
3373
|
# arithmetic, and an app wanting the space back reads `visible?` there.
|
|
2974
3374
|
#
|
|
2975
3375
|
# Fires on the flip only, before the subtree is invalidated, never for a
|
|
2976
|
-
# grandchild. {#visible=} repairs focus itself, so an override
|
|
2977
|
-
#
|
|
2978
|
-
# (`D_hook_visibility`).
|
|
3376
|
+
# grandchild. {#visible=} repairs focus itself, so an override has nothing
|
|
3377
|
+
# to inherit — it still calls `super`, per the class doc. Reached through
|
|
3378
|
+
# `__send__`, so it may declare any visibility (`D_hook_visibility`).
|
|
2979
3379
|
#
|
|
2980
3380
|
# _@param_ `_child` — the direct child whose flag changed.
|
|
2981
|
-
def
|
|
3381
|
+
def handle_child_visibility_changed: (Component _child) -> void
|
|
2982
3382
|
|
|
2983
|
-
# Mirror of {#
|
|
3383
|
+
# Mirror of {#handle_focus}: the component just lost focus, to another component
|
|
2984
3384
|
# or to nothing. The commit point a Tab-away still reaches — Tab is
|
|
2985
3385
|
# unconditional, so {Component::TextField#on_enter} never fires for a user
|
|
2986
3386
|
# who tabs out of a half-typed field:
|
|
2987
3387
|
#
|
|
2988
3388
|
# class TrimmedField < Component::TextField
|
|
2989
|
-
# protected def
|
|
3389
|
+
# protected def handle_blur
|
|
3390
|
+
# super
|
|
3391
|
+
# self.text = text.strip
|
|
3392
|
+
# false
|
|
3393
|
+
# end
|
|
2990
3394
|
# end
|
|
2991
3395
|
#
|
|
2992
3396
|
# Edge-triggered, and fired on the blurred component alone — never on the
|
|
2993
3397
|
# ancestors leaving the active chain with it, so a composed widget asking
|
|
2994
3398
|
# "did focus leave me *and* my children" overrides {#active=} instead
|
|
2995
3399
|
# ({Component::ComboBox} closes its dropdown from there). Focus that merely
|
|
2996
|
-
# *passes through* does blur: a container forwarding focus from {#
|
|
3400
|
+
# *passes through* does blur: a container forwarding focus from {#handle_focus}
|
|
2997
3401
|
# is blurred by its own forward.
|
|
2998
3402
|
#
|
|
2999
3403
|
# A notification, not a veto — focus has already moved, and the active-flag
|
|
@@ -3006,11 +3410,47 @@ module Tuile
|
|
|
3006
3410
|
# It fires wherever focus is *dropped*, not only where a user moved it, so
|
|
3007
3411
|
# two paths reach it with the tree mid-flight: the popup-close repair blurs
|
|
3008
3412
|
# an **already-detached** component, where {#invalidate} is the same silent
|
|
3009
|
-
# no-op as in {#
|
|
3413
|
+
# no-op as in {#handle_detached}, and {Screen#close} blurs on its way out. Keep
|
|
3010
3414
|
# it cheap; a raise propagates out of {Screen#focused=}. Protected because
|
|
3011
3415
|
# the framework calls it and an app never does — {Screen} reaches it with
|
|
3012
3416
|
# `__send__`, so an override may declare any visibility (`D_hook_visibility`).
|
|
3013
|
-
def
|
|
3417
|
+
def handle_blur: () -> void
|
|
3418
|
+
|
|
3419
|
+
# Called on every attached component (pre-order, popups included) when
|
|
3420
|
+
# {Screen#theme} changes — at {Screen#theme=} / {Screen#theme_def=} and on
|
|
3421
|
+
# OS appearance flips. The hook exists for app *content* whose colors were
|
|
3422
|
+
# baked in from the old theme (a {Label#text} / {List#lines=} {StyledString}
|
|
3423
|
+
# styled with `theme[:accent]`); rebuild it here by re-running the code that
|
|
3424
|
+
# rendered it. See book ch6 for why built-in accents need no such handling.
|
|
3425
|
+
#
|
|
3426
|
+
# Runs on the UI thread with {Screen#theme} already updated, so mutating
|
|
3427
|
+
# content (`text=`, `lines=`, …) is safe. Do not assign {Screen#theme=}
|
|
3428
|
+
# here. Subclasses overriding this must call `super` so an assigned
|
|
3429
|
+
# {#on_theme_changed=} listener keeps firing.
|
|
3430
|
+
#
|
|
3431
|
+
# Plumbing an app overrides and never calls, hence protected — and
|
|
3432
|
+
# {Screen}, not being a {Component}, fans it out through `__send__`, so an
|
|
3433
|
+
# override is free to declare any visibility (`D_hook_visibility`).
|
|
3434
|
+
#
|
|
3435
|
+
# _@return_ — is whatever the app's lambda happened to return.
|
|
3436
|
+
def handle_theme_changed: () -> void
|
|
3437
|
+
|
|
3438
|
+
# Called on every attached component (pre-order, popups included) when
|
|
3439
|
+
# {Screen#locale} changes — for state derived from the old conventions and
|
|
3440
|
+
# **pushed** somewhere, such as a date already rendered into a {Label}'s
|
|
3441
|
+
# text. Anything read at paint or parse time needs no override: the locale
|
|
3442
|
+
# change invalidates the whole tree.
|
|
3443
|
+
#
|
|
3444
|
+
# Runs on the UI thread with {Screen#locale} already updated. Subclasses
|
|
3445
|
+
# overriding it must call `super` so an assigned {#on_locale_changed=}
|
|
3446
|
+
# listener keeps firing.
|
|
3447
|
+
#
|
|
3448
|
+
# Plumbing an app overrides and never calls, hence protected — {Screen}
|
|
3449
|
+
# fans it out through `__send__`, so an override may declare any visibility
|
|
3450
|
+
# (`D_hook_visibility`).
|
|
3451
|
+
#
|
|
3452
|
+
# _@return_ — is whatever the app's lambda happened to return.
|
|
3453
|
+
def handle_locale_changed: () -> void
|
|
3014
3454
|
|
|
3015
3455
|
# The formatting conventions to render and parse by ({Screen#locale}), or
|
|
3016
3456
|
# {Locale::ISO} when there is no screen in the process — which a tree
|
|
@@ -3049,6 +3489,21 @@ module Tuile
|
|
|
3049
3489
|
# it directly.
|
|
3050
3490
|
def clear_outside_extent: () -> void
|
|
3051
3491
|
|
|
3492
|
+
# Blanks the {#extent} itself, for a *container* whose children don't cover
|
|
3493
|
+
# it — a {Component::Layout::Box}'s `spacing` column, the slack past the last
|
|
3494
|
+
# child, the span a child abandoned by going hidden or by a narrowing resize.
|
|
3495
|
+
#
|
|
3496
|
+
# In the *ambient* background, the same answer {#clear_outside_extent} gives
|
|
3497
|
+
# the dead tail: a gap between two children is not this widget's ink, so an
|
|
3498
|
+
# app's {#bg_color} tint covers it but a well of its own — a field's, a
|
|
3499
|
+
# validation error's — must not bleed into it.
|
|
3500
|
+
#
|
|
3501
|
+
# Called by the default {#repaint} for a container only; a leaf paints its
|
|
3502
|
+
# extent itself, and blanking that first is the re-emit `D_progress_bar`
|
|
3503
|
+
# bought back. Override it to decline when you paint your own ink into a
|
|
3504
|
+
# face cell no child covers.
|
|
3505
|
+
def clear_inside_extent: () -> void
|
|
3506
|
+
|
|
3052
3507
|
# The background this component paints when the app has set no {#bg_color} —
|
|
3053
3508
|
# `nil` by default, meaning "I have no surface of my own; whatever is behind
|
|
3054
3509
|
# me shows through". A widget that paints an opaque surface overrides it, and
|
|
@@ -3065,7 +3520,7 @@ module Tuile
|
|
|
3065
3520
|
# the cheap form and allocates nothing on the paint path.
|
|
3066
3521
|
#
|
|
3067
3522
|
# Read the theme here rather than in an ivar: this runs at paint time, so a
|
|
3068
|
-
# {Screen#theme=} restyles the widget with no {#
|
|
3523
|
+
# {Screen#theme=} restyles the widget with no {#handle_theme_changed} hook.
|
|
3069
3524
|
def default_bg_color: () -> (Color | Theme::Ref | ::Hash[untyped, untyped])?
|
|
3070
3525
|
|
|
3071
3526
|
# Final, and protected: it answers what the *framework* paints with, and an
|
|
@@ -3158,7 +3613,7 @@ module Tuile
|
|
|
3158
3613
|
) -> void
|
|
3159
3614
|
|
|
3160
3615
|
# Hands focus out of the subtree just hidden, if it was in there, through
|
|
3161
|
-
# the parent's {#
|
|
3616
|
+
# the parent's {#handle_child_removed} — see there for why hiding reuses the
|
|
3162
3617
|
# removal repair instead of growing a second one.
|
|
3163
3618
|
#
|
|
3164
3619
|
# The parent is necessarily showing (focus was inside it a moment ago, and
|
|
@@ -3256,43 +3711,25 @@ module Tuile
|
|
|
3256
3711
|
# callers! May be empty.
|
|
3257
3712
|
attr_reader children: ::Array[Component]
|
|
3258
3713
|
|
|
3259
|
-
#
|
|
3260
|
-
#
|
|
3261
|
-
#
|
|
3262
|
-
# baked in from the old theme (a {Label#text} / {List#lines=} {StyledString}
|
|
3263
|
-
# styled with `theme[:accent]`); rebuild it here by re-running the code that
|
|
3264
|
-
# rendered it. See book ch6 for why built-in accents need no such handling.
|
|
3714
|
+
# Optional zero-arg listener fired by the base {#handle_theme_changed} — the
|
|
3715
|
+
# composition-style alternative to overriding the method, for apps that
|
|
3716
|
+
# assemble stock components rather than subclass:
|
|
3265
3717
|
#
|
|
3266
|
-
#
|
|
3267
|
-
# content (`text=`, `lines=`, …) is safe. Do not assign {Screen#theme=}
|
|
3268
|
-
# here. Subclasses overriding this must call `super` so an assigned
|
|
3269
|
-
# {#on_theme_changed=} listener keeps firing.
|
|
3270
|
-
#
|
|
3271
|
-
# Plumbing an app overrides and never calls, hence protected — and
|
|
3272
|
-
# {Screen}, not being a {Component}, fans it out through `__send__`, so an
|
|
3273
|
-
# override is free to declare any visibility (`D_hook_visibility`).
|
|
3718
|
+
# label.on_theme_changed = -> { label.text = render_status_line }
|
|
3274
3719
|
attr_accessor on_theme_changed: Proc?
|
|
3275
3720
|
|
|
3276
|
-
#
|
|
3277
|
-
#
|
|
3278
|
-
#
|
|
3279
|
-
# text. Anything read at paint or parse time needs no override: the locale
|
|
3280
|
-
# change invalidates the whole tree.
|
|
3721
|
+
# Optional zero-arg listener fired by the base {#handle_locale_changed} — the
|
|
3722
|
+
# composition-style alternative to overriding the method, for an app that
|
|
3723
|
+
# rendered a date or a number into a stock component:
|
|
3281
3724
|
#
|
|
3282
|
-
#
|
|
3283
|
-
# overriding it must call `super` so an assigned {#on_locale_changed=}
|
|
3284
|
-
# listener keeps firing.
|
|
3285
|
-
#
|
|
3286
|
-
# Plumbing an app overrides and never calls, hence protected — {Screen}
|
|
3287
|
-
# fans it out through `__send__`, so an override may declare any visibility
|
|
3288
|
-
# (`D_hook_visibility`).
|
|
3725
|
+
# label.on_locale_changed = -> { label.text = due_date.strftime(fmt) }
|
|
3289
3726
|
attr_accessor on_locale_changed: Proc?
|
|
3290
3727
|
|
|
3291
3728
|
# A scrollable list of typed items, one row each, with cursor support.
|
|
3292
3729
|
#
|
|
3293
3730
|
# list = Component::List.new
|
|
3294
3731
|
# list.items = people
|
|
3295
|
-
# list.renderer = ->(p) { StyledString.plain(p.name) + screen.theme.
|
|
3732
|
+
# list.renderer = ->(p) { StyledString.plain(p.name) + screen.theme.fg(:muted, " #{p.email}") }
|
|
3296
3733
|
# list.cursor = List::Cursor.new # a bare list has none
|
|
3297
3734
|
# list.on_item_chosen = ->(index, person) { open(person) }
|
|
3298
3735
|
#
|
|
@@ -3374,7 +3811,7 @@ module Tuile
|
|
|
3374
3811
|
# _@param_ `key` — a key.
|
|
3375
3812
|
#
|
|
3376
3813
|
# _@return_ — true if the key was handled.
|
|
3377
|
-
def handle_key
|
|
3814
|
+
def handle_key?: (String key) -> bool
|
|
3378
3815
|
|
|
3379
3816
|
# Moves the cursor to the next item whose text contains `query`
|
|
3380
3817
|
# (case-insensitive substring match). Search wraps around the end of the
|
|
@@ -3414,8 +3851,17 @@ module Tuile
|
|
|
3414
3851
|
# _@return_ — whether the cursor moved there.
|
|
3415
3852
|
def select: (Integer index) -> bool
|
|
3416
3853
|
|
|
3854
|
+
# Moves the cursor to the pressed row and fires {#on_item_chosen}; what
|
|
3855
|
+
# each {Cursor} does with a press is its own.
|
|
3856
|
+
#
|
|
3857
|
+
# _@param_ `event`
|
|
3858
|
+
def handle_mouse_down?: (Mouse::DownEvent event) -> bool
|
|
3859
|
+
|
|
3860
|
+
# Scrolls four rows a notch, and declines — so the notch bubbles to an
|
|
3861
|
+
# ancestor scroller — once this list is at that end of its items.
|
|
3862
|
+
#
|
|
3417
3863
|
# _@param_ `event`
|
|
3418
|
-
def
|
|
3864
|
+
def handle_mouse_scroll?: (Mouse::ScrollEvent event) -> bool
|
|
3419
3865
|
|
|
3420
3866
|
# Paints the visible items into {#rect}, rendering the ones not already
|
|
3421
3867
|
# cached.
|
|
@@ -3436,7 +3882,7 @@ module Tuile
|
|
|
3436
3882
|
# populated before the popup was opened), the auto-scroll update
|
|
3437
3883
|
# was skipped because there was no viewport — re-run it now that there
|
|
3438
3884
|
# is one, so the list snaps to the bottom on first paint.
|
|
3439
|
-
def
|
|
3885
|
+
def handle_width_changed: () -> void
|
|
3440
3886
|
|
|
3441
3887
|
# Coerces and flattens a list of input entries into trimmed
|
|
3442
3888
|
# {StyledString} lines. Each entry becomes a {StyledString} (String
|
|
@@ -3524,7 +3970,7 @@ module Tuile
|
|
|
3524
3970
|
# negating the auto-scroll. Skipped when {#rect} is empty: without a
|
|
3525
3971
|
# viewport the "items minus viewport" formula yields `@items.size`,
|
|
3526
3972
|
# which would leave `scroll_top_row` past the last item once a real rect
|
|
3527
|
-
# arrives. {#
|
|
3973
|
+
# arrives. {#handle_width_changed} re-runs this hook when the rect grows so
|
|
3528
3974
|
# the snap-to-bottom intent is preserved.
|
|
3529
3975
|
#
|
|
3530
3976
|
# Gated on {#following?}: once the user scrolls up off the bottom the
|
|
@@ -3652,16 +4098,16 @@ module Tuile
|
|
|
3652
4098
|
# _@param_ `viewport_rows` — number of visible rows.
|
|
3653
4099
|
#
|
|
3654
4100
|
# _@return_ — true if the cursor moved.
|
|
3655
|
-
def handle_key
|
|
4101
|
+
def handle_key?: (String key, Integer item_count, Integer viewport_rows) -> bool
|
|
3656
4102
|
|
|
3657
|
-
# _@param_ `item_index` — the item
|
|
4103
|
+
# _@param_ `item_index` — the item pressed on.
|
|
3658
4104
|
#
|
|
3659
4105
|
# _@param_ `event` — the event.
|
|
3660
4106
|
#
|
|
3661
4107
|
# _@param_ `item_count` — number of items in the list.
|
|
3662
4108
|
#
|
|
3663
|
-
# _@return_ — true if the
|
|
3664
|
-
def
|
|
4109
|
+
# _@return_ — true if the cursor moved.
|
|
4110
|
+
def handle_mouse_down?: (Integer item_index, Mouse::DownEvent event, Integer item_count) -> bool
|
|
3665
4111
|
|
|
3666
4112
|
# Moves the cursor to the new position. Public only because of testing.
|
|
3667
4113
|
#
|
|
@@ -3701,14 +4147,14 @@ module Tuile
|
|
|
3701
4147
|
# _@param_ `_item_count`
|
|
3702
4148
|
#
|
|
3703
4149
|
# _@param_ `_viewport_rows`
|
|
3704
|
-
def handle_key
|
|
4150
|
+
def handle_key?: (String _key, Integer _item_count, Integer _viewport_rows) -> bool
|
|
3705
4151
|
|
|
3706
4152
|
# _@param_ `_item_index`
|
|
3707
4153
|
#
|
|
3708
4154
|
# _@param_ `_event`
|
|
3709
4155
|
#
|
|
3710
4156
|
# _@param_ `_item_count`
|
|
3711
|
-
def
|
|
4157
|
+
def handle_mouse_down?: (Integer _item_index, Mouse::DownEvent _event, Integer _item_count) -> bool
|
|
3712
4158
|
|
|
3713
4159
|
# _@param_ `_item_count`
|
|
3714
4160
|
def candidate_positions: (Integer _item_count) -> ::Array[Integer]
|
|
@@ -3736,7 +4182,7 @@ module Tuile
|
|
|
3736
4182
|
# _@param_ `event`
|
|
3737
4183
|
#
|
|
3738
4184
|
# _@param_ `_item_count`
|
|
3739
|
-
def
|
|
4185
|
+
def handle_mouse_down?: (Integer item_index, Mouse::DownEvent event, Integer _item_count) -> bool
|
|
3740
4186
|
|
|
3741
4187
|
# _@param_ `item_count`
|
|
3742
4188
|
def candidate_positions: (Integer item_count) -> ::Array[Integer]
|
|
@@ -3778,7 +4224,7 @@ module Tuile
|
|
|
3778
4224
|
# (what {Window} does with an absent footer); never detach it.
|
|
3779
4225
|
#
|
|
3780
4226
|
# Transparent to input: not {Component#focusable?},
|
|
3781
|
-
#
|
|
4227
|
+
# the mouse routes straight through it, and a departing occupant's
|
|
3782
4228
|
# focus repair is handed to the container.
|
|
3783
4229
|
class Slot < Component
|
|
3784
4230
|
include Tuile::Component::HasContent
|
|
@@ -3791,7 +4237,7 @@ module Tuile
|
|
|
3791
4237
|
# nothing to bubble from.
|
|
3792
4238
|
#
|
|
3793
4239
|
# _@param_ `child` — the just-detached occupant.
|
|
3794
|
-
def
|
|
4240
|
+
def handle_child_removed: (Component child) -> void
|
|
3795
4241
|
|
|
3796
4242
|
# _@param_ `content`
|
|
3797
4243
|
def layout: (Component content) -> void
|
|
@@ -3799,7 +4245,7 @@ module Tuile
|
|
|
3799
4245
|
# _@param_ `rect`
|
|
3800
4246
|
def rect=: (Rect rect) -> void
|
|
3801
4247
|
|
|
3802
|
-
def
|
|
4248
|
+
def handle_focus: () -> void
|
|
3803
4249
|
end
|
|
3804
4250
|
|
|
3805
4251
|
# A one-row strip of captions with exactly one of them selected — the map of
|
|
@@ -3829,7 +4275,7 @@ module Tuile
|
|
|
3829
4275
|
#
|
|
3830
4276
|
# {Tab} handles are minted by {#add_tab} and owned by the strip. There is no
|
|
3831
4277
|
# `items=`: a tab is identity plus its own state, so the set grows and
|
|
3832
|
-
# shrinks one tab at a time. See book ch7 and `
|
|
4278
|
+
# shrinks one tab at a time. See book ch7 and `design/decisions.md` `D_tabs`.
|
|
3833
4279
|
#
|
|
3834
4280
|
# == Sizing
|
|
3835
4281
|
# Assign a {#rect} (typically from the surrounding {Layout}). One wider than
|
|
@@ -3913,7 +4359,7 @@ module Tuile
|
|
|
3913
4359
|
#
|
|
3914
4360
|
# Both the focus highlight and the click hit test use it, so a click on the
|
|
3915
4361
|
# blank tail — or on a lower row, when the rect is taller than one —
|
|
3916
|
-
# selects nothing. It still *focuses*: {
|
|
4362
|
+
# selects nothing. It still *focuses*: {Mouse::Router}'s
|
|
3917
4363
|
# click-to-focus is ungated by geometry.
|
|
3918
4364
|
def extent: () -> Size
|
|
3919
4365
|
|
|
@@ -3922,13 +4368,13 @@ module Tuile
|
|
|
3922
4368
|
# bubbles to an ancestor; an empty strip handles nothing at all.
|
|
3923
4369
|
#
|
|
3924
4370
|
# _@param_ `key`
|
|
3925
|
-
def handle_key
|
|
4371
|
+
def handle_key?: (String key) -> bool
|
|
3926
4372
|
|
|
3927
|
-
# Selects the tab under a left
|
|
3928
|
-
#
|
|
4373
|
+
# Selects the tab under a left press; a press between tabs selects
|
|
4374
|
+
# nothing and still claims the strip.
|
|
3929
4375
|
#
|
|
3930
4376
|
# _@param_ `event`
|
|
3931
|
-
def
|
|
4377
|
+
def handle_mouse_down?: (Mouse::DownEvent event) -> bool
|
|
3932
4378
|
|
|
3933
4379
|
def repaint: () -> void
|
|
3934
4380
|
|
|
@@ -3938,7 +4384,7 @@ module Tuile
|
|
|
3938
4384
|
|
|
3939
4385
|
# The rect's *width* is the only part of it the offset depends on, so this
|
|
3940
4386
|
# hook is the whole geometry story; {Component#rect=} invalidates for us.
|
|
3941
|
-
def
|
|
4387
|
+
def handle_width_changed: () -> void
|
|
3942
4388
|
|
|
3943
4389
|
# Scrolls the minimum needed to show the selected segment whole, and is the
|
|
3944
4390
|
# sole writer of {#left_column}. Idempotent, so every mutation site can
|
|
@@ -4134,7 +4580,7 @@ module Tuile
|
|
|
4134
4580
|
# that carries its own background keeps it.
|
|
4135
4581
|
def repaint: () -> void
|
|
4136
4582
|
|
|
4137
|
-
def
|
|
4583
|
+
def handle_width_changed: () -> void
|
|
4138
4584
|
|
|
4139
4585
|
# Recomputes {@rows} for the current text and rect width.
|
|
4140
4586
|
# Each line is ellipsized to fit and padded with trailing spaces out to
|
|
@@ -4178,7 +4624,7 @@ module Tuile
|
|
|
4178
4624
|
#
|
|
4179
4625
|
# `q` and ESC close the popup — handled here, at the top of the popup's own
|
|
4180
4626
|
# subtree, so the key only arrives after every component on the focus chain
|
|
4181
|
-
# declined it (see {ScreenPane#handle_key}). That's why typing `q` into a
|
|
4627
|
+
# declined it (see {ScreenPane#handle_key?}). That's why typing `q` into a
|
|
4182
4628
|
# nested {Component::TextField} doesn't dismiss the popup: the field consumes
|
|
4183
4629
|
# it first.
|
|
4184
4630
|
#
|
|
@@ -4224,7 +4670,7 @@ module Tuile
|
|
|
4224
4670
|
# _@param_ `key`
|
|
4225
4671
|
#
|
|
4226
4672
|
# _@return_ — true if the key was handled.
|
|
4227
|
-
def handle_key
|
|
4673
|
+
def handle_key?: (String key) -> bool
|
|
4228
4674
|
|
|
4229
4675
|
# The box this popup asks for, *as set* — a {Fraction} comes back
|
|
4230
4676
|
# unresolved. {#rect} is the resolved answer. Named apart from `size`
|
|
@@ -4243,7 +4689,7 @@ module Tuile
|
|
|
4243
4689
|
#
|
|
4244
4690
|
# Buttons are tab stops — Tab and Shift+Tab will land on them as part of
|
|
4245
4691
|
# the standard focus cycle. Click-to-focus also works via the inherited
|
|
4246
|
-
# {Component#
|
|
4692
|
+
# {Component#handle_mouse_down?}.
|
|
4247
4693
|
#
|
|
4248
4694
|
# Assign a {#rect} (typically by the surrounding {Layout}) wide enough to
|
|
4249
4695
|
# show `[ caption ]` — that natural width is `caption.display_width + 4`.
|
|
@@ -4260,13 +4706,13 @@ module Tuile
|
|
|
4260
4706
|
def tab_stop?: () -> bool
|
|
4261
4707
|
|
|
4262
4708
|
# _@param_ `key`
|
|
4263
|
-
def handle_key
|
|
4709
|
+
def handle_key?: (String key) -> bool
|
|
4264
4710
|
|
|
4265
4711
|
# The cells the button actually paints: one row, `caption.display_width + 4`
|
|
4266
4712
|
# columns, clipped to {#rect}. Both the focus highlight and the click hit
|
|
4267
4713
|
# test use it, so a click on the blank tail of an over-wide rect — or on a
|
|
4268
4714
|
# lower row, when the rect is taller than one — does not fire {#on_click}.
|
|
4269
|
-
# It still *focuses*: {
|
|
4715
|
+
# It still *focuses*: {Mouse::Router}'s click-to-focus is ungated
|
|
4270
4716
|
# by geometry. Same rule as {Checkbox#extent}, which documents the two
|
|
4271
4717
|
# traps behind it.
|
|
4272
4718
|
def extent: () -> Size
|
|
@@ -4275,7 +4721,7 @@ module Tuile
|
|
|
4275
4721
|
# a click anywhere in {#rect} still focuses.
|
|
4276
4722
|
#
|
|
4277
4723
|
# _@param_ `event`
|
|
4278
|
-
def
|
|
4724
|
+
def handle_mouse_down?: (Mouse::DownEvent event) -> bool
|
|
4279
4725
|
|
|
4280
4726
|
def repaint: () -> void
|
|
4281
4727
|
|
|
@@ -4324,7 +4770,7 @@ module Tuile
|
|
|
4324
4770
|
# the popup. Layouts don't paint any visible chrome of their own
|
|
4325
4771
|
# (the auto-cleared background is just blank space), so this has no
|
|
4326
4772
|
# mouse-routing consequences — clicks on a gap area land back on the
|
|
4327
|
-
# Layout itself and the
|
|
4773
|
+
# Layout itself and the handle_focus cascade forwards to a tab stop.
|
|
4328
4774
|
def focusable?: () -> bool
|
|
4329
4775
|
|
|
4330
4776
|
# Adds a child component to this layout.
|
|
@@ -4335,7 +4781,7 @@ module Tuile
|
|
|
4335
4781
|
# _@param_ `child`
|
|
4336
4782
|
def remove: (Component child) -> void
|
|
4337
4783
|
|
|
4338
|
-
def
|
|
4784
|
+
def handle_focus: () -> void
|
|
4339
4785
|
|
|
4340
4786
|
# How much space a child gets along one axis of a {Box}: exactly {#cells},
|
|
4341
4787
|
# clamped to whatever is still unassigned.
|
|
@@ -4592,7 +5038,7 @@ module Tuile
|
|
|
4592
5038
|
# {Component#visible=} buys over `remove` plus `add(…, at:)`.
|
|
4593
5039
|
#
|
|
4594
5040
|
# _@param_ `_child`
|
|
4595
|
-
def
|
|
5041
|
+
def handle_child_visibility_changed: (Component _child) -> void
|
|
4596
5042
|
|
|
4597
5043
|
# Recomputes and assigns every child's rect, giving each an empty one
|
|
4598
5044
|
# when this layout's own rect — or {#inner_rect} — is empty.
|
|
@@ -4835,7 +5281,7 @@ module Tuile
|
|
|
4835
5281
|
# Home/End are declined too, so they stay available app-wide.
|
|
4836
5282
|
#
|
|
4837
5283
|
# There is no type-ahead: a hidden prefix buffer *is* the ComboBox query with
|
|
4838
|
-
# the feedback removed (`
|
|
5284
|
+
# the feedback removed (`design/decisions.md` `D_select`). Which is also why labels
|
|
4839
5285
|
# need no prefix-disambiguation.
|
|
4840
5286
|
#
|
|
4841
5287
|
# == Implementation details
|
|
@@ -4880,22 +5326,21 @@ module Tuile
|
|
|
4880
5326
|
# is left unhandled so it bubbles to an ancestor.
|
|
4881
5327
|
#
|
|
4882
5328
|
# _@param_ `key`
|
|
4883
|
-
def handle_key
|
|
5329
|
+
def handle_key?: (String key) -> bool
|
|
4884
5330
|
|
|
4885
5331
|
# The one row this Select paints — the full width, at the top of {#rect}.
|
|
4886
5332
|
# A single-slot container ({Component::Window}, {Component::Popup}) hands
|
|
4887
5333
|
# its content the whole inner rect, so a Select is routinely assigned more
|
|
4888
|
-
# height than it uses; {#repaint} clears that tail,
|
|
4889
|
-
#
|
|
5334
|
+
# height than it uses; {#repaint} clears that tail, a press in it never reaches
|
|
5335
|
+
# {#handle_mouse_down?}, and the dropdown hangs under this rather than under the
|
|
4890
5336
|
# unused space.
|
|
4891
5337
|
def extent: () -> Size
|
|
4892
5338
|
|
|
4893
|
-
# Toggles the dropdown on a left
|
|
4894
|
-
# affordance is its whole row, as the well advertises
|
|
4895
|
-
# so the click also focuses.
|
|
5339
|
+
# Toggles the dropdown on a left press anywhere in {#extent} — a field's
|
|
5340
|
+
# affordance is its whole row, as the well advertises.
|
|
4896
5341
|
#
|
|
4897
5342
|
# _@param_ `event`
|
|
4898
|
-
def
|
|
5343
|
+
def handle_mouse_down?: (Mouse::DownEvent event) -> bool
|
|
4899
5344
|
|
|
4900
5345
|
def repaint: () -> void
|
|
4901
5346
|
|
|
@@ -4975,7 +5420,7 @@ module Tuile
|
|
|
4975
5420
|
# Input fields are focusable by default (overrides {Component#focusable?});
|
|
4976
5421
|
# a read-only display field could override back to `false`. Only
|
|
4977
5422
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
4978
|
-
# composing wrappers, so it stays per-class (`
|
|
5423
|
+
# composing wrappers, so it stays per-class (`design/decisions.md`
|
|
4979
5424
|
# `D_integer_field`).
|
|
4980
5425
|
def focusable?: () -> bool
|
|
4981
5426
|
|
|
@@ -5131,7 +5576,7 @@ module Tuile
|
|
|
5131
5576
|
# Adds `caption="…"` to {Component#inspect}, omitted while empty.
|
|
5132
5577
|
def inspect_details: () -> ::Array[String]
|
|
5133
5578
|
|
|
5134
|
-
def
|
|
5579
|
+
def handle_focus: () -> void
|
|
5135
5580
|
|
|
5136
5581
|
# _@return_ — optional chrome embedded into the bottom border
|
|
5137
5582
|
# line, mirroring {#caption} on the top line. Empty by default; hidden
|
|
@@ -5160,7 +5605,7 @@ module Tuile
|
|
|
5160
5605
|
# **{#focusable?} and {#modal?} move together — flip both or neither.** The
|
|
5161
5606
|
# defaults here are inert (`false`, `false`): a bare overlay floats without
|
|
5162
5607
|
# disturbing focus or key dispatch. {Component::Popup} flips both. What must
|
|
5163
|
-
# not appear is a *focusable non-modal* overlay: {ScreenPane#handle_key}
|
|
5608
|
+
# not appear is a *focusable non-modal* overlay: {ScreenPane#handle_key?}
|
|
5164
5609
|
# scopes delivery to the topmost modal popup or else the tiled content, so
|
|
5165
5610
|
# such an overlay would hold focus outside the key scope, where delivery
|
|
5166
5611
|
# reaches nobody and *every* keystroke goes dead until Tab recovers. A
|
|
@@ -5211,9 +5656,9 @@ module Tuile
|
|
|
5211
5656
|
def visible=: (bool _value) -> void
|
|
5212
5657
|
|
|
5213
5658
|
# Whether a left click outside this overlay closes it (default true). The
|
|
5214
|
-
# pane does the closing — {ScreenPane#
|
|
5215
|
-
# overlays *before*
|
|
5216
|
-
# *after*, so a widget that toggles its own overlay from a click on its
|
|
5659
|
+
# pane does the closing — {ScreenPane#dismissing_popups_outside} snapshots
|
|
5660
|
+
# the open overlays *before* the press is routed and closes the
|
|
5661
|
+
# dismissable ones *after*, so a widget that toggles its own overlay from a click on its
|
|
5217
5662
|
# face (a {Component::Select}, a {Component::MenuBar} title) still toggles
|
|
5218
5663
|
# correctly: the delivered click closes the overlay and the dismissal then
|
|
5219
5664
|
# no-ops on it, rather than closing and reopening it. Only `:left`
|
|
@@ -5249,7 +5694,7 @@ module Tuile
|
|
|
5249
5694
|
# overlay = Component::Overlay.new(content: label).open # construct and mount
|
|
5250
5695
|
#
|
|
5251
5696
|
# There is deliberately no class-level `Overlay.open` factory — see
|
|
5252
|
-
# `
|
|
5697
|
+
# `design/decisions.md` `D_popup_open`; returning `self` is what keeps the
|
|
5253
5698
|
# one-liner above available without one.
|
|
5254
5699
|
def open: () -> self
|
|
5255
5700
|
|
|
@@ -5267,7 +5712,7 @@ module Tuile
|
|
|
5267
5712
|
|
|
5268
5713
|
# Fires {#on_close}. A subclass overriding this **must** call `super`, or
|
|
5269
5714
|
# the overlay's driver never hears that it closed.
|
|
5270
|
-
def
|
|
5715
|
+
def handle_detached: () -> void
|
|
5271
5716
|
|
|
5272
5717
|
# Content fills the overlay's full rect — an Overlay has no border to
|
|
5273
5718
|
# subtract.
|
|
@@ -5275,7 +5720,7 @@ module Tuile
|
|
|
5275
5720
|
# _@param_ `content`
|
|
5276
5721
|
def layout: (Component content) -> void
|
|
5277
5722
|
|
|
5278
|
-
def
|
|
5723
|
+
def handle_focus: () -> void
|
|
5279
5724
|
|
|
5280
5725
|
# _@return_ — see {#close_on_outside_click?}.
|
|
5281
5726
|
attr_writer close_on_outside_click: bool
|
|
@@ -5298,12 +5743,12 @@ module Tuile
|
|
|
5298
5743
|
# A callback taking no arguments, fired once this overlay has left the
|
|
5299
5744
|
# screen — **however it left**: {#close}, a direct {Screen#remove_popup},
|
|
5300
5745
|
# an outside click, or teardown via {Screen#close}. That unconditionality
|
|
5301
|
-
# is the point, so it hangs off {#
|
|
5746
|
+
# is the point, so it hangs off {#handle_detached} rather than {#close}; a
|
|
5302
5747
|
# driver keeping its own record of open overlays reconciles it here and
|
|
5303
5748
|
# cannot drift ({Component::MenuBar::Cascade} is the worked example).
|
|
5304
5749
|
#
|
|
5305
5750
|
# It fires *after* the overlay is detached, so {#open?} is already false
|
|
5306
|
-
# and the usual {Component#
|
|
5751
|
+
# and the usual {Component#handle_detached} caveats apply: release state, don't
|
|
5307
5752
|
# inspect the tree, keep it trivial (it may run while the pane is mid-way
|
|
5308
5753
|
# through closing a batch of overlays, and a raise propagates).
|
|
5309
5754
|
attr_accessor on_close: Proc?
|
|
@@ -5385,7 +5830,7 @@ module Tuile
|
|
|
5385
5830
|
#
|
|
5386
5831
|
# Both the focus highlight and the click hit test use it, so a click on the
|
|
5387
5832
|
# blank tail — or on a lower row, when the rect is taller than one — does
|
|
5388
|
-
# not toggle. It still *focuses*: {
|
|
5833
|
+
# not toggle. It still *focuses*: {Mouse::Router}'s click-to-focus
|
|
5389
5834
|
# is ungated by geometry, and the tail is the field's own row.
|
|
5390
5835
|
#
|
|
5391
5836
|
# The extent ignores {Component#bg_color}: an inherited tint paints the dead
|
|
@@ -5397,13 +5842,13 @@ module Tuile
|
|
|
5397
5842
|
# to an ancestor.
|
|
5398
5843
|
#
|
|
5399
5844
|
# _@param_ `key`
|
|
5400
|
-
def handle_key
|
|
5845
|
+
def handle_key?: (String key) -> bool
|
|
5401
5846
|
|
|
5402
|
-
# Toggles on a left
|
|
5403
|
-
#
|
|
5847
|
+
# Toggles on a left press; a press on the dead tail past {#extent} focuses
|
|
5848
|
+
# the checkbox without reaching here.
|
|
5404
5849
|
#
|
|
5405
5850
|
# _@param_ `event`
|
|
5406
|
-
def
|
|
5851
|
+
def handle_mouse_down?: (Mouse::DownEvent event) -> bool
|
|
5407
5852
|
|
|
5408
5853
|
def repaint: () -> void
|
|
5409
5854
|
|
|
@@ -5444,7 +5889,7 @@ module Tuile
|
|
|
5444
5889
|
# Input fields are focusable by default (overrides {Component#focusable?});
|
|
5445
5890
|
# a read-only display field could override back to `false`. Only
|
|
5446
5891
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
5447
|
-
# composing wrappers, so it stays per-class (`
|
|
5892
|
+
# composing wrappers, so it stays per-class (`design/decisions.md`
|
|
5448
5893
|
# `D_integer_field`).
|
|
5449
5894
|
def focusable?: () -> bool
|
|
5450
5895
|
|
|
@@ -5515,7 +5960,7 @@ module Tuile
|
|
|
5515
5960
|
#
|
|
5516
5961
|
# {Item} handles are minted by {#add_item} and nest via the *same* method, so
|
|
5517
5962
|
# depth is unlimited. There is no removal, no reordering and no dynamic
|
|
5518
|
-
# rebuilding: a menu is built once, at construction. See `
|
|
5963
|
+
# rebuilding: a menu is built once, at construction. See `design/decisions.md`
|
|
5519
5964
|
# `D_menu_bar`.
|
|
5520
5965
|
#
|
|
5521
5966
|
# == Sizing
|
|
@@ -5577,7 +6022,7 @@ module Tuile
|
|
|
5577
6022
|
#
|
|
5578
6023
|
# Both the highlight and the click hit test use it, so a click on the blank
|
|
5579
6024
|
# tail — or on a lower row, when the rect is taller than one — opens
|
|
5580
|
-
# nothing. It still *focuses*: {
|
|
6025
|
+
# nothing. It still *focuses*: {Mouse::Router}'s click-to-focus is
|
|
5581
6026
|
# ungated by geometry.
|
|
5582
6027
|
def extent: () -> Size
|
|
5583
6028
|
|
|
@@ -5593,7 +6038,7 @@ module Tuile
|
|
|
5593
6038
|
# Closes the cascade, so a bar removed from the tree can't strand its
|
|
5594
6039
|
# panels on the pane — they are the {ScreenPane}'s children, not the bar's,
|
|
5595
6040
|
# so nothing else would take them down.
|
|
5596
|
-
def
|
|
6041
|
+
def handle_detached: () -> void
|
|
5597
6042
|
|
|
5598
6043
|
# Offers the key to the open cascade first, then to the strip's own
|
|
5599
6044
|
# LEFT/RIGHT/Enter/Space/Down.
|
|
@@ -5603,14 +6048,14 @@ module Tuile
|
|
|
5603
6048
|
# submenu — and both step to the sibling menu.
|
|
5604
6049
|
#
|
|
5605
6050
|
# _@param_ `key`
|
|
5606
|
-
def handle_key
|
|
6051
|
+
def handle_key?: (String key) -> bool
|
|
5607
6052
|
|
|
5608
|
-
# Opens the menu under a left
|
|
5609
|
-
# open one;
|
|
5610
|
-
#
|
|
6053
|
+
# Opens the menu under a left press, or closes it when it is already the
|
|
6054
|
+
# open one; a press on the strip's padding claims the press and does
|
|
6055
|
+
# nothing else.
|
|
5611
6056
|
#
|
|
5612
6057
|
# _@param_ `event`
|
|
5613
|
-
def
|
|
6058
|
+
def handle_mouse_down?: (Mouse::DownEvent event) -> bool
|
|
5614
6059
|
|
|
5615
6060
|
def repaint: () -> void
|
|
5616
6061
|
|
|
@@ -5629,7 +6074,7 @@ module Tuile
|
|
|
5629
6074
|
# The rect's *width* is the only part of it the offset depends on, so this
|
|
5630
6075
|
# hook is the whole geometry story; {Component#rect=} invalidates for us,
|
|
5631
6076
|
# and {#rect=} closes the cascade rather than re-anchoring it.
|
|
5632
|
-
def
|
|
6077
|
+
def handle_width_changed: () -> void
|
|
5633
6078
|
|
|
5634
6079
|
# Scrolls the minimum needed to show the highlighted segment whole, and is
|
|
5635
6080
|
# the sole writer of {#left_column}. Idempotent, so every mutation site can
|
|
@@ -5719,7 +6164,7 @@ module Tuile
|
|
|
5719
6164
|
# _@param_ `key`
|
|
5720
6165
|
#
|
|
5721
6166
|
# _@return_ — whether a mnemonic claimed the key.
|
|
5722
|
-
def handle_mnemonic
|
|
6167
|
+
def handle_mnemonic?: (String key) -> bool
|
|
5723
6168
|
|
|
5724
6169
|
# Moves the highlight along the strip, clamping at both ends. Consumes the
|
|
5725
6170
|
# key even at an end, as {Tabs} does.
|
|
@@ -5841,18 +6286,18 @@ module Tuile
|
|
|
5841
6286
|
# machinery of {MenuBar}; an app never names it.
|
|
5842
6287
|
#
|
|
5843
6288
|
# cascade.open_below(segment_rect, item) # Enter/Down on the strip
|
|
5844
|
-
# return true if cascade.handle_key(key) # MenuBar#handle_key
|
|
6289
|
+
# return true if cascade.handle_key?(key) # MenuBar#handle_key?, first
|
|
5845
6290
|
# cascade.close # focus lost, or rect changed
|
|
5846
6291
|
#
|
|
5847
6292
|
# A panel is a **non-modal overlay, not a child**, so it never takes focus:
|
|
5848
6293
|
# focus stays on the {MenuBar} for the whole interaction and every key
|
|
5849
|
-
# arrives via {MenuBar#handle_key}, which offers it here first. That is
|
|
6294
|
+
# arrives via {MenuBar#handle_key?}, which offers it here first. That is
|
|
5850
6295
|
# {Component::Select}'s architecture extended to N levels, and it is why
|
|
5851
6296
|
# nothing in the key-dispatch ladder changes.
|
|
5852
6297
|
#
|
|
5853
6298
|
# Widths are measured here, per level — the panel is as wide as the level's
|
|
5854
6299
|
# widest label — because {ListDropdown} deliberately measures nothing
|
|
5855
|
-
# itself (`
|
|
6300
|
+
# itself (`design/decisions.md` `D_select`).
|
|
5856
6301
|
#
|
|
5857
6302
|
# == Implementation details
|
|
5858
6303
|
# While open it consumes **everything** except the two keys that mean
|
|
@@ -5892,7 +6337,7 @@ module Tuile
|
|
|
5892
6337
|
# _@return_ — `true` when consumed — almost always, while open.
|
|
5893
6338
|
# `false` when closed, and for the two sideways keys {MenuBar} answers
|
|
5894
6339
|
# (see the class docs).
|
|
5895
|
-
def handle_key
|
|
6340
|
+
def handle_key?: (String key) -> bool
|
|
5896
6341
|
|
|
5897
6342
|
# Activates the deepest level's item bound to `key` — the drill-or-fire
|
|
5898
6343
|
# the mnemonic shares with Enter. The highlight moves there *first*, so a
|
|
@@ -5903,7 +6348,7 @@ module Tuile
|
|
|
5903
6348
|
#
|
|
5904
6349
|
# _@return_ — whether an item on the deepest level claimed it. A
|
|
5905
6350
|
# miss is never offered to a shallower level.
|
|
5906
|
-
def handle_mnemonic
|
|
6351
|
+
def handle_mnemonic?: (String key) -> bool
|
|
5907
6352
|
|
|
5908
6353
|
# _@return_ — the deepest open panel.
|
|
5909
6354
|
def deepest: () -> ListDropdown
|
|
@@ -5992,9 +6437,16 @@ module Tuile
|
|
|
5992
6437
|
# The dropdown is a {ListDropdown}, tinted to read as a floating panel; see
|
|
5993
6438
|
# it for the theming knob.
|
|
5994
6439
|
#
|
|
6440
|
+
# == The inner field is private machinery
|
|
6441
|
+
# It has no public accessor: its buffer is the *query*, so swapping the field
|
|
6442
|
+
# would break the filtering. What is worth reaching is re-exposed here
|
|
6443
|
+
# ({#placeholder}, {#cursor_position}); a **spec** reaches the field itself:
|
|
6444
|
+
#
|
|
6445
|
+
# field = Testing.get(Component::TextField, in: combo)
|
|
6446
|
+
# field.text = "ap" # type a query without a real loop
|
|
6447
|
+
#
|
|
5995
6448
|
# UI-thread-confined, like every component (see {Screen}).
|
|
5996
6449
|
class ComboBox < Component
|
|
5997
|
-
include Tuile::Component::HasContent
|
|
5998
6450
|
include Tuile::Component::HasValue
|
|
5999
6451
|
include Tuile::Component::HasPlaceholder
|
|
6000
6452
|
|
|
@@ -6020,12 +6472,13 @@ module Tuile
|
|
|
6020
6472
|
# _@param_ `text`
|
|
6021
6473
|
def placeholder=: (String? text) -> void
|
|
6022
6474
|
|
|
6023
|
-
#
|
|
6024
|
-
# field via {#layout}.
|
|
6475
|
+
# Resizes the field and re-anchors the dropdown if it is open.
|
|
6025
6476
|
#
|
|
6026
6477
|
# _@param_ `new_rect`
|
|
6027
6478
|
def rect=: (Rect new_rect) -> void
|
|
6028
6479
|
|
|
6480
|
+
def handle_focus: () -> void
|
|
6481
|
+
|
|
6029
6482
|
# Closes the dropdown and reverts an uncommitted query when the combo
|
|
6030
6483
|
# leaves the focus chain — so tabbing away doesn't strand an open menu or
|
|
6031
6484
|
# a half-typed filter. Safe against re-entrancy: focus never sits inside
|
|
@@ -6048,10 +6501,13 @@ module Tuile
|
|
|
6048
6501
|
# _@param_ `key`
|
|
6049
6502
|
#
|
|
6050
6503
|
# _@return_ — true if consumed.
|
|
6051
|
-
def handle_key
|
|
6504
|
+
def handle_key?: (String key) -> bool
|
|
6052
6505
|
|
|
6506
|
+
# Toggles the dropdown on a left press on the ▾ cell — the only cell of
|
|
6507
|
+
# this component's own that is not the field's.
|
|
6508
|
+
#
|
|
6053
6509
|
# _@param_ `event`
|
|
6054
|
-
def
|
|
6510
|
+
def handle_mouse_down?: (Mouse::DownEvent event) -> bool
|
|
6055
6511
|
|
|
6056
6512
|
def repaint: () -> void
|
|
6057
6513
|
|
|
@@ -6067,12 +6523,10 @@ module Tuile
|
|
|
6067
6523
|
# hangs under this rather than under the unused space below it.
|
|
6068
6524
|
def extent: () -> Size
|
|
6069
6525
|
|
|
6070
|
-
#
|
|
6071
|
-
#
|
|
6072
|
-
#
|
|
6073
|
-
|
|
6074
|
-
# _@param_ `field`
|
|
6075
|
-
def layout: (Component field) -> void
|
|
6526
|
+
# Declines the default's blank: `field` covers every column of the face but
|
|
6527
|
+
# the last, and this combo paints the `▾` into that one, so blanking would
|
|
6528
|
+
# only dirty a cell it is about to repaint (`D_progress_bar`).
|
|
6529
|
+
def clear_inside_extent: () -> void
|
|
6076
6530
|
|
|
6077
6531
|
# Dismisses the dropdown and puts the current value's label back in the
|
|
6078
6532
|
# field, undoing an uncommitted query.
|
|
@@ -6105,7 +6559,7 @@ module Tuile
|
|
|
6105
6559
|
# Sets the field's text without triggering a refilter — for programmatic
|
|
6106
6560
|
# value changes and query reverts, which must not spring the dropdown.
|
|
6107
6561
|
# Every programmatic write to the field goes through here; a direct
|
|
6108
|
-
# `
|
|
6562
|
+
# `field.text =` reaches the field's `on_change` and pops the dropdown
|
|
6109
6563
|
# open on a {#value=} the user never asked to browse.
|
|
6110
6564
|
# Parks the caret at the end: `text=` only *clamps* the caret, so a
|
|
6111
6565
|
# shorter query replaced by a longer label would otherwise strand it
|
|
@@ -6153,7 +6607,7 @@ module Tuile
|
|
|
6153
6607
|
# Input fields are focusable by default (overrides {Component#focusable?});
|
|
6154
6608
|
# a read-only display field could override back to `false`. Only
|
|
6155
6609
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
6156
|
-
# composing wrappers, so it stays per-class (`
|
|
6610
|
+
# composing wrappers, so it stays per-class (`design/decisions.md`
|
|
6157
6611
|
# `D_integer_field`).
|
|
6158
6612
|
def focusable?: () -> bool
|
|
6159
6613
|
|
|
@@ -6183,14 +6637,15 @@ module Tuile
|
|
|
6183
6637
|
# represent is invalid on the face too, even with no verdict written.
|
|
6184
6638
|
def error_ink?: () -> bool
|
|
6185
6639
|
|
|
6186
|
-
def on_focus: () -> void
|
|
6187
|
-
|
|
6188
6640
|
# _@return_ — the candidate items.
|
|
6189
6641
|
attr_accessor items: ::Array[untyped]
|
|
6190
6642
|
|
|
6191
6643
|
# _@return_ — item -> shown label (a `String` or
|
|
6192
6644
|
# {StyledString}); the field shows its `#to_s`, the list its styled form.
|
|
6193
6645
|
attr_accessor item_label: (Proc | Method)
|
|
6646
|
+
|
|
6647
|
+
# _@return_ — the inner field, holding the query.
|
|
6648
|
+
attr_reader field: TextField
|
|
6194
6649
|
end
|
|
6195
6650
|
|
|
6196
6651
|
# The value seam every input component shares: a settable/gettable {#value}
|
|
@@ -6249,7 +6704,7 @@ module Tuile
|
|
|
6249
6704
|
# Input fields are focusable by default (overrides {Component#focusable?});
|
|
6250
6705
|
# a read-only display field could override back to `false`. Only
|
|
6251
6706
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
6252
|
-
# composing wrappers, so it stays per-class (`
|
|
6707
|
+
# composing wrappers, so it stays per-class (`design/decisions.md`
|
|
6253
6708
|
# `D_integer_field`).
|
|
6254
6709
|
def focusable?: () -> bool
|
|
6255
6710
|
|
|
@@ -6311,11 +6766,11 @@ module Tuile
|
|
|
6311
6766
|
# designing around:
|
|
6312
6767
|
#
|
|
6313
6768
|
# - A hidden pane is invisible to *everything*: the Tab cycle, focus
|
|
6314
|
-
# cascades, repaint, the cursor, `
|
|
6769
|
+
# cascades, repaint, the cursor, `walk_tree` walks. No gates anywhere.
|
|
6315
6770
|
# - Its state survives, because state is ivars — scroll position, caret,
|
|
6316
6771
|
# list cursor, text are all exactly as the user left them, and mutating a
|
|
6317
6772
|
# hidden pane is safe (`invalidate` while detached is a silent no-op).
|
|
6318
|
-
# - {Component#
|
|
6773
|
+
# - {Component#handle_detached} / {Component#handle_attached} fire on every switch,
|
|
6319
6774
|
# so a pane holding a mounted-lifetime resource — a {Component::ProgressBar}'s
|
|
6320
6775
|
# ticker — releases it while hidden and re-acquires it on return. A pane
|
|
6321
6776
|
# that must keep something alive while hidden can't; that something
|
|
@@ -6325,7 +6780,7 @@ module Tuile
|
|
|
6325
6780
|
# `children` is `[strip, pane]`, the strip pinned at index 0, so pre-order
|
|
6326
6781
|
# traversal gives the strip-then-pane Tab order for free. The swap follows
|
|
6327
6782
|
# the slot-swap recipe {Component#detach_child} documents — detach, rewire,
|
|
6328
|
-
# `
|
|
6783
|
+
# `handle_child_removed` last, so the focus repair sees the new occupant.
|
|
6329
6784
|
#
|
|
6330
6785
|
# Panes live in an identity-keyed `Tab => Component` map here rather than in
|
|
6331
6786
|
# a slot on {Tabs::Tab}: the strip's tab array stays the sole ordering
|
|
@@ -6396,14 +6851,14 @@ module Tuile
|
|
|
6396
6851
|
|
|
6397
6852
|
# Sends focus to the strip: a sheet is a container, and the strip is where
|
|
6398
6853
|
# a tab switch is driven from. The pane is a Tab press away.
|
|
6399
|
-
def
|
|
6854
|
+
def handle_focus: () -> void
|
|
6400
6855
|
|
|
6401
6856
|
# Lands focus on the strip rather than on `self` when the focused pane is
|
|
6402
6857
|
# swapped out — a bare container can't use keys, and the user's last
|
|
6403
6858
|
# action was a tab switch.
|
|
6404
6859
|
#
|
|
6405
6860
|
# _@param_ `child`
|
|
6406
|
-
def
|
|
6861
|
+
def handle_child_removed: (Component child) -> void
|
|
6407
6862
|
|
|
6408
6863
|
# Makes the visible pane match `strip.selected`, swapping if it doesn't.
|
|
6409
6864
|
# Idempotent and the sole writer of `@pane`: it derives everything from
|
|
@@ -6468,7 +6923,7 @@ module Tuile
|
|
|
6468
6923
|
# class PromptArea < Component::TextArea
|
|
6469
6924
|
# protected
|
|
6470
6925
|
#
|
|
6471
|
-
# def handle_text_input_key(key)
|
|
6926
|
+
# def handle_text_input_key?(key)
|
|
6472
6927
|
# return recall_previous if key == Keys::UP_ARROW && caret_row.zero?
|
|
6473
6928
|
# return recall_next if key == Keys::DOWN_ARROW && caret_row == row_count - 1
|
|
6474
6929
|
#
|
|
@@ -6504,21 +6959,21 @@ module Tuile
|
|
|
6504
6959
|
def cursor_position: () -> Point?
|
|
6505
6960
|
|
|
6506
6961
|
# _@param_ `event`
|
|
6507
|
-
def
|
|
6962
|
+
def handle_mouse_down?: (Mouse::DownEvent event) -> bool
|
|
6508
6963
|
|
|
6509
6964
|
def repaint: () -> void
|
|
6510
6965
|
|
|
6511
|
-
def
|
|
6966
|
+
def handle_text_mutated: () -> void
|
|
6512
6967
|
|
|
6513
|
-
def
|
|
6968
|
+
def handle_caret_mutated: () -> void
|
|
6514
6969
|
|
|
6515
6970
|
# HOME/END and CTRL+U act on the caret's **row** — the wrapped one, not
|
|
6516
6971
|
# the `\n` line — so CTRL+U kills back to exactly where HOME would go.
|
|
6517
6972
|
#
|
|
6518
6973
|
# _@param_ `key`
|
|
6519
|
-
def handle_text_input_key
|
|
6974
|
+
def handle_text_input_key?: (String key) -> bool
|
|
6520
6975
|
|
|
6521
|
-
def
|
|
6976
|
+
def handle_width_changed: () -> void
|
|
6522
6977
|
|
|
6523
6978
|
# _@return_ — the current wrap of {#text} at {Rect#width}.
|
|
6524
6979
|
def wrap: () -> WrappedText
|
|
@@ -6777,8 +7232,8 @@ module Tuile
|
|
|
6777
7232
|
# splice a range in place. Turn on {#auto_scroll} to keep the latest content
|
|
6778
7233
|
# in view.
|
|
6779
7234
|
#
|
|
6780
|
-
# Meant to be the content of a {Window} — focus indication
|
|
6781
|
-
#
|
|
7235
|
+
# Meant to be the content of a {Window} — focus indication relies on the
|
|
7236
|
+
# surrounding window chrome.
|
|
6782
7237
|
class TextView < Component
|
|
6783
7238
|
def initialize: () -> void
|
|
6784
7239
|
|
|
@@ -6913,10 +7368,13 @@ module Tuile
|
|
|
6913
7368
|
# an unfocused view scrolls it (dispatch gates on focus, this doesn't).
|
|
6914
7369
|
#
|
|
6915
7370
|
# _@param_ `key`
|
|
6916
|
-
def handle_key
|
|
7371
|
+
def handle_key?: (String key) -> bool
|
|
6917
7372
|
|
|
7373
|
+
# Scrolls four rows a notch, and declines — so the notch bubbles to an
|
|
7374
|
+
# ancestor scroller — once this view is at that end of its text.
|
|
7375
|
+
#
|
|
6918
7376
|
# _@param_ `event`
|
|
6919
|
-
def
|
|
7377
|
+
def handle_mouse_scroll?: (Mouse::ScrollEvent event) -> bool
|
|
6920
7378
|
|
|
6921
7379
|
# Paints the text into {#rect}.
|
|
6922
7380
|
#
|
|
@@ -6930,7 +7388,7 @@ module Tuile
|
|
|
6930
7388
|
# Rewraps the text on width changes — {#wrap_width} is {#rect}`.width`
|
|
6931
7389
|
# minus {#scrollbar_columns}, and the latter varies with the width too.
|
|
6932
7390
|
# A {#scrollbar_visibility=} flip rewraps from its own setter instead.
|
|
6933
|
-
def
|
|
7391
|
+
def handle_width_changed: () -> void
|
|
6934
7392
|
|
|
6935
7393
|
# Validates and unpacks a {#replace}-style range argument into
|
|
6936
7394
|
# inclusive `[from, to]` line indices. An `Integer` `n` becomes
|
|
@@ -7292,7 +7750,7 @@ module Tuile
|
|
|
7292
7750
|
# empty). Give it a single-row {#rect}:
|
|
7293
7751
|
#
|
|
7294
7752
|
# field = Component::DateField.new
|
|
7295
|
-
# field.on_value_change = ->(d) { puts d.inspect } # Date or nil, per
|
|
7753
|
+
# field.on_value_change = ->(d) { puts d.inspect } # Date or nil, per commit
|
|
7296
7754
|
# field.value = Date.new(2026, 9, 4) # field shows "2026-09-04"
|
|
7297
7755
|
# field.placeholder # => "yyyy-mm-dd"
|
|
7298
7756
|
# field.clear # empties it; value => nil
|
|
@@ -7349,6 +7807,19 @@ module Tuile
|
|
|
7349
7807
|
# `20`, `202` stay quiet, leaving the field (or pressing ENTER) reddens what
|
|
7350
7808
|
# did not parse, and the next edit clears it again.
|
|
7351
7809
|
#
|
|
7810
|
+
# == The value notice waits for the same gesture
|
|
7811
|
+
# A prefix of a date can also parse *cleanly*: typing `1.1.2024` into a
|
|
7812
|
+
# `%d.%m.%Y` field passes through `1.1.2`, a perfectly good 1st of January
|
|
7813
|
+
# in the year 2. So {HasValue#on_value_change} does not fire per keystroke,
|
|
7814
|
+
# but when the user leaves the field or presses ENTER — and a form
|
|
7815
|
+
# recalculating from it never sees that year 2.
|
|
7816
|
+
#
|
|
7817
|
+
# {#value} does *not* wait: it is a live parse of the buffer at every
|
|
7818
|
+
# moment, so a save gate reached without leaving the field reads the date
|
|
7819
|
+
# on screen. Nor does a change nobody had to type — a {#value=}, an Up/Down
|
|
7820
|
+
# step, a {#clear} and a reparse under new {#formats} all fire as they
|
|
7821
|
+
# happen.
|
|
7822
|
+
#
|
|
7352
7823
|
# == Implementation details
|
|
7353
7824
|
# - **The buffer is the single source of truth.** {#value} is a parse of it,
|
|
7354
7825
|
# recomputed on read — so {#formats=} and {#calendar_start=} can change the
|
|
@@ -7456,6 +7927,11 @@ module Tuile
|
|
|
7456
7927
|
# way, so input it could not parse starts painting the well.
|
|
7457
7928
|
def commit: () -> void
|
|
7458
7929
|
|
|
7930
|
+
# `false`: a prefix of a date can parse cleanly (`1.1.2` for `1.1.2024`),
|
|
7931
|
+
# so the notice settles onto the commit gestures, exactly as the ink
|
|
7932
|
+
# does. The class docs carry the case.
|
|
7933
|
+
def notify_on_edit?: () -> bool
|
|
7934
|
+
|
|
7459
7935
|
# Every prefix of a date is bad input, so the well is latched to the
|
|
7460
7936
|
# commit gestures instead of painted per keystroke: `2`, `20`, `202` on
|
|
7461
7937
|
# the way to `2026-09-04` never redden, and a date the field cannot parse
|
|
@@ -7465,11 +7941,11 @@ module Tuile
|
|
|
7465
7941
|
|
|
7466
7942
|
# An edit is the user having another go, so the well goes quiet again
|
|
7467
7943
|
# until the next commit gesture.
|
|
7468
|
-
def
|
|
7944
|
+
def handle_editor_change: () -> void
|
|
7469
7945
|
|
|
7470
7946
|
# Re-derives the hint (which was *pushed* into the editor, so a repaint
|
|
7471
7947
|
# alone would keep the old one) and rewrites a buffer that still parses.
|
|
7472
|
-
def
|
|
7948
|
+
def handle_locale_changed: () -> void
|
|
7473
7949
|
|
|
7474
7950
|
# _@param_ `flag`
|
|
7475
7951
|
def settle: (bool flag) -> void
|
|
@@ -7571,7 +8047,7 @@ module Tuile
|
|
|
7571
8047
|
# - an **index** counts characters into {#text} — {#caret},
|
|
7572
8048
|
# {#max_text_length}, `text[i]`, every edit;
|
|
7573
8049
|
# - a **column** counts terminal cells — {#rect}, {#cursor_position}, a
|
|
7574
|
-
# {
|
|
8050
|
+
# {Mouse::DownEvent}, and the private horizontal scroll offset `left_column`.
|
|
7575
8051
|
#
|
|
7576
8052
|
# They coincide only while every glyph is one column wide. A fullwidth CJK
|
|
7577
8053
|
# char is two columns and a combining mark zero, so index 3 of `"日本語"` is
|
|
@@ -7596,11 +8072,11 @@ module Tuile
|
|
|
7596
8072
|
|
|
7597
8073
|
def cursor_position: () -> Point?
|
|
7598
8074
|
|
|
7599
|
-
# Places the caret at the
|
|
8075
|
+
# Places the caret at the pressed column. A press on the right half of a
|
|
7600
8076
|
# wide glyph lands *after* it, as in any editor.
|
|
7601
8077
|
#
|
|
7602
8078
|
# _@param_ `event`
|
|
7603
|
-
def
|
|
8079
|
+
def handle_mouse_down?: (Mouse::DownEvent event) -> bool
|
|
7604
8080
|
|
|
7605
8081
|
def repaint: () -> void
|
|
7606
8082
|
|
|
@@ -7608,12 +8084,12 @@ module Tuile
|
|
|
7608
8084
|
# "clear what I typed".
|
|
7609
8085
|
#
|
|
7610
8086
|
# _@param_ `key`
|
|
7611
|
-
def handle_text_input_key
|
|
8087
|
+
def handle_text_input_key?: (String key) -> bool
|
|
7612
8088
|
|
|
7613
8089
|
# Keeps the paste's **first line** and drops the rest, then trims what's
|
|
7614
8090
|
# left to what {#max_text_length} still allows:
|
|
7615
8091
|
#
|
|
7616
|
-
# f.handle_paste("name\nstreet\ncity")
|
|
8092
|
+
# f.handle_paste("name\nstreet\ncity")
|
|
7617
8093
|
# f.text # => "name"
|
|
7618
8094
|
#
|
|
7619
8095
|
# Overshooting the cap trims rather than rejects, which is what typing the
|
|
@@ -7623,11 +8099,11 @@ module Tuile
|
|
|
7623
8099
|
# _@param_ `text`
|
|
7624
8100
|
def preprocess_paste: (String text) -> String
|
|
7625
8101
|
|
|
7626
|
-
def
|
|
8102
|
+
def handle_text_mutated: () -> void
|
|
7627
8103
|
|
|
7628
|
-
def
|
|
8104
|
+
def handle_caret_mutated: () -> void
|
|
7629
8105
|
|
|
7630
|
-
def
|
|
8106
|
+
def handle_width_changed: () -> void
|
|
7631
8107
|
|
|
7632
8108
|
# What the field paints in place of {#text}: one display character per
|
|
7633
8109
|
# {#text} character, in order. `column_at` measures `display_text[0, i]` as
|
|
@@ -7804,6 +8280,19 @@ module Tuile
|
|
|
7804
8280
|
# the field (or pressing ENTER) reddens what did not parse, and the next
|
|
7805
8281
|
# edit clears it again.
|
|
7806
8282
|
#
|
|
8283
|
+
# == The value notice waits for the same gesture
|
|
8284
|
+
# A prefix of a time can also parse *cleanly*: typing `13:45` passes
|
|
8285
|
+
# through `13:4`, a perfectly good four minutes past one. So
|
|
8286
|
+
# {HasValue#on_value_change} does not fire per keystroke, but when the user
|
|
8287
|
+
# leaves the field or presses ENTER — and a form recalculating from it
|
|
8288
|
+
# never sees those intermediate readings.
|
|
8289
|
+
#
|
|
8290
|
+
# {#value} does *not* wait: it is a live parse of the buffer at every
|
|
8291
|
+
# moment, so a save gate reached without leaving the field reads the time
|
|
8292
|
+
# on screen. Nor does a change nobody had to type — a {#value=}, a
|
|
8293
|
+
# {#set_to}, an arrow-key step, a {#clear} and a reparse under a new
|
|
8294
|
+
# {#step} all fire as they happen.
|
|
8295
|
+
#
|
|
7807
8296
|
# == Implementation details
|
|
7808
8297
|
# - **The buffer is the single source of truth.** {#value} is a parse of it,
|
|
7809
8298
|
# recomputed on read — so {#step=} and a {Screen#locale=} can change the
|
|
@@ -7927,13 +8416,18 @@ module Tuile
|
|
|
7927
8416
|
#
|
|
7928
8417
|
# _@return_ — `true` for the two page keys, else whatever `super`
|
|
7929
8418
|
# returns.
|
|
7930
|
-
def handle_key
|
|
8419
|
+
def handle_key?: (String key) -> bool
|
|
7931
8420
|
|
|
7932
8421
|
# Rewrites a buffer that parses in the primary format, leaving one that
|
|
7933
8422
|
# does not exactly as the user typed it — and settles the field either
|
|
7934
8423
|
# way, so input it could not parse starts painting the well.
|
|
7935
8424
|
def commit: () -> void
|
|
7936
8425
|
|
|
8426
|
+
# `false`: a prefix of a time can parse cleanly (`13:4` for `13:45`), so
|
|
8427
|
+
# the notice settles onto the commit gestures, exactly as the ink does.
|
|
8428
|
+
# The class docs carry the case.
|
|
8429
|
+
def notify_on_edit?: () -> bool
|
|
8430
|
+
|
|
7937
8431
|
# Every prefix of a time is bad input, so the well is latched to the
|
|
7938
8432
|
# commit gestures instead of painted per keystroke: `1`, `13`, `13:` on
|
|
7939
8433
|
# the way to `13:45` never redden, and a time the field cannot parse
|
|
@@ -7943,9 +8437,9 @@ module Tuile
|
|
|
7943
8437
|
|
|
7944
8438
|
# An edit is the user having another go, so the well goes quiet again
|
|
7945
8439
|
# until the next commit gesture.
|
|
7946
|
-
def
|
|
8440
|
+
def handle_editor_change: () -> void
|
|
7947
8441
|
|
|
7948
|
-
def
|
|
8442
|
+
def handle_locale_changed: () -> void
|
|
7949
8443
|
|
|
7950
8444
|
# Re-derives the hint (which was *pushed* into the editor, so a repaint
|
|
7951
8445
|
# alone would keep the old one) and rewrites a buffer that still parses —
|
|
@@ -8128,7 +8622,7 @@ module Tuile
|
|
|
8128
8622
|
# def bad_input_settled? = @settled # set on commit, cleared on an edit
|
|
8129
8623
|
#
|
|
8130
8624
|
# It gates the **ink only**: {#bad_input?} is a pull, and a save gate
|
|
8131
|
-
# asking at a click must get the answer settled or not (`
|
|
8625
|
+
# asking at a click must get the answer settled or not (`design/decisions.md`
|
|
8132
8626
|
# `D_bad_input`).
|
|
8133
8627
|
def bad_input_settled?: () -> bool
|
|
8134
8628
|
|
|
@@ -8253,7 +8747,7 @@ module Tuile
|
|
|
8253
8747
|
# _@param_ `rect`
|
|
8254
8748
|
def rect=: (Rect rect) -> void
|
|
8255
8749
|
|
|
8256
|
-
def
|
|
8750
|
+
def handle_focus: () -> void
|
|
8257
8751
|
|
|
8258
8752
|
# _@return_ — the current content component.
|
|
8259
8753
|
attr_accessor content: Component?
|
|
@@ -8370,7 +8864,7 @@ module Tuile
|
|
|
8370
8864
|
# _@param_ `new_rect`
|
|
8371
8865
|
def rect=: (Rect new_rect) -> void
|
|
8372
8866
|
|
|
8373
|
-
def
|
|
8867
|
+
def handle_focus: () -> void
|
|
8374
8868
|
|
|
8375
8869
|
# _@return_ — the presented items.
|
|
8376
8870
|
def items: () -> ::Array[untyped]
|
|
@@ -8393,7 +8887,7 @@ module Tuile
|
|
|
8393
8887
|
# neither of us wants bubbles on to an ancestor.
|
|
8394
8888
|
#
|
|
8395
8889
|
# _@param_ `key`
|
|
8396
|
-
def handle_key
|
|
8890
|
+
def handle_key?: (String key) -> bool
|
|
8397
8891
|
|
|
8398
8892
|
# Selects the item on row `index`; an index outside {#items} is ignored —
|
|
8399
8893
|
# {List::Cursor::None}'s `-1` would otherwise select the *last* item.
|
|
@@ -8447,7 +8941,7 @@ module Tuile
|
|
|
8447
8941
|
# Input fields are focusable by default (overrides {Component#focusable?});
|
|
8448
8942
|
# a read-only display field could override back to `false`. Only
|
|
8449
8943
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
8450
|
-
# composing wrappers, so it stays per-class (`
|
|
8944
|
+
# composing wrappers, so it stays per-class (`design/decisions.md`
|
|
8451
8945
|
# `D_integer_field`).
|
|
8452
8946
|
def focusable?: () -> bool
|
|
8453
8947
|
|
|
@@ -8482,9 +8976,10 @@ module Tuile
|
|
|
8482
8976
|
|
|
8483
8977
|
# The composed {List}: an app may *tune* it — its scrollbar, its cursor,
|
|
8484
8978
|
# `show_cursor_when_inactive` — but never replace it, since this group's
|
|
8485
|
-
# renderer and selection are wired into this one.
|
|
8486
|
-
#
|
|
8487
|
-
# instead of forwarded
|
|
8979
|
+
# renderer and selection are wired into this one (`design/decisions.md`
|
|
8980
|
+
# `D_has_content`). Those knobs are {List} concepts rather than group
|
|
8981
|
+
# concepts, which is why they are reached here instead of forwarded
|
|
8982
|
+
# (`D_wrapping_field`).
|
|
8488
8983
|
attr_reader list: List
|
|
8489
8984
|
|
|
8490
8985
|
# _@return_ — item -> row label (a `String`, {StyledString}, or
|
|
@@ -8518,20 +9013,20 @@ module Tuile
|
|
|
8518
9013
|
# (floor {MIN_CAP_WIDTH}) and {HEIGHT_FRACTION} tall, and **grows but never
|
|
8519
9014
|
# shrinks** while it lives; a long message wraps to {MAX_ROWS_PER_MESSAGE}
|
|
8520
9015
|
# rows and is then ellipsized, and entries past the height cap wait unpainted.
|
|
8521
|
-
# `
|
|
9016
|
+
# `design/decisions.md` `D_notification` has why each of those is what it is.
|
|
8522
9017
|
#
|
|
8523
9018
|
# Three things it deliberately doesn't do:
|
|
8524
9019
|
#
|
|
8525
9020
|
# - **Take focus, or receive keys.** An {Overlay} sits off the
|
|
8526
|
-
# key-dispatch scope ({ScreenPane#handle_key}), so no key arrives here at
|
|
8527
|
-
# all. A left click *on the box* dismisses ({#
|
|
9021
|
+
# key-dispatch scope ({ScreenPane#handle_key?}), so no key arrives here at
|
|
9022
|
+
# all. A left click *on the box* dismisses ({#handle_mouse_down?}); an app
|
|
8528
9023
|
# wanting a key registers a global shortcut and calls {Overlay#close}. A
|
|
8529
9024
|
# click *elsewhere* does not — this is the one overlay with
|
|
8530
9025
|
# {Overlay#close_on_outside_click?} false, since a toast is timed and an
|
|
8531
9026
|
# unrelated click is not about it.
|
|
8532
9027
|
# - **Follow a theme flip.** A `Theme::Ref` `color:` is resolved once, when
|
|
8533
9028
|
# the message is added — a toast lives seconds, so there is no
|
|
8534
|
-
# {Component#
|
|
9029
|
+
# {Component#handle_theme_changed} rebuild.
|
|
8535
9030
|
# - **Take a size.** An {Overlay} has no declared box; the messages decide
|
|
8536
9031
|
# this one's, in {#reposition}.
|
|
8537
9032
|
class Notification < Tuile::Component::Overlay
|
|
@@ -8577,20 +9072,21 @@ module Tuile
|
|
|
8577
9072
|
# from the width.
|
|
8578
9073
|
def reposition: () -> void
|
|
8579
9074
|
|
|
8580
|
-
# A left
|
|
8581
|
-
#
|
|
8582
|
-
# nuke the box on a stray spin.
|
|
8583
|
-
#
|
|
8584
|
-
# Deliberately *replaces* rather than augments: neither `super` nor
|
|
8585
|
-
# {HasContent#handle_mouse} may run, since both end at a
|
|
8586
|
-
# `screen.focused = …` inside this subtree (see {Overlay#focusable?}).
|
|
9075
|
+
# A left press dismisses the whole box, every message with it. Every other
|
|
9076
|
+
# press is claimed and inert, so nothing beneath the toast acts on it.
|
|
8587
9077
|
#
|
|
8588
9078
|
# _@param_ `event`
|
|
8589
|
-
def
|
|
9079
|
+
def handle_mouse_down?: (Mouse::DownEvent event) -> bool
|
|
9080
|
+
|
|
9081
|
+
# Claimed and inert: a stray wheel spin over the toast must neither nuke it
|
|
9082
|
+
# nor reach whatever it covers.
|
|
9083
|
+
#
|
|
9084
|
+
# _@param_ `_event`
|
|
9085
|
+
def handle_mouse_scroll?: (Mouse::ScrollEvent _event) -> bool
|
|
8590
9086
|
|
|
8591
|
-
def
|
|
9087
|
+
def handle_attached: () -> void
|
|
8592
9088
|
|
|
8593
|
-
def
|
|
9089
|
+
def handle_detached: () -> void
|
|
8594
9090
|
|
|
8595
9091
|
# Retires the oldest message, closing the box when it was the last. Runs on
|
|
8596
9092
|
# the event-loop thread, from the ticker.
|
|
@@ -8599,7 +9095,7 @@ module Tuile
|
|
|
8599
9095
|
# Syncs the retirement clock from the invariant "something to retire, and on
|
|
8600
9096
|
# screen" — the sole writer of `@ticker`. Four sites change whether it is
|
|
8601
9097
|
# wanted (append, a retirement that empties the queue, {#close}, detach),
|
|
8602
|
-
# which is the 2×2 a start-in-{#
|
|
9098
|
+
# which is the 2×2 a start-in-{#handle_attached} / cancel-in-{#handle_detached} pair
|
|
8603
9099
|
# gets half wrong. The early return is also what keeps an append from
|
|
8604
9100
|
# *restarting* the clock and extending the oldest message's life.
|
|
8605
9101
|
def sync_ticker: () -> void
|
|
@@ -8646,6 +9142,19 @@ module Tuile
|
|
|
8646
9142
|
#
|
|
8647
9143
|
# _@param_ `rows`
|
|
8648
9144
|
def join_rows: (::Array[StyledString] rows) -> StyledString
|
|
9145
|
+
|
|
9146
|
+
# The message rows, minus the wheel — the one thing a plain
|
|
9147
|
+
# {Component::TextView} would add here. On a short terminal the queued
|
|
9148
|
+
# messages are taller than the box, and the box is unfocusable, so
|
|
9149
|
+
# scrolling them would be a capability the keyboard cannot reach
|
|
9150
|
+
# (`D_mouse`); they are meant to *wait* until the ticker retires the ones
|
|
9151
|
+
# above.
|
|
9152
|
+
class View < Tuile::Component::TextView
|
|
9153
|
+
# _@param_ `_event`
|
|
9154
|
+
#
|
|
9155
|
+
# _@return_ — true — claimed on the box's behalf, and inert.
|
|
9156
|
+
def handle_mouse_scroll?: (Mouse::ScrollEvent _event) -> bool
|
|
9157
|
+
end
|
|
8649
9158
|
end
|
|
8650
9159
|
|
|
8651
9160
|
# A one-row progress bar: a run of `█` growing left to right across {#rect},
|
|
@@ -8684,7 +9193,7 @@ module Tuile
|
|
|
8684
9193
|
# The `█`/`░` pair is the same one {VerticalScrollBar} uses — East-Asian
|
|
8685
9194
|
# Ambiguous and Neutral respectively, so under an ambiguous-as-wide terminal
|
|
8686
9195
|
# the rendered length would vary with the fill level. Shipped anyway, per
|
|
8687
|
-
# `
|
|
9196
|
+
# `design/decisions.md` `D_ambiguous_width`: a bar that rhymes with the scrollbar
|
|
8688
9197
|
# beats a third convention, and if that bet is ever reversed both swap
|
|
8689
9198
|
# together.
|
|
8690
9199
|
class ProgressBar < Component
|
|
@@ -8730,9 +9239,9 @@ module Tuile
|
|
|
8730
9239
|
# _@param_ `flag` — coerced; truthiness decides.
|
|
8731
9240
|
def indeterminate=: (bool flag) -> void
|
|
8732
9241
|
|
|
8733
|
-
def
|
|
9242
|
+
def handle_attached: () -> void
|
|
8734
9243
|
|
|
8735
|
-
def
|
|
9244
|
+
def handle_detached: () -> void
|
|
8736
9245
|
|
|
8737
9246
|
# Paints the bar on the first row of {#rect} and blanks the rest.
|
|
8738
9247
|
#
|
|
@@ -8811,7 +9320,7 @@ module Tuile
|
|
|
8811
9320
|
#
|
|
8812
9321
|
# == Implementation details
|
|
8813
9322
|
# An includer overrides {#bad_input_message} and nothing else. Two rules
|
|
8814
|
-
# bind that override, and `
|
|
9323
|
+
# bind that override, and `design/decisions.md` `D_bad_input` has the why:
|
|
8815
9324
|
#
|
|
8816
9325
|
# - **Empty input is not bad input.** Return `nil` for an empty buffer even
|
|
8817
9326
|
# though it parses to nothing, or every blank *optional* field blocks a
|
|
@@ -8859,7 +9368,7 @@ module Tuile
|
|
|
8859
9368
|
# def bad_input_settled? = @settled # set on commit, cleared on an edit
|
|
8860
9369
|
#
|
|
8861
9370
|
# It gates the **ink only**: {#bad_input?} is a pull, and a save gate
|
|
8862
|
-
# asking at a click must get the answer settled or not (`
|
|
9371
|
+
# asking at a click must get the answer settled or not (`design/decisions.md`
|
|
8863
9372
|
# `D_bad_input`).
|
|
8864
9373
|
def bad_input_settled?: () -> bool
|
|
8865
9374
|
|
|
@@ -8966,7 +9475,7 @@ module Tuile
|
|
|
8966
9475
|
# def bad_input_settled? = @settled # set on commit, cleared on an edit
|
|
8967
9476
|
#
|
|
8968
9477
|
# It gates the **ink only**: {#bad_input?} is a pull, and a save gate
|
|
8969
|
-
# asking at a click must get the answer settled or not (`
|
|
9478
|
+
# asking at a click must get the answer settled or not (`design/decisions.md`
|
|
8970
9479
|
# `D_bad_input`).
|
|
8971
9480
|
def bad_input_settled?: () -> bool
|
|
8972
9481
|
|
|
@@ -9132,7 +9641,7 @@ module Tuile
|
|
|
9132
9641
|
#
|
|
9133
9642
|
# _@param_ `rows` — how many rows there are to show — the content count, not the height; more than fits turns the scrollbar on. `0` collapses the dropdown to an empty rect (drivers close instead).
|
|
9134
9643
|
#
|
|
9135
|
-
# _@param_ `width` — the panel's width in columns, clamped to the screen. **Required, with no default:** `anchor.width` is the *parent's* width and would be meaningless here, so the caller measures (see `
|
|
9644
|
+
# _@param_ `width` — the panel's width in columns, clamped to the screen. **Required, with no default:** `anchor.width` is the *parent's* width and would be meaningless here, so the caller measures (see `design/decisions.md` `D_select` on why the width policy stays with the driver).
|
|
9136
9645
|
#
|
|
9137
9646
|
# _@param_ `max_rows` — rows shown before the list scrolls.
|
|
9138
9647
|
def anchor_beside: (
|
|
@@ -9234,18 +9743,33 @@ module Tuile
|
|
|
9234
9743
|
end
|
|
9235
9744
|
|
|
9236
9745
|
# A {Window} that lists options identified by single keyboard keys, asks
|
|
9237
|
-
# the user to pick one, and fires a callback with the picked key.
|
|
9746
|
+
# the user to pick one, and fires a callback with the picked key. Each row
|
|
9747
|
+
# is `"<key> <caption>"`:
|
|
9748
|
+
#
|
|
9749
|
+
# PickerWindow.open("Sort by", [%w[n name], %w[s size]]) { sort_by(_1) }
|
|
9750
|
+
#
|
|
9751
|
+
# ┌Sort by───────┐
|
|
9752
|
+
# │n name │
|
|
9753
|
+
# │s size │
|
|
9754
|
+
# └──────────────┘
|
|
9238
9755
|
#
|
|
9239
9756
|
# Usable tiled (just add to a {Layout} and read picks via the block) or
|
|
9240
9757
|
# as a popup via {.open}, which wraps it in a {Popup} that closes itself
|
|
9241
9758
|
# after a pick. ESC / `q` close without firing the callback.
|
|
9759
|
+
#
|
|
9760
|
+
# Captions paint in the terminal's own foreground. To color them, hand in
|
|
9761
|
+
# {StyledString} captions — the picker styles nothing itself, so an app's
|
|
9762
|
+
# own token applies per option:
|
|
9763
|
+
#
|
|
9764
|
+
# PickerWindow.open("File", [["o", StyledString.plain("Open")],
|
|
9765
|
+
# ["d", theme.fg(:danger, "Delete")]]) { … }
|
|
9242
9766
|
class PickerWindow < Tuile::Component::Window
|
|
9243
9767
|
MAX_ITEMS: Integer
|
|
9244
9768
|
|
|
9245
9769
|
# _@param_ `caption` — the window caption.
|
|
9246
9770
|
#
|
|
9247
|
-
# _@param_ `options` — pairs of keyboard key and option caption.
|
|
9248
|
-
def initialize: (String caption, ::Array[[String,
|
|
9771
|
+
# _@param_ `options` — pairs of keyboard key and option caption. A caption goes through {StyledString.parse}, so a plain String, an ANSI-coded one (what {Theme#fg} returns) and a {StyledString} are all accepted.
|
|
9772
|
+
def initialize: (String caption, ::Array[[String, StyledString]] options) ?{ (String key) -> void } -> void
|
|
9249
9773
|
|
|
9250
9774
|
# Handles an option-key press. Reached by bubbling: the inner {List}
|
|
9251
9775
|
# (the focused component) sees the key first and handles cursor/Enter
|
|
@@ -9253,7 +9777,7 @@ module Tuile
|
|
|
9253
9777
|
# option's `key` picks that option.
|
|
9254
9778
|
#
|
|
9255
9779
|
# _@param_ `key`
|
|
9256
|
-
def handle_key
|
|
9780
|
+
def handle_key?: (String key) -> bool
|
|
9257
9781
|
|
|
9258
9782
|
# Opens a picker as a popup. Picking an option fires `block`, then
|
|
9259
9783
|
# closes the popup; ESC / `q` close without firing `block`.
|
|
@@ -9263,7 +9787,7 @@ module Tuile
|
|
|
9263
9787
|
# _@param_ `options`
|
|
9264
9788
|
#
|
|
9265
9789
|
# _@return_ — the wrapping popup.
|
|
9266
|
-
def self.open: (String caption, ::Array[[String,
|
|
9790
|
+
def self.open: (String caption, ::Array[[String, StyledString]] options) ?{ (String key) -> void } -> Popup
|
|
9267
9791
|
|
|
9268
9792
|
# _@param_ `key`
|
|
9269
9793
|
def select_option: (String key) -> void
|
|
@@ -9277,13 +9801,15 @@ module Tuile
|
|
|
9277
9801
|
# @!attribute [r] key
|
|
9278
9802
|
# @return [String] the keyboard key that picks this option.
|
|
9279
9803
|
# @!attribute [r] caption
|
|
9280
|
-
# @return [
|
|
9804
|
+
# @return [StyledString] the option caption, coerced from whatever the
|
|
9805
|
+
# constructor was handed.
|
|
9281
9806
|
class Option
|
|
9282
9807
|
# _@return_ — the keyboard key that picks this option.
|
|
9283
9808
|
attr_reader key: String
|
|
9284
9809
|
|
|
9285
|
-
# _@return_ — the option caption
|
|
9286
|
-
|
|
9810
|
+
# _@return_ — the option caption, coerced from whatever the
|
|
9811
|
+
# constructor was handed.
|
|
9812
|
+
attr_reader caption: StyledString
|
|
9287
9813
|
end
|
|
9288
9814
|
end
|
|
9289
9815
|
|
|
@@ -9350,7 +9876,7 @@ module Tuile
|
|
|
9350
9876
|
# _@param_ `new_rect`
|
|
9351
9877
|
def rect=: (Rect new_rect) -> void
|
|
9352
9878
|
|
|
9353
|
-
def
|
|
9879
|
+
def handle_focus: () -> void
|
|
9354
9880
|
|
|
9355
9881
|
# _@return_ — the presented items.
|
|
9356
9882
|
def items: () -> ::Array[untyped]
|
|
@@ -9377,7 +9903,7 @@ module Tuile
|
|
|
9377
9903
|
# neither of us wants bubbles on to an ancestor.
|
|
9378
9904
|
#
|
|
9379
9905
|
# _@param_ `key`
|
|
9380
|
-
def handle_key
|
|
9906
|
+
def handle_key?: (String key) -> bool
|
|
9381
9907
|
|
|
9382
9908
|
# Flips membership of the item on row `index`; an index outside {#items} is
|
|
9383
9909
|
# ignored — {List::Cursor::None}'s `-1` would otherwise toggle the *last*
|
|
@@ -9430,7 +9956,7 @@ module Tuile
|
|
|
9430
9956
|
# Input fields are focusable by default (overrides {Component#focusable?});
|
|
9431
9957
|
# a read-only display field could override back to `false`. Only
|
|
9432
9958
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
9433
|
-
# composing wrappers, so it stays per-class (`
|
|
9959
|
+
# composing wrappers, so it stays per-class (`design/decisions.md`
|
|
9434
9960
|
# `D_integer_field`).
|
|
9435
9961
|
def focusable?: () -> bool
|
|
9436
9962
|
|
|
@@ -9465,9 +9991,10 @@ module Tuile
|
|
|
9465
9991
|
|
|
9466
9992
|
# The composed {List}: an app may *tune* it — its scrollbar, its cursor,
|
|
9467
9993
|
# `show_cursor_when_inactive` — but never replace it, since this group's
|
|
9468
|
-
# renderer and selection are wired into this one.
|
|
9469
|
-
#
|
|
9470
|
-
# instead of forwarded
|
|
9994
|
+
# renderer and selection are wired into this one (`design/decisions.md`
|
|
9995
|
+
# `D_has_content`). Those knobs are {List} concepts rather than group
|
|
9996
|
+
# concepts, which is why they are reached here instead of forwarded
|
|
9997
|
+
# (`D_wrapping_field`).
|
|
9471
9998
|
attr_reader list: List
|
|
9472
9999
|
|
|
9473
10000
|
# _@return_ — item -> row label (a `String`, {StyledString}, or
|
|
@@ -9517,7 +10044,7 @@ module Tuile
|
|
|
9517
10044
|
# There is deliberately no content slot: the body is prose ({#message=}
|
|
9518
10045
|
# takes a component for the rare rich body, but the dialog then cannot
|
|
9519
10046
|
# measure it). A dialog collecting *input* is not a confirm dialog — build a
|
|
9520
|
-
# `Popup.new(content: your_layout)`. See `
|
|
10047
|
+
# `Popup.new(content: your_layout)`. See `design/decisions.md` `D_confirm_window`
|
|
9521
10048
|
# for the API rationale.
|
|
9522
10049
|
class ConfirmWindow < Tuile::Component::Window
|
|
9523
10050
|
BODY_SCROLL_KEYS: ::Array[String]
|
|
@@ -9584,8 +10111,10 @@ module Tuile
|
|
|
9584
10111
|
def measured_size: (?Size reference) -> Size
|
|
9585
10112
|
|
|
9586
10113
|
# Focus lands on the first button rather than cascading into the message
|
|
9587
|
-
# body, which sits before the button row in the tree.
|
|
9588
|
-
|
|
10114
|
+
# body, which sits before the button row in the tree. `super` is reached
|
|
10115
|
+
# only when there is no button to take it: {HasContent#handle_focus} *is*
|
|
10116
|
+
# the cascade this override exists to skip.
|
|
10117
|
+
def handle_focus: () -> void
|
|
9589
10118
|
|
|
9590
10119
|
# Handles the dialog-wide keys: Left/Right move between the buttons,
|
|
9591
10120
|
# {BODY_SCROLL_KEYS} are hand-fed to the message body, and a mnemonic
|
|
@@ -9596,7 +10125,7 @@ module Tuile
|
|
|
9596
10125
|
# _@param_ `key`
|
|
9597
10126
|
#
|
|
9598
10127
|
# _@return_ — true if the key was handled.
|
|
9599
|
-
def handle_key
|
|
10128
|
+
def handle_key?: (String key) -> bool
|
|
9600
10129
|
|
|
9601
10130
|
# Opens an acknowledgement dialog: a message and one button, whose press
|
|
9602
10131
|
# is the dismissal. The button exists for discoverability — ESC and `q`
|
|
@@ -9751,7 +10280,7 @@ module Tuile
|
|
|
9751
10280
|
# nothing — where red *text* is invisible on the empty field that is the
|
|
9752
10281
|
# required-field case, and invisible again on content carrying colors of its
|
|
9753
10282
|
# own. It takes two tokens rather than one because a focused invalid field
|
|
9754
|
-
# still has to look focused (`
|
|
10283
|
+
# still has to look focused (`design/decisions.md` `D_has_validation`).
|
|
9755
10284
|
#
|
|
9756
10285
|
# The well reaches the whole widget with nothing forwarding it: a composed
|
|
9757
10286
|
# field's inner face is marked {Component::BG_INHERIT} and a group's {List}
|
|
@@ -9774,7 +10303,7 @@ module Tuile
|
|
|
9774
10303
|
#
|
|
9775
10304
|
# Unlike `bad_input?`, this fact is *discrete* — asserted at a click or a
|
|
9776
10305
|
# binder pass, not recomputed per keystroke — which is why it carries a
|
|
9777
|
-
# change notice where `bad_input?` deliberately doesn't (`
|
|
10306
|
+
# change notice where `bad_input?` deliberately doesn't (`design/decisions.md`
|
|
9778
10307
|
# `D_bad_input`, `D_has_validation`).
|
|
9779
10308
|
module HasValidation
|
|
9780
10309
|
# _@return_ — why the field is invalid, or `nil` when it
|
|
@@ -9864,6 +10393,217 @@ module Tuile
|
|
|
9864
10393
|
attr_accessor revealed: (bool | Object)
|
|
9865
10394
|
end
|
|
9866
10395
|
|
|
10396
|
+
# A one-row field pairing a {DateField} and a {TimeField} behind a single
|
|
10397
|
+
# `DateTime`. Give it a single-row {#rect}, 16 columns or wider:
|
|
10398
|
+
#
|
|
10399
|
+
# [2026-09-14] [13:45]
|
|
10400
|
+
# ↑ the blank column is {Layout::Box#spacing}, not a component
|
|
10401
|
+
#
|
|
10402
|
+
# f = Component::DateTimeField.new
|
|
10403
|
+
# f.on_value_change = ->(dt) { puts dt.inspect } # DateTime or nil, per commit
|
|
10404
|
+
# f.value = DateTime.new(2026, 9, 14, 13, 45) # "2026-09-14" / "13:45"
|
|
10405
|
+
# f.clear # empties both halves
|
|
10406
|
+
#
|
|
10407
|
+
# Neither half is labelled: each paints the hint derived from its own format
|
|
10408
|
+
# (`yyyy-mm-dd`, `hh:mm`), which names it while it is empty — the moment
|
|
10409
|
+
# naming matters. The *caption* ("Starts at") belongs to the layout around
|
|
10410
|
+
# the field, as it does for every field (`design/decisions.md`
|
|
10411
|
+
# `D_caption_ownership`).
|
|
10412
|
+
#
|
|
10413
|
+
# == Tune the halves; don't replace them
|
|
10414
|
+
# They are exposed read-only, so everything they configure is reached
|
|
10415
|
+
# directly rather than forwarded through a second set of names:
|
|
10416
|
+
#
|
|
10417
|
+
# f.date_field.formats = "%d.%m.%Y" # ambiguous if it were `f.formats=`
|
|
10418
|
+
# f.date_field.calendar_start = Date::ITALY
|
|
10419
|
+
# f.time_field.step = 900 # Up/Down walk a quarter hour
|
|
10420
|
+
#
|
|
10421
|
+
# Two of their knobs are **claimed** by this field and must not be
|
|
10422
|
+
# reassigned: each half's {HasValue#on_value_change} (that is how the
|
|
10423
|
+
# composite hears them) and each half's {Component#bg_color} (see the well
|
|
10424
|
+
# rule below).
|
|
10425
|
+
#
|
|
10426
|
+
# == The value is a `DateTime` at +00:00
|
|
10427
|
+
# Both halves feed it with no adapter, and the offset is a placeholder
|
|
10428
|
+
# rather than a zone — {TimeField}'s epoch cost, taken the same way: a value
|
|
10429
|
+
# that is visibly wrong where an instant was meant beats one that is subtly
|
|
10430
|
+
# wrong. Combine it with a zone at your own boundary (`f.value&.to_time`).
|
|
10431
|
+
#
|
|
10432
|
+
# Lenient in, strict out, so an input carrying more than the halves can hold
|
|
10433
|
+
# does not round-trip:
|
|
10434
|
+
#
|
|
10435
|
+
# f.value = Time.now # takes today's date and the wall clock
|
|
10436
|
+
# f.value == DateTime.now # => false — the zone went, and the seconds with it
|
|
10437
|
+
#
|
|
10438
|
+
# == Three states, and only one of them is this field's own fault
|
|
10439
|
+
# {HasValue#value} is non-nil **iff both halves parse**, so a half going bad
|
|
10440
|
+
# nils the whole value ({HasBadInput}: a field holds bad input *or* a value,
|
|
10441
|
+
# never both). Who reddens follows from whether the fault is attributable:
|
|
10442
|
+
#
|
|
10443
|
+
# date half time half value bad_input? red
|
|
10444
|
+
# 2026-09-14 13:45 DateTime no nobody
|
|
10445
|
+
# (empty) (empty) nil no — empty is not bad input nobody
|
|
10446
|
+
# 2026-99-99 13:45 nil "not a valid date" the date half
|
|
10447
|
+
# 2026-09-14 (empty) nil "needs both a date and a time" this field
|
|
10448
|
+
#
|
|
10449
|
+
# A half's bad input is the half's to paint, on its own latch, and this
|
|
10450
|
+
# field paints nothing. Half-filled is nobody else's, so this field reddens
|
|
10451
|
+
# whole — but **only while it is not active**: it judges you when you leave
|
|
10452
|
+
# and goes quiet when you come back to fix it. A validator's verdict
|
|
10453
|
+
# ({HasValidation#error_message=}) is by definition not attributable either,
|
|
10454
|
+
# and reddens whole with no latch at all.
|
|
10455
|
+
#
|
|
10456
|
+
# The one cost: **ENTER does not redden this field**, where it reddens a
|
|
10457
|
+
# half. A save gate on ENTER over a date with no time still reads
|
|
10458
|
+
# {HasBadInput#bad_input?} true and gets the message; only the ink waits for
|
|
10459
|
+
# the blur.
|
|
10460
|
+
#
|
|
10461
|
+
# == Implementation details
|
|
10462
|
+
# - **The halves keep their own wells, and this field's ink is *synced* onto
|
|
10463
|
+
# them.** `error_bg_color` sits at the top of the background chain, so a
|
|
10464
|
+
# child answering {Component#default_bg_color} — every field does — never
|
|
10465
|
+
# inherits an ancestor's error level, so marking only this field would
|
|
10466
|
+
# leave the halves untouched and reach no cell at all. So the halves are
|
|
10467
|
+
# marked {Component::BG_INHERIT} exactly while this field inks, and `nil`
|
|
10468
|
+
# otherwise. A guilty half's *own* error well still beats the mark, which
|
|
10469
|
+
# is what keeps the ink rule free of arithmetic.
|
|
10470
|
+
# - **The spacing column is nobody's surface** — {Component#clear_inside_extent}
|
|
10471
|
+
# blanks it in the ambient background, so the two wells read as two fields
|
|
10472
|
+
# rather than one long one and each half keeps its own focus highlight.
|
|
10473
|
+
# - **A half announces from its own `value=` and its Up/Down step** — the
|
|
10474
|
+
# other half of {AbstractWrappingField#notify_on_edit?}'s contract — so
|
|
10475
|
+
# writing a value into both halves would announce a half-assembled
|
|
10476
|
+
# `DateTime`. Suppressed while applying, and announced once from this
|
|
10477
|
+
# field's own diff.
|
|
10478
|
+
# - **Nothing else is wired.** Focus forwards through {Layout#handle_focus},
|
|
10479
|
+
# the mouse routes down through {Mouse::Router}, each half commits
|
|
10480
|
+
# on its own blur (Tab between them canonicalizes the date and leaves this
|
|
10481
|
+
# field active), and ENTER commits inside the half and keeps bubbling to
|
|
10482
|
+
# the scope's default button.
|
|
10483
|
+
#
|
|
10484
|
+
# UI-thread-confined, like every component (see {Screen}).
|
|
10485
|
+
class DateTimeField < Tuile::Component::Layout::Horizontal
|
|
10486
|
+
include Tuile::Component::HasValue
|
|
10487
|
+
include Tuile::Component::HasBadInput
|
|
10488
|
+
HALF_FILLED_MESSAGE: String
|
|
10489
|
+
CIVIL_PARTS: ::Array[Symbol]
|
|
10490
|
+
DATE_WEIGHT: Integer
|
|
10491
|
+
TIME_WEIGHT: Integer
|
|
10492
|
+
DEFAULT_PLACEMENT: ::Hash[Symbol, Object]
|
|
10493
|
+
ALIGNMENTS: ::Array[Symbol]
|
|
10494
|
+
|
|
10495
|
+
def initialize: () -> void
|
|
10496
|
+
|
|
10497
|
+
# _@return_ — the two halves assembled, on the calendar
|
|
10498
|
+
# {DateField#calendar_start} parsed the date in; `nil` unless both parse.
|
|
10499
|
+
def value: () -> DateTime?
|
|
10500
|
+
|
|
10501
|
+
# Writes the date into one half and the time of day into the other, firing
|
|
10502
|
+
# {HasValue#on_value_change} once if the value actually changed.
|
|
10503
|
+
#
|
|
10504
|
+
# _@param_ `new_value` — anything carrying both a civil date and a time of day; `nil` empties both halves.
|
|
10505
|
+
def value=: ((DateTime | Time)? new_value) -> void
|
|
10506
|
+
|
|
10507
|
+
# `nil`, not a pair of nils: a field with no parseable date *and* time is
|
|
10508
|
+
# empty.
|
|
10509
|
+
def empty_value: () -> void
|
|
10510
|
+
|
|
10511
|
+
# Empties the *input* of both halves, not just the value — either may be
|
|
10512
|
+
# holding glyphs no parse could use ({HasBadInput}).
|
|
10513
|
+
def clear: () -> void
|
|
10514
|
+
|
|
10515
|
+
# The guilty half's own report, the date's first when both are bad; else
|
|
10516
|
+
# the one fault no half can wear, a half-filled pair.
|
|
10517
|
+
def bad_input_message: () -> String?
|
|
10518
|
+
|
|
10519
|
+
# Sets the verdict and syncs the halves' wells onto it.
|
|
10520
|
+
#
|
|
10521
|
+
# _@param_ `new_message`
|
|
10522
|
+
def error_message=: ((String | StyledString)? new_message) -> void
|
|
10523
|
+
|
|
10524
|
+
# Syncs the halves' wells on both focus edges — this field inks its
|
|
10525
|
+
# half-filled fault only once you have left it.
|
|
10526
|
+
#
|
|
10527
|
+
# _@param_ `flag`
|
|
10528
|
+
def active=: (bool flag) -> void
|
|
10529
|
+
|
|
10530
|
+
# _@return_ — the full width, one row — so a taller rect gets the
|
|
10531
|
+
# ambient background rather than this field's well ({Component#extent}).
|
|
10532
|
+
def extent: () -> Size
|
|
10533
|
+
|
|
10534
|
+
# The ink rule in the class doc, as an expression.
|
|
10535
|
+
#
|
|
10536
|
+
# No latch ivar, deliberately: every input here is a fact something
|
|
10537
|
+
# announces, which is what lets the well sync have a complete call list. A
|
|
10538
|
+
# half's `bad_input?` moves with every keystroke and announces nothing at
|
|
10539
|
+
# all by design, so a latch of this field's own could not follow it.
|
|
10540
|
+
def bad_input_settled?: () -> bool
|
|
10541
|
+
|
|
10542
|
+
# _@return_ — whether a half is holding input its own value cannot
|
|
10543
|
+
# represent, and so wears the error itself.
|
|
10544
|
+
def attributable?: () -> bool
|
|
10545
|
+
|
|
10546
|
+
# One idempotent sync over one condition, this field the sole writer of
|
|
10547
|
+
# its halves' {Component#bg_color} — the shape a hook-owned resource takes.
|
|
10548
|
+
# Called from the three places {HasValidation#error_ink?} can change: a
|
|
10549
|
+
# verdict, a focus edge, and a half's announcement. Leave one out and this
|
|
10550
|
+
# field stops inking while its halves stay marked, i.e. both halves flat
|
|
10551
|
+
# with their wells gone.
|
|
10552
|
+
def sync_half_wells: () -> void
|
|
10553
|
+
|
|
10554
|
+
def handle_half_change: () -> void
|
|
10555
|
+
|
|
10556
|
+
# Runs `block` with the halves' notices suppressed, so a value written
|
|
10557
|
+
# into both is announced once rather than half-assembled.
|
|
10558
|
+
def applying: () -> void
|
|
10559
|
+
|
|
10560
|
+
def fire_if_changed: () -> void
|
|
10561
|
+
|
|
10562
|
+
# _@return_ — true iff the field is holding input its value cannot
|
|
10563
|
+
# represent.
|
|
10564
|
+
def bad_input?: () -> bool
|
|
10565
|
+
|
|
10566
|
+
# Widens {HasValidation#error_ink?}: bad input paints the invalid well
|
|
10567
|
+
# too, with no verdict written — once {#bad_input_settled?} says the
|
|
10568
|
+
# report may be shown.
|
|
10569
|
+
def error_ink?: () -> bool
|
|
10570
|
+
|
|
10571
|
+
# _@return_ — why the field is invalid, or `nil` when it
|
|
10572
|
+
# is not; `nil` until something sets it.
|
|
10573
|
+
def error_message: () -> StyledString?
|
|
10574
|
+
|
|
10575
|
+
# The invalid well, picked up by everything this component paints —
|
|
10576
|
+
# including the inner face of a composed field and the {List} of a group,
|
|
10577
|
+
# neither of which forwards anything: both declare no background of their
|
|
10578
|
+
# own, so the ordinary chain walks up to this (overrides
|
|
10579
|
+
# {Component#error_bg_color}).
|
|
10580
|
+
def error_bg_color: () -> Color?
|
|
10581
|
+
|
|
10582
|
+
# Adds `error_message=…` to {Component#inspect}, omitted while valid — so
|
|
10583
|
+
# a {Testing.get} failure dump says which field is already flagged.
|
|
10584
|
+
def inspect_details: () -> ::Array[String]
|
|
10585
|
+
|
|
10586
|
+
# Empty of *value*: a field whose parse is partial reports `true` while the
|
|
10587
|
+
# user is looking at glyphs it could not use, so ask
|
|
10588
|
+
# {HasBadInput#bad_input?} first.
|
|
10589
|
+
#
|
|
10590
|
+
# _@return_ — true iff {#value} equals {#empty_value}.
|
|
10591
|
+
def empty?: () -> bool
|
|
10592
|
+
|
|
10593
|
+
# Input fields are focusable by default (overrides {Component#focusable?});
|
|
10594
|
+
# a read-only display field could override back to `false`. Only
|
|
10595
|
+
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
10596
|
+
# composing wrappers, so it stays per-class (`design/decisions.md`
|
|
10597
|
+
# `D_integer_field`).
|
|
10598
|
+
def focusable?: () -> bool
|
|
10599
|
+
|
|
10600
|
+
# _@return_ — the left half; tune it, never replace it.
|
|
10601
|
+
attr_reader date_field: DateField
|
|
10602
|
+
|
|
10603
|
+
# _@return_ — the right half; tune it, never replace it.
|
|
10604
|
+
attr_reader time_field: TimeField
|
|
10605
|
+
end
|
|
10606
|
+
|
|
9867
10607
|
# The hint a field paints in its own cells while it holds nothing — a format
|
|
9868
10608
|
# the user could not otherwise guess:
|
|
9869
10609
|
#
|
|
@@ -9883,7 +10623,7 @@ module Tuile
|
|
|
9883
10623
|
# Include it in a field whose *input shape* is unguessable from an empty
|
|
9884
10624
|
# well. Not in {Select}, the near miss: a blank face plus `▾` already reads
|
|
9885
10625
|
# as "nothing picked", so an absent enum *value* needs no hint the way an
|
|
9886
|
-
# unguessable input *format* does (`
|
|
10626
|
+
# unguessable input *format* does (`design/decisions.md` `D_select`).
|
|
9887
10627
|
#
|
|
9888
10628
|
# == Implementation details
|
|
9889
10629
|
# The ink is {Theme#placeholder_color}, calibrated to be *barely* visible —
|
|
@@ -10008,7 +10748,7 @@ module Tuile
|
|
|
10008
10748
|
# def bad_input_settled? = @settled # set on commit, cleared on an edit
|
|
10009
10749
|
#
|
|
10010
10750
|
# It gates the **ink only**: {#bad_input?} is a pull, and a save gate
|
|
10011
|
-
# asking at a click must get the answer settled or not (`
|
|
10751
|
+
# asking at a click must get the answer settled or not (`design/decisions.md`
|
|
10012
10752
|
# `D_bad_input`).
|
|
10013
10753
|
def bad_input_settled?: () -> bool
|
|
10014
10754
|
|
|
@@ -10075,7 +10815,7 @@ module Tuile
|
|
|
10075
10815
|
# f.text = "e\u{0301}x" # a decomposed e-acute then "x": 3 chars, 2 glyphs
|
|
10076
10816
|
# f.caret = 1 # into the middle of the e-acute …
|
|
10077
10817
|
# f.caret # => 2, its end — where the caret already drew
|
|
10078
|
-
# f.handle_key(Keys::BACKSPACE)
|
|
10818
|
+
# f.handle_key?(Keys::BACKSPACE)
|
|
10079
10819
|
# f.text # => "x": the whole glyph went, not its accent
|
|
10080
10820
|
#
|
|
10081
10821
|
# Insertion stays character-native, so `String#insert` merges a typed
|
|
@@ -10085,11 +10825,11 @@ module Tuile
|
|
|
10085
10825
|
# Subclasses implement the layout-specific pieces ({#cursor_position},
|
|
10086
10826
|
# {#repaint}) and add their own keys (HOME/END, ENTER, UP/DOWN,
|
|
10087
10827
|
# printable insertion) by overriding the protected
|
|
10088
|
-
# {#handle_text_input_key} hook — `super` falls through to the common
|
|
10828
|
+
# {#handle_text_input_key?} hook — `super` falls through to the common
|
|
10089
10829
|
# navigation handling.
|
|
10090
10830
|
#
|
|
10091
10831
|
# == Customizing a field is subclassing it, and there are two seams
|
|
10092
|
-
# To change what **keys** do, override {#handle_text_input_key}; to
|
|
10832
|
+
# To change what **keys** do, override {#handle_text_input_key?}; to
|
|
10093
10833
|
# constrain what the buffer may **hold**, override {#insert_text}, which
|
|
10094
10834
|
# every insertion runs through — typed, pasted, or the ENTER newline:
|
|
10095
10835
|
#
|
|
@@ -10097,7 +10837,7 @@ module Tuile
|
|
|
10097
10837
|
# protected
|
|
10098
10838
|
#
|
|
10099
10839
|
# # ENTER submits instead of falling through to the parent.
|
|
10100
|
-
# def handle_text_input_key(key)
|
|
10840
|
+
# def handle_text_input_key?(key)
|
|
10101
10841
|
# return super unless key == Keys::ENTER
|
|
10102
10842
|
#
|
|
10103
10843
|
# submit(text)
|
|
@@ -10127,7 +10867,7 @@ module Tuile
|
|
|
10127
10867
|
# which insertion does *not* pass through.
|
|
10128
10868
|
# - {#preprocess_paste} — sanitizer for {#handle_paste}, run before the
|
|
10129
10869
|
# clipboard reaches {#insert_text} ({TextField} keeps its first line).
|
|
10130
|
-
# - {#
|
|
10870
|
+
# - {#handle_text_mutated} / {#handle_caret_mutated} — post-mutation side
|
|
10131
10871
|
# effects (e.g. {TextArea} invalidates its wrap cache and scrolls to
|
|
10132
10872
|
# keep the caret visible).
|
|
10133
10873
|
class AbstractStringField < Component
|
|
@@ -10149,23 +10889,20 @@ module Tuile
|
|
|
10149
10889
|
|
|
10150
10890
|
def tab_stop?: () -> bool
|
|
10151
10891
|
|
|
10152
|
-
# Handles a key, by delegating to the {#handle_text_input_key} hook a
|
|
10153
|
-
# subclass overrides. Dispatch ({ScreenPane#handle_key}) only routes keys
|
|
10892
|
+
# Handles a key, by delegating to the {#handle_text_input_key?} hook a
|
|
10893
|
+
# subclass overrides. Dispatch ({ScreenPane#handle_key?}) only routes keys
|
|
10154
10894
|
# here when this input is on the focus chain, so there is no {#active?}
|
|
10155
10895
|
# gate.
|
|
10156
10896
|
#
|
|
10157
10897
|
# _@param_ `key`
|
|
10158
|
-
def handle_key
|
|
10898
|
+
def handle_key?: (String key) -> bool
|
|
10159
10899
|
|
|
10160
10900
|
# Inserts pasted text at the caret as **one** mutation, so {#on_change}
|
|
10161
10901
|
# fires once for the whole paste rather than once per character.
|
|
10162
10902
|
# {#preprocess_paste} filters it first.
|
|
10163
10903
|
#
|
|
10164
10904
|
# _@param_ `text`
|
|
10165
|
-
|
|
10166
|
-
# _@return_ — always true — a field consumes every paste: an empty
|
|
10167
|
-
# one, and one its {#insert_text} rejects wholesale.
|
|
10168
|
-
def handle_paste: (String text) -> bool
|
|
10905
|
+
def handle_paste: (String text) -> void
|
|
10169
10906
|
|
|
10170
10907
|
# Input filter for {#handle_paste}, the paste-side counterpart of
|
|
10171
10908
|
# {#preprocess_text}. Strips the C0 control characters a text buffer
|
|
@@ -10241,14 +10978,14 @@ module Tuile
|
|
|
10241
10978
|
# Hook called after {#text} has been mutated, before invalidation /
|
|
10242
10979
|
# {#on_change}. Default no-op. Subclasses use this to invalidate caches
|
|
10243
10980
|
# ({TextArea}'s wrap cache) and update derived state.
|
|
10244
|
-
def
|
|
10981
|
+
def handle_text_mutated: () -> void
|
|
10245
10982
|
|
|
10246
10983
|
# Hook called after {#caret} has been mutated, before invalidation.
|
|
10247
10984
|
# Default no-op. Subclasses use this to keep the caret visible
|
|
10248
10985
|
# ({TextArea}'s vertical scroll).
|
|
10249
|
-
def
|
|
10986
|
+
def handle_caret_mutated: () -> void
|
|
10250
10987
|
|
|
10251
|
-
# Dispatch hook for {#handle_key}. Handles ESC and the editing keys that
|
|
10988
|
+
# Dispatch hook for {#handle_key?}. Handles ESC and the editing keys that
|
|
10252
10989
|
# have identical semantics in single-line and multi-line inputs:
|
|
10253
10990
|
# LEFT/RIGHT arrows (one grapheme cluster per press, so a press always
|
|
10254
10991
|
# moves), CTRL+LEFT/CTRL+RIGHT for word jumps, and CTRL+W, which deletes
|
|
@@ -10260,7 +10997,7 @@ module Tuile
|
|
|
10260
10997
|
# _@param_ `key`
|
|
10261
10998
|
#
|
|
10262
10999
|
# _@return_ — true if the key was handled.
|
|
10263
|
-
def handle_text_input_key
|
|
11000
|
+
def handle_text_input_key?: (String key) -> bool
|
|
10264
11001
|
|
|
10265
11002
|
# Removes the whole grapheme cluster before the caret — one press, one
|
|
10266
11003
|
# glyph, whatever it is built from (a ZWJ emoji family and a three-jamo
|
|
@@ -10328,7 +11065,7 @@ module Tuile
|
|
|
10328
11065
|
# Input fields are focusable by default (overrides {Component#focusable?});
|
|
10329
11066
|
# a read-only display field could override back to `false`. Only
|
|
10330
11067
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
10331
|
-
# composing wrappers, so it stays per-class (`
|
|
11068
|
+
# composing wrappers, so it stays per-class (`design/decisions.md`
|
|
10332
11069
|
# `D_integer_field`).
|
|
10333
11070
|
def focusable?: () -> bool
|
|
10334
11071
|
|
|
@@ -10444,6 +11181,12 @@ module Tuile
|
|
|
10444
11181
|
# default button still sees it; only an {#on_enter} of this field's own
|
|
10445
11182
|
# consumes it, which is {TextField#on_enter}'s existing contract.
|
|
10446
11183
|
#
|
|
11184
|
+
# == When the value notice fires
|
|
11185
|
+
# Per edit by default. A field whose grammar is not prefix-closed sets
|
|
11186
|
+
# {#notify_on_edit?} to `false` and lets the notice settle onto those same
|
|
11187
|
+
# two gestures, so a form is never handed a half-typed date that happens to
|
|
11188
|
+
# parse ({DateField}, {TimeField}).
|
|
11189
|
+
#
|
|
10447
11190
|
# == Implementation details
|
|
10448
11191
|
# - **{HasValue#value} and {#value=} raise until overridden.** The inherited
|
|
10449
11192
|
# pair stores into `@value` and never touches the editor, so a subclass
|
|
@@ -10454,7 +11197,7 @@ module Tuile
|
|
|
10454
11197
|
# - **The editor's `on_change` and `on_enter` slots are claimed** — for that
|
|
10455
11198
|
# guard, and to commit before an app's ENTER handler runs. A slot cannot
|
|
10456
11199
|
# be shared, so a subclass reacting to buffer edits overrides
|
|
10457
|
-
# {#
|
|
11200
|
+
# {#handle_editor_change} (every edit), {#value=} or {#commit} rather than
|
|
10458
11201
|
# reassigning either.
|
|
10459
11202
|
# - **Not for a field whose editor is a *filter*.** This base assumes the
|
|
10460
11203
|
# buffer is a rendering of the value, so an edit may change the value.
|
|
@@ -10500,7 +11243,7 @@ module Tuile
|
|
|
10500
11243
|
#
|
|
10501
11244
|
# _@return_ — whatever `super` returns — committing never consumes
|
|
10502
11245
|
# the key.
|
|
10503
|
-
def handle_key
|
|
11246
|
+
def handle_key?: (String key) -> bool
|
|
10504
11247
|
|
|
10505
11248
|
# _@return_ — the editor's caret — the hardware cursor is
|
|
10506
11249
|
# delegated to it.
|
|
@@ -10513,7 +11256,7 @@ module Tuile
|
|
|
10513
11256
|
# _@param_ `flag`
|
|
10514
11257
|
def active=: (bool flag) -> void
|
|
10515
11258
|
|
|
10516
|
-
def
|
|
11259
|
+
def handle_focus: () -> void
|
|
10517
11260
|
|
|
10518
11261
|
# _@param_ `new_rect`
|
|
10519
11262
|
def rect=: (Rect new_rect) -> void
|
|
@@ -10523,13 +11266,27 @@ module Tuile
|
|
|
10523
11266
|
# field rewrites its buffer from.
|
|
10524
11267
|
def commit: () -> void
|
|
10525
11268
|
|
|
11269
|
+
# Whether an edit of the buffer fires {HasValue#on_value_change} as it
|
|
11270
|
+
# happens. `true` here, which is right wherever every buffer state is a
|
|
11271
|
+
# value the user might mean: an {IntegerField} passing through `4` on the
|
|
11272
|
+
# way to `42` really does hold 4 for that keystroke. A field whose
|
|
11273
|
+
# grammar is **not prefix-closed** answers `false` and lets the notice
|
|
11274
|
+
# settle onto the commit gestures instead ({DateField}, `D_date_field`).
|
|
11275
|
+
#
|
|
11276
|
+
# Only the *push* settles: {HasValue#value} stays a live parse of the
|
|
11277
|
+
# buffer either way. And overriding this is half the job — {#commit} is
|
|
11278
|
+
# covered here, but the field must fire from its own `value=` too, or a
|
|
11279
|
+
# programmatic write and an Up/Down step go unannounced until the next
|
|
11280
|
+
# commit.
|
|
11281
|
+
def notify_on_edit?: () -> bool
|
|
11282
|
+
|
|
10526
11283
|
# Called whenever the editor's buffer changes, however the characters
|
|
10527
11284
|
# arrived — a typed key, a paste, or a {#value=} of this field's own. It
|
|
10528
11285
|
# is named for the *editor*, not for the user, because those last two are
|
|
10529
11286
|
# not input. No-op by default; override it to drop state that describes
|
|
10530
11287
|
# the *previous* buffer, as a field latching whether its input has settled
|
|
10531
11288
|
# must ({HasBadInput}).
|
|
10532
|
-
def
|
|
11289
|
+
def handle_editor_change: () -> void
|
|
10533
11290
|
|
|
10534
11291
|
# Places the editor across the whole rect; override to reserve cells for a
|
|
10535
11292
|
# face of your own.
|
|
@@ -10542,6 +11299,11 @@ module Tuile
|
|
|
10542
11299
|
# widget) and {Component#bg_color} set here reaches the cells it paints.
|
|
10543
11300
|
def default_bg_color: () -> Color
|
|
10544
11301
|
|
|
11302
|
+
# Every commit gesture runs through here, so a field holding its notice
|
|
11303
|
+
# ({#notify_on_edit?}) announces from one place rather than three; the
|
|
11304
|
+
# diff guard makes the call free for a field that fired on the way in.
|
|
11305
|
+
def commit_and_notify: () -> void
|
|
11306
|
+
|
|
10545
11307
|
# Re-emits {HasValue#on_value_change}, but only when {#value} differs from
|
|
10546
11308
|
# the last one fired — so a buffer edit that leaves the value alone
|
|
10547
11309
|
# (`"7"`→`"07"`) stays silent.
|
|
@@ -10564,7 +11326,7 @@ module Tuile
|
|
|
10564
11326
|
# Input fields are focusable by default (overrides {Component#focusable?});
|
|
10565
11327
|
# a read-only display field could override back to `false`. Only
|
|
10566
11328
|
# `focusable?` lives here — `tab_stop?` diverges between leaf fields and
|
|
10567
|
-
# composing wrappers, so it stays per-class (`
|
|
11329
|
+
# composing wrappers, so it stays per-class (`design/decisions.md`
|
|
10568
11330
|
# `D_integer_field`).
|
|
10569
11331
|
def focusable?: () -> bool
|
|
10570
11332
|
|
|
@@ -10781,7 +11543,7 @@ module Tuile
|
|
|
10781
11543
|
def running?: () -> bool
|
|
10782
11544
|
|
|
10783
11545
|
# _@return_ — true if this thread is the one running {#run_loop}.
|
|
10784
|
-
def
|
|
11546
|
+
def in_loop_thread?: () -> bool
|
|
10785
11547
|
|
|
10786
11548
|
# Stops ongoing {#run_loop}. The stop may not be immediate: {#run_loop} may
|
|
10787
11549
|
# process a bunch of events before terminating.
|
|
@@ -10815,7 +11577,7 @@ module Tuile
|
|
|
10815
11577
|
#
|
|
10816
11578
|
# {Screen#event_loop} routes it to {Component#handle_paste} down the focus
|
|
10817
11579
|
# chain. It never enters the key ladder: no Tab traversal, no global
|
|
10818
|
-
# shortcut, no {Component#handle_key}.
|
|
11580
|
+
# shortcut, no {Component#handle_key?}.
|
|
10819
11581
|
#
|
|
10820
11582
|
# @!attribute [r] text
|
|
10821
11583
|
# @return [String] the pasted text, `\n`-normalized by
|
|
@@ -11012,13 +11774,83 @@ module Tuile
|
|
|
11012
11774
|
# Screen.instance.background_color = Color.rgb(30, 30, 46)
|
|
11013
11775
|
#
|
|
11014
11776
|
# Takes the same path a real reply does — a changed color fires
|
|
11015
|
-
# {Component#
|
|
11777
|
+
# {Component#handle_theme_changed} across the tree and invalidates it.
|
|
11016
11778
|
# There is no such writer on {Screen}: the value is a report from the
|
|
11017
11779
|
# terminal, not a setting.
|
|
11018
11780
|
#
|
|
11019
11781
|
# _@param_ `color`
|
|
11020
11782
|
def background_color=: (Color color) -> void
|
|
11021
11783
|
|
|
11784
|
+
# Plays a whole click at a screen cell — the press, then the release that
|
|
11785
|
+
# ends its grab:
|
|
11786
|
+
#
|
|
11787
|
+
# screen.click(save_button.rect.left, save_button.rect.top)
|
|
11788
|
+
#
|
|
11789
|
+
# Routed exactly as the terminal's own report would be ({Mouse::Router}), so
|
|
11790
|
+
# it focuses, dismisses popups and bubbles.
|
|
11791
|
+
#
|
|
11792
|
+
# _@param_ `x` — 0-based column.
|
|
11793
|
+
#
|
|
11794
|
+
# _@param_ `y` — 0-based row.
|
|
11795
|
+
#
|
|
11796
|
+
# _@param_ `button` — `:left`, `:middle` or `:right`.
|
|
11797
|
+
def click: (Integer x, Integer y, ?button: Symbol) -> void
|
|
11798
|
+
|
|
11799
|
+
# Half a {#click}, for a spec about the grab — what is claimed, what the
|
|
11800
|
+
# drag does, what the release lands on.
|
|
11801
|
+
#
|
|
11802
|
+
# _@param_ `x` — 0-based column.
|
|
11803
|
+
#
|
|
11804
|
+
# _@param_ `y` — 0-based row.
|
|
11805
|
+
#
|
|
11806
|
+
# _@param_ `button` — `:left`, `:middle` or `:right`.
|
|
11807
|
+
def press: (Integer x, Integer y, ?button: Symbol) -> void
|
|
11808
|
+
|
|
11809
|
+
# The other half of {#press}.
|
|
11810
|
+
#
|
|
11811
|
+
# _@param_ `x` — 0-based column.
|
|
11812
|
+
#
|
|
11813
|
+
# _@param_ `y` — 0-based row.
|
|
11814
|
+
def release: (Integer x, Integer y) -> void
|
|
11815
|
+
|
|
11816
|
+
# One wheel notch over a cell.
|
|
11817
|
+
#
|
|
11818
|
+
# _@param_ `direction` — `:up`, `:down`, `:left` or `:right`.
|
|
11819
|
+
#
|
|
11820
|
+
# _@param_ `x` — 0-based column.
|
|
11821
|
+
#
|
|
11822
|
+
# _@param_ `y` — 0-based row.
|
|
11823
|
+
def scroll: (Symbol direction, Integer x, Integer y) -> void
|
|
11824
|
+
|
|
11825
|
+
# Moves the pointer, firing the enter/exit hooks the new position implies —
|
|
11826
|
+
# or, while a press is grabbed, one {Component#handle_mouse_drag}.
|
|
11827
|
+
#
|
|
11828
|
+
# _@param_ `x` — 0-based column.
|
|
11829
|
+
#
|
|
11830
|
+
# _@param_ `y` — 0-based row.
|
|
11831
|
+
#
|
|
11832
|
+
# _@param_ `button` — the button held while moving, if any.
|
|
11833
|
+
def move: (Integer x, Integer y, ?button: Symbol?) -> void
|
|
11834
|
+
|
|
11835
|
+
# Plays a whole drag: the press at the first point, one move per point
|
|
11836
|
+
# after it, and the release at the last.
|
|
11837
|
+
#
|
|
11838
|
+
# screen.drag([2, 1], [3, 2], [4, 3]) # three reports, a diagonal
|
|
11839
|
+
# screen.drag(canvas.rect.top_left, [9, 9]) # the coarsest drag there is
|
|
11840
|
+
#
|
|
11841
|
+
# **Reports only the points given**, as the wire does: at ~84 reports a
|
|
11842
|
+
# second (`R_mouse_reporting`) a quick drag genuinely skips cells, so
|
|
11843
|
+
# interpolating them would let a spec assert a continuity no terminal
|
|
11844
|
+
# delivers. A one-point drag is a {#click} — use that.
|
|
11845
|
+
#
|
|
11846
|
+
# _@param_ `points` — two or more positions, as {Point}s or `[x, y]` pairs.
|
|
11847
|
+
#
|
|
11848
|
+
# _@param_ `button` — `:left`, `:middle` or `:right`.
|
|
11849
|
+
def drag: (*::Array[(Point | [Integer, Integer])] points, ?button: Symbol) -> void
|
|
11850
|
+
|
|
11851
|
+
# _@param_ `point`
|
|
11852
|
+
def coerce_point: ((Point | [Integer, Integer]) point) -> Point
|
|
11853
|
+
|
|
11022
11854
|
# No terminal probing in tests: skip {TerminalBackground.detect}
|
|
11023
11855
|
# (which would write an OSC 11 query to the test runner's TTY and
|
|
11024
11856
|
# steal its input) and pin the deterministic default. The color is nil
|
|
@@ -11046,59 +11878,6 @@ module Tuile
|
|
|
11046
11878
|
attr_reader prints: ::Array[String]
|
|
11047
11879
|
end
|
|
11048
11880
|
|
|
11049
|
-
# A mouse event.
|
|
11050
|
-
#
|
|
11051
|
-
# @!attribute [r] button
|
|
11052
|
-
# @return [Symbol, nil] one of `:left`, `:middle`, `:right`, `:scroll_up`,
|
|
11053
|
-
# `:scroll_down`, `:scroll_left`, `:scroll_right`; `nil` if not known.
|
|
11054
|
-
# @!attribute [r] x
|
|
11055
|
-
# @return [Integer] x coordinate, 0-based.
|
|
11056
|
-
# @!attribute [r] y
|
|
11057
|
-
# @return [Integer] y coordinate, 0-based.
|
|
11058
|
-
class MouseEvent
|
|
11059
|
-
# _@return_ — the event's position.
|
|
11060
|
-
def point: () -> Point
|
|
11061
|
-
|
|
11062
|
-
# Checks whether given key is a mouse event key. Returns true on the X10
|
|
11063
|
-
# `\e[M` prefix regardless of length — {.parse} is the place that
|
|
11064
|
-
# validates the full 6-byte shape and raises on malformed input.
|
|
11065
|
-
#
|
|
11066
|
-
# _@param_ `key` — key read via {Keys.getkey}
|
|
11067
|
-
#
|
|
11068
|
-
# _@return_ — true if it is a mouse event
|
|
11069
|
-
def self.mouse_event?: (String key) -> bool
|
|
11070
|
-
|
|
11071
|
-
# Parses an X10 mouse report (`\e[M` + 3 bytes: button, x, y).
|
|
11072
|
-
#
|
|
11073
|
-
# Raises {Tuile::Error} when `key` starts with the mouse prefix but is
|
|
11074
|
-
# not exactly 6 bytes long. Both shorter and longer inputs are bugs in
|
|
11075
|
-
# the upstream key-reader: a shorter prefix means the tail was lost on
|
|
11076
|
-
# the way in, and a longer one means we over-consumed into the next
|
|
11077
|
-
# escape sequence. We refuse to silently truncate either case because
|
|
11078
|
-
# the trailing `\e` of an over-read corrupts the *next* getkey, and the
|
|
11079
|
-
# corruption then surfaces as garbled keystrokes in focused inputs
|
|
11080
|
-
# rather than as a parser failure pointing at the actual cause.
|
|
11081
|
-
#
|
|
11082
|
-
# _@param_ `key` — key read via {Keys.getkey}
|
|
11083
|
-
#
|
|
11084
|
-
# _@return_ — `nil` if `key` is not a mouse event
|
|
11085
|
-
def self.parse: (String key) -> MouseEvent?
|
|
11086
|
-
|
|
11087
|
-
def self.start_tracking: () -> String
|
|
11088
|
-
|
|
11089
|
-
def self.stop_tracking: () -> String
|
|
11090
|
-
|
|
11091
|
-
# _@return_ — one of `:left`, `:middle`, `:right`, `:scroll_up`,
|
|
11092
|
-
# `:scroll_down`, `:scroll_left`, `:scroll_right`; `nil` if not known.
|
|
11093
|
-
attr_reader button: Symbol?
|
|
11094
|
-
|
|
11095
|
-
# _@return_ — x coordinate, 0-based.
|
|
11096
|
-
attr_reader x: Integer
|
|
11097
|
-
|
|
11098
|
-
# _@return_ — y coordinate, 0-based.
|
|
11099
|
-
attr_reader y: Integer
|
|
11100
|
-
end
|
|
11101
|
-
|
|
11102
11881
|
# The structural root of the {Screen}'s component tree.
|
|
11103
11882
|
#
|
|
11104
11883
|
# {Screen} is a singleton runtime owner (event loop, lock, terminal IO,
|
|
@@ -11106,7 +11885,7 @@ module Tuile
|
|
|
11106
11885
|
# {#content} and the {#popups} stack. Putting them under a single Component
|
|
11107
11886
|
# parent gives focus traversal a real root, makes {Component#attached?} a
|
|
11108
11887
|
# one-liner, and lets popup-focus repair fall out of the standard
|
|
11109
|
-
# {Component#
|
|
11888
|
+
# {Component#handle_child_removed} hook.
|
|
11110
11889
|
#
|
|
11111
11890
|
# The pane owns no chrome of its own — no status bar, no reserved row.
|
|
11112
11891
|
# {#content} gets the full pane rect, and an app that wants a status line
|
|
@@ -11146,7 +11925,7 @@ module Tuile
|
|
|
11146
11925
|
# _@param_ `window`
|
|
11147
11926
|
def remove_popup: (Component window) -> void
|
|
11148
11927
|
|
|
11149
|
-
# Unmounts everything: each child is detached — firing {Component#
|
|
11928
|
+
# Unmounts everything: each child is detached — firing {Component#handle_detached}
|
|
11150
11929
|
# down its subtree — and every slot is emptied. Terminal; the pane isn't
|
|
11151
11930
|
# reusable afterwards, and {Screen#close} is its only caller.
|
|
11152
11931
|
#
|
|
@@ -11185,7 +11964,7 @@ module Tuile
|
|
|
11185
11964
|
def repaint: () -> void
|
|
11186
11965
|
|
|
11187
11966
|
# Delivers a key to {Screen#focused}, then bubbles it up the focus chain —
|
|
11188
|
-
# the first component whose `handle_key
|
|
11967
|
+
# the first component whose `handle_key?` returns true wins.
|
|
11189
11968
|
#
|
|
11190
11969
|
# Bubbling stops at the *scope* root: the topmost *modal* popup when one is
|
|
11191
11970
|
# open, else the tiled {#content}. Focus that is nil or sits outside the
|
|
@@ -11203,36 +11982,34 @@ module Tuile
|
|
|
11203
11982
|
# _@param_ `key`
|
|
11204
11983
|
#
|
|
11205
11984
|
# _@return_ — true if the key was handled.
|
|
11206
|
-
def handle_key
|
|
11985
|
+
def handle_key?: (String key) -> bool
|
|
11207
11986
|
|
|
11208
11987
|
# Delivers pasted text to {Screen#focused} — and to nobody else.
|
|
11209
11988
|
#
|
|
11210
|
-
# Scoped exactly like {#handle_key} (focus that is nil or sits outside the
|
|
11989
|
+
# Scoped exactly like {#handle_key?} (focus that is nil or sits outside the
|
|
11211
11990
|
# modal scope receives nothing, which is what keeps a popup modal) but
|
|
11212
11991
|
# **not bubbled**: an ancestor is never offered a paste its descendant
|
|
11213
11992
|
# declined, and unhandled text is dropped. Why keys bubble and pastes
|
|
11214
11993
|
# don't: `D_bracketed_paste`.
|
|
11215
11994
|
#
|
|
11216
11995
|
# _@param_ `text`
|
|
11217
|
-
|
|
11218
|
-
# _@return_ — true if the focused component consumed it.
|
|
11219
|
-
def handle_paste: (String text) -> bool
|
|
11996
|
+
def handle_paste: (String text) -> void
|
|
11220
11997
|
|
|
11221
|
-
# Mouse
|
|
11222
|
-
#
|
|
11223
|
-
# open
|
|
11224
|
-
#
|
|
11225
|
-
# inside it route to it (e.g. click-to-select), clicks elsewhere reach the
|
|
11226
|
-
# content beneath.
|
|
11998
|
+
# Where {Mouse::Router} starts its walk for a pointer at `point`: the
|
|
11999
|
+
# topmost popup containing it, else {#content} — unless a modal popup is
|
|
12000
|
+
# open, which eats the event even outside its rect. A non-modal overlay
|
|
12001
|
+
# blocks nothing: a point outside it reaches the content beneath.
|
|
11227
12002
|
#
|
|
11228
|
-
#
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
#
|
|
12003
|
+
# _@param_ `point`
|
|
12004
|
+
def mouse_root_at: (Point point) -> Component?
|
|
12005
|
+
|
|
12006
|
+
# Runs the press delivery in the block, then *dismisses* the open popups a
|
|
12007
|
+
# left press landed outside of that asked for it
|
|
12008
|
+
# ({Component::Overlay#close_on_outside_click?}). A dismissed popup is
|
|
11232
12009
|
# closed rather than told.
|
|
11233
12010
|
#
|
|
11234
12011
|
# "Outside" is measured against the {Component::Overlay#owner} chain, not
|
|
11235
|
-
# against one rect and not against stacking order: the popup the
|
|
12012
|
+
# against one rect and not against stacking order: the popup the press hit
|
|
11236
12013
|
# is kept, and so is every popup that one *belongs to*, transitively. That
|
|
11237
12014
|
# is what stops a dialog being dismissed by a click on a dropdown its own
|
|
11238
12015
|
# field opened, and a menu cascade being dismissed by a click on one of its
|
|
@@ -11242,10 +12019,10 @@ module Tuile
|
|
|
11242
12019
|
#
|
|
11243
12020
|
# Two halves of the ordering are load-bearing, and both are specced:
|
|
11244
12021
|
#
|
|
11245
|
-
# - **Snapshot before
|
|
12022
|
+
# - **Snapshot before the block.** A popup the delivered press *opens* must
|
|
11246
12023
|
# not be in the set (it would immediately dismiss itself — every
|
|
11247
12024
|
# {Component::Select} would be unopenable by mouse).
|
|
11248
|
-
# - **Close after
|
|
12025
|
+
# - **Close after the block.** A widget toggling its own overlay from a press
|
|
11249
12026
|
# on its face closes it during delivery, and {Component::Popup#close} is
|
|
11250
12027
|
# idempotent, so the dismissal no-ops. Close *first* and the widget sees
|
|
11251
12028
|
# a shut overlay and reopens it — a Select's dropdown could then never be
|
|
@@ -11254,10 +12031,12 @@ module Tuile
|
|
|
11254
12031
|
# The snapshot is a fresh array for a third reason: a handler may close
|
|
11255
12032
|
# further popups, and `@popups` must not be mutated mid-iteration.
|
|
11256
12033
|
#
|
|
11257
|
-
# _@param_ `
|
|
11258
|
-
|
|
12034
|
+
# _@param_ `point`
|
|
12035
|
+
#
|
|
12036
|
+
# _@param_ `left` — whether the press was the left button; no other button dismisses.
|
|
12037
|
+
def dismissing_popups_outside: (Point point, left: bool) -> void
|
|
11259
12038
|
|
|
11260
|
-
# Focus repair when a child detaches. Default {Component#
|
|
12039
|
+
# Focus repair when a child detaches. Default {Component#handle_child_removed}
|
|
11261
12040
|
# would refocus to `self` (the pane), which isn't a useful focus target.
|
|
11262
12041
|
# Instead, route focus to the first interactable widget in the now-topmost
|
|
11263
12042
|
# modal popup; falling back to the focus snapshotted when this popup was opened
|
|
@@ -11270,7 +12049,7 @@ module Tuile
|
|
|
11270
12049
|
# `q`/ESC still has somewhere to dispatch from.
|
|
11271
12050
|
#
|
|
11272
12051
|
# _@param_ `child`
|
|
11273
|
-
def
|
|
12052
|
+
def handle_child_removed: (Component child) -> void
|
|
11274
12053
|
|
|
11275
12054
|
# The overlays a click counts as landing *inside*: the one it hit, plus
|
|
11276
12055
|
# every overlay that one belongs to, up the {Component::Overlay#owner}
|
|
@@ -11283,6 +12062,11 @@ module Tuile
|
|
|
11283
12062
|
# _@param_ `hit` — the overlay the click landed in, if any.
|
|
11284
12063
|
def kept_by: (Component::Overlay? hit) -> ::Array[Component::Overlay]
|
|
11285
12064
|
|
|
12065
|
+
# _@param_ `point`
|
|
12066
|
+
#
|
|
12067
|
+
# _@return_ — the topmost popup containing `point`.
|
|
12068
|
+
def popup_at: (Point point) -> Component::Overlay?
|
|
12069
|
+
|
|
11286
12070
|
# _@param_ `component`
|
|
11287
12071
|
#
|
|
11288
12072
|
# _@return_ — `component` itself when it is an
|
|
@@ -11814,7 +12598,7 @@ module Tuile
|
|
|
11814
12598
|
def running?: () -> bool
|
|
11815
12599
|
|
|
11816
12600
|
# _@return_ — always true.
|
|
11817
|
-
def
|
|
12601
|
+
def in_loop_thread?: () -> bool
|
|
11818
12602
|
|
|
11819
12603
|
def stop: () -> void
|
|
11820
12604
|
|
|
@@ -12034,7 +12818,7 @@ module Tuile
|
|
|
12034
12818
|
# track is a solid column carrying no information, so `row_count <= height`
|
|
12035
12819
|
# paints track at every row — while {#handle_height} / {#handle_start} /
|
|
12036
12820
|
# {#handle_end} still report the covering handle. Ink only: the caller's bar
|
|
12037
|
-
# keeps its column and its content width (`
|
|
12821
|
+
# keeps its column and its content width (`design/decisions.md` `D_scrollbar_ink`).
|
|
12038
12822
|
class VerticalScrollBar
|
|
12039
12823
|
# One cell, exactly: a painter concatenates the glyph onto a row already
|
|
12040
12824
|
# padded to fill the rest of its rect, so a two-column one pushes every
|