tuile 0.10.0 → 0.12.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 +109 -64
- data/DECISIONS.md +1299 -22
- data/README.md +18 -13
- data/TERMINOLOGY.md +61 -0
- data/book/02-repaint.md +1 -1
- data/book/03-layout.md +154 -9
- data/book/05-focus.md +2 -0
- data/book/06-theming.md +1 -1
- data/book/07-components.md +202 -37
- data/book/README.md +3 -1
- data/examples/file_commander.rb +5 -4
- data/examples/sampler.rb +320 -133
- data/ideas/arrow-key-navigation.md +205 -0
- data/ideas/new-components.md +26 -12
- data/lib/tuile/buffer.rb +7 -7
- data/lib/tuile/component/big_decimal_field.rb +199 -0
- data/lib/tuile/component/button.rb +1 -1
- data/lib/tuile/component/checkbox.rb +11 -10
- data/lib/tuile/component/checkbox_group.rb +31 -26
- data/lib/tuile/component/combo_box.rb +18 -33
- data/lib/tuile/component/float_field.rb +161 -0
- data/lib/tuile/component/info_window.rb +1 -1
- data/lib/tuile/component/label.rb +14 -14
- data/lib/tuile/component/layout/box.rb +316 -0
- data/lib/tuile/component/layout/horizontal.rb +40 -0
- data/lib/tuile/component/layout/vertical.rb +41 -0
- data/lib/tuile/component/layout.rb +149 -1
- data/lib/tuile/component/list.rb +291 -216
- data/lib/tuile/component/list_dropdown.rb +82 -24
- data/lib/tuile/component/notification.rb +317 -0
- data/lib/tuile/component/picker_window.rb +3 -3
- data/lib/tuile/component/popup.rb +8 -10
- data/lib/tuile/component/progress_bar.rb +1 -1
- data/lib/tuile/component/radio_group.rb +32 -30
- data/lib/tuile/component/select.rb +251 -0
- data/lib/tuile/component/text_area/wrapped_text.rb +320 -0
- data/lib/tuile/component/text_area.rb +79 -273
- data/lib/tuile/component/text_field.rb +1 -1
- data/lib/tuile/component/text_view.rb +191 -177
- data/lib/tuile/component/window.rb +8 -8
- data/lib/tuile/component.rb +5 -5
- data/lib/tuile/screen.rb +1 -1
- data/lib/tuile/styled_string.rb +25 -15
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile/vertical_scroll_bar.rb +6 -6
- data/lib/tuile.rb +4 -0
- data/sig/tuile.rbs +1670 -406
- metadata +11 -1
|
@@ -24,7 +24,7 @@ module Tuile
|
|
|
24
24
|
@content = nil
|
|
25
25
|
# Optional bottom-row widget slot (e.g. a search field), spanning the
|
|
26
26
|
# full inner width; and optional bottom-border chrome text embedded in
|
|
27
|
-
# the border
|
|
27
|
+
# the border row (mutually exclusive — the component, when present,
|
|
28
28
|
# occupies the row and hides the text).
|
|
29
29
|
@footer = nil
|
|
30
30
|
@footer_text = StyledString::EMPTY
|
|
@@ -53,7 +53,7 @@ module Tuile
|
|
|
53
53
|
return if @footer_text == new_text
|
|
54
54
|
|
|
55
55
|
@footer_text = new_text
|
|
56
|
-
invalidate # repaint the bottom border
|
|
56
|
+
invalidate # repaint the bottom border row
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
# Sets the bottom-row widget slot. The footer occupies the bottom border
|
|
@@ -147,10 +147,10 @@ module Tuile
|
|
|
147
147
|
content.rect = Rect.new(rect.left + 1, rect.top + 1, rect.width - 1 - @border_right, rect.height - 2)
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
-
# Paints the window border via {Component#
|
|
150
|
+
# Paints the window border via {Component#draw_text}/{Component#draw_char},
|
|
151
151
|
# so the border cells inherit {Component#effective_bg_color} — a
|
|
152
152
|
# {Component#bg_color} on the window tints border and content alike. Both
|
|
153
|
-
# border
|
|
153
|
+
# border rows are clipped by *display* width, so no caption overflows the
|
|
154
154
|
# box; when the window is active the whole border — the caption's own
|
|
155
155
|
# colors included — is drawn in {Theme#active_border_color}.
|
|
156
156
|
# @return [void]
|
|
@@ -165,15 +165,15 @@ module Tuile
|
|
|
165
165
|
|
|
166
166
|
fg = active? ? screen.theme.active_border_color : nil
|
|
167
167
|
bar = StyledString::Style.new(fg: fg)
|
|
168
|
-
|
|
168
|
+
draw_text(left, top, top_border(inner_w, fg).slice(0, w))
|
|
169
169
|
(1..(h - 2)).each do |dy|
|
|
170
170
|
draw_char(left, top + dy, "│", bar)
|
|
171
171
|
draw_char(left + w - 1, top + dy, "│", bar)
|
|
172
172
|
end
|
|
173
|
-
|
|
173
|
+
draw_text(left, top + h - 1, bottom_border(inner_w, fg).slice(0, w)) if h >= 2
|
|
174
174
|
end
|
|
175
175
|
|
|
176
|
-
# Builds the top border
|
|
176
|
+
# Builds the top border row: corners, {#caption} embedded at its own
|
|
177
177
|
# width, dashes filling the remainder. The caption keeps its own styling
|
|
178
178
|
# unless `fg` is set — an active window's border claims it.
|
|
179
179
|
# @param inner_w [Integer] the border's interior width.
|
|
@@ -186,7 +186,7 @@ module Tuile
|
|
|
186
186
|
StyledString.styled("┌", fg: fg) + title + dashes + StyledString.styled("┐", fg: fg)
|
|
187
187
|
end
|
|
188
188
|
|
|
189
|
-
# Builds the bottom border
|
|
189
|
+
# Builds the bottom border row. The corners take the border color; the
|
|
190
190
|
# interior is plain dashes when a {#footer} component occupies the row
|
|
191
191
|
# (it overpaints them) or when there's no chrome, otherwise it carries
|
|
192
192
|
# {#footer_text} embedded at its own width — keeping the text's own
|
data/lib/tuile/component.rb
CHANGED
|
@@ -211,7 +211,7 @@ module Tuile
|
|
|
211
211
|
# Called on every attached component (pre-order, popups included) when
|
|
212
212
|
# {Screen#theme} changes — at {Screen#theme=} / {Screen#theme_def=} and on
|
|
213
213
|
# OS appearance flips. The hook exists for app *content* whose colors were
|
|
214
|
-
# baked in from the old theme (a {Label#text} / {List#lines} {StyledString}
|
|
214
|
+
# baked in from the old theme (a {Label#text} / {List#lines=} {StyledString}
|
|
215
215
|
# styled with `theme[:accent]`); rebuild it here by re-running the code that
|
|
216
216
|
# rendered it. See book ch6 for why built-in accents need no such handling.
|
|
217
217
|
#
|
|
@@ -444,7 +444,7 @@ module Tuile
|
|
|
444
444
|
screen.buffer.fill(area, bg ? StyledString::Style.new(bg:) : StyledString::Style::DEFAULT)
|
|
445
445
|
end
|
|
446
446
|
|
|
447
|
-
# {Buffer#
|
|
447
|
+
# {Buffer#set_text} wrapper that fills {#effective_bg_color} behind any span
|
|
448
448
|
# with no bg of its own (via {StyledString#under_bg}), so an inherited
|
|
449
449
|
# {#bg_color} shows through the content a component paints. A no-op layer
|
|
450
450
|
# when nothing is inherited. Self-painters (those skipping the {#repaint}
|
|
@@ -453,11 +453,11 @@ module Tuile
|
|
|
453
453
|
# @param y [Integer] row.
|
|
454
454
|
# @param styled [StyledString]
|
|
455
455
|
# @return [void]
|
|
456
|
-
def
|
|
457
|
-
screen.buffer.
|
|
456
|
+
def draw_text(x, y, styled)
|
|
457
|
+
screen.buffer.set_text(x, y, styled.under_bg(effective_bg_color))
|
|
458
458
|
end
|
|
459
459
|
|
|
460
|
-
# {#
|
|
460
|
+
# {#draw_text}'s single-grapheme counterpart: writes `grapheme` at `(x, y)`,
|
|
461
461
|
# filling {#effective_bg_color} when `style` carries no bg of its own.
|
|
462
462
|
# @param x [Integer] column.
|
|
463
463
|
# @param y [Integer] row.
|
data/lib/tuile/screen.rb
CHANGED
|
@@ -114,7 +114,7 @@ module Tuile
|
|
|
114
114
|
attr_reader :color_scheme
|
|
115
115
|
|
|
116
116
|
# @return [Buffer] the back buffer components paint into
|
|
117
|
-
# ({Buffer#
|
|
117
|
+
# ({Buffer#set_text} / {Buffer#fill} / {Buffer#set_char}).
|
|
118
118
|
attr_reader :buffer
|
|
119
119
|
|
|
120
120
|
# Handler invoked when a {StandardError} escapes an event handler inside
|
data/lib/tuile/styled_string.rb
CHANGED
|
@@ -536,31 +536,40 @@ module Tuile
|
|
|
536
536
|
result
|
|
537
537
|
end
|
|
538
538
|
|
|
539
|
-
# Word-wraps to
|
|
539
|
+
# Word-wraps to rows that each fit within `width` display
|
|
540
540
|
# columns, preserving spans and styles across breaks. Greedy word-wrap,
|
|
541
541
|
# hard-break for words wider than `width`, leading whitespace dropped on
|
|
542
542
|
# wrapped continuations, hard `"\n"` breaks preserved as separate output
|
|
543
543
|
# lines.
|
|
544
544
|
#
|
|
545
|
+
# An indent is content, so it survives onto the first row — but there is no
|
|
546
|
+
# hanging indent:
|
|
547
|
+
#
|
|
548
|
+
# StyledString.plain(" read config").wrap(20).map(&:to_s)
|
|
549
|
+
# # => [" read config"] indent kept; the line never wrapped
|
|
550
|
+
# StyledString.plain(" read config").wrap(6).map(&:to_s)
|
|
551
|
+
# # => [" read", "config"] ...but a continuation starts at column 0
|
|
552
|
+
#
|
|
545
553
|
# Whitespace runs are space or tab; other characters are treated as word
|
|
546
554
|
# content. When a single character is wider than `width` (e.g. a 2-column
|
|
547
555
|
# CJK character with `width = 1`), it is still emitted on its own line at
|
|
548
556
|
# its natural width. The "no line exceeds `width`" guarantee therefore
|
|
549
|
-
# holds whenever every character is at most `width` columns wide.
|
|
557
|
+
# holds whenever every character is at most `width` columns wide. An indent
|
|
558
|
+
# that alone exceeds `width` is dropped rather than given a row of its own.
|
|
550
559
|
#
|
|
551
560
|
# @param width [Integer, nil] target column width. `nil` or `<= 0` skips
|
|
552
561
|
# wrapping and returns each hard-line as-is, so callers can pass a
|
|
553
562
|
# stale viewport width without crashing.
|
|
554
|
-
# @return [Array<StyledString>] one entry per
|
|
563
|
+
# @return [Array<StyledString>] one entry per output row.
|
|
555
564
|
# An empty receiver returns `[]`.
|
|
556
565
|
def wrap(width)
|
|
557
566
|
return [] if empty?
|
|
558
567
|
|
|
559
|
-
|
|
560
|
-
return
|
|
568
|
+
input_lines = lines
|
|
569
|
+
return input_lines if width.nil? || width <= 0
|
|
561
570
|
|
|
562
571
|
result = []
|
|
563
|
-
|
|
572
|
+
input_lines.each { |line| result.concat(wrap_one(line, width)) }
|
|
564
573
|
result
|
|
565
574
|
end
|
|
566
575
|
|
|
@@ -708,20 +717,21 @@ module Tuile
|
|
|
708
717
|
self.class.new(out)
|
|
709
718
|
end
|
|
710
719
|
|
|
711
|
-
# @param
|
|
720
|
+
# @param line [StyledString] one line — no embedded `"\n"`.
|
|
712
721
|
# @param width [Integer]
|
|
713
722
|
# @return [Array<StyledString>]
|
|
714
|
-
def wrap_one(
|
|
715
|
-
return [
|
|
723
|
+
def wrap_one(line, width)
|
|
724
|
+
return [line] if line.empty?
|
|
716
725
|
|
|
717
726
|
result = []
|
|
718
727
|
line_glyphs = []
|
|
719
728
|
line_w = 0
|
|
720
729
|
|
|
721
|
-
tokenize_for_wrap(
|
|
730
|
+
tokenize_for_wrap(line).each do |type, glyphs, w|
|
|
722
731
|
if type == :space
|
|
723
|
-
if line_w.zero?
|
|
724
|
-
#
|
|
732
|
+
if line_w.zero? && (!result.empty? || w > width)
|
|
733
|
+
# Nothing to emit: a continuation's leading run was consumed by the
|
|
734
|
+
# break, and an indent wider than the viewport conveys no nesting.
|
|
725
735
|
elsif line_w + w <= width
|
|
726
736
|
line_glyphs.concat(glyphs)
|
|
727
737
|
line_w += w
|
|
@@ -753,18 +763,18 @@ module Tuile
|
|
|
753
763
|
# a cluster is the unit a terminal draws, so measuring its parts separately
|
|
754
764
|
# would both mis-total an emoji sequence and let a wrap break a letter away
|
|
755
765
|
# from its combining mark.
|
|
756
|
-
# @param
|
|
766
|
+
# @param line [StyledString]
|
|
757
767
|
# @return [Array<Array>] tokens shaped `[type, glyphs, w]` where `type` is
|
|
758
768
|
# `:space` or `:word`, `glyphs` is an `Array<[String, Style, Integer]>`
|
|
759
769
|
# (grapheme cluster, style, display width), and `w` is the token's total
|
|
760
770
|
# width.
|
|
761
|
-
def tokenize_for_wrap(
|
|
771
|
+
def tokenize_for_wrap(line)
|
|
762
772
|
tokens = []
|
|
763
773
|
current_glyphs = []
|
|
764
774
|
current_w = 0
|
|
765
775
|
current_type = nil
|
|
766
776
|
|
|
767
|
-
each_glyph_with_style(
|
|
777
|
+
each_glyph_with_style(line) do |g, s|
|
|
768
778
|
type = [" ", "\t"].include?(g) ? :space : :word
|
|
769
779
|
gw = Buffer.display_width(g)
|
|
770
780
|
if current_type && current_type != type
|
data/lib/tuile/version.rb
CHANGED
|
@@ -18,20 +18,20 @@ module Tuile
|
|
|
18
18
|
|
|
19
19
|
# @param height [Integer] number of rows in the scrollbar (== viewport
|
|
20
20
|
# height).
|
|
21
|
-
# @param
|
|
22
|
-
# @param
|
|
23
|
-
def initialize(height,
|
|
21
|
+
# @param row_count [Integer] total number of content rows.
|
|
22
|
+
# @param scroll_top_row [Integer] index of the first visible content row.
|
|
23
|
+
def initialize(height, row_count:, scroll_top_row:)
|
|
24
24
|
@height = height
|
|
25
25
|
|
|
26
26
|
return unless height >= 1
|
|
27
27
|
|
|
28
|
-
if
|
|
28
|
+
if row_count <= height
|
|
29
29
|
@handle_height = height
|
|
30
30
|
@handle_start = 0
|
|
31
31
|
@handle_end = height - 1
|
|
32
32
|
else
|
|
33
|
-
@handle_height = [(height * height /
|
|
34
|
-
@handle_start = (height *
|
|
33
|
+
@handle_height = [(height * height / row_count.to_f).ceil, 1].max
|
|
34
|
+
@handle_start = (height * scroll_top_row / row_count.to_f).floor
|
|
35
35
|
@handle_end = @handle_start + @handle_height - 1
|
|
36
36
|
end
|
|
37
37
|
end
|
data/lib/tuile.rb
CHANGED
|
@@ -32,5 +32,9 @@ module Tuile
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
loader = Zeitwerk::Loader.for_gem
|
|
35
|
+
# Keeps Tuile's one optional dependency optional: the file requires
|
|
36
|
+
# `bigdecimal` at load, so a host app calling Zeitwerk::Loader.eager_load_all
|
|
37
|
+
# would otherwise raise LoadError for a component it never names.
|
|
38
|
+
loader.do_not_eager_load("#{__dir__}/tuile/component/big_decimal_field.rb")
|
|
35
39
|
loader.setup
|
|
36
40
|
end
|