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
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tuile
|
|
4
|
+
class Component
|
|
5
|
+
# A one-column scrollbar the user can drag, and press the track of to page.
|
|
6
|
+
# It is chrome a container owns: you hand it a column, keep it told how
|
|
7
|
+
# tall the content is and how far it has scrolled, and listen for the row
|
|
8
|
+
# it asks for:
|
|
9
|
+
#
|
|
10
|
+
# @scrollbar = VerticalScrollBar.new(row_count: @content_rows)
|
|
11
|
+
# @scrollbar.on_scroll_request { self.scroll_top_row = _1.scroll_top_row }
|
|
12
|
+
# add_child(@scrollbar)
|
|
13
|
+
# # …and from wherever the geometry settles:
|
|
14
|
+
# @scrollbar.rect = Rect.new(rect.width - 1, 0, 1, rect.height)
|
|
15
|
+
# @scrollbar.scroll_top_row = @scroll_top_row
|
|
16
|
+
#
|
|
17
|
+
# **It never moves itself.** Both gestures compute a row and fire
|
|
18
|
+
# {#on_scroll_request}; nothing changes until the owner assigns
|
|
19
|
+
# {#scroll_top_row=}. So an unwired bar is inert — the honest picture,
|
|
20
|
+
# nothing behind it having scrolled either.
|
|
21
|
+
#
|
|
22
|
+
# **Dragging needs `run_event_loop(capture_mouse: :drag)`**: the `:clicks`
|
|
23
|
+
# default asks the terminal for no motion reports, so
|
|
24
|
+
# {Component#handle_mouse_drag} never fires and the handle sits still.
|
|
25
|
+
# Pressing the track works at every level.
|
|
26
|
+
#
|
|
27
|
+
# Its track is the whole {#rect} — no arrow buttons — so `rect.height` is
|
|
28
|
+
# the viewport height. The glyphs are the app-global {handle_char} /
|
|
29
|
+
# {track_char} pair and the color is `Theme#scrollbar_color`, read at paint
|
|
30
|
+
# time. {Scroller} is the worked example; why a component rather than drag
|
|
31
|
+
# handlers on the owner, and why the handle sits where it does, are
|
|
32
|
+
# `D_draggable_scrollbar`.
|
|
33
|
+
class VerticalScrollBar < Component
|
|
34
|
+
class << self
|
|
35
|
+
# The glyph drawn where the handle covers a row, `█` by default. Set
|
|
36
|
+
# the pair at startup for a lazygit-style bar:
|
|
37
|
+
#
|
|
38
|
+
# Tuile::Component::VerticalScrollBar.handle_char = "▐"
|
|
39
|
+
# Tuile::Component::VerticalScrollBar.track_char = "│"
|
|
40
|
+
#
|
|
41
|
+
# Process-global, and assigning invalidates nothing — a change after
|
|
42
|
+
# the first paint shows up only where something repaints anyway. Why
|
|
43
|
+
# app-global rather than per instance is `D_scrollbar_ink`.
|
|
44
|
+
# @return [String]
|
|
45
|
+
attr_reader :handle_char
|
|
46
|
+
|
|
47
|
+
# The glyph drawn on the rows the handle doesn't cover, `░` by
|
|
48
|
+
# default — and on every row when nothing scrolls ({#repaint}).
|
|
49
|
+
# @return [String]
|
|
50
|
+
attr_reader :track_char
|
|
51
|
+
|
|
52
|
+
# @param char [String]
|
|
53
|
+
# @return [String]
|
|
54
|
+
# @raise [TypeError] when `char` is not a String.
|
|
55
|
+
# @raise [ArgumentError] when `char` is not exactly one grapheme
|
|
56
|
+
# cluster one column wide.
|
|
57
|
+
def handle_char=(char)
|
|
58
|
+
@handle_char = StyledString.validate_glyph(char, :handle_char)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param char [String] see {handle_char=}.
|
|
62
|
+
# @return [String]
|
|
63
|
+
def track_char=(char)
|
|
64
|
+
@track_char = StyledString.validate_glyph(char, :track_char)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
self.handle_char = "█"
|
|
69
|
+
self.track_char = "░"
|
|
70
|
+
|
|
71
|
+
# What {#on_scroll_request} fires.
|
|
72
|
+
#
|
|
73
|
+
# @!attribute [r] source
|
|
74
|
+
# @return [VerticalScrollBar] the bar that was dragged or pressed.
|
|
75
|
+
# @!attribute [r] scroll_top_row
|
|
76
|
+
# @return [Integer] the content row the user is asking to see at the
|
|
77
|
+
# top, clamped so it never asks past the last scrollable row.
|
|
78
|
+
ScrollRequestEvent = Data.define(:source, :scroll_top_row) { include Tuile::Event }
|
|
79
|
+
|
|
80
|
+
# @!method on_scroll_request
|
|
81
|
+
# Fired with a {ScrollRequestEvent} when a drag or a track press asks
|
|
82
|
+
# for a row other than the current one — never from {#scroll_top_row=},
|
|
83
|
+
# so an owner assigning the row it was just handed loops nothing.
|
|
84
|
+
#
|
|
85
|
+
# **Empty means a bar that does not move**: it reports the request and
|
|
86
|
+
# waits to be told, so nothing at all happens until somebody listens.
|
|
87
|
+
# @return [Listeners]
|
|
88
|
+
listener :on_scroll_request
|
|
89
|
+
|
|
90
|
+
# @param row_count [Integer] see {#row_count=}.
|
|
91
|
+
# @param scroll_top_row [Integer] see {#scroll_top_row=}.
|
|
92
|
+
def initialize(row_count: 0, scroll_top_row: 0)
|
|
93
|
+
super()
|
|
94
|
+
@row_count = validate_rows(row_count, :row_count)
|
|
95
|
+
@scroll_top_row = validate_rows(scroll_top_row, :scroll_top_row)
|
|
96
|
+
@drag_origin = nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @return [Integer] how many rows of content the bar stands for.
|
|
100
|
+
attr_reader :row_count
|
|
101
|
+
|
|
102
|
+
# @return [Integer] the content row currently at the top of the viewport.
|
|
103
|
+
attr_reader :scroll_top_row
|
|
104
|
+
|
|
105
|
+
# @param count [Integer] `>= 0`.
|
|
106
|
+
# @raise [ArgumentError] unless a non-negative Integer.
|
|
107
|
+
# @return [void]
|
|
108
|
+
def row_count=(count)
|
|
109
|
+
count = validate_rows(count, :row_count)
|
|
110
|
+
return if @row_count == count
|
|
111
|
+
|
|
112
|
+
@row_count = count
|
|
113
|
+
invalidate
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Not clamped against {#row_count} — the owner's scroll state is
|
|
117
|
+
# authoritative, and a bar that silently disagreed with it would paint a
|
|
118
|
+
# handle where the content is not.
|
|
119
|
+
# @param row [Integer] `>= 0`.
|
|
120
|
+
# @raise [ArgumentError] unless a non-negative Integer.
|
|
121
|
+
# @return [void]
|
|
122
|
+
def scroll_top_row=(row)
|
|
123
|
+
row = validate_rows(row, :scroll_top_row)
|
|
124
|
+
return if @scroll_top_row == row
|
|
125
|
+
|
|
126
|
+
@scroll_top_row = row
|
|
127
|
+
invalidate
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @return [Boolean] whether there is content out of view — `false` also
|
|
131
|
+
# for an empty rect, and what {#repaint} asks before drawing a handle.
|
|
132
|
+
def scrollable? = rect.height >= 1 && @row_count > rect.height
|
|
133
|
+
|
|
134
|
+
# Rows the handle covers, capped at one below the track so there is
|
|
135
|
+
# always somewhere to drag to: a 10-row viewport onto 11 rows of content
|
|
136
|
+
# gets a 9-row handle, not an immovable 10-row one.
|
|
137
|
+
# @return [Integer] `0` for an empty rect, the whole track when the
|
|
138
|
+
# content fits (nothing is *painted* then — see {#repaint}).
|
|
139
|
+
def handle_height
|
|
140
|
+
return rect.height unless scrollable?
|
|
141
|
+
|
|
142
|
+
(rect.height**2.0 / @row_count).ceil.clamp(1, [rect.height - 1, 1].max)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The handle's first row, mapped over the *free* track so that the last
|
|
146
|
+
# scrollable row puts the handle's last row at the bottom of the track.
|
|
147
|
+
# @return [Integer] 0-based row within the track; `0` when the content
|
|
148
|
+
# fits.
|
|
149
|
+
def handle_start
|
|
150
|
+
free = free_rows
|
|
151
|
+
return 0 if free.zero?
|
|
152
|
+
|
|
153
|
+
(free * @scroll_top_row.clamp(0, max_top) / max_top.to_f).round
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# One column wide, however wide a rect it was handed — which is also what
|
|
157
|
+
# the router hit-tests, so a press on the dead columns beside the bar
|
|
158
|
+
# reaches whatever is behind it instead.
|
|
159
|
+
# @return [Size]
|
|
160
|
+
def extent = Size.new([1, rect.width].min, rect.height)
|
|
161
|
+
|
|
162
|
+
# Claims every left press: on the handle it starts a drag, on the track
|
|
163
|
+
# it pages towards the pointer.
|
|
164
|
+
# @param event [Mouse::DownEvent]
|
|
165
|
+
# @return [Boolean]
|
|
166
|
+
def handle_mouse_down?(event)
|
|
167
|
+
return false unless event.button == :left
|
|
168
|
+
|
|
169
|
+
@drag_origin = nil
|
|
170
|
+
if scrollable? && event.y >= handle_start && event.y < handle_start + handle_height
|
|
171
|
+
@drag_origin = [event.y, @scroll_top_row]
|
|
172
|
+
else
|
|
173
|
+
request(@scroll_top_row + (event.y < handle_start ? -page_rows : page_rows))
|
|
174
|
+
end
|
|
175
|
+
true
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Scrolls by how far the pointer has moved *since the press*, not by
|
|
179
|
+
# where it is. Absolute positioning would jump on the first report: the
|
|
180
|
+
# track has a row per several rows of content, so inverting the handle's
|
|
181
|
+
# rounded position does not give back the row it came from.
|
|
182
|
+
# @param event [Mouse::DragEvent]
|
|
183
|
+
# @return [void]
|
|
184
|
+
def handle_mouse_drag(event)
|
|
185
|
+
super
|
|
186
|
+
return if @drag_origin.nil? || free_rows.zero?
|
|
187
|
+
|
|
188
|
+
y_at_press, top_at_press = @drag_origin
|
|
189
|
+
request(top_at_press + ((event.y - y_at_press) * max_top / free_rows.to_f).round)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# @param event [Mouse::UpEvent]
|
|
193
|
+
# @return [void]
|
|
194
|
+
def handle_mouse_up(event)
|
|
195
|
+
super
|
|
196
|
+
@drag_origin = nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Paints {track_char} down the column with {handle_char} over it — and
|
|
200
|
+
# bare track when {#scrollable?} is false, a solid handle filling the
|
|
201
|
+
# track carrying no information (`D_scrollbar_ink`).
|
|
202
|
+
# @param canvas [Canvas] see {Component#repaint}.
|
|
203
|
+
# @return [void]
|
|
204
|
+
def repaint(canvas)
|
|
205
|
+
super
|
|
206
|
+
return if rect.empty?
|
|
207
|
+
|
|
208
|
+
style = StyledString::Style.new(fg: screen.theme.scrollbar_color)
|
|
209
|
+
handle = (handle_start...handle_start + handle_height) if scrollable?
|
|
210
|
+
rect.height.times do |row|
|
|
211
|
+
# Named, not `self.class` — a class-level ivar is not inherited, so a
|
|
212
|
+
# subclassed bar would read nil off its own class.
|
|
213
|
+
glyph = handle&.cover?(row) ? VerticalScrollBar.handle_char : VerticalScrollBar.track_char
|
|
214
|
+
canvas.set_char(0, row, glyph, style)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# @return [Array<String>]
|
|
219
|
+
def inspect_details = super + ["#{scroll_top_row}/#{row_count} rows"]
|
|
220
|
+
|
|
221
|
+
private
|
|
222
|
+
|
|
223
|
+
# @return [Integer] the largest {#scroll_top_row} that still fills the
|
|
224
|
+
# viewport; `0` when the content fits.
|
|
225
|
+
def max_top = [@row_count - rect.height, 0].max
|
|
226
|
+
|
|
227
|
+
# @return [Integer] track rows the handle can travel over; `0` means no
|
|
228
|
+
# drag is possible.
|
|
229
|
+
def free_rows = scrollable? ? rect.height - handle_height : 0
|
|
230
|
+
|
|
231
|
+
# @return [Integer] a viewport, at least one row.
|
|
232
|
+
def page_rows = [rect.height, 1].max
|
|
233
|
+
|
|
234
|
+
# Fires {#on_scroll_request} for `row`, clamped; silent on a no-op.
|
|
235
|
+
# @param row [Integer]
|
|
236
|
+
# @return [void]
|
|
237
|
+
def request(row)
|
|
238
|
+
row = row.clamp(0, max_top)
|
|
239
|
+
return if row == @scroll_top_row
|
|
240
|
+
|
|
241
|
+
on_scroll_request.fire(ScrollRequestEvent.new(source: self, scroll_top_row: row))
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# @param rows [Integer]
|
|
245
|
+
# @param name [Symbol] the accessor, for the message.
|
|
246
|
+
# @return [Integer] `rows`.
|
|
247
|
+
# @raise [ArgumentError] unless `rows` is a non-negative Integer.
|
|
248
|
+
def validate_rows(rows, name)
|
|
249
|
+
unless rows.is_a?(Integer) && !rows.negative?
|
|
250
|
+
raise ArgumentError, "#{name} expects a non-negative Integer, got #{rows.inspect}"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
rows
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -66,17 +66,10 @@ module Tuile
|
|
|
66
66
|
# @return [void]
|
|
67
67
|
def footer=(new_footer)
|
|
68
68
|
@footer_slot.content = new_footer
|
|
69
|
-
|
|
69
|
+
invalidate_layout
|
|
70
70
|
invalidate # repaint border row that the footer covers/uncovers
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
# @param new_rect [Rect]
|
|
74
|
-
# @return [void]
|
|
75
|
-
def rect=(new_rect)
|
|
76
|
-
super
|
|
77
|
-
layout_footer
|
|
78
|
-
end
|
|
79
|
-
|
|
80
73
|
# @param value [Boolean]
|
|
81
74
|
# @return [void]
|
|
82
75
|
def scrollbar=(value)
|
|
@@ -88,7 +81,7 @@ module Tuile
|
|
|
88
81
|
content.scrollbar_visibility = value ? :visible : :gone
|
|
89
82
|
@border_right = value ? 0 : 1
|
|
90
83
|
invalidate
|
|
91
|
-
|
|
84
|
+
invalidate_layout
|
|
92
85
|
end
|
|
93
86
|
|
|
94
87
|
# Fully repaints the window: the border ring here, the interior through
|
|
@@ -103,57 +96,65 @@ module Tuile
|
|
|
103
96
|
# (`D_component_contract`). The ring is this window's own paint and the
|
|
104
97
|
# interior is the content's, so the only cell nobody covers is an
|
|
105
98
|
# interior with no content in it — cleared here, exactly.
|
|
99
|
+
# @param canvas [Canvas] see {Component#repaint}.
|
|
106
100
|
# @return [void]
|
|
107
|
-
def repaint
|
|
101
|
+
def repaint(canvas)
|
|
108
102
|
return if rect.empty?
|
|
109
103
|
|
|
110
|
-
|
|
104
|
+
inner = content_rect
|
|
105
|
+
canvas.fill(inner) if content.nil? && !inner.empty?
|
|
111
106
|
invalidate_children
|
|
112
|
-
repaint_border
|
|
107
|
+
repaint_border(canvas)
|
|
113
108
|
end
|
|
114
109
|
|
|
115
110
|
protected
|
|
116
111
|
|
|
117
|
-
#
|
|
112
|
+
# The content takes the interior, the footer slot the bottom border row.
|
|
118
113
|
# @return [void]
|
|
119
|
-
def
|
|
114
|
+
def relayout
|
|
115
|
+
content&.rect = content_rect
|
|
116
|
+
layout_footer
|
|
117
|
+
end
|
|
120
118
|
|
|
121
119
|
# The interior the content fills: inside the border on three sides, and on
|
|
122
120
|
# the fourth only while there is a right border — {#scrollbar=} drops it so
|
|
123
121
|
# the content's own bar takes that column.
|
|
122
|
+
#
|
|
123
|
+
# One rect for both jobs, since a window paints and places its children in
|
|
124
|
+
# the same coordinates: it is what {#relayout} assigns the content, and what
|
|
125
|
+
# {#repaint} blanks when there is no content to cover it.
|
|
124
126
|
# @return [Rect] may be {Rect#empty? empty}, for a window too small to have
|
|
125
127
|
# an inside.
|
|
126
128
|
def content_rect
|
|
127
|
-
Rect.new(
|
|
129
|
+
Rect.new(1, 1, rect.width - 1 - @border_right, rect.height - 2)
|
|
128
130
|
end
|
|
129
131
|
|
|
130
|
-
# Paints the window border via {
|
|
131
|
-
# so the border cells inherit {
|
|
132
|
+
# Paints the window border via {Canvas#set_text} / {Canvas#set_char},
|
|
133
|
+
# so the border cells inherit {ComponentBackground#effective} — a
|
|
132
134
|
# {Component#bg_color} on the window tints border and content alike. Both
|
|
133
135
|
# border rows are clipped by *display* width, so no caption overflows the
|
|
134
136
|
# box; when the window is active the whole border — the caption's own
|
|
135
137
|
# colors included — is drawn in {Theme#active_border_color}.
|
|
138
|
+
# @param canvas [Canvas] the surface to paint onto.
|
|
136
139
|
# @return [void]
|
|
137
|
-
def repaint_border
|
|
140
|
+
def repaint_border(canvas)
|
|
138
141
|
return if rect.empty?
|
|
139
142
|
|
|
140
143
|
w = rect.width
|
|
141
144
|
h = rect.height
|
|
142
|
-
top = rect.top
|
|
143
|
-
left = rect.left
|
|
144
145
|
inner_w = [w - 2, 0].max
|
|
145
146
|
|
|
146
147
|
fg = active? ? screen.theme.active_border_color : nil
|
|
147
148
|
bar = StyledString::Style.new(fg: fg)
|
|
148
|
-
|
|
149
|
-
(1..(h - 2)).each do |
|
|
150
|
-
|
|
149
|
+
canvas.set_text(0, 0, top_border(inner_w, fg).slice(0, w))
|
|
150
|
+
(1..(h - 2)).each do |row|
|
|
151
|
+
canvas.set_char(0, row, "│", bar)
|
|
151
152
|
# Skipped once {#scrollbar=} has given that column to the content: the
|
|
152
153
|
# bar would paint over the border anyway, and painting it first only
|
|
153
154
|
# dirties the column into every frame's diff (`D_component_contract`).
|
|
154
|
-
|
|
155
|
+
canvas.set_char(w - 1, row, "│", bar) if @border_right.positive?
|
|
155
156
|
end
|
|
156
|
-
|
|
157
|
+
canvas.set_text(0, h - 1, bottom_border(inner_w, fg).slice(0, w)) if h >= 2
|
|
157
158
|
end
|
|
158
159
|
|
|
159
160
|
# Builds the top border row: corners, {#caption} embedded at its own
|
|
@@ -204,7 +205,7 @@ module Tuile
|
|
|
204
205
|
end
|
|
205
206
|
|
|
206
207
|
width = [rect.width - 2, 0].max
|
|
207
|
-
@footer_slot.rect = Rect.new(
|
|
208
|
+
@footer_slot.rect = Rect.new(1, rect.height - 1, width, 1)
|
|
208
209
|
end
|
|
209
210
|
end
|
|
210
211
|
end
|