ratatui_ruby 0.7.2 → 0.7.3

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/.builds/ruby-3.2.yml +1 -1
  3. data/.builds/ruby-3.3.yml +1 -1
  4. data/.builds/ruby-3.4.yml +1 -1
  5. data/.builds/ruby-4.0.0.yml +1 -1
  6. data/AGENTS.md +4 -3
  7. data/CHANGELOG.md +28 -0
  8. data/README.md +2 -2
  9. data/doc/concepts/application_testing.md +4 -2
  10. data/doc/contributors/developing_examples.md +7 -7
  11. data/doc/contributors/upstream_requests/tab_rects.md +173 -0
  12. data/doc/contributors/upstream_requests/title_rects.md +132 -0
  13. data/doc/contributors/v1.0.0_blockers.md +46 -739
  14. data/doc/troubleshooting/tui_output.md +76 -0
  15. data/examples/widget_barchart/README.md +1 -1
  16. data/examples/widget_block/README.md +1 -1
  17. data/examples/widget_overlay/README.md +1 -1
  18. data/ext/ratatui_ruby/Cargo.lock +1 -1
  19. data/ext/ratatui_ruby/Cargo.toml +1 -1
  20. data/ext/ratatui_ruby/src/lib.rs +2 -2
  21. data/ext/ratatui_ruby/src/rendering.rs +9 -0
  22. data/ext/ratatui_ruby/src/style.rs +22 -2
  23. data/ext/ratatui_ruby/src/text.rs +26 -0
  24. data/ext/ratatui_ruby/src/widgets/chart.rs +5 -0
  25. data/lib/ratatui_ruby/style/style.rb +1 -0
  26. data/lib/ratatui_ruby/test_helper/snapshot.rb +60 -21
  27. data/lib/ratatui_ruby/test_helper/snapshots/axis_labels_alignment.ansi +24 -0
  28. data/lib/ratatui_ruby/test_helper/snapshots/axis_labels_alignment.txt +24 -0
  29. data/lib/ratatui_ruby/test_helper/snapshots/barchart_styled_label.ansi +5 -0
  30. data/lib/ratatui_ruby/test_helper/snapshots/barchart_styled_label.txt +5 -0
  31. data/lib/ratatui_ruby/test_helper/snapshots/chart_rendering.ansi +24 -0
  32. data/lib/ratatui_ruby/test_helper/snapshots/chart_rendering.txt +24 -0
  33. data/lib/ratatui_ruby/test_helper/snapshots/half_block_marker.ansi +12 -0
  34. data/lib/ratatui_ruby/test_helper/snapshots/half_block_marker.txt +12 -0
  35. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_bottom.ansi +12 -0
  36. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_bottom.txt +12 -0
  37. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_left.ansi +12 -0
  38. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_left.txt +12 -0
  39. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_right.ansi +12 -0
  40. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_right.txt +12 -0
  41. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_top.ansi +12 -0
  42. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_top.txt +12 -0
  43. data/lib/ratatui_ruby/test_helper/snapshots/my_snapshot.txt +1 -0
  44. data/lib/ratatui_ruby/test_helper/snapshots/styled_axis_title.ansi +10 -0
  45. data/lib/ratatui_ruby/test_helper/snapshots/styled_axis_title.txt +10 -0
  46. data/lib/ratatui_ruby/test_helper/snapshots/styled_dataset_name.ansi +10 -0
  47. data/lib/ratatui_ruby/test_helper/snapshots/styled_dataset_name.txt +10 -0
  48. data/lib/ratatui_ruby/test_helper/terminal.rb +3 -0
  49. data/lib/ratatui_ruby/test_helper.rb +1 -1
  50. data/lib/ratatui_ruby/version.rb +1 -1
  51. data/lib/ratatui_ruby/widgets/bar_chart.rb +3 -2
  52. data/lib/ratatui_ruby/widgets/block.rb +42 -0
  53. data/lib/ratatui_ruby/widgets/chart.rb +9 -4
  54. data/lib/ratatui_ruby/widgets/sparkline.rb +3 -2
  55. data/lib/ratatui_ruby.rb +128 -9
  56. metadata +26 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52112877db56e3edf7a4baa4c542b55570e8b1172a172934e229c0730d33ca2c
