tuile 0.14.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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +159 -49
  3. data/README.md +53 -17
  4. data/book/04-event-loop.md +12 -12
  5. data/book/05-focus.md +152 -22
  6. data/book/06-theming.md +105 -25
  7. data/book/07-components.md +531 -50
  8. data/book/08-testing.md +100 -20
  9. data/book/10-locale.md +216 -0
  10. data/book/README.md +19 -9
  11. data/examples/file_commander.rb +14 -5
  12. data/examples/hello_world.rb +17 -4
  13. data/examples/sampler.rb +654 -40
  14. data/lib/tuile/component/abstract_string_field.rb +114 -68
  15. data/lib/tuile/component/abstract_wrapping_field.rb +279 -0
  16. data/lib/tuile/component/big_decimal_field.rb +52 -79
  17. data/lib/tuile/component/button.rb +8 -8
  18. data/lib/tuile/component/checkbox.rb +9 -9
  19. data/lib/tuile/component/checkbox_group.rb +38 -21
  20. data/lib/tuile/component/combo_box.rb +102 -59
  21. data/lib/tuile/component/confirm_window.rb +7 -5
  22. data/lib/tuile/component/date_field.rb +347 -0
  23. data/lib/tuile/component/date_time_field.rb +275 -0
  24. data/lib/tuile/component/float_field.rb +57 -82
  25. data/lib/tuile/component/has_bad_input.rb +88 -0
  26. data/lib/tuile/component/has_caption.rb +8 -0
  27. data/lib/tuile/component/has_content.rb +32 -13
  28. data/lib/tuile/component/has_placeholder.rb +62 -0
  29. data/lib/tuile/component/has_validation.rb +115 -0
  30. data/lib/tuile/component/has_value.rb +28 -1
  31. data/lib/tuile/component/integer_field.rb +51 -78
  32. data/lib/tuile/component/label.rb +7 -39
  33. data/lib/tuile/component/layout/box.rb +90 -19
  34. data/lib/tuile/component/layout.rb +15 -5
  35. data/lib/tuile/component/list.rb +53 -38
  36. data/lib/tuile/component/list_dropdown.rb +7 -3
  37. data/lib/tuile/component/menu_bar/cascade.rb +5 -5
  38. data/lib/tuile/component/menu_bar.rb +18 -18
  39. data/lib/tuile/component/notification.rb +32 -18
  40. data/lib/tuile/component/overlay.rb +26 -8
  41. data/lib/tuile/component/picker_window.rb +27 -8
  42. data/lib/tuile/component/popup.rb +2 -2
  43. data/lib/tuile/component/progress_bar.rb +10 -4
  44. data/lib/tuile/component/radio_group.rb +41 -23
  45. data/lib/tuile/component/select.rb +23 -16
  46. data/lib/tuile/component/slot.rb +3 -3
  47. data/lib/tuile/component/tab_sheet.rb +6 -6
  48. data/lib/tuile/component/tabs.rb +11 -11
  49. data/lib/tuile/component/text_area.rb +26 -18
  50. data/lib/tuile/component/text_field.rb +55 -26
  51. data/lib/tuile/component/text_view.rb +40 -19
  52. data/lib/tuile/component/time_field.rb +479 -0
  53. data/lib/tuile/component/window.rb +26 -13
  54. data/lib/tuile/component.rb +635 -131
  55. data/lib/tuile/event_queue.rb +4 -4
  56. data/lib/tuile/fake_event_queue.rb +1 -1
  57. data/lib/tuile/fake_screen.rb +95 -3
  58. data/lib/tuile/final.rb +75 -0
  59. data/lib/tuile/locale.rb +851 -0
  60. data/lib/tuile/mouse/router.rb +217 -0
  61. data/lib/tuile/mouse.rb +177 -0
  62. data/lib/tuile/screen.rb +219 -68
  63. data/lib/tuile/screen_pane.rb +51 -42
  64. data/lib/tuile/styled_string.rb +5 -5
  65. data/lib/tuile/testing.rb +198 -0
  66. data/lib/tuile/theme.rb +110 -32
  67. data/lib/tuile/version.rb +1 -1
  68. data/lib/tuile/vertical_scroll_bar.rb +88 -12
  69. data/lib/tuile.rb +1 -0
  70. data/sig/tuile.rbs +4595 -825
  71. metadata +14 -9
  72. data/COMPARISON.md +0 -101
  73. data/DECISIONS.md +0 -5422
  74. data/TERMINOLOGY.md +0 -71
  75. data/ideas/arrow-key-navigation.md +0 -221
  76. data/ideas/modal-backdrop.md +0 -24
  77. data/ideas/new-components.md +0 -124
  78. data/ideas/per-component-buffers.md +0 -55
  79. data/lib/tuile/mouse_event.rb +0 -68
@@ -1,13 +1,83 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tuile
4
- # A vertical scrollbar that computes which character to draw at each row.
4
+ # Which glyph to draw at each row of a scrollbar. Built fresh per repaint from
5
+ # the viewport height and the content's scroll state, then asked row by row —
6
+ # the caller styles what comes back:
5
7
  #
