tuile 0.15.0 → 0.17.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 +229 -80
- data/README.md +49 -24
- data/book/02-repaint.md +47 -19
- data/book/03-layout.md +98 -49
- data/book/04-event-loop.md +17 -16
- data/book/05-focus.md +106 -34
- data/book/06-theming.md +108 -38
- data/book/07-components.md +249 -46
- data/book/08-testing.md +134 -32
- data/book/10-locale.md +3 -3
- data/book/README.md +11 -10
- data/examples/file_commander.rb +52 -32
- data/examples/hello_world.rb +18 -5
- data/examples/sampler.rb +576 -146
- data/lib/tuile/buffer.rb +12 -1
- data/lib/tuile/canvas/backend.rb +46 -0
- data/lib/tuile/canvas.rb +212 -0
- data/lib/tuile/color.rb +38 -9
- data/lib/tuile/component/abstract_string_field.rb +96 -97
- data/lib/tuile/component/abstract_wrapping_field.rb +99 -58
- data/lib/tuile/component/big_decimal_field.rb +7 -6
- data/lib/tuile/component/button.rb +27 -19
- data/lib/tuile/component/checkbox.rb +21 -19
- data/lib/tuile/component/checkbox_group.rb +17 -18
- data/lib/tuile/component/combo_box.rb +69 -64
- data/lib/tuile/component/confirm_window.rb +34 -27
- data/lib/tuile/component/date_field.rb +50 -18
- data/lib/tuile/component/date_time_field.rb +319 -0
- data/lib/tuile/component/fill.rb +93 -0
- data/lib/tuile/component/float_field.rb +7 -6
- data/lib/tuile/component/form_item.rb +250 -0
- data/lib/tuile/component/form_layout.rb +206 -0
- data/lib/tuile/component/has_bad_input.rb +99 -28
- data/lib/tuile/component/has_caption.rb +14 -5
- data/lib/tuile/component/has_content.rb +8 -15
- data/lib/tuile/component/has_placeholder.rb +1 -1
- data/lib/tuile/component/has_validation.rb +40 -14
- data/lib/tuile/component/has_value.rb +71 -17
- data/lib/tuile/component/integer_field.rb +7 -6
- data/lib/tuile/component/label.rb +8 -15
- data/lib/tuile/component/layout/absolute.rb +86 -0
- data/lib/tuile/component/layout/box.rb +38 -60
- data/lib/tuile/component/layout.rb +127 -13
- data/lib/tuile/component/list.rb +233 -120
- data/lib/tuile/component/list_dropdown.rb +151 -91
- data/lib/tuile/component/menu_bar/cascade.rb +102 -32
- data/lib/tuile/component/menu_bar.rb +102 -82
- data/lib/tuile/component/notification.rb +76 -49
- data/lib/tuile/component/overlay.rb +217 -58
- data/lib/tuile/component/password_field.rb +1 -8
- data/lib/tuile/component/picker_window.rb +41 -17
- data/lib/tuile/component/popup.rb +15 -26
- data/lib/tuile/component/progress_bar.rb +17 -11
- data/lib/tuile/component/radio_group.rb +16 -17
- data/lib/tuile/component/scroller.rb +266 -0
- data/lib/tuile/component/select.rb +26 -43
- data/lib/tuile/component/slot.rb +4 -5
- data/lib/tuile/component/tab_sheet.rb +27 -34
- data/lib/tuile/component/tabs.rb +49 -34
- data/lib/tuile/component/text_area/wrapped_text.rb +1 -1
- data/lib/tuile/component/text_area.rb +32 -28
- data/lib/tuile/component/text_field.rb +68 -50
- data/lib/tuile/component/text_view.rb +157 -89
- data/lib/tuile/component/time_field.rb +51 -21
- data/lib/tuile/component/vertical_scroll_bar.rb +257 -0
- data/lib/tuile/component/window.rb +27 -26
- data/lib/tuile/component.rb +653 -323
- data/lib/tuile/component_background.rb +177 -0
- data/lib/tuile/component_util.rb +43 -0
- data/lib/tuile/event.rb +29 -0
- data/lib/tuile/event_queue.rb +18 -4
- data/lib/tuile/fake_event_queue.rb +1 -1
- data/lib/tuile/fake_screen.rb +120 -7
- data/lib/tuile/keys.rb +15 -6
- data/lib/tuile/layout_pass.rb +180 -0
- data/lib/tuile/listeners.rb +219 -0
- data/lib/tuile/mouse/router.rb +233 -0
- data/lib/tuile/mouse.rb +244 -0
- data/lib/tuile/point.rb +6 -0
- data/lib/tuile/rect.rb +33 -0
- data/lib/tuile/screen.rb +510 -138
- data/lib/tuile/screen_pane.rb +185 -67
- data/lib/tuile/strict_layout.rb +127 -0
- data/lib/tuile/styled_string.rb +144 -14
- data/lib/tuile/testing/gestures.rb +35 -0
- data/lib/tuile/testing.rb +316 -42
- data/lib/tuile/theme.rb +192 -53
- data/lib/tuile/theme_def.rb +4 -0
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile.rb +53 -0
- data/sig/tuile.rbs +6084 -1507
- metadata +19 -17
- 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/lib/tuile/vertical_scroll_bar.rb +0 -122
data/lib/tuile/buffer.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Tuile
|
|
4
4
|
# An in-memory grid of styled cells mirroring the terminal screen. This is
|
|
5
5
|
# the back buffer behind flicker-free rendering: components paint into it
|
|
6
|
-
#
|
|
6
|
+
# through a {Canvas}, whose {Canvas::Backend} it is, instead of writing escape
|
|
7
7
|
# sequences straight to the terminal, and {#flush} emits the minimal escape
|
|
8
8
|
# string needed to bring a terminal — one that already matches the buffer's
|
|
9
9
|
# state as of the previous flush — up to date. Only cells that actually
|
|
@@ -34,6 +34,10 @@ module Tuile
|
|
|
34
34
|
# Overwriting either half of a wide glyph blanks the orphaned half, so the
|
|
35
35
|
# grid never holds a dangling continuation or a headless one.
|
|
36
36
|
class Buffer
|
|
37
|
+
# The three primitives below are exactly {Canvas::Backend}'s, so a canvas
|
|
38
|
+
# paints into a buffer with no adapter between them.
|
|
39
|
+
include Canvas::Backend
|
|
40
|
+
|
|
37
41
|
# One screen cell: a single grapheme cluster, the {StyledString::Style} it's
|
|
38
42
|
# drawn in, and a dirty flag. Mutable by design (see {Buffer} "Dirty
|
|
39
43
|
# tracking") — the grid rewrites cells in place. A continuation cell (right
|
|
@@ -317,6 +321,13 @@ module Tuile
|
|
|
317
321
|
region_cells(rect).map { |row| row.map(&:grapheme).join }
|
|
318
322
|
end
|
|
319
323
|
|
|
324
|
+
# {#region_text} over the whole buffer, for one {Testing.paint} returned:
|
|
325
|
+
#
|
|
326
|
+
# Testing.paint(window).text # => ["┌Caption─────┐", "│ alpha │", …]
|
|
327
|
+
#
|
|
328
|
+
# @return [Array<String>] the plain text of every row, top to bottom.
|
|
329
|
+
def text = region_text(Rect.new(0, 0, @width, @height))
|
|
330
|
+
|
|
320
331
|
# @param rect [Rect]
|
|
321
332
|
# @return [Array<String>] each row within `rect` rendered to ANSI, top to
|
|
322
333
|
# bottom — byte-identical to what a component's per-row `set_text` over
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tuile
|
|
4
|
+
class Canvas
|
|
5
|
+
# Where a {Canvas}'s cells land: three primitives taking a fully resolved
|
|
6
|
+
# style. Include it to *be* one — {Tuile::Buffer} does, and needs no adapter:
|
|
7
|
+
#
|
|
8
|
+
# Canvas.new(screen.buffer)
|
|
9
|
+
#
|
|
10
|
+
# A backend is dumb by contract: no theme, no component, no background
|
|
11
|
+
# chain, no translation, and no default arguments — the canvas resolves all
|
|
12
|
+
# of that and hands down a whole style at a coordinate in the backend's own
|
|
13
|
+
# grid. What varies here is only *where* the cells go;
|
|
14
|
+
# how a write is transformed on the way there belongs to the {Canvas}, which
|
|
15
|
+
# is final. See `D_canvas`.
|
|
16
|
+
#
|
|
17
|
+
# **`include`, never `prepend`.** A class's own methods win over an included
|
|
18
|
+
# module's, which is what lets {Tuile::Buffer} carry this while defining all
|
|
19
|
+
# three itself; prepended, these bodies would sit *ahead* of the class and
|
|
20
|
+
# every write would raise.
|
|
21
|
+
#
|
|
22
|
+
# Out-of-range writes are **dropped, not raised** — the terminal-edge clip
|
|
23
|
+
# belongs here, which is what lets a component paint a row the terminal is
|
|
24
|
+
# too narrow for without checking first.
|
|
25
|
+
module Backend
|
|
26
|
+
# @param x [Integer] starting column, in this backend's own grid —
|
|
27
|
+
# the canvas has already applied its {Canvas#origin}.
|
|
28
|
+
# @param y [Integer] row, likewise.
|
|
29
|
+
# @param styled [StyledString] the text of one row; newlines are not handled.
|
|
30
|
+
# @return [void]
|
|
31
|
+
def set_text(x, y, styled) = raise(NotImplementedError, "#{self.class}#set_text")
|
|
32
|
+
|
|
33
|
+
# @param x [Integer] column, in this backend's own grid.
|
|
34
|
+
# @param y [Integer] row, likewise.
|
|
35
|
+
# @param grapheme [String] one grapheme cluster.
|
|
36
|
+
# @param style [StyledString::Style] fully resolved.
|
|
37
|
+
# @return [void]
|
|
38
|
+
def set_char(x, y, grapheme, style) = raise(NotImplementedError, "#{self.class}#set_char")
|
|
39
|
+
|
|
40
|
+
# @param rect [Rect] in this backend's own grid.
|
|
41
|
+
# @param style [StyledString::Style] fully resolved; only its `bg` shows.
|
|
42
|
+
# @return [void]
|
|
43
|
+
def fill(rect, style) = raise(NotImplementedError, "#{self.class}#fill")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/tuile/canvas.rb
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tuile
|
|
4
|
+
# The paint context a component draws through: a {Backend} that says where
|
|
5
|
+
# cells land, plus the state every write needs — the {#origin} that puts
|
|
6
|
+
# `(0, 0)` at the component's own top-left, and the background to fill in
|
|
7
|
+
# behind content that carries none.
|
|
8
|
+
#
|
|
9
|
+
# A component never builds one. It paints onto the canvas its
|
|
10
|
+
# {Component#repaint} was handed, already loaded with that component's
|
|
11
|
+
# {ComponentBackground#effective} and positioned at its {Component#rect}, and
|
|
12
|
+
# derives a second for the cells that are not its own ink:
|
|
13
|
+
#
|
|
14
|
+
# def repaint(canvas)
|
|
15
|
+
# canvas.set_text(0, 0, label) # my well
|
|
16
|
+
# canvas.with(bg_color: bg.ambient) { _1.fill(tail) } # not my ink
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# Three methods, in **paint coordinates**: `(0, 0)` is the component's own
|
|
20
|
+
# top-left, not the screen's, and {Component#local_rect} is its whole rect
|
|
21
|
+
# over here. Passing {Component#rect} instead lands the write at twice the
|
|
22
|
+
# offset, silently. Everything outside painting stays screen-space — `rect`,
|
|
23
|
+
# {Mouse::Event}, {Component#cursor_position} — which `D_canvas` argues.
|
|
24
|
+
#
|
|
25
|
+
# A `nil` {#bg_color} is the terminal default, never "inherit" — inheritance
|
|
26
|
+
# is resolved before a canvas is built ({Screen#canvas_for}).
|
|
27
|
+
#
|
|
28
|
+
# == Implementation details
|
|
29
|
+
#
|
|
30
|
+
# Frozen, and final by convention: {#with} yields a *derived* canvas and never
|
|
31
|
+
# mutates, so there are no save/restore pairs and no state to leave dangling
|
|
32
|
+
# for whatever paints next. Subclassing is not the extension point — {Backend}
|
|
33
|
+
# is, and it is the half that varies. See `D_canvas`.
|
|
34
|
+
#
|
|
35
|
+
# UI-thread-confined.
|
|
36
|
+
class Canvas
|
|
37
|
+
# @return [Backend] where the cells land.
|
|
38
|
+
attr_reader :backend
|
|
39
|
+
|
|
40
|
+
# @return [Color, nil] the background painted behind content that states
|
|
41
|
+
# none; `nil` leaves the terminal default showing.
|
|
42
|
+
attr_reader :bg_color
|
|
43
|
+
|
|
44
|
+
# Where this canvas's `(0, 0)` sits in {#backend} coordinates — the
|
|
45
|
+
# component's {Component#rect}`.top_left`, as {Screen#canvas_for} built it.
|
|
46
|
+
# A component never reads it: adding the offset back by hand is the one
|
|
47
|
+
# thing it exists to make unnecessary.
|
|
48
|
+
# @return [Point]
|
|
49
|
+
attr_reader :origin
|
|
50
|
+
|
|
51
|
+
# The region of the {#backend}'s grid this canvas may write to —
|
|
52
|
+
# {Screen#clip_for}'s answer moved into backend coordinates, or `nil` for a
|
|
53
|
+
# canvas nobody bounded, which costs one test per write and nothing else.
|
|
54
|
+
# Every canvas {Screen#canvas_for} builds carries one; {Screen#canvas}, the
|
|
55
|
+
# root, is the `nil` case.
|
|
56
|
+
#
|
|
57
|
+
# In **backend** coordinates, like {#origin} and unlike every argument the
|
|
58
|
+
# three paint methods take: a canvas's *state* says where it sits in the
|
|
59
|
+
# world, its arguments are in paint coordinates (`D_clip`).
|
|
60
|
+
#
|
|
61
|
+
# An {Rect#empty? empty} clip is not `nil`: it means *paint nothing*, and it
|
|
62
|
+
# is what a component that can show nothing gets — collapsed, or scrolled
|
|
63
|
+
# clean out of its viewport. Every write is judged against the clip on its
|
|
64
|
+
# own terms, so this needs no special case; it is simply the case where they
|
|
65
|
+
# all fail.
|
|
66
|
+
#
|
|
67
|
+
# The one cell it does not protect: {Buffer#put_char} blanks the head of a
|
|
68
|
+
# wide glyph whose continuation half a clipped write overwrites, one column
|
|
69
|
+
# outside. That is the terminal's physical truth, and better than the
|
|
70
|
+
# dangling half-glyph the alternative leaves.
|
|
71
|
+
# @return [Rect, nil]
|
|
72
|
+
attr_reader :clip
|
|
73
|
+
|
|
74
|
+
# @param backend [Backend]
|
|
75
|
+
# @param bg_color [Color, nil] already resolved — a canvas consults no
|
|
76
|
+
# component and no theme.
|
|
77
|
+
# @param origin [Point] the {#origin}; the default paints in backend
|
|
78
|
+
# coordinates, which is what {Screen#canvas} is.
|
|
79
|
+
# @param clip [Rect, nil] the {#clip}, in backend coordinates —
|
|
80
|
+
# {Screen#canvas_for} converts.
|
|
81
|
+
# @raise [Error] if `backend` does not include {Backend}, which is worth
|
|
82
|
+
# catching here rather than mid-paint.
|
|
83
|
+
def initialize(backend, bg_color: nil, origin: Point::ZERO, clip: nil)
|
|
84
|
+
raise Error, "#{backend.class} must include Tuile::Canvas::Backend" unless backend.is_a?(Backend)
|
|
85
|
+
|
|
86
|
+
@backend = backend
|
|
87
|
+
@bg_color = bg_color
|
|
88
|
+
@origin = origin
|
|
89
|
+
@clip = clip
|
|
90
|
+
@blank_style = bg_color ? StyledString::Style.new(bg: bg_color) : StyledString::Style::DEFAULT
|
|
91
|
+
freeze
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Yields a canvas onto the same backend with a different background, for the
|
|
95
|
+
# span of the block — the *only* way to change it.
|
|
96
|
+
#
|
|
97
|
+
# canvas.with(bg_color: bg.ambient) do |c|
|
|
98
|
+
# c.fill(right)
|
|
99
|
+
# c.fill(below)
|
|
100
|
+
# end
|
|
101
|
+
#
|
|
102
|
+
# The receiver is untouched, so nothing has to be restored afterwards and
|
|
103
|
+
# the block cannot leave the wrong background on for whatever paints next.
|
|
104
|
+
# The {#origin} and the {#clip} ride along, so a derived canvas paints in
|
|
105
|
+
# the same coordinates and is bounded the same way.
|
|
106
|
+
# @param bg_color [Color, nil] the background inside the block.
|
|
107
|
+
# @return [Object] the block's value.
|
|
108
|
+
# @raise [Error] if no block is given — a derived canvas nobody scoped is
|
|
109
|
+
# the dangling state this shape exists to make impossible.
|
|
110
|
+
def with(bg_color:)
|
|
111
|
+
raise Error, "Canvas#with needs a block: with(bg_color:) { |canvas| … }" unless block_given?
|
|
112
|
+
return yield self if bg_color == @bg_color
|
|
113
|
+
|
|
114
|
+
yield Canvas.new(@backend, bg_color:, origin: @origin, clip: @clip)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Writes a {StyledString}, filling {#bg_color} behind any span that states
|
|
118
|
+
# no background of its own — so an inherited tint, or an invalid field's
|
|
119
|
+
# error well, shows through content the component did not colour.
|
|
120
|
+
# @param x [Integer] starting column, relative to {#origin}.
|
|
121
|
+
# @param y [Integer] row, relative to {#origin}.
|
|
122
|
+
# @param styled [StyledString] the text of one row; newlines are not handled.
|
|
123
|
+
# @return [void]
|
|
124
|
+
def set_text(x, y, styled)
|
|
125
|
+
col = x + @origin.x
|
|
126
|
+
row = y + @origin.y
|
|
127
|
+
return @backend.set_text(col, row, styled.under_bg(@bg_color)) if @clip.nil?
|
|
128
|
+
return unless clipped_row?(row)
|
|
129
|
+
|
|
130
|
+
write_clipped_text(col, row, styled)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# {#set_text}'s single-grapheme counterpart.
|
|
134
|
+
# @param x [Integer] column, relative to {#origin}.
|
|
135
|
+
# @param y [Integer] row, relative to {#origin}.
|
|
136
|
+
# @param grapheme [String] one grapheme cluster.
|
|
137
|
+
# @param style [StyledString::Style] its `bg`, when set, wins over {#bg_color}.
|
|
138
|
+
# @return [void]
|
|
139
|
+
def set_char(x, y, grapheme, style = StyledString::Style::DEFAULT)
|
|
140
|
+
style = style.merge(bg: @bg_color) if @bg_color && style.bg.nil?
|
|
141
|
+
col = x + @origin.x
|
|
142
|
+
row = y + @origin.y
|
|
143
|
+
return @backend.set_char(col, row, grapheme, style) if @clip.nil?
|
|
144
|
+
return unless clipped_row?(row)
|
|
145
|
+
|
|
146
|
+
# A zero-width cluster still lands in one cell, and that cell is what the
|
|
147
|
+
# clip judges.
|
|
148
|
+
width = [Buffer.display_width(grapheme), 1].max
|
|
149
|
+
from = [col, @clip.left].max
|
|
150
|
+
to = [col + width, @clip.left + @clip.width].min
|
|
151
|
+
return if to <= from
|
|
152
|
+
|
|
153
|
+
# Half of a wide glyph is unrenderable, so the columns the clip keeps are
|
|
154
|
+
# blanked instead — {Buffer#put_char}'s own policy at the terminal's edge.
|
|
155
|
+
return @backend.fill(Rect.new(from, row, to - from, 1), @blank_style) if to - from < width
|
|
156
|
+
|
|
157
|
+
@backend.set_char(col, row, grapheme, style)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Blanks `area` to {#bg_color}.
|
|
161
|
+
#
|
|
162
|
+
# Only for cells nothing is about to paint over: {Buffer::Cell#set} dirties
|
|
163
|
+
# on a real change, so blanking a cell that is then redrawn re-emits it.
|
|
164
|
+
# @param area [Rect] relative to {#origin} — {Component#local_rect} for the
|
|
165
|
+
# whole of a component, never its {Component#rect}.
|
|
166
|
+
# @return [void]
|
|
167
|
+
def fill(area)
|
|
168
|
+
# The early-out {#set_text} gets from `clipped_row?`: an empty clip keeps
|
|
169
|
+
# no cell at all, so neither rectangle below is worth building.
|
|
170
|
+
return if @clip && @clip.empty?
|
|
171
|
+
|
|
172
|
+
area = area.moved_by(@origin)
|
|
173
|
+
@backend.fill(@clip.nil? ? area : area.intersect(@clip), @blank_style)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
# @param row [Integer] a row in backend coordinates.
|
|
179
|
+
# @return [Boolean] whether {#clip} keeps it. Callers have already checked
|
|
180
|
+
# that there *is* a clip.
|
|
181
|
+
def clipped_row?(row) = row >= @clip.top && row < @clip.top + @clip.height
|
|
182
|
+
|
|
183
|
+
# {#set_text} for the case that has to think: cut `styled` to the clip's
|
|
184
|
+
# columns and write what survived where it really belongs.
|
|
185
|
+
#
|
|
186
|
+
# {StyledString#slice} **drops** a cluster the boundary falls inside rather
|
|
187
|
+
# than splitting one, at the start as readily as at the end — so the kept
|
|
188
|
+
# text can begin a column later than the cut asked for, and the write
|
|
189
|
+
# position is derived from it rather than assumed. The column a dropped
|
|
190
|
+
# cluster half-covered is blanked (`D_clip`).
|
|
191
|
+
# @param col [Integer] starting column, in backend coordinates.
|
|
192
|
+
# @param row [Integer] row, likewise; the caller has checked the clip keeps it.
|
|
193
|
+
# @param styled [StyledString] the text of one row, uncoloured as handed in.
|
|
194
|
+
# @return [void]
|
|
195
|
+
def write_clipped_text(col, row, styled)
|
|
196
|
+
right = @clip.left + @clip.width
|
|
197
|
+
width = styled.display_width
|
|
198
|
+
from = [col, @clip.left].max
|
|
199
|
+
to = [col + width, right].min
|
|
200
|
+
return if to <= from
|
|
201
|
+
|
|
202
|
+
kept = col < @clip.left ? styled.slice(@clip.left - col, width) : styled
|
|
203
|
+
start = col + width - kept.display_width
|
|
204
|
+
kept = kept.slice(0, right - start) if start + kept.display_width > right
|
|
205
|
+
stop = start + kept.display_width
|
|
206
|
+
|
|
207
|
+
@backend.fill(Rect.new(from, row, start - from, 1), @blank_style) if start > from
|
|
208
|
+
@backend.fill(Rect.new(stop, row, to - stop, 1), @blank_style) if to > stop
|
|
209
|
+
@backend.set_text(start, row, kept.under_bg(@bg_color)) unless kept.empty?
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
data/lib/tuile/color.rb
CHANGED
|
@@ -122,6 +122,27 @@ module Tuile
|
|
|
122
122
|
# @return [Symbol, Integer, Array<Integer>]
|
|
123
123
|
attr_reader :value
|
|
124
124
|
|
|
125
|
+
# This color's red, green and blue, for an app doing color math — a
|
|
126
|
+
# contrast check, a tint derived from {Screen#background_color}:
|
|
127
|
+
#
|
|
128
|
+
# Color.hex("#ff6400").rgb # => [255, 100, 0]
|
|
129
|
+
# Color::DEEP_SKY_BLUE1.rgb # => [0, 175, 255], palette cell 39
|
|
130
|
+
# Color::MAGENTA.rgb # => nil, the terminal's scheme decides
|
|
131
|
+
# Color.palette(5).rgb # => nil, likewise
|
|
132
|
+
#
|
|
133
|
+
# `nil` for the 16 named colors, in either spelling, because the scheme
|
|
134
|
+
# remaps them and any answer would be a guess. Indices 16..255 answer
|
|
135
|
+
# xterm's cube and grey ramp: a terminal *may* redefine those too (OSC 4),
|
|
136
|
+
# but in practice none does.
|
|
137
|
+
#
|
|
138
|
+
# @return [Array<Integer>, nil] frozen, each channel 0..255.
|
|
139
|
+
def rgb
|
|
140
|
+
case @value
|
|
141
|
+
when Array then @value
|
|
142
|
+
when Integer then PALETTE_RGB[@value]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
125
146
|
# SGR parameter codes for emitting this color as either a foreground
|
|
126
147
|
# (`target: :fg`) or background (`target: :bg`). Returned as an array so
|
|
127
148
|
# callers can splice them into a multi-attribute SGR (e.g. bold + color).
|
|
@@ -214,17 +235,12 @@ module Tuile
|
|
|
214
235
|
|
|
215
236
|
private
|
|
216
237
|
|
|
217
|
-
#
|
|
218
|
-
#
|
|
219
|
-
#
|
|
238
|
+
# {#rgb}, except an index 0..15 answers xterm's default for it — the guess
|
|
239
|
+
# `:ansi16` quantization needs something to match against, and {#rgb}
|
|
240
|
+
# must not hand out. Only ever asked of a non-Symbol value.
|
|
220
241
|
# @return [Array<Integer>] red, green and blue, each 0..255.
|
|
221
242
|
def rgb_triple
|
|
222
|
-
|
|
223
|
-
return ANSI16_RGB[@value] if @value < 16
|
|
224
|
-
return [8 + (10 * (@value - 232))] * 3 if @value >= 232
|
|
225
|
-
|
|
226
|
-
cube = @value - 16
|
|
227
|
-
[CUBE_LEVELS[cube / 36], CUBE_LEVELS[(cube / 6) % 6], CUBE_LEVELS[cube % 6]]
|
|
243
|
+
rgb || ANSI16_RGB[@value]
|
|
228
244
|
end
|
|
229
245
|
|
|
230
246
|
# Written flat — destructured rather than splatted, `x * x` rather than
|
|
@@ -293,6 +309,19 @@ module Tuile
|
|
|
293
309
|
CUBE_INDEX = Array.new(256) { |c| (0...6).min_by { |i| (CUBE_LEVELS[i] - c).abs } }.freeze
|
|
294
310
|
private_constant :CUBE_INDEX
|
|
295
311
|
|
|
312
|
+
# Palette index 0..255 → its RGB, frozen, or nil for 0..15 — what {#rgb}
|
|
313
|
+
# answers for an index, precomputed so it allocates nothing.
|
|
314
|
+
# @return [Array<Array<Integer>, nil>]
|
|
315
|
+
PALETTE_RGB = Array.new(256) do |index|
|
|
316
|
+
if index < 16 then nil
|
|
317
|
+
elsif index >= 232 then ([8 + (10 * (index - 232))] * 3).freeze
|
|
318
|
+
else
|
|
319
|
+
cube = index - 16
|
|
320
|
+
[CUBE_LEVELS[cube / 36], CUBE_LEVELS[(cube / 6) % 6], CUBE_LEVELS[cube % 6]].freeze
|
|
321
|
+
end
|
|
322
|
+
end.freeze
|
|
323
|
+
private_constant :PALETTE_RGB
|
|
324
|
+
|
|
296
325
|
# xterm's default RGB for each of the 16 named colors, in {COLOR_SYMBOLS}
|
|
297
326
|
# order — what `:ansi16` quantization matches against. A terminal scheme
|
|
298
327
|
# may redefine these; see {#quantize}.
|