tuile 0.11.0 → 0.13.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/DECISIONS.md +1970 -14
  4. data/README.md +136 -491
  5. data/TERMINOLOGY.md +70 -0
  6. data/book/01-first-app.md +22 -17
  7. data/book/02-repaint.md +19 -6
  8. data/book/03-layout.md +12 -11
  9. data/book/05-focus.md +133 -18
  10. data/book/06-theming.md +6 -3
  11. data/book/07-components.md +498 -38
  12. data/book/08-testing.md +18 -4
  13. data/book/README.md +7 -5
  14. data/examples/file_commander.rb +27 -20
  15. data/examples/hello_world.rb +17 -5
  16. data/examples/sampler.rb +422 -66
  17. data/ideas/arrow-key-navigation.md +16 -0
  18. data/ideas/new-components.md +16 -10
  19. data/lib/tuile/ansi.rb +10 -0
  20. data/lib/tuile/buffer.rb +7 -7
  21. data/lib/tuile/component/abstract_string_field.rb +36 -0
  22. data/lib/tuile/component/button.rb +1 -1
  23. data/lib/tuile/component/checkbox.rb +1 -1
  24. data/lib/tuile/component/checkbox_group.rb +31 -26
  25. data/lib/tuile/component/combo_box.rb +13 -8
  26. data/lib/tuile/component/info_window.rb +1 -1
  27. data/lib/tuile/component/label.rb +14 -14
  28. data/lib/tuile/component/list.rb +313 -216
  29. data/lib/tuile/component/list_dropdown.rb +100 -10
  30. data/lib/tuile/component/menu_bar/cascade.rb +255 -0
  31. data/lib/tuile/component/menu_bar.rb +582 -0
  32. data/lib/tuile/component/notification.rb +320 -0
  33. data/lib/tuile/component/picker_window.rb +3 -8
  34. data/lib/tuile/component/popup.rb +83 -19
  35. data/lib/tuile/component/progress_bar.rb +1 -1
  36. data/lib/tuile/component/radio_group.rb +32 -30
  37. data/lib/tuile/component/select.rb +10 -8
  38. data/lib/tuile/component/tab_sheet.rb +242 -0
  39. data/lib/tuile/component/tabs.rb +528 -0
  40. data/lib/tuile/component/text_area/wrapped_text.rb +320 -0
  41. data/lib/tuile/component/text_area.rb +84 -277
  42. data/lib/tuile/component/text_field.rb +24 -7
  43. data/lib/tuile/component/text_view.rb +197 -180
  44. data/lib/tuile/component/window.rb +8 -8
  45. data/lib/tuile/component.rb +43 -18
  46. data/lib/tuile/event_queue.rb +25 -1
  47. data/lib/tuile/fake_screen.rb +14 -0
  48. data/lib/tuile/keys.rb +65 -0
  49. data/lib/tuile/screen.rb +95 -78
  50. data/lib/tuile/screen_pane.rb +109 -27
  51. data/lib/tuile/styled_string.rb +52 -12
  52. data/lib/tuile/version.rb +1 -1
  53. data/lib/tuile/vertical_scroll_bar.rb +6 -6
  54. data/sig/tuile.rbs +2307 -516
  55. metadata +9 -3
  56. data/mise.toml +0 -2
data/book/README.md CHANGED
@@ -66,17 +66,19 @@ one, not to fill an outline.
66
66
  `focusable?`, and the three-rung order in which a keystroke is offered
67
67
  to the tree — Tab, global shortcuts, then `handle_key` delivered to