4
- data.tar.gz: 1f46e0c790f33828dba5ed4cfa6ea6d56d5525634cc587d0986005e287c6b876
3
+ metadata.gz: f76aaa4da70206d5855ab0d4d7a9356bd57117c1d4ff6d243e86db8bb6e53be9
4
+ data.tar.gz: 88b96b0a71f595d7ca4b255d90f29b3aac3e2fcc02f323d2db9adfaa233bf4a0
5
5
  SHA512:
6
- metadata.gz: 5ee6665ca41a8029b67f9d99ac4908ad5ab124cc8d21952d7a42e3eb3bce0dfd2ef77e19b69294a7c5e2bb1a8bb745047a8f7599712e497ba0c66d3e8607e351
7
- data.tar.gz: '085180a5bab1b831521750b42398087109129fda7d20fe369b7aa4ff5c746e691e09f82a4251bc62ca964508ed21d9276ace9970ed91fec3fd979923ea2937a5'
6
+ metadata.gz: 9d8bc1bc8aa2ae2625302e0024f83c6b30459551377f111e9703e9ed9f70fe0c79cc5cd6af57d9770bd5cc7ecfc93e9e8472f7ebde314b1c989a9cff87110bba
7
+ data.tar.gz: b0d285b610e38669c9c764b492e384ae2e9f84739b5572096d70a74818b4ce87f2025c06d9055e55e3e7966c8f0ba9768533ba683ea232a3a77a224ac716f8ed
data/.builds/ruby-3.2.yml CHANGED
@@ -16,7 +16,7 @@ packages:
16
16
  - clang
17
17
  - git
18
18
  artifacts:
19
- - ratatui_ruby/pkg/ratatui_ruby-0.7.2.gem
19
+ - ratatui_ruby/pkg/ratatui_ruby-0.7.3.gem
20
20
  sources:
21
21
  - https://git.sr.ht/~kerrick/ratatui_ruby
22
22
  tasks:
data/.builds/ruby-3.3.yml CHANGED
@@ -16,7 +16,7 @@ packages:
16
16
  - clang
17
17
  - git
18
18
  artifacts:
19
- - ratatui_ruby/pkg/ratatui_ruby-0.7.2.gem
19
+ - ratatui_ruby/pkg/ratatui_ruby-0.7.3.gem
20
20
  sources:
21
21
  - https://git.sr.ht/~kerrick/ratatui_ruby
22
22
  tasks:
data/.builds/ruby-3.4.yml CHANGED
@@ -16,7 +16,7 @@ packages:
16
16
  - clang
17
17
  - git
18
18
  artifacts:
19
- - ratatui_ruby/pkg/ratatui_ruby-0.7.2.gem
19
+ - ratatui_ruby/pkg/ratatui_ruby-0.7.3.gem
20
20
  sources:
21
21
  - https://git.sr.ht/~kerrick/ratatui_ruby
22
22
  tasks:
@@ -16,7 +16,7 @@ packages:
16
16
  - clang
17
17
  - git
18
18
  artifacts:
19
- - ratatui_ruby/pkg/ratatui_ruby-0.7.2.gem
19
+ - ratatui_ruby/pkg/ratatui_ruby-0.7.3.gem
20
20
  sources:
21
21
  - https://git.sr.ht/~kerrick/ratatui_ruby
22
22
  tasks:
data/AGENTS.md CHANGED
@@ -30,7 +30,8 @@ Architecture:
30
30
  - Every file MUST begin with an SPDX-compliant header. Use `AGPL-3.0-or-later` for code; `CC-BY-SA-4.0` for documentation. `reuse annotate` can help you generate the header. **For Ruby files**, wrap SPDX comments in `#--` / `#++` to hide them from RDoc output.
31
31
  - Every line of Ruby MUST be covered by tests that would stand up to mutation testing.
32
32
  - Tests must be meaningful and verify specific behavior or rendering output; simply verifying that code "doesn't crash" is insufficient and unacceptable.
33
- - For UI widgets, this means using `with_test_terminal` to verify EVERY character of the terminal buffer's content.
33
+ - **Prefer snapshot tests** (`assert_snapshots`, plural) over manual `buffer_content` assertions for UI widgets. Snapshots are self-documenting and easier to maintain.
34
+ - For UI widgets, use `with_test_terminal` and snapshot assertions to verify terminal buffer content.
34
35
  - Every line of Rust MUST be covered by tests that would stand up to mutation testing.
