tuile 0.16.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 +108 -0
- data/README.md +21 -12
- data/book/02-repaint.md +47 -19
- data/book/03-layout.md +98 -49
- data/book/04-event-loop.md +5 -4
- data/book/05-focus.md +12 -9
- data/book/06-theming.md +55 -17
- data/book/07-components.md +188 -40
- data/book/08-testing.md +115 -15
- data/book/10-locale.md +1 -1
- data/book/README.md +5 -5
- data/examples/file_commander.rb +38 -27
- data/examples/hello_world.rb +1 -1
- data/examples/sampler.rb +225 -169
- 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 +81 -80
- data/lib/tuile/component/abstract_wrapping_field.rb +59 -54
- data/lib/tuile/component/big_decimal_field.rb +7 -6
- data/lib/tuile/component/button.rb +19 -11
- data/lib/tuile/component/checkbox.rb +12 -10
- data/lib/tuile/component/checkbox_group.rb +11 -13
- data/lib/tuile/component/combo_box.rb +30 -40
- data/lib/tuile/component/confirm_window.rb +27 -22
- data/lib/tuile/component/date_field.rb +27 -20
- data/lib/tuile/component/date_time_field.rb +75 -31
- 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 +98 -27
- data/lib/tuile/component/has_caption.rb +14 -5
- data/lib/tuile/component/has_content.rb +5 -12
- data/lib/tuile/component/has_validation.rb +39 -13
- data/lib/tuile/component/has_value.rb +70 -16
- 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 -63
- data/lib/tuile/component/layout.rb +124 -10
- data/lib/tuile/component/list.rb +197 -94
- data/lib/tuile/component/list_dropdown.rb +148 -88
- data/lib/tuile/component/menu_bar/cascade.rb +97 -27
- data/lib/tuile/component/menu_bar.rb +84 -64
- data/lib/tuile/component/notification.rb +44 -31
- data/lib/tuile/component/overlay.rb +210 -52
- data/lib/tuile/component/password_field.rb +1 -8
- data/lib/tuile/component/picker_window.rb +15 -10
- data/lib/tuile/component/popup.rb +13 -24
- data/lib/tuile/component/progress_bar.rb +7 -7
- data/lib/tuile/component/radio_group.rb +10 -12
- data/lib/tuile/component/scroller.rb +266 -0
- data/lib/tuile/component/select.rb +15 -31
- data/lib/tuile/component/slot.rb +1 -2
- data/lib/tuile/component/tab_sheet.rb +21 -28
- data/lib/tuile/component/tabs.rb +39 -24
- data/lib/tuile/component/text_area/wrapped_text.rb +1 -1
- data/lib/tuile/component/text_area.rb +21 -19
- data/lib/tuile/component/text_field.rb +55 -39
- data/lib/tuile/component/text_view.rb +143 -79
- data/lib/tuile/component/time_field.rb +26 -21
- data/lib/tuile/component/vertical_scroll_bar.rb +257 -0
- data/lib/tuile/component/window.rb +27 -26
- data/lib/tuile/component.rb +481 -259
- 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 +14 -0
- data/lib/tuile/fake_screen.rb +41 -10
- 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 +51 -35
- data/lib/tuile/mouse.rb +96 -29
- data/lib/tuile/point.rb +6 -0
- data/lib/tuile/rect.rb +33 -0
- data/lib/tuile/screen.rb +419 -84
- data/lib/tuile/screen_pane.rb +144 -31
- data/lib/tuile/strict_layout.rb +127 -0
- data/lib/tuile/styled_string.rb +139 -9
- data/lib/tuile/testing/gestures.rb +35 -0
- data/lib/tuile/testing.rb +310 -36
- data/lib/tuile/theme.rb +170 -19
- 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 +4951 -1158
- metadata +16 -2
- data/lib/tuile/vertical_scroll_bar.rb +0 -122
|
@@ -22,7 +22,16 @@ module Tuile
|
|
|
22
22
|
# the highlight, Enter or Space activates a row or opens its submenu, RIGHT
|
|
23
23
|
# opens a submenu, LEFT returns to the previous menu, ESC closes one level.
|
|
24
24
|
# LEFT at the first level and RIGHT on a row with no submenu step to the
|
|
25
|
-
# sibling menu, as they do in every menu bar.
|
|
25
|
+
# sibling menu, as they do in every menu bar. A menu stepped to that way is
|
|
26
|
+
# only *shown*: it opens with no row highlighted, so RIGHT steps on again,
|
|
27
|
+
# and Down, Enter or Space moves onto its first row, Up onto its last. Book
|
|
28
|
+
# ch7 has the table.
|
|
29
|
+
#
|
|
30
|
+
# The walk is governed by *menu mode*, not by whether a panel is up: a
|
|
31
|
+
# top-level item with no menu shows nothing when you step onto it, and the
|
|
32
|
+
# step after it still opens the next menu. ESC leaves the mode there — the
|
|
33
|
+
# one place the strip claims ESC, an unclaimed one being what stops the loop
|
|
34
|
+
# (`D_quit_key`). Enter on such an item fires it and enters no mode.
|
|
26
35
|
#
|
|
27
36
|
# == Mnemonics
|
|
28
37
|
# An item given a `mnemonic:` answers to that letter, underlined in its
|
|
@@ -93,6 +102,14 @@ module Tuile
|
|
|
93
102
|
#
|
|
94
103
|
# Apps don't construct items; {MenuBar#add_item} and {#add_item} do.
|
|
95
104
|
class Item
|
|
105
|
+
extend Listeners::Declare
|
|
106
|
+
|
|
107
|
+
# What {#on_click} fires.
|
|
108
|
+
#
|
|
109
|
+
# @!attribute [r] source
|
|
110
|
+
# @return [Item] the item that was activated.
|
|
111
|
+
ClickEvent = Data.define(:source) { include Tuile::Event }
|
|
112
|
+
|
|
96
113
|
# @param caption [StyledString] already coerced by the caller.
|
|
97
114
|
# @param mnemonic [String, nil] already validated by the caller, in the
|
|
98
115
|
# case it was given in.
|
|
@@ -101,7 +118,7 @@ module Tuile
|
|
|
101
118
|
@caption = caption
|
|
102
119
|
@mnemonic = mnemonic&.downcase
|
|
103
120
|
@cued_caption = build_cued_caption(caption, mnemonic)
|
|
104
|
-
|
|
121
|
+
self.on_click << on_click if on_click
|
|
105
122
|
@items = []
|
|
106
123
|
end
|
|
107
124
|
|
|
@@ -126,10 +143,12 @@ module Tuile
|
|
|
126
143
|
# convention, like {Component#children} — grow it through {#add_item}.
|
|
127
144
|
attr_reader :items
|
|
128
145
|
|
|
129
|
-
#
|
|
130
|
-
# activated (Enter, Space or
|
|
131
|
-
# {Button#on_click}. Never fired on an item
|
|
132
|
-
|
|
146
|
+
# @!method on_click
|
|
147
|
+
# Fired with a {ClickEvent} when the item is activated (Enter, Space or
|
|
148
|
+
# a left click), exactly as {Button#on_click}. Never fired on an item
|
|
149
|
+
# with children.
|
|
150
|
+
# @return [Listeners]
|
|
151
|
+
listener :on_click
|
|
133
152
|
|
|
134
153
|
# @return [Boolean] whether this item opens a submenu, i.e. has children.
|
|
135
154
|
def submenu? = !@items.empty?
|
|
@@ -140,7 +159,7 @@ module Tuile
|
|
|
140
159
|
# @param mnemonic [String, nil] the letter that activates this child
|
|
141
160
|
# while *this* item's children are the live level; see
|
|
142
161
|
# {MenuBar#add_item}.
|
|
143
|
-
# @yield optional `on_click`
|
|
162
|
+
# @yield optional `on_click` listener; same as registering one on {#on_click}.
|
|
144
163
|
# @raise [ArgumentError] see {MenuBar#add_item}.
|
|
145
164
|
# @return [Item]
|
|
146
165
|
def add_item(caption = nil, mnemonic: nil, &on_click)
|
|
@@ -251,14 +270,14 @@ module Tuile
|
|
|
251
270
|
Size.new([painted_width - @left_column, rect.width].min, 1)
|
|
252
271
|
end
|
|
253
272
|
|
|
254
|
-
#
|
|
273
|
+
# Closes the cascade — only on a *changed* rect, which is all this hook
|
|
274
|
+
# hears: a layout re-assigning the same rect (which {Layout::Box} does on
|
|
275
|
+
# any child mutation) must not.
|
|
276
|
+
# @param old_rect [Rect]
|
|
255
277
|
# @return [void]
|
|
256
|
-
def
|
|
257
|
-
# Only a *changed* rect closes the menu: a layout re-assigning the same
|
|
258
|
-
# rect (which {Layout::Box} does on any child mutation) must not.
|
|
259
|
-
changed = rect != new_rect
|
|
278
|
+
def handle_rect_changed(old_rect)
|
|
260
279
|
super
|
|
261
|
-
@cascade.close
|
|
280
|
+
@cascade.close
|
|
262
281
|
end
|
|
263
282
|
|
|
264
283
|
# Closes the cascade when the strip leaves the focus chain, so tabbing (or
|
|
@@ -281,11 +300,11 @@ module Tuile
|
|
|
281
300
|
end
|
|
282
301
|
|
|
283
302
|
# Offers the key to the open cascade first, then to the strip's own
|
|
284
|
-
# LEFT/RIGHT/Enter/Space/Down.
|
|
303
|
+
# LEFT/RIGHT/Enter/Space/Down/ESC.
|
|
285
304
|
#
|
|
286
305
|
# With a cascade open, the only keys reaching the strip are the two the
|
|
287
|
-
# cascade declines — LEFT at the first level, RIGHT
|
|
288
|
-
#
|
|
306
|
+
# cascade declines — LEFT at the first level, RIGHT with no submenu under
|
|
307
|
+
# the highlight — and both step to the sibling menu.
|
|
289
308
|
# @param key [String]
|
|
290
309
|
# @return [Boolean]
|
|
291
310
|
def handle_key?(key)
|
|
@@ -294,19 +313,12 @@ module Tuile
|
|
|
294
313
|
return true if handle_mnemonic?(key)
|
|
295
314
|
return true if @cascade.handle_key?(key)
|
|
296
315
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
else
|
|
304
|
-
case key
|
|
305
|
-
when Keys::LEFT_ARROW then move_highlight(-1)
|
|
306
|
-
when Keys::RIGHT_ARROW then move_highlight(1)
|
|
307
|
-
when Keys::ENTER, " ", Keys::DOWN_ARROW then open_highlighted
|
|
308
|
-
else false
|
|
309
|
-
end
|
|
316
|
+
case key
|
|
317
|
+
when Keys::LEFT_ARROW then @cascade.browsing? ? step_menu(-1) : move_highlight(-1)
|
|
318
|
+
when Keys::RIGHT_ARROW then @cascade.browsing? ? step_menu(1) : move_highlight(1)
|
|
319
|
+
when Keys::ENTER, " ", Keys::DOWN_ARROW then open_highlighted
|
|
320
|
+
when Keys::ESC then leave_menus
|
|
321
|
+
else false
|
|
310
322
|
end
|
|
311
323
|
end
|
|
312
324
|
|
|
@@ -330,14 +342,15 @@ module Tuile
|
|
|
330
342
|
true
|
|
331
343
|
end
|
|
332
344
|
|
|
345
|
+
# @param canvas [Canvas] see {Component#repaint}.
|
|
333
346
|
# @return [void]
|
|
334
|
-
def repaint
|
|
347
|
+
def repaint(canvas)
|
|
335
348
|
super
|
|
336
349
|
return if rect.empty?
|
|
337
350
|
|
|
338
351
|
row = strip_row.slice(@left_column, rect.width)
|
|
339
|
-
|
|
340
|
-
draw_cues(row)
|
|
352
|
+
canvas.set_text(0, 0, row)
|
|
353
|
+
draw_cues(canvas, row)
|
|
341
354
|
end
|
|
342
355
|
|
|
343
356
|
private
|
|
@@ -368,11 +381,10 @@ module Tuile
|
|
|
368
381
|
invalidate
|
|
369
382
|
end
|
|
370
383
|
|
|
371
|
-
# The
|
|
372
|
-
#
|
|
373
|
-
# and {#rect=} closes the cascade rather than re-anchoring it.
|
|
384
|
+
# The offset follows the width; {Component#rect=} invalidates for us,
|
|
385
|
+
# and {#handle_rect_changed} closes the cascade rather than re-anchoring it.
|
|
374
386
|
# @return [void]
|
|
375
|
-
def
|
|
387
|
+
def relayout
|
|
376
388
|
super
|
|
377
389
|
adjust_left_column
|
|
378
390
|
end
|
|
@@ -427,23 +439,25 @@ module Tuile
|
|
|
427
439
|
# make the window width a function of the offset computed from it. Painted
|
|
428
440
|
# focused or not: overflow is a fact about the captions and the rect, not
|
|
429
441
|
# about focus.
|
|
442
|
+
# @param canvas [Canvas] the surface to paint onto.
|
|
430
443
|
# @param row [StyledString] the windowed row, as painted.
|
|
431
444
|
# @return [void]
|
|
432
|
-
def draw_cues(row)
|
|
433
|
-
draw_cue(row, 0, "<") if @left_column.positive?
|
|
434
|
-
draw_cue(row, rect.width - 1, ">") if @left_column + rect.width < painted_width
|
|
445
|
+
def draw_cues(canvas, row)
|
|
446
|
+
draw_cue(canvas, row, 0, "<") if @left_column.positive?
|
|
447
|
+
draw_cue(canvas, row, rect.width - 1, ">") if @left_column + rect.width < painted_width
|
|
435
448
|
end
|
|
436
449
|
|
|
437
450
|
# The cue keeps the style of the cell it covers, so one landing on the
|
|
438
451
|
# highlighted segment doesn't punch a default-background hole in its
|
|
439
452
|
# highlight.
|
|
453
|
+
# @param canvas [Canvas] the surface to paint onto.
|
|
440
454
|
# @param row [StyledString] the windowed row.
|
|
441
455
|
# @param column [Integer] relative to {#rect}`.left`.
|
|
442
456
|
# @param glyph [String]
|
|
443
457
|
# @return [void]
|
|
444
|
-
def draw_cue(row, column, glyph)
|
|
458
|
+
def draw_cue(canvas, row, column, glyph)
|
|
445
459
|
style = row.slice(column, 1).spans.first&.style || StyledString::Style::DEFAULT
|
|
446
|
-
|
|
460
|
+
canvas.set_char(column, 0, glyph, style)
|
|
447
461
|
end
|
|
448
462
|
|
|
449
463
|
# One `[item, start_column, width]` triple per top-level item, in strip
|
|
@@ -470,17 +484,19 @@ module Tuile
|
|
|
470
484
|
# @return [Integer, nil] the index of the item painted at `point`; `nil`
|
|
471
485
|
# for the blank tail or a row the strip doesn't paint.
|
|
472
486
|
def index_at(point)
|
|
473
|
-
return nil unless
|
|
487
|
+
return nil unless local_extent_rect.contains?(point)
|
|
474
488
|
|
|
475
|
-
column = point.x
|
|
489
|
+
column = point.x + @left_column
|
|
476
490
|
segments.index { |_item, start, width| column >= start && column < start + width }
|
|
477
491
|
end
|
|
478
492
|
|
|
493
|
+
# The cascade hangs off {ScreenPane}, so it shares no offset with this
|
|
494
|
+
# strip and has to be given screen coordinates (`D_relative_rect`).
|
|
479
495
|
# @param index [Integer]
|
|
480
496
|
# @return [Rect] the segment's cells on screen — the cascade's anchor.
|
|
481
497
|
def segment_rect(index)
|
|
482
498
|
_item, start, width = segments[index]
|
|
483
|
-
Rect.new(
|
|
499
|
+
Rect.new(0, 0, width, 1).at(to_screen(Point.new(start - @left_column, 0)))
|
|
484
500
|
end
|
|
485
501
|
|
|
486
502
|
# @return [StyledString] the whole strip as one row, unclipped. {#repaint}
|
|
@@ -536,21 +552,33 @@ module Tuile
|
|
|
536
552
|
true
|
|
537
553
|
end
|
|
538
554
|
|
|
539
|
-
# Steps to the
|
|
540
|
-
#
|
|
541
|
-
#
|
|
542
|
-
#
|
|
543
|
-
# in.
|
|
555
|
+
# Steps to the neighbour and shows *its* menu; a top-level button shows
|
|
556
|
+
# nothing and keeps menu mode. The cascade is left alone when the
|
|
557
|
+
# highlight is already at an end: reopening the same menu would throw away
|
|
558
|
+
# the submenu the user is standing in.
|
|
544
559
|
#
|
|
545
560
|
# It deliberately never *activates*. An item arrowed past is highlighted,
|
|
546
561
|
# not pressed, so a top-level button waits for Enter or Space — otherwise
|
|
547
562
|
# walking the strip would fire every button on it.
|
|
548
563
|
# @param delta [Integer] `+1` / `-1`.
|
|
549
|
-
# @return [Boolean] always `true`:
|
|
564
|
+
# @return [Boolean] always `true`: menu mode swallows the key either way.
|
|
550
565
|
def step_menu(delta)
|
|
551
566
|
was = @highlighted_index
|
|
552
567
|
move_highlight(delta)
|
|
553
|
-
|
|
568
|
+
return true if @highlighted_index == was
|
|
569
|
+
|
|
570
|
+
@cascade.step_to(segment_rect(@highlighted_index), items[@highlighted_index])
|
|
571
|
+
true
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# Leaves menu mode — the ESC that gets you out where stepping has landed
|
|
575
|
+
# on a top-level item with no menu, so there is no panel left to close.
|
|
576
|
+
# Declined otherwise, so ESC keeps its app-level meaning (`D_quit_key`).
|
|
577
|
+
# @return [Boolean]
|
|
578
|
+
def leave_menus
|
|
579
|
+
return false unless @cascade.browsing?
|
|
580
|
+
|
|
581
|
+
@cascade.close
|
|
554
582
|
true
|
|
555
583
|
end
|
|
556
584
|
|
|
@@ -562,20 +590,12 @@ module Tuile
|
|
|
562
590
|
return false if items.empty?
|
|
563
591
|
|
|
564
592
|
item = items[@highlighted_index]
|
|
565
|
-
show_highlighted_menu
|
|
566
|
-
# Fired after the close above, exactly as {Cascade} activates a leaf: an
|
|
567
|
-
# action that opens a dialog must not paint it under a menu.
|
|
568
|
-
item.on_click&.call unless item.submenu?
|
|
569
|
-
true
|
|
570
|
-
end
|
|
571
|
-
|
|
572
|
-
# Shows the highlighted item's menu, closing the cascade when it has none.
|
|
573
|
-
# @return [void]
|
|
574
|
-
def show_highlighted_menu
|
|
575
|
-
item = items[@highlighted_index]
|
|
576
|
-
return @cascade.close unless item.submenu?
|
|
577
|
-
|
|
578
593
|
@cascade.open_below(segment_rect(@highlighted_index), item)
|
|
594
|
+
# Fired after the close inside `open_below`, exactly as {Cascade}
|
|
595
|
+
# activates a leaf: an action that opens a dialog must not paint it
|
|
596
|
+
# under a menu.
|
|
597
|
+
item.on_click.fire(Item::ClickEvent.new(source: item)) unless item.submenu?
|
|
598
|
+
true
|
|
579
599
|
end
|
|
580
600
|
end
|
|
581
601
|
end
|
|
@@ -42,8 +42,7 @@ module Tuile
|
|
|
42
42
|
# - **Follow a theme flip.** A `Theme::Ref` `color:` is resolved once, when
|
|
43
43
|
# the message is added — a toast lives seconds, so there is no
|
|
44
44
|
# {Component#handle_theme_changed} rebuild.
|
|
45
|
-
# - **Take a size.**
|
|
46
|
-
# this one's, in {#reposition}.
|
|
45
|
+
# - **Take a size.** The messages decide the box, in {#declared_size_in}.
|
|
47
46
|
class Notification < Overlay
|
|
48
47
|
# Most messages held at once, counting both the painted ones and any
|
|
49
48
|
# waiting for room. Chosen from reading time rather than geometry: the
|
|
@@ -117,8 +116,6 @@ module Tuile
|
|
|
117
116
|
private_class_method :new
|
|
118
117
|
|
|
119
118
|
def initialize
|
|
120
|
-
# Built before `super`, because Overlay#initialize assigns the content
|
|
121
|
-
# and our #reposition override reads every one of these.
|
|
122
119
|
@messages = []
|
|
123
120
|
@high_water = 0
|
|
124
121
|
@ticker = nil
|
|
@@ -152,31 +149,22 @@ module Tuile
|
|
|
152
149
|
|
|
153
150
|
@messages << message
|
|
154
151
|
@high_water = [@high_water, natural_width(message)].max
|
|
155
|
-
|
|
152
|
+
restack
|
|
156
153
|
sync_ticker
|
|
157
154
|
end
|
|
158
155
|
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
# @return [void]
|
|
169
|
-
def reposition
|
|
170
|
-
if @messages.empty?
|
|
171
|
-
self.rect = Rect.new(0, 0, 0, 0)
|
|
172
|
-
return
|
|
173
|
-
end
|
|
156
|
+
# @return [Overlay::TopRight] a notification opens in the corner.
|
|
157
|
+
def default_placement = TopRight[]
|
|
158
|
+
|
|
159
|
+
# The box its messages need: as wide as the widest message held so far
|
|
160
|
+
# and as tall as they wrap to at that width, each capped by the screen.
|
|
161
|
+
# @param screen_size [Size]
|
|
162
|
+
# @return [Size]
|
|
163
|
+
def declared_size_in(screen_size)
|
|
164
|
+
return Size.new(0, 0) if @messages.empty?
|
|
174
165
|
|
|
175
|
-
width = box_width
|
|
176
|
-
|
|
177
|
-
height = [rows.size + 2, cap_height].min
|
|
178
|
-
@view.text = join_rows(rows)
|
|
179
|
-
self.rect = Rect.new([screen.size.width - width, 0].max, 0, width, height)
|
|
166
|
+
width = box_width(screen_size)
|
|
167
|
+
Size.new(width, [wrapped_rows(width).size + 2, cap_height(screen_size)].min)
|
|
180
168
|
end
|
|
181
169
|
|
|
182
170
|
# A left press dismisses the whole box, every message with it. Every other
|
|
@@ -218,14 +206,36 @@ module Tuile
|
|
|
218
206
|
sync_ticker
|
|
219
207
|
end
|
|
220
208
|
|
|
209
|
+
protected
|
|
210
|
+
|
|
211
|
+
# Wraps the messages to the width the pane gave the box — the wrap the
|
|
212
|
+
# height in {#declared_size_in} was measured with.
|
|
213
|
+
# @return [void]
|
|
214
|
+
def relayout
|
|
215
|
+
super
|
|
216
|
+
@view.text = join_rows(wrapped_rows(rect.width)) if rect.width > 2
|
|
217
|
+
end
|
|
218
|
+
|
|
221
219
|
private
|
|
222
220
|
|
|
221
|
+
# A message came or went: the text is re-wrapped here, and the box is
|
|
222
|
+
# re-measured by the pane.
|
|
223
|
+
# @return [void]
|
|
224
|
+
def restack
|
|
225
|
+
invalidate_layout
|
|
226
|
+
reposition
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# @param width [Integer] the box width, border included.
|
|
230
|
+
# @return [Array<StyledString>] every message, wrapped inside the border.
|
|
231
|
+
def wrapped_rows(width) = @messages.flat_map { |message| wrap_message(message, width - 2) }
|
|
232
|
+
|
|
223
233
|
# Retires the oldest message, closing the box when it was the last. Runs on
|
|
224
234
|
# the event-loop thread, from the ticker.
|
|
225
235
|
# @return [void]
|
|
226
236
|
def retire_oldest
|
|
227
237
|
@messages.shift
|
|
228
|
-
@messages.empty? ? close :
|
|
238
|
+
@messages.empty? ? close : restack
|
|
229
239
|
sync_ticker
|
|
230
240
|
end
|
|
231
241
|
|
|
@@ -269,17 +279,20 @@ module Tuile
|
|
|
269
279
|
# is applied here, last. Storing the clamped value instead would let a
|
|
270
280
|
# SIGWINCH that narrows the terminal ratchet the box permanently down to
|
|
271
281
|
# the narrow cap, with nothing to restore it when the terminal widens.
|
|
282
|
+
# @param screen_size [Size]
|
|
272
283
|
# @return [Integer]
|
|
273
|
-
def box_width = [@high_water + 2, cap_width].min
|
|
284
|
+
def box_width(screen_size) = [@high_water + 2, cap_width(screen_size)].min
|
|
274
285
|
|
|
286
|
+
# @param screen_size [Size]
|
|
275
287
|
# @return [Integer]
|
|
276
|
-
def cap_width
|
|
277
|
-
[[(
|
|
288
|
+
def cap_width(screen_size)
|
|
289
|
+
[[(screen_size.width * WIDTH_FRACTION).to_i, MIN_CAP_WIDTH].max, screen_size.width].min
|
|
278
290
|
end
|
|
279
291
|
|
|
292
|
+
# @param screen_size [Size]
|
|
280
293
|
# @return [Integer] at least 3: two border rows plus one row of message.
|
|
281
|
-
def cap_height
|
|
282
|
-
[[(
|
|
294
|
+
def cap_height(screen_size)
|
|
295
|
+
[[(screen_size.height * HEIGHT_FRACTION).to_i, 3].max, screen_size.height].min
|
|
283
296
|
end
|
|
284
297
|
|
|
285
298
|
# Wraps one message to `width` columns, capped at {MAX_ROWS_PER_MESSAGE}
|