68
68
  focus and bubbling up its ancestors. Why scope-wide keys (pane jumps, a
69
- form's default button) belong on an ancestor, and how `keyboard_hint`
70
- drives the status bar.
69
+ form's default button) belong on an ancestor, why a paste rides its own
70
+ path rather than the ladder, and how to write a status line over
71
+ `on_focus_changed` — Tuile draws none for you.
71
72
  6. **[Theming](06-theming.md).** Semantic color tokens read at paint
72
73
  time, opt-in component backgrounds that inherit down the tree
73
74
  (`bg_color`), light/dark auto-detection at startup and live OS
74
75
  appearance flips, pairing variants in a `ThemeDef`, app-specific custom
75
76
  tokens, and rebuilding theme-derived content in `on_theme_changed`.
76
77
  7. **[The component library](07-components.md).** A narrative tour of
77
- the shipped toolbox — Window, List, the text inputs and views,
78
- ProgressBar, Popup, and the window conveniences — framed around
79
- *when and why* you reach for each. Signatures stay in the rdoc.
78
+ the shipped toolbox — the text inputs and views, the value fields, the
79
+ selectors, Button, ProgressBar, Window, TabSheet, MenuBar, Popup and the
80
+ window conveniences — framed around *when and why* you reach for each.
81
+ Signatures stay in the rdoc.
80
82
  8. **[Testing a Tuile app](08-testing.md).** The testing approach:
81
83
  `FakeScreen`, asserting against the painted buffer, driving
82
84
  invalidation, and PTY-based end-to-end tests of runnable scripts.
@@ -4,7 +4,9 @@
4
4
  # Tuile two-pane file commander. Two windows side by side, each showing a
5
5
  # directory listing. Tab switches active pane; arrows / jk move the cursor;
6
6
  # Enter descends into a directory (no-op on a regular file); Backspace
7
- # ascends to the parent. The header label shows the active pane's cwd.
7
+ # ascends to the parent. The header label shows the active pane's cwd, and a
8
+ # static status line spells out the keys — Tuile draws no status bar and
9
+ # reserves no row, so both are ordinary children of the layout.
8
10
  # Unreadable directories surface an InfoWindow. Layout follows the
9
11
  # terminal on resize (WINCH) — the framework dispatches a TTYSizeEvent and
10
12
  # the layout's `rect=` rebuilds the geometry.
@@ -33,6 +35,7 @@ module FileCommanderExample
33
35
  def initialize(start_dir)
34
36
  super()
35
37
  self.cursor = Tuile::Component::List::Cursor.new
38
+ self.renderer = ->(entry) { Rainbow(entry[:display]).color(TYPE_COLORS[entry[:type]]) }
36
39
  @cwd = File.expand_path(start_dir)
37
40
  @on_cwd_changed = nil
38
41
  load_entries
@@ -60,8 +63,8 @@ module FileCommanderExample
60
63
 
61
64
  private
62
65
 
63
- def descend(_index, line)
64
- target = File.expand_path(File.join(@cwd, Rainbow.uncolor(line).chomp("/")))
66
+ def descend(_index, entry)
67
+ target = File.expand_path(File.join(@cwd, entry[:name]))
65
68
  change_to(target) if File.directory?(target)
66
69
  end
67
70
 
@@ -75,7 +78,7 @@ module FileCommanderExample
75
78
  @cwd = path
76
79
  load_entries
77
80
  self.cursor = Tuile::Component::List::Cursor.new
78
- self.top_line = 0
81
+ self.scroll_top_row = 0
79
82
  @on_cwd_changed&.call
80
83
  rescue SystemCallError => e
81
84
  @cwd = previous
@@ -89,7 +92,7 @@ module FileCommanderExample
89
92
  { name: name, type: classify(path), display: is_dir ? "#{name}/" : name, dir_first: is_dir ? 0 : 1 }
90
93
  end
91
94
  entries.sort_by! { |e| [e[:dir_first], e[:name].downcase] }
92
- self.lines = entries.map { |e| Rainbow(e[:display]).color(TYPE_COLORS[e[:type]]) }
95
+ self.items = entries
93
96
  end
94
97
 
95
98
  # Classify by symlink first so a symlink-to-dir still reads as a link.
@@ -106,18 +109,6 @@ module FileCommanderExample
106
109
  end
107
110
  end
108
111
 
109
- # A pane window that advertises navigation shortcuts in the status bar.
110
- # The active window's `keyboard_hint` is rendered by {Tuile::Screen}
111
- # alongside the global `q` quit hint, so all the user-facing controls
112
- # land in one place.
113
- class PaneWindow < Tuile::Component::Window
114
- def keyboard_hint
115
- "Tab #{screen.theme.hint("Switch")} " \
116
- "Enter #{screen.theme.hint("Open")} " \
117
- "Bksp #{screen.theme.hint("Up")}"
118
- end
119
- end
120
-
121
112
  # Top-level layout. Header label on the first row, two side-by-side
122
113
  # windows below. `rect=` re-runs on the initial mount and on every WINCH,
123
114
  # so the split tracks the terminal size automatically.
@@ -127,19 +118,34 @@ module FileCommanderExample
127
118
  @header = Tuile::Component::Label.new
128
119
  add(@header)
129
120
 
130
- @left_window = PaneWindow.new
121
+ @left_window = Tuile::Component::Window.new
131
122
  @left_list = DirList.new(left_dir)
132
123
  @left_list.on_cwd_changed = method(:refresh_header)
133
124
  @left_window.content = @left_list
134
125
  @left_window.scrollbar = true
135
126
  add(@left_window)
136
127
 
137
- @right_window = PaneWindow.new
128
+ @right_window = Tuile::Component::Window.new
138
129
  @right_list = DirList.new(right_dir)
139
130
  @right_list.on_cwd_changed = method(:refresh_header)
140
131
  @right_window.content = @right_list
141
132
  @right_window.scrollbar = true
142
133
  add(@right_window)
134
+
135
+ # The status line. Every key here works in both panes, so the row never
136
+ # changes and nothing needs to watch focus — a status line is only worth
137
+ # wiring to Tuile::Screen#on_focus_changed= when its text actually varies
138
+ # with the focused component. `theme.hint` bakes its colors in, so the
139
+ # one thing this label does watch is a light/dark flip.
140
+ @status = Tuile::Component::Label.new
141
+ render_status = lambda do
142
+ t = screen.theme
143
+ @status.text = "q #{t.hint("quit")} Tab #{t.hint("Switch")} " \
144
+ "Enter #{t.hint("Open")} Bksp #{t.hint("Up")}"
145
+ end
146
+ render_status.call
147
+ @status.on_theme_changed = render_status
148
+ add(@status)
143
149
  end
144
150
 
145
151
  attr_reader :left_window
@@ -149,8 +155,9 @@ module FileCommanderExample
149
155
  return if rect.empty?
150
156
 
151
157
  @header.rect = Tuile::Rect.new(rect.left, rect.top, rect.width, 1)
158
+ @status.rect = Tuile::Rect.new(rect.left, rect.top + rect.height - 1, rect.width, 1)
152
159
  body_top = rect.top + 1
153
- body_height = [rect.height - 1, 0].max
160
+ body_height = [rect.height - 2, 0].max
154
161
  half = rect.width / 2
155
162
  @left_window.rect = Tuile::Rect.new(rect.left, body_top, half, body_height)
156
163
  @right_window.rect = Tuile::Rect.new(rect.left + half, body_top,
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Tuile hello-world. A Window wrapping a Label.
4
+ # Tuile hello-world. A Window wrapping a Label, over a status line the app
5
+ # owns — Tuile draws no chrome of its own and reserves no row.
5
6
  #
6
7
  # Run from the gem root:
7
8
  # bundle exec ruby -Ilib examples/hello_world.rb
@@ -14,12 +15,23 @@ require "tuile"
14
15
  # Tuile::Screen.instance during invalidate/repaint hooks.
15
16
  screen = Tuile::Screen.new
16
17
 
17
- label = Tuile::Component::Label.new("Hello, world!")
18
-
19
18
  window = Tuile::Component::Window.new("Tuile")
20
- window.content = label
19
+ window.content = Tuile::Component::Label.new("Hello, world!")
20
+
21
+ # The status line. `theme.hint` styles the *description* half of a "key what"
22
+ # pair, and bakes the color in — so the label rebuilds itself from
23
+ # `on_theme_changed` to follow a light/dark flip.
24
+ status = Tuile::Component::Label.new
25
+ render_status = -> { status.text = "q #{screen.theme.hint("quit")}" }
26
+ render_status.call
27
+ status.on_theme_changed = render_status
28
+
29
+ # One row for the status line, everything else to the window.
30
+ root = Tuile::Component::Layout::Vertical.new
31
+ root.add(window, Tuile::Component::Layout::Expand[1])
32
+ root.add(status, Tuile::Component::Layout::Fixed[1])
21
33
 
22
- screen.content = window
34
+ screen.content = root
23
35
  window.focus
24
36
  begin
25
37
  screen.run_event_loop