35
36
  - Tests must be meaningful; simply verifying that code "doesn't crash" or "compiles" is insufficient and unacceptable.
36
37
  - Each widget implementation must have a `tests` module with unit tests verifying basic rendering.
@@ -137,8 +138,8 @@ Before considering a task complete and returning control to the user, you **MUST
137
138
 
138
139
  1. **Default Rake Task Passes:** Run `bin/agent_rake` (no args). Confirm it passes with ZERO errors **or warnings**.
139
140
  - You will save time if you run `bin/agent_rake rubocop:autocorrect` first.
140
- - If you think the build is looking for deleted files, it is not. Instead, tell the user and **ask them to stage changes**.
141
+ - If you think the build is looking for deleted files, it is not. Instead, explain to the user why staging is needed and use the `run_command` tool with `git add -A` so they get a Run button with context.
141
142
  2. **Documentation Updated:** If public APIs or observable behavior changed, update relevant RDoc, rustdoc, `doc/` files, `README.md`, and/or `ratatui_ruby-wiki` files.
142
143
  3. **Changelog Updated:** If public APIs, observable behavior, or gemspec dependencies have changed, update [CHANGELOG.md](CHANGELOG.md)'s **Unreleased** section.
143
144
  4. **Commit Message Suggested:** You **MUST** ensure the final message to the user includes a suggested commit message block. This is NOT optional.
144
- - You MUST also remind the user to add an AI attribution footer.
145
+ - You MUST also check `git log -n1` to see the current standard AI footer ("Generated with" and "Co-Authored-By") and include it in your suggested message.
data/CHANGELOG.md CHANGED
@@ -12,10 +12,37 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
12
12
 
13
13
  ### Added
14
14
 
15
+ - **Block Inner Area Calculation**: `Block#inner(area)` method computes the inner content area given an outer `Rect`, accounting for borders and padding. Essential for layout calculations when you need to know usable space inside a block.
16
+ - **Deferred Warnings During TUI Sessions**: Experimental feature warnings (like `Paragraph#line_count`) are now automatically queued during active TUI sessions and flushed to stderr after `restore_terminal`. This prevents warnings from corrupting the TUI display. See `doc/troubleshooting/tui_output.md` for details on handling terminal output during TUI sessions.
17
+ - **Session State Tracking**: `RatatuiRuby.terminal_active?` indicates whether a TUI session is active. Calling `init_terminal` or `init_test_terminal` while a session is already active now raises `Error::Invariant`.
18
+ - **Error::Invariant**: New error class for state invariant violations (e.g., double-init). Distinct from `Error::Safety` (lifetime violations) and `Error::Terminal` (operational I/O failures).
15
19
  ### Changed
16
20
 
17
21
  ### Fixed
18
22
 
23
+ - **Direct Text Rendering**: `Text::Line` and `Text::Span` can now be rendered directly as widgets via `frame.render_widget(line, area)` without wrapping in a `Paragraph`. Previously, these text primitives were silently ignored when passed to `render_widget`.
24
+ - **Text Line Alignment**: `Text::Line` `alignment:` parameter is now respected during rendering. Previously, the alignment was ignored and text always rendered left-aligned.
25
+
26
+ ### Removed
27
+
28
+ ## [0.7.3] - 2026-01-04
29
+
30
+ ### Added
31
+
32
+ - **Symbol Shortcuts for `bar_set`**: `Sparkline` and `BarChart` now accept `:nine_levels` (full 9-character gradient) and `:three_levels` (simplified empty/half/full) as intuitive shortcuts instead of requiring custom character hashes.
33
+ - **`:half_block` Marker**: `Chart` `Dataset` now supports `:half_block` marker for higher resolution rendering using ▀ and ▄ characters.
34
+ - **`assert_snapshots` Method**: `RatatuiRuby::TestHelper#assert_snapshots` (plural) calls both `assert_plain_snapshot` and `assert_rich_snapshot` with the same name, generating both `.txt` and `.ansi` files for documentation and display purposes.
35
+ - **Edge-Center Legend Positions**: `Chart` `legend_position` now accepts `:top`, `:bottom`, `:left`, and `:right` in addition to the existing corner positions (`:top_left`, `:top_right`, `:bottom_left`, `:bottom_right`).
36
+ - **`:reset` Color**: `Style` `fg` and `bg` now accept `:reset` to explicitly clear any inherited foreground or background color, restoring the terminal's default.
37
+
38
+ ### Changed
39
+
40
+ ### Deprecated
41
+
42
+ - **`assert_snapshot`**: Use `assert_snapshots` (plural) instead, or `assert_plain_snapshot` if you only need plain text. The old method name lacked clarity about whether it captured plain text or styled ANSI output.
43
+
44
+ ### Fixed
45
+
19
46
  ### Removed