6
- # Uses `█` for the handle (filled track) and `░` for the empty track. There
7
- # are no up/down arrows; the full height is used as the track. Handle
8
- # geometry is precomputed in the constructor as {#handle_height},
9
- # {#handle_start}, and {#handle_end}.
8
+ # bar = VerticalScrollBar.new(10, row_count: 20, scroll_top_row: 0)
9
+ # bar.scrollbar_char(0) # => "█" the handle: 20 rows of content, 10 shown
10
+ # bar.scrollbar_char(9) # => "░" the track below it
11
+ # StyledString.styled(bar.scrollbar_char(row), fg: screen.theme.scrollbar_color)
12
+ #
13
+ # No arrows — the full height is the track — and no color of its own, so this
14
+ # class reaches no {Screen}. {handle_char=} / {track_char=} swap the two
15
+ # glyphs app-wide.
16
+ #
17
+ # **No handle is drawn when the content fits.** A handle covering the whole
18
+ # track is a solid column carrying no information, so `row_count <= height`
19
+ # paints track at every row — while {#handle_height} / {#handle_start} /
20
+ # {#handle_end} still report the covering handle. Ink only: the caller's bar
21
+ # keeps its column and its content width (`design/decisions.md` `D_scrollbar_ink`).
10
22
  class VerticalScrollBar
23
+ class << self
24
+ # The glyph drawn where the handle covers a row, `█` by default. Set the
25
+ # pair at startup for a lazygit-style bar:
26
+ #
27
+ # Tuile::VerticalScrollBar.handle_char = "▐"
28
+ # Tuile::VerticalScrollBar.track_char = "│"
29
+ #
30
+ # Process-global, and assigning invalidates nothing — a change after the
31
+ # first paint shows up only where something repaints anyway.
32
+ # @return [String]
33
+ attr_reader :handle_char
34
+
35
+ # The glyph drawn on the rows the handle doesn't cover, `░` by default —
36
+ # and on every row when the content fits, see the class docs.
37
+ # @return [String]
38
+ attr_reader :track_char
39
+
40
+ # @param char [String]
41
+ # @return [String]
42
+ # @raise [TypeError] when `char` is not a String.
43
+ # @raise [ArgumentError] when `char` is not exactly one grapheme cluster
44
+ # one column wide.
45
+ def handle_char=(char)
46
+ @handle_char = validate_glyph(char, :handle_char)
47
+ end
48
+
49
+ # @param char [String] see {handle_char=}.
50
+ # @return [String]
51
+ def track_char=(char)
52
+ @track_char = validate_glyph(char, :track_char)
53
+ end
54
+
55
+ private
56
+
57
+ # One cell, exactly: a painter concatenates the glyph onto a row already
58
+ # padded to fill the rest of its rect, so a two-column one pushes every
59
+ # painted row past `rect.width` — silently, with nothing in the frame to
60
+ # point at.
61
+ # @param char [String]
62
+ # @param name [Symbol] the accessor, for the message.
63
+ # @return [String] frozen.
64
+ def validate_glyph(char, name)
65
+ raise TypeError, "#{name} must be a String, got #{char.inspect}" unless char.is_a?(String)
66
+
67
+ unless char.grapheme_clusters.size == 1
68
+ raise ArgumentError, "#{name} must be exactly one grapheme cluster, got #{char.inspect}"
69
+ end
70
+
71
+ width = StyledString.plain(char).display_width
72
+ raise ArgumentError, "#{name} must be one column wide, got #{char.inspect} (#{width})" unless width == 1
73
+
74
+ -char
75
+ end
76
+ end
77
+
78
+ self.handle_char = "█"
79
+ self.track_char = "░"
80
+
11
81
  # @return [Integer] number of track rows the handle occupies (height >= 1
12
82
  # only).
13
83
  attr_reader :handle_height
@@ -22,25 +92,31 @@ module Tuile
22
92
  # @param scroll_top_row [Integer] index of the first visible content row.
23
93
  def initialize(height, row_count:, scroll_top_row:)
24
94
  @height = height
95
+ @scrollable = row_count > height
25
96
 
26
97
  return unless height >= 1
27
98
 
28
- if row_count <= height
29
- @handle_height = height
30
- @handle_start = 0
31
- @handle_end = height - 1
32
- else
99
+ if @scrollable
33
100
  @handle_height = [(height * height / row_count.to_f).ceil, 1].max
34
101
  @handle_start = (height * scroll_top_row / row_count.to_f).floor
35
102
  @handle_end = @handle_start + @handle_height - 1
103
+ else
104
+ @handle_height = height
105
+ @handle_start = 0
106
+ @handle_end = height - 1
36
107
  end
37
108
  end
38
109
 
39
- # Returns the scrollbar character for the given viewport row.
110
+ # The glyph for one viewport row: {VerticalScrollBar.handle_char} where the
111
+ # handle covers it, {VerticalScrollBar.track_char} elsewhere — and at every
112
+ # row when the content fits (see the class docs).
40
113
  # @param row_in_viewport [Integer] 0-based row index within the viewport.
41
114
  # @return [String] single scrollbar character.
42
115
  def scrollbar_char(row_in_viewport)
43
- row_in_viewport >= @handle_start && row_in_viewport <= @handle_end ? "█" : "░"
116
+ return self.class.track_char unless @scrollable
117
+
118
+ on_handle = row_in_viewport >= @handle_start && row_in_viewport <= @handle_end
119
+ on_handle ? self.class.handle_char : self.class.track_char
44
120
  end
45
121
  end
46
122
  end
data/lib/tuile.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "concurrent"
4
+ require "date"
4
5
  require "io/console"
5
6
  require "logger"
6
7
  require "singleton"