20
47
 
21
48
  ## [0.7.2] - 2026-01-04
@@ -388,6 +415,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
388
415
  - **Testing Support**: Included `RatatuiRuby::TestHelper` and RSpec integration to make testing your TUI applications possible.
389
416
 
390
417
  [Unreleased]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/HEAD
418
+ [0.7.3]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.7.3
391
419
  [0.7.2]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.7.2
392
420
  [0.7.1]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.7.1
393
421
  [0.7.0]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.7.0
data/README.md CHANGED
@@ -6,10 +6,10 @@
6
6
 
7
7
  [![
8
8
  builds.sr.ht status](https://builds.sr.ht/~kerrick/ratatui_ruby.svg)](https://builds.sr.ht/~kerrick/ratatui_ruby?) [![
9
- License](https://img.shields.io/badge/dynamic/regex?url=https%3A%2F%2Fgit.sr.ht%2F~kerrick%2Fratatui_ruby%2Fblob%2Fmain%2Fratatui_ruby.gemspec&search=spec%5C.license%20%3D%20%22(.*)%22&replace=%241&label=License&color=a2c93e)](https://spdx.org/licenses/AGPL-3.0-or-later.html) [![
9
+ License](https://img.shields.io/badge/dynamic/regex?url=https%3A%2F%2Fgit.sr.ht%2F~kerrick%2Fratatui_ruby%2Fblob%2Fstable%2Fratatui_ruby.gemspec&search=spec%5C.license%20%3D%20%22(.*)%22&replace=%241&label=License&color=a2c93e)](https://spdx.org/licenses/AGPL-3.0-or-later.html) [![
10
10
  Gem Total Downloads](https://img.shields.io/gem/dt/ratatui_ruby)](https://rubygems.org/gems/ratatui_ruby) [![
11
11
  Gem Version](https://img.shields.io/gem/v/ratatui_ruby)](https://rubygems.org/gems/ratatui_ruby) [![
12
- Ratatui Version](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fgit.sr.ht%2F~kerrick%2Fratatui_ruby%2Fblob%2Fmain%2Fext%2Fratatui_ruby%2FCargo.toml&query=%24.dependencies.ratatui.version&prefix=v&logo=ratatui&label=Ratatui)](https://crates.io/crates/ratatui/0.30) [![
12
+ Ratatui Version](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fgit.sr.ht%2F~kerrick%2Fratatui_ruby%2Fblob%2Fstable%2Fext%2Fratatui_ruby%2FCargo.toml&query=%24.dependencies.ratatui.version&prefix=v&logo=ratatui&label=Ratatui)](https://crates.io/crates/ratatui/0.30) [![
13
13
  Mailing List: Discussion](https://img.shields.io/badge/mailing_list-discussion-5865F2.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0iaWNvbiBpY29uLXRhYmxlciBpY29ucy10YWJsZXItb3V0bGluZSBpY29uLXRhYmxlci1tYWlsIj48cGF0aCBzdHJva2U9Im5vbmUiIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz48cGF0aCBkPSJNMyA3YTIgMiAwIDAgMSAyIC0yaDE0YTIgMiAwIDAgMSAyIDJ2MTBhMiAyIDAgMCAxIC0yIDJoLTE0YTIgMiAwIDAgMSAtMiAtMnYtMTB6IiAvPjxwYXRoIGQ9Ik0zIDdsOSA2bDkgLTYiIC8+PC9zdmc+Cg==)](https://lists.sr.ht/~kerrick/ratatui_ruby-discuss) [![
14
14
  Mailing List: Development](https://img.shields.io/badge/mailing_list-development-4954d5.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0iaWNvbiBpY29uLXRhYmxlciBpY29ucy10YWJsZXItb3V0bGluZSBpY29uLXRhYmxlci1tYWlsIj48cGF0aCBzdHJva2U9Im5vbmUiIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz48cGF0aCBkPSJNMyA3YTIgMiAwIDAgMSAyIC0yaDE0YTIgMiAwIDAgMSAyIDJ2MTBhMiAyIDAgMCAxIC0yIDJoLTE0YTIgMiAwIDAgMSAtMiAtMnYtMTB6IiAvPjxwYXRoIGQ9Ik0zIDdsOSA2bDkgLTYiIC8+PC9zdmc+Cg==)](https://lists.sr.ht/~kerrick/ratatui_ruby-devel) [![
15
15
  Mailing List: Announcements](https://img.shields.io/badge/mailing_list-announcements-3b44ac.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0iaWNvbiBpY29uLXRhYmxlciBpY29ucy10YWJsZXItb3V0bGluZSBpY29uLXRhYmxlci1tYWlsIj48cGF0aCBzdHJva2U9Im5vbmUiIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz48cGF0aCBkPSJNMyA3YTIgMiAwIDAgMSAyIC0yaDE0YTIgMiAwIDAgMSAyIDJ2MTBhMiAyIDAgMCAxIC0yIDJoLTE0YTIgMiAwIDAgMSAtMiAtMnYtMTB6IiAvPjxwYXRoIGQ9Ik0zIDdsOSA2bDkgLTYiIC8+PC9zdmc+Cg==)](https://lists.sr.ht/~kerrick/ratatui_ruby-announce)
@@ -103,15 +103,17 @@ See [RatatuiRuby::TestHelper::EventInjection](../lib/ratatui_ruby/test_helper/ev
103
103
 
104
104
  Snapshots let you verify complex layouts without manually asserting every line.
105
105
 
106
- Use `assert_snapshot` to compare the current screen against a stored reference file.
106
+ Use `assert_snapshots` to compare the current screen against stored reference files.
107
107
 
108
108
  ```ruby
109
109
  with_test_terminal do
110
110
  MyApp.new.run
111
- assert_snapshot("dashboard_view")
111
+ assert_snapshots("dashboard_view")
112
112
  end
113
113
  ```
114
114
 
115
+ This generates both `.txt` (plain text) and `.ansi` (styled) snapshot files. The `.ansi` files contain ANSI escape codes—`cat` them in a terminal to see exactly what the screen looked like. For a visual tour of your test suite, try `cat **/*.ansi` in any shell that supports globbing.
116
+
115
117
  ### Handling Non-Determinism
116
118
 
117
119
  Snapshots must be deterministic. Random data or current timestamps will cause test failures ("flakes").
@@ -112,7 +112,7 @@ examples/
112
112
  test/examples/
113
113
  my_example/
114
114
  test_app.rb ← REQUIRED: Tests (centralized, not local to example)
115
- snapshots/ ← Auto-created by assert_snapshot
115
+ snapshots/ ← Auto-created by snapshot assertions
116
116
  initial_render.txt
117
117
 
118
118
  sig/examples/
@@ -223,7 +223,7 @@ class TestMyExampleApp < Minitest::Test
223
223
  with_test_terminal do
224
224
  inject_key(:q)
225
225
  @app.run
226
- assert_snapshot("initial_render")
226
+ assert_snapshots("initial_render")
227
227
  end
228
228
  end
229
229
  end
@@ -231,7 +231,7 @@ end
231
231
 
232
232
  ## Snapshot Testing Pattern (REQUIRED)
233
233
 
234
- All example tests MUST use snapshot testing via the `assert_snapshot` API, not manual content assertions.
234
+ All example tests MUST use snapshot testing via the `assert_snapshots` API, not manual content assertions.
235
235
 
236
236
  ### Why Snapshots
237
237
 
@@ -249,7 +249,7 @@ def test_initial_render
249
249
  inject_key(:q)
250
250
  @app.run
251
251
 
252
- assert_snapshot("initial_render")
252
+ assert_snapshots("initial_render")
253
253
  end
254
254
  end
255
255
  ```
@@ -261,8 +261,8 @@ Snapshot auto-saved to: `test/examples/widget_foo/snapshots/initial_render.txt`
261
261
  For examples with timestamps, random data, or other non-deterministic output:
262
262
 
263
263
  ```ruby
264
- private def assert_normalized_snapshot(snapshot_name)
265
- assert_snapshot(snapshot_name) do |actual|
264
+ private def assert_normalized_snapshots(snapshot_name)
265
+ assert_plain_snapshot(snapshot_name) do |actual|
266
266
  actual.map do |line|
267
267
  line.gsub(/\d{2}:\d{2}:\d{2}/, "XX:XX:XX") # Mask timestamps
268
268
  .gsub(/Random ID: \d+/, "Random ID: XXX") # Mask random values
@@ -276,7 +276,7 @@ def test_after_event
276
276
  inject_key(:q)
277
277
  @app.run
278
278
 
279
- assert_normalized_snapshot("after_event")
279
+ assert_normalized_snapshots("after_event")
280
280
  end
281
281
  end
282
282
  ```
@@ -0,0 +1,173 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Feature Request: Expose Tabs Title Rects
7
+
8
+ ## Summary
9
+
10
+ `Tabs` computes the bounding rect for each tab title during rendering but does not expose this information. Interactive applications need these rects for mouse click hit-testing.
11
+
12
+ ## The Problem
13
+
14
+ Building clickable tab interfaces requires knowing where each tab renders. When a user clicks within the tabs area, the application cannot determine which specific tab was clicked without duplicating the internal layout algorithm.
15
+
16
+ Currently, the only options are:
17
+
18
+ 1. **Recompute the layout manually.** Duplicate the logic from `render_tabs`, accounting for padding, dividers, and title widths. This is fragile—any upstream change breaks the user's code.
19
+ 2. **Use coarse hit-testing.** Check if a click is anywhere in the tabs area, then guess based on x-position. This breaks when titles have different widths or styled content.
20
+
21
+ Neither approach is satisfactory.
22
+
23
+ ## Use Case
24
+
25
+ Consider a TUI with a tabbed interface:
26
+
27
+ ```
28
+ ┌Announce v0.7.3───────────────────────────────emate┐
29
+ │ Preview Email ▸ Preview Commit ▸ Announce │
30
+ │ │
31
+ └───────────────────────────────────────────────────┘
32
+ ```
33
+
34
+ The application wants to detect clicks on individual tab titles (`"Preview Email"`, `"Preview Commit"`, `"Announce"`) and switch to that tab.
35
+
36
+ Without title rects, the application must manually compute where each tab renders:
37
+
38
+ ```rust
39
+ // Manual calculation - fragile and duplicates internal logic
40
+ let divider_width = 3; // " ▸ " is 3 characters
41
+ let content_row = area.y + 1; // Skip top border
42
+ let mut x = area.x + 2; // Skip border + padding
43
+
44
+ let tab_rects: Vec<Rect> = titles.iter().map(|title| {
45
+ let tab_width = title.len() as u16;
46
+ let rect = Rect::new(x, content_row, tab_width, 1);
47
+ x += tab_width + divider_width;
48
+ rect
49
+ }).collect();
50
+ ```
51
+
52
+ This duplicates private logic from `Tabs::render_tabs` and breaks when:
53
+
54
+ - Padding is configured differently (`padding_left`, `padding_right`)
55
+ - Divider width changes
56
+ - Upstream layout logic changes
57
+ - A block is present (affects inner area calculation)
58
+
59
+ ## Current State (v0.30.0)
60
+
61
+ `Tabs` has a private `render_tabs` method that computes title areas:
62
+
63
+ ```rust
64
+ // From src/widgets/tabs.rs - private rendering logic
65
+ fn render_tabs(&self, tabs_area: Rect, buf: &mut Buffer) {
66
+ let mut x = tabs_area.left();
67
+ for (i, title) in self.titles.iter().enumerate() {
68
+ // ...padding and title rendering...
69
+
70
+ // Title rect is computed here but not exposed
71
+ if Some(i) == self.selected {
72
+ buf.set_style(
73
+ Rect {
74
+ x,
75
+ y: tabs_area.top(),
76
+ width: pos.0.saturating_sub(x),
77
+ height: 1,
78
+ },
79
+ self.highlight_style,
80
+ );
81
+ }
82
+ // ...
83
+ }
84
+ }
85
+ ```
86
+
87
+ The rect is computed for applying `highlight_style` but is not accessible to users.
88
+
89
+ ## Proposed API
90
+
91
+ Following the pattern established by `Block::inner(area)`, add a pure computation method that takes an area and returns computed sub-rects without rendering:
92
+
93
+ ```rust
94
+ impl Tabs {
95
+ /// Returns the bounding rect for each tab title given an area.
96
+ ///
97
+ /// The rects are returned in the same order as titles were added.
98
+ /// Useful for hit-testing mouse clicks against specific tabs.
99
+ ///
100
+ /// # Example
101
+ ///
102
+ /// ```rust
103
+ /// let tabs = Tabs::new(["Tab 1", "Tab 2", "Tab 3"])
104
+ /// .divider(" | ");
105
+ ///
106
+ /// let rects = tabs.title_rects(area);
107
+ /// for (i, rect) in rects.iter().enumerate() {
108
+ /// if rect.contains(mouse_position) {
109
+ /// selected_tab = i;
110
+ /// break;
111
+ /// }
112
+ /// }
113
+ /// ```
114
+ pub fn title_rects(&self, area: Rect) -> Vec<Rect> { ... }
115
+ }
116
+ ```
117
+
118
+ Alternatively, a single-lookup method:
119
+
120
+ ```rust
121
+ /// Returns the rect for the tab at the given index.
122
+ pub fn title_rect(&self, area: Rect, index: usize) -> Option<Rect> { ... }
123
+ ```
124
+
125
+ ## Workaround
126
+
127
+ Without this API, users must replicate the tab layout algorithm. Here is the current approach used in RatatuiRuby:
128
+
129
+ ```rust
130
+ // Manually compute tab title positions
131
+ let divider_width = 3; // " ▸ " is 3 characters
132
+ let content_row = area.y + 1; // Skip top border
133
+ let mut x = area.x + 2; // Skip left border + padding
134
+
135
+ let tab_rects: Vec<Rect> = TABS.iter().map(|title| {
136
+ let tab_width = title.len() as u16;
137
+ let rect = Rect::new(x, content_row, tab_width, 1);
138
+ x += tab_width + divider_width;
139
+ rect
140
+ }).collect();
141
+
142
+ // Hit testing
143
+ for (i, rect) in tab_rects.iter().enumerate() {
144
+ if rect.contains(click_position) {
145
+ current_tab = i;
146
+ break;
147
+ }
148
+ }
149
+ ```
150
+
151
+ This works for simple cases but breaks when:
152
+
153
+ - `padding_left` or `padding_right` are non-default
154
+ - The divider is styled (Span width differs from string length)
155
+ - A block wraps the tabs (inner area differs)
156
+ - Title text is styled (Line width differs from string length)
157
+
158
+ ## Impact
159
+
160
+ This feature benefits any application with clickable tabs:
161
+
162
+ - Tab-based navigation interfaces
163
+ - Multi-panel applications with panel selectors
164
+ - Mode switchers (edit/view/preview)
165
+ - Category selectors
166
+
167
+ The `Tabs` widget is commonly used for navigation. Mouse interaction is a natural expectation for TUI applications running in modern terminals with mouse support.
168
+
169
+ ---
170
+
171
+ This issue includes creative contributions from Claude (Anthropic) via Antigravity (Google). [https://declare-ai.org/1.0.0/creative.html](https://declare-ai.org/1.0.0/creative.html)
172
+
173
+ *Discovered while implementing click handling for tab navigation in RatatuiRuby.*
@@ -0,0 +1,132 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Feature Request: Expose Block Title Rects
7
+
8
+ ## Summary
9
+
10
+ `Block` computes the position of each title during rendering but does not expose this information. Interactive applications need title rects for mouse click hit-testing.
11
+
12
+ ## The Problem
13
+
14
+ Building clickable TUI interfaces requires knowing where widgets render. When a block has multiple titles (e.g., a left-aligned title and a right-aligned toggle label), each title occupies a specific screen region. The application cannot query these regions.
15
+
16
+ Currently, the only options are:
17
+
18
+ 1. **Recompute the layout manually.** Duplicate the logic from `render_left_titles`, `render_right_titles`, and `render_center_titles`. This is fragile—any upstream change breaks the user's code.
19
+ 2. **Use coarse hit-testing.** Check if a click is anywhere in the block's top border row. This cannot distinguish between multiple titles.
20
+
21
+ Neither approach is satisfactory.
22
+
23
+ ## Use Case
24
+
25
+ Consider a TUI with a tabbed interface where the block's title bar contains:
26
+
27
+ - A left-aligned title: `"Announce v0.7.3"`
28
+ - A right-aligned toggle: `"emate"` (clickable to switch email clients)
29
+
30
+ ```
31
+ ┌Announce v0.7.3───────────────────────────────emate┐
32
+ │ Preview Email ▸ Preview Commit ▸ Announce │
33
+ │ │
34
+ └───────────────────────────────────────────────────┘
35
+ ```
36
+
37
+ The application wants to:
38
+
39
+ 1. Detect clicks on `"emate"` and toggle the email client
40
+ 2. Detect clicks on tab names and switch tabs
41
+ 3. Ignore clicks on the border characters
42
+
43
+ Without title rects, the application must manually compute where `"emate"` renders based on block width, borders, alignment, and title content. This duplicates private logic from `Block::render_right_titles`.
44
+
45
+ ## Current State (v0.30.0)
46
+
47
+ `Block` has private methods that compute title areas:
48
+
49
+ ```rust
50
+ // Private - not accessible to users
51
+ fn titles_area(&self, area: Rect, position: Position) -> Rect { ... }
52
+ fn render_left_titles(&self, position: Position, area: Rect, buf: &mut Buffer) { ... }
53
+ fn render_center_titles(&self, position: Position, area: Rect, buf: &mut Buffer) { ... }
54
+ fn render_right_titles(&self, position: Position, area: Rect, buf: &mut Buffer) { ... }
55
+ ```
56
+
57
+ The `titles_area` method computes the general title region. The `render_*_titles` methods compute individual title rects during rendering but do not expose them.
58
+
59
+ ## Proposed API
60
+
61
+ Following the pattern established by `Block::inner(area)`, add a pure computation method that takes an area and returns computed sub-rects without rendering:
62
+
63
+ ```rust
64
+ impl Block {
65
+ /// Returns the bounding rect for each title given an area.
66
+ ///
67
+ /// The rects are returned in the same order as titles were added.
68
+ /// Useful for hit-testing mouse clicks against specific titles.
69
+ ///
70
+ /// # Example
71
+ ///
72
+ /// ```rust
73
+ /// let block = Block::bordered()
74
+ /// .title_top("Left Title")
75
+ /// .title_top(Line::from("Right").right_aligned());
76
+ ///
77
+ /// let rects = block.title_rects(area);
78
+ /// if rects[1].contains(mouse_position) {
79
+ /// // Clicked on "Right"
80
+ /// }
81
+ /// ```
82
+ pub fn title_rects(&self, area: Rect) -> Vec<Rect> { ... }
83
+ }
84
+ ```
85
+
86
+ Alternatively, expose the individual areas by alignment:
87
+
88
+ ```rust
89
+ /// Returns the rect for the title at the given index.
90
+ pub fn title_rect(&self, area: Rect, index: usize) -> Option<Rect> { ... }
91
+
92
+ /// Returns the titles area for a given position (top or bottom).
93
+ pub fn titles_area(&self, area: Rect, position: Position) -> Rect { ... }
94
+ ```
95
+
96
+ ## Workaround
97
+
98
+ Without this API, users must replicate the title layout algorithm. Here is the current approach used in RatatuiRuby:
99
+
100
+ ```rust
101
+ // Manually compute right-aligned title position
102
+ let email_label_width = email_client.len() as u16;
103
+ let email_label_x = area.x + area.width - email_label_width - 2; // border + padding
104
+ let email_label_rect = Rect::new(email_label_x, area.y, email_label_width, 1);
105
+
106
+ if email_label_rect.contains(click_position) {
107
+ toggle_email_client();
108
+ }
109
+ ```
110
+
111
+ This works for simple cases but breaks when:
112
+
113
+ - Borders are not all present (`Borders::LEFT` affects x offset)
114
+ - Multiple right-aligned titles exist (spacing affects position)
115
+ - Upstream layout logic changes
116
+
117
+ ## Impact
118
+
119
+ This feature benefits any application with interactive block titles:
120
+
121
+ - Clickable tabs in title bars
122
+ - Toggle buttons in headers
123
+ - Breadcrumb navigation
124
+ - Window controls (minimize/maximize/close buttons in the title area)
125
+
126
+ Related discussion: [ratatui#738](https://github.com/ratatui/ratatui/issues/738) (title positioning behavior)
127
+
128
+ ---
129
+
130
+ This issue includes creative contributions from Claude (Anthropic) via Antigravity (Google). [https://declare-ai.org/1.0.0/creative.html](https://declare-ai.org/1.0.0/creative.html)
131
+
132
+ *Discovered while implementing click handling for block title toggles in RatatuiRuby.*