ratatui_ruby 0.4.0 → 0.6.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/.builds/ruby-3.2.yml +1 -1
- data/.builds/ruby-3.3.yml +1 -1
- data/.builds/ruby-3.4.yml +1 -1
- data/.builds/ruby-4.0.0.yml +1 -1
- data/AGENTS.md +98 -176
- data/CHANGELOG.md +80 -6
- data/README.md +19 -7
- data/REUSE.toml +15 -0
- data/doc/application_architecture.md +179 -45
- data/doc/application_testing.md +80 -32
- data/doc/contributors/design/ruby_frontend.md +48 -8
- data/doc/contributors/design/rust_backend.md +1 -0
- data/doc/contributors/developing_examples.md +191 -48
- data/doc/contributors/documentation_style.md +7 -0
- data/doc/contributors/examples_audit/p1_high.md +21 -0
- data/doc/contributors/examples_audit/p2_moderate.md +81 -0
- data/doc/contributors/examples_audit.md +41 -0
- data/doc/contributors/index.md +2 -0
- data/doc/event_handling.md +21 -7
- data/doc/images/app_all_events.png +0 -0
- data/doc/images/app_color_picker.png +0 -0
- data/doc/images/app_login_form.png +0 -0
- data/doc/images/app_stateful_interaction.png +0 -0
- data/doc/images/verify_quickstart_dsl.png +0 -0
- data/doc/images/verify_quickstart_layout.png +0 -0
- data/doc/images/verify_quickstart_lifecycle.png +0 -0
- data/doc/images/verify_readme_usage.png +0 -0
- data/doc/images/widget_barchart_demo.png +0 -0
- data/doc/images/widget_block_demo.png +0 -0
- data/doc/images/widget_box_demo.png +0 -0
- data/doc/images/widget_calendar_demo.png +0 -0
- data/doc/images/widget_canvas_demo.png +0 -0
- data/doc/images/widget_cell_demo.png +0 -0
- data/doc/images/widget_center_demo.png +0 -0
- data/doc/images/widget_chart_demo.png +0 -0
- data/doc/images/widget_gauge_demo.png +0 -0
- data/doc/images/widget_layout_split.png +0 -0
- data/doc/images/widget_line_gauge_demo.png +0 -0
- data/doc/images/widget_list_demo.png +0 -0
- data/doc/images/widget_overlay_demo.png +0 -0
- data/doc/images/widget_ratatui_logo_demo.png +0 -0
- data/doc/images/widget_ratatui_mascot_demo.png +0 -0
- data/doc/images/widget_render.png +0 -0
- data/doc/images/widget_rich_text.png +0 -0
- data/doc/images/widget_scroll_text.png +0 -0
- data/doc/images/widget_scrollbar_demo.png +0 -0
- data/doc/images/widget_sparkline_demo.png +0 -0
- data/doc/images/widget_style_colors.png +0 -0
- data/doc/images/widget_table_demo.png +0 -0
- data/doc/images/widget_table_flex.png +0 -0
- data/doc/images/widget_tabs_demo.png +0 -0
- data/doc/images/widget_text_width.png +0 -0
- data/doc/interactive_design.md +25 -30
- data/doc/quickstart.md +150 -130
- data/doc/terminal_limitations.md +92 -0
- data/examples/app_all_events/README.md +99 -0
- data/examples/app_all_events/app.rb +96 -0
- data/examples/app_all_events/model/app_model.rb +157 -0
- data/examples/app_all_events/model/event_color_cycle.rb +41 -0
- data/examples/app_all_events/model/event_entry.rb +92 -0
- data/examples/app_all_events/model/msg.rb +37 -0
- data/examples/app_all_events/model/timestamp.rb +54 -0
- data/examples/app_all_events/update.rb +73 -0
- data/examples/app_all_events/view/app_view.rb +78 -0
- data/examples/app_all_events/view/controls_view.rb +52 -0
- data/examples/app_all_events/view/counts_view.rb +59 -0
- data/examples/app_all_events/view/live_view.rb +70 -0
- data/examples/app_all_events/view/log_view.rb +55 -0
- data/examples/app_all_events/view.rb +7 -0
- data/examples/app_color_picker/README.md +134 -0
- data/examples/app_color_picker/app.rb +74 -0
- data/examples/app_color_picker/clipboard.rb +84 -0
- data/examples/app_color_picker/color.rb +191 -0
- data/examples/app_color_picker/controls.rb +90 -0
- data/examples/app_color_picker/copy_dialog.rb +166 -0
- data/examples/app_color_picker/export_pane.rb +126 -0
- data/examples/app_color_picker/harmony.rb +56 -0
- data/examples/app_color_picker/input.rb +174 -0
- data/examples/app_color_picker/main_container.rb +178 -0
- data/examples/app_color_picker/palette.rb +109 -0
- data/examples/app_login_form/README.md +47 -0
- data/examples/{login_form → app_login_form}/app.rb +38 -42
- data/examples/app_stateful_interaction/README.md +31 -0
- data/examples/app_stateful_interaction/app.rb +272 -0
- data/examples/timeout_demo.rb +43 -0
- data/examples/verify_quickstart_dsl/README.md +48 -0
- data/examples/{quickstart_dsl → verify_quickstart_dsl}/app.rb +17 -6
- data/examples/verify_quickstart_layout/README.md +71 -0
- data/examples/verify_quickstart_layout/app.rb +71 -0
- data/examples/verify_quickstart_lifecycle/README.md +56 -0
- data/examples/verify_quickstart_lifecycle/app.rb +54 -0
- data/examples/verify_readme_usage/README.md +43 -0
- data/examples/verify_readme_usage/app.rb +40 -0
- data/examples/widget_barchart_demo/README.md +49 -0
- data/examples/widget_barchart_demo/app.rb +238 -0
- data/examples/widget_block_demo/README.md +34 -0
- data/examples/widget_block_demo/app.rb +256 -0
- data/examples/widget_box_demo/README.md +45 -0
- data/examples/{box_demo → widget_box_demo}/app.rb +99 -65
- data/examples/widget_calendar_demo/README.md +39 -0
- data/examples/widget_calendar_demo/app.rb +109 -0
- data/examples/widget_canvas_demo/README.md +27 -0
- data/examples/widget_canvas_demo/app.rb +123 -0
- data/examples/widget_cell_demo/README.md +36 -0
- data/examples/widget_cell_demo/app.rb +111 -0
- data/examples/widget_center_demo/README.md +29 -0
- data/examples/widget_center_demo/app.rb +116 -0
- data/examples/widget_chart_demo/README.md +41 -0
- data/examples/widget_chart_demo/app.rb +218 -0
- data/examples/widget_gauge_demo/README.md +41 -0
- data/examples/widget_gauge_demo/app.rb +212 -0
- data/examples/widget_layout_split/README.md +44 -0
- data/examples/widget_layout_split/app.rb +246 -0
- data/examples/widget_line_gauge_demo/README.md +41 -0
- data/examples/widget_line_gauge_demo/app.rb +217 -0
- data/examples/widget_list_demo/README.md +49 -0
- data/examples/widget_list_demo/app.rb +366 -0
- data/examples/widget_map_demo/README.md +39 -0
- data/examples/{map_demo → widget_map_demo}/app.rb +24 -21
- data/examples/widget_overlay_demo/app.rb +248 -0
- data/examples/widget_popup_demo/README.md +36 -0
- data/examples/widget_popup_demo/app.rb +104 -0
- data/examples/widget_ratatui_logo_demo/README.md +34 -0
- data/examples/widget_ratatui_logo_demo/app.rb +103 -0
- data/examples/widget_ratatui_mascot_demo/README.md +34 -0
- data/examples/widget_ratatui_mascot_demo/app.rb +93 -0
- data/examples/widget_rect/README.md +38 -0
- data/examples/widget_rect/app.rb +205 -0
- data/examples/widget_render/README.md +37 -0
- data/examples/widget_render/app.rb +184 -0
- data/examples/widget_rich_text/README.md +35 -0
- data/examples/widget_rich_text/app.rb +166 -0
- data/examples/widget_scroll_text/README.md +37 -0
- data/examples/widget_scroll_text/app.rb +107 -0
- data/examples/widget_scrollbar_demo/README.md +37 -0
- data/examples/widget_scrollbar_demo/app.rb +153 -0
- data/examples/widget_sparkline_demo/README.md +42 -0
- data/examples/widget_sparkline_demo/app.rb +275 -0
- data/examples/widget_style_colors/README.md +34 -0
- data/examples/widget_style_colors/app.rb +19 -21
- data/examples/widget_table_demo/README.md +48 -0
- data/examples/widget_table_demo/app.rb +239 -0
- data/examples/widget_tabs_demo/README.md +41 -0
- data/examples/widget_tabs_demo/app.rb +181 -0
- data/examples/widget_text_width/README.md +35 -0
- data/examples/widget_text_width/app.rb +106 -0
- data/ext/ratatui_ruby/Cargo.lock +11 -4
- data/ext/ratatui_ruby/Cargo.toml +2 -1
- data/ext/ratatui_ruby/src/events.rs +359 -62
- data/ext/ratatui_ruby/src/frame.rs +227 -0
- data/ext/ratatui_ruby/src/lib.rs +110 -27
- data/ext/ratatui_ruby/src/rendering.rs +8 -4
- data/ext/ratatui_ruby/src/string_width.rs +101 -0
- data/ext/ratatui_ruby/src/style.rs +138 -57
- data/ext/ratatui_ruby/src/terminal.rs +42 -22
- data/ext/ratatui_ruby/src/text.rs +14 -7
- data/ext/ratatui_ruby/src/widgets/barchart.rs +74 -54
- data/ext/ratatui_ruby/src/widgets/block.rs +7 -6
- data/ext/ratatui_ruby/src/widgets/canvas.rs +21 -3
- data/ext/ratatui_ruby/src/widgets/chart.rs +20 -10
- data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
- data/ext/ratatui_ruby/src/widgets/layout.rs +9 -4
- data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
- data/ext/ratatui_ruby/src/widgets/list.rs +211 -12
- data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
- data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
- data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
- data/ext/ratatui_ruby/src/widgets/paragraph.rs +1 -1
- data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +19 -8
- data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +17 -10
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +97 -3
- data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
- data/ext/ratatui_ruby/src/widgets/sparkline.rs +14 -11
- data/ext/ratatui_ruby/src/widgets/table.rs +121 -5
- data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
- data/ext/ratatui_ruby/src/widgets/tabs.rs +11 -11
- data/lib/ratatui_ruby/cell.rb +7 -7
- data/lib/ratatui_ruby/event/key/character.rb +35 -0
- data/lib/ratatui_ruby/event/key/media.rb +44 -0
- data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
- data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
- data/lib/ratatui_ruby/event/key/system.rb +45 -0
- data/lib/ratatui_ruby/event/key.rb +112 -52
- data/lib/ratatui_ruby/event/mouse.rb +3 -3
- data/lib/ratatui_ruby/event/none.rb +43 -0
- data/lib/ratatui_ruby/event/paste.rb +1 -1
- data/lib/ratatui_ruby/event.rb +56 -4
- data/lib/ratatui_ruby/frame.rb +183 -0
- data/lib/ratatui_ruby/list_state.rb +88 -0
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +13 -13
- data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +1 -5
- data/lib/ratatui_ruby/schema/bar_chart.rb +217 -217
- data/lib/ratatui_ruby/schema/block.rb +163 -168
- data/lib/ratatui_ruby/schema/calendar.rb +66 -67
- data/lib/ratatui_ruby/schema/canvas.rb +63 -63
- data/lib/ratatui_ruby/schema/center.rb +46 -46
- data/lib/ratatui_ruby/schema/chart.rb +135 -143
- data/lib/ratatui_ruby/schema/clear.rb +42 -42
- data/lib/ratatui_ruby/schema/constraint.rb +76 -76
- data/lib/ratatui_ruby/schema/cursor.rb +30 -25
- data/lib/ratatui_ruby/schema/gauge.rb +54 -52
- data/lib/ratatui_ruby/schema/layout.rb +87 -87
- data/lib/ratatui_ruby/schema/line_gauge.rb +62 -62
- data/lib/ratatui_ruby/schema/list.rb +103 -80
- data/lib/ratatui_ruby/schema/list_item.rb +41 -0
- data/lib/ratatui_ruby/schema/overlay.rb +31 -31
- data/lib/ratatui_ruby/schema/paragraph.rb +80 -80
- data/lib/ratatui_ruby/schema/ratatui_logo.rb +10 -6
- data/lib/ratatui_ruby/schema/ratatui_mascot.rb +10 -5
- data/lib/ratatui_ruby/schema/rect.rb +99 -56
- data/lib/ratatui_ruby/schema/scrollbar.rb +119 -119
- data/lib/ratatui_ruby/schema/shape/label.rb +1 -1
- data/lib/ratatui_ruby/schema/sparkline.rb +111 -110
- data/lib/ratatui_ruby/schema/style.rb +66 -46
- data/lib/ratatui_ruby/schema/table.rb +126 -115
- data/lib/ratatui_ruby/schema/tabs.rb +66 -67
- data/lib/ratatui_ruby/schema/text.rb +69 -1
- data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
- data/lib/ratatui_ruby/session/autodoc.rb +482 -0
- data/lib/ratatui_ruby/session.rb +55 -23
- data/lib/ratatui_ruby/table_state.rb +90 -0
- data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
- data/lib/ratatui_ruby/test_helper/snapshot.rb +390 -0
- data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
- data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
- data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
- data/lib/ratatui_ruby/test_helper.rb +66 -193
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +100 -51
- data/{examples/sparkline_demo → sig/examples/app_all_events}/app.rbs +3 -2
- data/sig/examples/app_all_events/model/event_entry.rbs +16 -0
- data/sig/examples/app_all_events/model/events.rbs +15 -0
- data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
- data/sig/examples/app_all_events/view/app_view.rbs +8 -0
- data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
- data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
- data/sig/examples/app_all_events/view/live_view.rbs +6 -0
- data/sig/examples/app_all_events/view/log_view.rbs +6 -0
- data/sig/examples/app_all_events/view.rbs +8 -0
- data/sig/examples/app_all_events/view_state.rbs +15 -0
- data/{examples/list_demo → sig/examples/app_color_picker}/app.rbs +2 -2
- data/sig/examples/app_login_form/app.rbs +11 -0
- data/sig/examples/app_stateful_interaction/app.rbs +33 -0
- data/sig/examples/verify_quickstart_dsl/app.rbs +11 -0
- data/sig/examples/verify_quickstart_lifecycle/app.rbs +11 -0
- data/sig/examples/verify_readme_usage/app.rbs +11 -0
- data/sig/examples/widget_block_demo/app.rbs +32 -0
- data/sig/examples/widget_box_demo/app.rbs +11 -0
- data/sig/examples/widget_calendar_demo/app.rbs +11 -0
- data/sig/examples/widget_cell_demo/app.rbs +11 -0
- data/sig/examples/widget_chart_demo/app.rbs +11 -0
- data/{examples/gauge_demo → sig/examples/widget_gauge_demo}/app.rbs +4 -0
- data/sig/examples/widget_layout_split/app.rbs +10 -0
- data/sig/examples/widget_line_gauge_demo/app.rbs +11 -0
- data/sig/examples/widget_list_demo/app.rbs +12 -0
- data/sig/examples/widget_map_demo/app.rbs +11 -0
- data/sig/examples/widget_popup_demo/app.rbs +11 -0
- data/sig/examples/widget_ratatui_logo_demo/app.rbs +11 -0
- data/sig/examples/widget_ratatui_mascot_demo/app.rbs +11 -0
- data/sig/examples/widget_rect/app.rbs +12 -0
- data/sig/examples/widget_render/app.rbs +10 -0
- data/sig/examples/widget_rich_text/app.rbs +11 -0
- data/sig/examples/widget_scroll_text/app.rbs +11 -0
- data/sig/examples/widget_scrollbar_demo/app.rbs +11 -0
- data/sig/examples/widget_sparkline_demo/app.rbs +10 -0
- data/{examples → sig/examples}/widget_style_colors/app.rbs +1 -1
- data/sig/examples/widget_table_demo/app.rbs +11 -0
- data/sig/examples/widget_text_width/app.rbs +10 -0
- data/sig/ratatui_ruby/event.rbs +11 -1
- data/sig/ratatui_ruby/frame.rbs +11 -0
- data/sig/ratatui_ruby/list_state.rbs +13 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +5 -4
- data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
- data/sig/ratatui_ruby/schema/draw.rbs +4 -0
- data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
- data/sig/ratatui_ruby/schema/layout.rbs +1 -1
- data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
- data/sig/ratatui_ruby/schema/list.rbs +4 -2
- data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
- data/sig/ratatui_ruby/schema/rect.rbs +3 -0
- data/sig/ratatui_ruby/schema/style.rbs +3 -3
- data/sig/ratatui_ruby/schema/table.rbs +3 -1
- data/sig/ratatui_ruby/schema/text.rbs +8 -6
- data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
- data/sig/ratatui_ruby/session.rbs +107 -0
- data/sig/ratatui_ruby/table_state.rbs +15 -0
- data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
- data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
- data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
- data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
- data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
- data/sig/ratatui_ruby/test_helper.rbs +5 -4
- data/tasks/autodoc/examples.rb +79 -0
- data/tasks/autodoc/inventory.rb +63 -0
- data/tasks/autodoc/member.rb +56 -0
- data/tasks/autodoc/name.rb +19 -0
- data/tasks/autodoc/notice.rb +26 -0
- data/tasks/autodoc/rbs.rb +38 -0
- data/tasks/autodoc/rdoc.rb +45 -0
- data/tasks/autodoc.rake +53 -0
- data/tasks/bump/changelog.rb +3 -3
- data/tasks/bump/history.rb +2 -2
- data/tasks/bump/links.rb +67 -0
- data/tasks/doc.rake +600 -6
- data/tasks/example_viewer.html.erb +172 -0
- data/tasks/lint.rake +8 -4
- data/tasks/resources/index.html.erb +6 -0
- data/tasks/sourcehut.rake +70 -30
- data/tasks/terminal_preview/app_screenshot.rb +14 -6
- data/tasks/terminal_preview/crash_report.rb +7 -9
- data/tasks/terminal_preview/launcher_script.rb +4 -6
- data/tasks/terminal_preview/preview_collection.rb +4 -6
- data/tasks/terminal_preview/safety_confirmation.rb +3 -5
- data/tasks/terminal_preview/saved_screenshot.rb +10 -11
- data/tasks/terminal_preview/terminal_window.rb +7 -9
- data/tasks/test.rake +1 -1
- data/tasks/website/index_page.rb +3 -3
- data/tasks/website/version.rb +10 -10
- data/tasks/website/version_menu.rb +10 -12
- data/tasks/website/versioned_documentation.rb +49 -17
- data/tasks/website/website.rb +6 -8
- data/tasks/website.rake +4 -4
- metadata +232 -127
- data/LICENSES/BSD-2-Clause.txt +0 -9
- data/doc/contributors/better_dx.md +0 -543
- data/doc/contributors/example_analysis.md +0 -82
- data/doc/images/all_events.png +0 -0
- data/doc/images/block_padding.png +0 -0
- data/doc/images/block_titles.png +0 -0
- data/doc/images/box_demo.png +0 -0
- data/doc/images/calendar_demo.png +0 -0
- data/doc/images/cell_demo.png +0 -0
- data/doc/images/chart_demo.png +0 -0
- data/doc/images/flex_layout.png +0 -0
- data/doc/images/gauge_demo.png +0 -0
- data/doc/images/line_gauge_demo.png +0 -0
- data/doc/images/list_demo.png +0 -0
- data/doc/images/list_styles.png +0 -0
- data/doc/images/login_form.png +0 -0
- data/doc/images/quickstart_dsl.png +0 -0
- data/doc/images/quickstart_lifecycle.png +0 -0
- data/doc/images/readme_usage.png +0 -0
- data/doc/images/rich_text.png +0 -0
- data/doc/images/scroll_text.png +0 -0
- data/doc/images/scrollbar_demo.png +0 -0
- data/doc/images/sparkline_demo.png +0 -0
- data/doc/images/table_flex.png +0 -0
- data/doc/images/table_select.png +0 -0
- data/examples/all_events/app.rb +0 -169
- data/examples/all_events/app.rbs +0 -7
- data/examples/all_events/test_app.rb +0 -139
- data/examples/analytics/app.rb +0 -258
- data/examples/analytics/app.rbs +0 -7
- data/examples/analytics/test_app.rb +0 -132
- data/examples/block_padding/app.rb +0 -63
- data/examples/block_padding/app.rbs +0 -7
- data/examples/block_padding/test_app.rb +0 -31
- data/examples/block_titles/app.rb +0 -61
- data/examples/block_titles/app.rbs +0 -7
- data/examples/block_titles/test_app.rb +0 -34
- data/examples/box_demo/app.rbs +0 -7
- data/examples/box_demo/test_app.rb +0 -88
- data/examples/calendar_demo/app.rb +0 -101
- data/examples/calendar_demo/app.rbs +0 -7
- data/examples/calendar_demo/test_app.rb +0 -108
- data/examples/cell_demo/app.rb +0 -108
- data/examples/cell_demo/app.rbs +0 -7
- data/examples/cell_demo/test_app.rb +0 -36
- data/examples/chart_demo/app.rb +0 -203
- data/examples/chart_demo/app.rbs +0 -7
- data/examples/chart_demo/test_app.rb +0 -102
- data/examples/custom_widget/app.rb +0 -51
- data/examples/custom_widget/app.rbs +0 -7
- data/examples/custom_widget/test_app.rb +0 -30
- data/examples/flex_layout/app.rb +0 -156
- data/examples/flex_layout/app.rbs +0 -7
- data/examples/flex_layout/test_app.rb +0 -65
- data/examples/gauge_demo/app.rb +0 -182
- data/examples/gauge_demo/test_app.rb +0 -120
- data/examples/hit_test/app.rb +0 -175
- data/examples/hit_test/app.rbs +0 -7
- data/examples/hit_test/test_app.rb +0 -102
- data/examples/line_gauge_demo/app.rb +0 -190
- data/examples/line_gauge_demo/app.rbs +0 -7
- data/examples/line_gauge_demo/test_app.rb +0 -129
- data/examples/list_demo/app.rb +0 -253
- data/examples/list_demo/test_app.rb +0 -237
- data/examples/list_styles/app.rb +0 -140
- data/examples/list_styles/app.rbs +0 -7
- data/examples/list_styles/test_app.rb +0 -157
- data/examples/login_form/app.rbs +0 -7
- data/examples/login_form/test_app.rb +0 -51
- data/examples/map_demo/app.rbs +0 -7
- data/examples/map_demo/test_app.rb +0 -149
- data/examples/mouse_events/app.rb +0 -97
- data/examples/mouse_events/app.rbs +0 -7
- data/examples/mouse_events/test_app.rb +0 -53
- data/examples/popup_demo/app.rb +0 -103
- data/examples/popup_demo/app.rbs +0 -7
- data/examples/popup_demo/test_app.rb +0 -54
- data/examples/quickstart_dsl/app.rbs +0 -7
- data/examples/quickstart_dsl/test_app.rb +0 -29
- data/examples/quickstart_lifecycle/app.rb +0 -39
- data/examples/quickstart_lifecycle/app.rbs +0 -7
- data/examples/quickstart_lifecycle/test_app.rb +0 -29
- data/examples/ratatui_logo_demo/app.rb +0 -79
- data/examples/ratatui_logo_demo/app.rbs +0 -7
- data/examples/ratatui_logo_demo/test_app.rb +0 -51
- data/examples/ratatui_mascot_demo/app.rb +0 -84
- data/examples/ratatui_mascot_demo/app.rbs +0 -7
- data/examples/ratatui_mascot_demo/test_app.rb +0 -47
- data/examples/readme_usage/app.rb +0 -29
- data/examples/readme_usage/app.rbs +0 -7
- data/examples/readme_usage/test_app.rb +0 -29
- data/examples/rich_text/app.rb +0 -141
- data/examples/rich_text/app.rbs +0 -7
- data/examples/rich_text/test_app.rb +0 -166
- data/examples/scroll_text/app.rb +0 -103
- data/examples/scroll_text/app.rbs +0 -7
- data/examples/scroll_text/test_app.rb +0 -110
- data/examples/scrollbar_demo/app.rb +0 -143
- data/examples/scrollbar_demo/app.rbs +0 -7
- data/examples/scrollbar_demo/test_app.rb +0 -77
- data/examples/sparkline_demo/app.rb +0 -240
- data/examples/sparkline_demo/test_app.rb +0 -107
- data/examples/table_flex/app.rb +0 -65
- data/examples/table_flex/app.rbs +0 -7
- data/examples/table_flex/test_app.rb +0 -36
- data/examples/table_select/app.rb +0 -198
- data/examples/table_select/app.rbs +0 -7
- data/examples/table_select/test_app.rb +0 -180
- data/examples/widget_style_colors/test_app.rb +0 -48
- data/tasks/bump/comparison_links.rb +0 -41
- /data/doc/images/{analytics.png → app_analytics.png} +0 -0
- /data/doc/images/{custom_widget.png → app_custom_widget.png} +0 -0
- /data/doc/images/{mouse_events.png → app_mouse_events.png} +0 -0
- /data/doc/images/{map_demo.png → widget_map_demo.png} +0 -0
- /data/doc/images/{popup_demo.png → widget_popup_demo.png} +0 -0
- /data/doc/images/{hit_test.png → widget_rect.png} +0 -0
- /data/{doc/images/ratatui_logo_demo.png → exe/.gitkeep} +0 -0
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
use crate::style::{parse_block, parse_style};
|
|
5
|
+
use crate::text::{parse_line, parse_span};
|
|
6
|
+
use crate::widgets::list_state::RubyListState;
|
|
5
7
|
use bumpalo::Bump;
|
|
6
8
|
use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
|
|
7
9
|
use ratatui::{
|
|
8
10
|
layout::Rect,
|
|
9
11
|
text::Line,
|
|
10
|
-
widgets::{HighlightSpacing, List, ListState},
|
|
12
|
+
widgets::{HighlightSpacing, List, ListItem, ListState},
|
|
11
13
|
Frame,
|
|
12
14
|
};
|
|
13
15
|
|
|
@@ -27,10 +29,12 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
27
29
|
let scroll_padding_val: Value = node.funcall("scroll_padding", ())?;
|
|
28
30
|
let block_val: Value = node.funcall("block", ())?;
|
|
29
31
|
|
|
30
|
-
let mut items: Vec<
|
|
32
|
+
let mut items: Vec<ListItem> = Vec::new();
|
|
31
33
|
for i in 0..items_array.len() {
|
|
32
|
-
let index = isize::try_from(i)
|
|
33
|
-
|
|
34
|
+
let index = isize::try_from(i)
|
|
35
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
36
|
+
let item_val: Value = items_array.entry(index)?;
|
|
37
|
+
let item = parse_list_item(item_val)?;
|
|
34
38
|
items.push(item);
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -46,6 +50,12 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
46
50
|
state.select(Some(index));
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
let offset_val: Value = node.funcall("offset", ())?;
|
|
54
|
+
if !offset_val.is_nil() {
|
|
55
|
+
let offset: usize = offset_val.funcall("to_int", ())?;
|
|
56
|
+
*state.offset_mut() = offset;
|
|
57
|
+
}
|
|
58
|
+
|
|
49
59
|
let mut list = List::new(items);
|
|
50
60
|
|
|
51
61
|
let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
|
|
@@ -100,6 +110,173 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
100
110
|
Ok(())
|
|
101
111
|
}
|
|
102
112
|
|
|
113
|
+
/// Renders a List with an external state object.
|
|
114
|
+
///
|
|
115
|
+
/// This function ignores `selected_index` and `offset` from the widget.
|
|
116
|
+
/// The State object is the single source of truth for selection and scroll position.
|
|
117
|
+
pub fn render_stateful(
|
|
118
|
+
frame: &mut Frame,
|
|
119
|
+
area: Rect,
|
|
120
|
+
node: Value,
|
|
121
|
+
state_wrapper: Value,
|
|
122
|
+
) -> Result<(), Error> {
|
|
123
|
+
let bump = Bump::new();
|
|
124
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
125
|
+
|
|
126
|
+
// Extract the RubyListState wrapper
|
|
127
|
+
let state: &RubyListState = TryConvert::try_convert(state_wrapper)?;
|
|
128
|
+
|
|
129
|
+
// Build items
|
|
130
|
+
let items_val: Value = node.funcall("items", ())?;
|
|
131
|
+
let items_array = magnus::RArray::from_value(items_val)
|
|
132
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array"))?;
|
|
133
|
+
|
|
134
|
+
let mut items: Vec<ListItem> = Vec::new();
|
|
135
|
+
for i in 0..items_array.len() {
|
|
136
|
+
let index = isize::try_from(i)
|
|
137
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
138
|
+
let item_val: Value = items_array.entry(index)?;
|
|
139
|
+
let item = parse_list_item(item_val)?;
|
|
140
|
+
items.push(item);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Build widget (ignoring selected_index and offset — State is truth)
|
|
144
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
145
|
+
let highlight_style_val: Value = node.funcall("highlight_style", ())?;
|
|
146
|
+
let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
|
|
147
|
+
let repeat_highlight_symbol_val: Value = node.funcall("repeat_highlight_symbol", ())?;
|
|
148
|
+
let highlight_spacing_sym: Symbol = node.funcall("highlight_spacing", ())?;
|
|
149
|
+
let direction_val: Value = node.funcall("direction", ())?;
|
|
150
|
+
let scroll_padding_val: Value = node.funcall("scroll_padding", ())?;
|
|
151
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
152
|
+
|
|
153
|
+
let symbol: String = if highlight_symbol_val.is_nil() {
|
|
154
|
+
String::new()
|
|
155
|
+
} else {
|
|
156
|
+
String::try_convert(highlight_symbol_val)?
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
let mut list = List::new(items);
|
|
160
|
+
|
|
161
|
+
let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
|
|
162
|
+
"always" => HighlightSpacing::Always,
|
|
163
|
+
"never" => HighlightSpacing::Never,
|
|
164
|
+
_ => HighlightSpacing::WhenSelected,
|
|
165
|
+
};
|
|
166
|
+
list = list.highlight_spacing(highlight_spacing);
|
|
167
|
+
|
|
168
|
+
if !highlight_symbol_val.is_nil() {
|
|
169
|
+
list = list.highlight_symbol(Line::from(symbol));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if !repeat_highlight_symbol_val.is_nil() {
|
|
173
|
+
let repeat: bool = TryConvert::try_convert(repeat_highlight_symbol_val)?;
|
|
174
|
+
list = list.repeat_highlight_symbol(repeat);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if !direction_val.is_nil() {
|
|
178
|
+
let direction_sym: magnus::Symbol = TryConvert::try_convert(direction_val)?;
|
|
179
|
+
let direction_str = direction_sym.name().unwrap();
|
|
180
|
+
match direction_str.as_ref() {
|
|
181
|
+
"top_to_bottom" => list = list.direction(ratatui::widgets::ListDirection::TopToBottom),
|
|
182
|
+
"bottom_to_top" => list = list.direction(ratatui::widgets::ListDirection::BottomToTop),
|
|
183
|
+
_ => {
|
|
184
|
+
return Err(Error::new(
|
|
185
|
+
ruby.exception_arg_error(),
|
|
186
|
+
"direction must be :top_to_bottom or :bottom_to_top",
|
|
187
|
+
))
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if !scroll_padding_val.is_nil() {
|
|
193
|
+
let padding: usize = TryConvert::try_convert(scroll_padding_val)?;
|
|
194
|
+
list = list.scroll_padding(padding);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if !style_val.is_nil() {
|
|
198
|
+
list = list.style(parse_style(style_val)?);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if !highlight_style_val.is_nil() {
|
|
202
|
+
list = list.highlight_style(parse_style(highlight_style_val)?);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if !block_val.is_nil() {
|
|
206
|
+
list = list.block(parse_block(block_val, &bump)?);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Borrow the inner ListState, render, and release the borrow immediately
|
|
210
|
+
{
|
|
211
|
+
let mut inner_state = state.borrow_mut();
|
|
212
|
+
frame.render_stateful_widget(list, area, &mut inner_state);
|
|
213
|
+
}
|
|
214
|
+
// Borrow is now released
|
|
215
|
+
|
|
216
|
+
Ok(())
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Parses a Ruby list item into a ratatui `ListItem`.
|
|
220
|
+
///
|
|
221
|
+
/// Accepts:
|
|
222
|
+
/// - `String`: Plain text item
|
|
223
|
+
/// - `Text::Span`: A single styled fragment
|
|
224
|
+
/// - `Text::Line`: A line composed of multiple spans
|
|
225
|
+
/// - `RatatuiRuby::ListItem`: A `ListItem` object with content and optional style
|
|
226
|
+
fn parse_list_item(value: Value) -> Result<ListItem<'static>, Error> {
|
|
227
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
228
|
+
|
|
229
|
+
// Check if it's a RatatuiRuby::ListItem
|
|
230
|
+
if let Ok(class_obj) = value.funcall::<_, _, Value>("class", ()) {
|
|
231
|
+
if let Ok(class_name) = class_obj.funcall::<_, _, String>("name", ()) {
|
|
232
|
+
if class_name.contains("ListItem") {
|
|
233
|
+
// Extract content and style from the ListItem
|
|
234
|
+
let content_val: Value = value.funcall("content", ())?;
|
|
235
|
+
let style_val: Value = value.funcall("style", ())?;
|
|
236
|
+
|
|
237
|
+
// Parse content as a Line
|
|
238
|
+
let line = if let Ok(s) = String::try_convert(content_val) {
|
|
239
|
+
Line::from(s)
|
|
240
|
+
} else if let Ok(line) = parse_line(content_val) {
|
|
241
|
+
line
|
|
242
|
+
} else if let Ok(span) = parse_span(content_val) {
|
|
243
|
+
Line::from(vec![span])
|
|
244
|
+
} else {
|
|
245
|
+
Line::from("")
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Parse and apply style if present
|
|
249
|
+
let mut item = ListItem::new(line);
|
|
250
|
+
if !style_val.is_nil() {
|
|
251
|
+
item = item.style(parse_style(style_val)?);
|
|
252
|
+
}
|
|
253
|
+
return Ok(item);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Try as String
|
|
259
|
+
if let Ok(s) = String::try_convert(value) {
|
|
260
|
+
return Ok(ListItem::new(Line::from(s)));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Try as Line
|
|
264
|
+
if let Ok(line) = parse_line(value) {
|
|
265
|
+
return Ok(ListItem::new(line));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Try as Span
|
|
269
|
+
if let Ok(span) = parse_span(value) {
|
|
270
|
+
return Ok(ListItem::new(Line::from(vec![span])));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Fallback
|
|
274
|
+
Err(Error::new(
|
|
275
|
+
ruby.exception_type_error(),
|
|
276
|
+
"expected String, Text::Span, Text::Line, or ListItem",
|
|
277
|
+
))
|
|
278
|
+
}
|
|
279
|
+
|
|
103
280
|
#[cfg(test)]
|
|
104
281
|
mod tests {
|
|
105
282
|
use super::*;
|
|
@@ -142,21 +319,41 @@ mod tests {
|
|
|
142
319
|
fn test_repeat_highlight_symbol() {
|
|
143
320
|
let items = vec!["Item 1", "Item 2"];
|
|
144
321
|
let list_without_repeat = List::new(items.clone()).highlight_symbol(Line::from(">> "));
|
|
145
|
-
let list_with_repeat = List::new(items)
|
|
146
|
-
|
|
322
|
+
let list_with_repeat = List::new(items)
|
|
323
|
+
.highlight_symbol(Line::from(">> "))
|
|
324
|
+
.repeat_highlight_symbol(true);
|
|
325
|
+
|
|
147
326
|
let mut state = ListState::default();
|
|
148
327
|
state.select(Some(0));
|
|
149
328
|
|
|
150
329
|
let mut buf1 = Buffer::empty(Rect::new(0, 0, 10, 2));
|
|
151
330
|
use ratatui::widgets::StatefulWidget;
|
|
152
|
-
StatefulWidget::render(
|
|
331
|
+
StatefulWidget::render(
|
|
332
|
+
list_without_repeat,
|
|
333
|
+
Rect::new(0, 0, 10, 2),
|
|
334
|
+
&mut buf1,
|
|
335
|
+
&mut state,
|
|
336
|
+
);
|
|
153
337
|
|
|
154
338
|
let mut buf2 = Buffer::empty(Rect::new(0, 0, 10, 2));
|
|
155
|
-
StatefulWidget::render(
|
|
339
|
+
StatefulWidget::render(
|
|
340
|
+
list_with_repeat,
|
|
341
|
+
Rect::new(0, 0, 10, 2),
|
|
342
|
+
&mut buf2,
|
|
343
|
+
&mut state,
|
|
344
|
+
);
|
|
156
345
|
|
|
157
346
|
// Both should render, but the behavior might differ based on content width
|
|
158
|
-
let content1 = buf1
|
|
159
|
-
|
|
347
|
+
let content1 = buf1
|
|
348
|
+
.content()
|
|
349
|
+
.iter()
|
|
350
|
+
.map(|c| c.symbol())
|
|
351
|
+
.collect::<String>();
|
|
352
|
+
let content2 = buf2
|
|
353
|
+
.content()
|
|
354
|
+
.iter()
|
|
355
|
+
.map(|c| c.symbol())
|
|
356
|
+
.collect::<String>();
|
|
160
357
|
assert!(!content1.is_empty());
|
|
161
358
|
assert!(!content2.is_empty());
|
|
162
359
|
}
|
|
@@ -164,8 +361,10 @@ mod tests {
|
|
|
164
361
|
#[test]
|
|
165
362
|
fn test_scroll_padding() {
|
|
166
363
|
let items = vec!["Item 1", "Item 2", "Item 3", "Item 4"];
|
|
167
|
-
let list = List::new(items)
|
|
168
|
-
|
|
364
|
+
let list = List::new(items)
|
|
365
|
+
.scroll_padding(1)
|
|
366
|
+
.highlight_symbol(Line::from(">> "));
|
|
367
|
+
|
|
169
368
|
let mut state = ListState::default();
|
|
170
369
|
state.select(Some(1));
|
|
171
370
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
//! `ListState` wrapper for exposing Ratatui's `ListState` to Ruby.
|
|
5
|
+
//!
|
|
6
|
+
//! This module provides `RubyListState`, a Magnus-wrapped struct that holds
|
|
7
|
+
//! a `RefCell<ListState>` for interior mutability during stateful rendering.
|
|
8
|
+
//!
|
|
9
|
+
//! # Design
|
|
10
|
+
//!
|
|
11
|
+
//! When using `render_stateful_widget`, the State object is the single source
|
|
12
|
+
//! of truth for selection and offset. Widget properties (`selected_index`, `offset`)
|
|
13
|
+
//! are ignored in stateful mode.
|
|
14
|
+
//!
|
|
15
|
+
//! # Safety
|
|
16
|
+
//!
|
|
17
|
+
//! The `RefCell` is borrowed only during the `render_stateful_widget` call.
|
|
18
|
+
//! The borrow is released immediately after to avoid double-borrow panics
|
|
19
|
+
//! if a user inspects state inside a custom widget's render method.
|
|
20
|
+
|
|
21
|
+
use magnus::{function, method, prelude::*, Error, Module, Ruby};
|
|
22
|
+
use ratatui::widgets::ListState;
|
|
23
|
+
use std::cell::RefCell;
|
|
24
|
+
|
|
25
|
+
/// A wrapper around Ratatui's `ListState` exposed to Ruby.
|
|
26
|
+
///
|
|
27
|
+
/// This struct uses `RefCell` for interior mutability, allowing the state
|
|
28
|
+
/// to be updated during rendering while remaining accessible from Ruby.
|
|
29
|
+
#[magnus::wrap(class = "RatatuiRuby::ListState")]
|
|
30
|
+
pub struct RubyListState {
|
|
31
|
+
inner: RefCell<ListState>,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
impl RubyListState {
|
|
35
|
+
/// Creates a new `RubyListState` with optional initial selection.
|
|
36
|
+
///
|
|
37
|
+
/// # Arguments
|
|
38
|
+
///
|
|
39
|
+
/// * `selected` - Optional initial selection index
|
|
40
|
+
pub fn new(selected: Option<usize>) -> Self {
|
|
41
|
+
let mut state = ListState::default();
|
|
42
|
+
if let Some(idx) = selected {
|
|
43
|
+
state.select(Some(idx));
|
|
44
|
+
}
|
|
45
|
+
Self {
|
|
46
|
+
inner: RefCell::new(state),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Sets the selected index.
|
|
51
|
+
///
|
|
52
|
+
/// Pass `nil` to deselect.
|
|
53
|
+
pub fn select(&self, index: Option<usize>) {
|
|
54
|
+
self.inner.borrow_mut().select(index);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Returns the currently selected index, or `nil` if nothing is selected.
|
|
58
|
+
pub fn selected(&self) -> Option<usize> {
|
|
59
|
+
self.inner.borrow().selected()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Returns the current scroll offset.
|
|
63
|
+
///
|
|
64
|
+
/// This is the critical read-back method. After `render_stateful_widget`,
|
|
65
|
+
/// this returns the scroll position calculated by Ratatui to keep the
|
|
66
|
+
/// selection visible.
|
|
67
|
+
pub fn offset(&self) -> usize {
|
|
68
|
+
self.inner.borrow().offset()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Scrolls down by the given number of items.
|
|
72
|
+
pub fn scroll_down_by(&self, amount: u16) {
|
|
73
|
+
self.inner.borrow_mut().scroll_down_by(amount);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Scrolls up by the given number of items.
|
|
77
|
+
pub fn scroll_up_by(&self, amount: u16) {
|
|
78
|
+
self.inner.borrow_mut().scroll_up_by(amount);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Borrows the inner `ListState` mutably for rendering.
|
|
82
|
+
///
|
|
83
|
+
/// # Safety
|
|
84
|
+
///
|
|
85
|
+
/// The caller must ensure the borrow is released before returning
|
|
86
|
+
/// control to Ruby to avoid double-borrow panics.
|
|
87
|
+
pub fn borrow_mut(&self) -> std::cell::RefMut<'_, ListState> {
|
|
88
|
+
self.inner.borrow_mut()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Registers the `ListState` class with Ruby.
|
|
93
|
+
pub fn register(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
|
|
94
|
+
let class = module.define_class("ListState", ruby.class_object())?;
|
|
95
|
+
class.define_singleton_method("new", function!(RubyListState::new, 1))?;
|
|
96
|
+
class.define_method("select", method!(RubyListState::select, 1))?;
|
|
97
|
+
class.define_method("selected", method!(RubyListState::selected, 0))?;
|
|
98
|
+
class.define_method("offset", method!(RubyListState::offset, 0))?;
|
|
99
|
+
class.define_method("scroll_down_by", method!(RubyListState::scroll_down_by, 1))?;
|
|
100
|
+
class.define_method("scroll_up_by", method!(RubyListState::scroll_up_by, 1))?;
|
|
101
|
+
Ok(())
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[cfg(test)]
|
|
105
|
+
mod tests {
|
|
106
|
+
use super::*;
|
|
107
|
+
|
|
108
|
+
#[test]
|
|
109
|
+
fn test_new_with_no_selection() {
|
|
110
|
+
let state = RubyListState::new(None);
|
|
111
|
+
assert_eq!(state.selected(), None);
|
|
112
|
+
assert_eq!(state.offset(), 0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#[test]
|
|
116
|
+
fn test_new_with_selection() {
|
|
117
|
+
let state = RubyListState::new(Some(5));
|
|
118
|
+
assert_eq!(state.selected(), Some(5));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#[test]
|
|
122
|
+
fn test_select_and_deselect() {
|
|
123
|
+
let state = RubyListState::new(None);
|
|
124
|
+
state.select(Some(3));
|
|
125
|
+
assert_eq!(state.selected(), Some(3));
|
|
126
|
+
state.select(None);
|
|
127
|
+
assert_eq!(state.selected(), None);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[test]
|
|
131
|
+
fn test_scroll_operations() {
|
|
132
|
+
let state = RubyListState::new(None);
|
|
133
|
+
state.scroll_down_by(5);
|
|
134
|
+
// Note: scroll operations affect offset, but the exact behavior
|
|
135
|
+
// depends on the list size which is determined during rendering
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -13,11 +13,14 @@ pub mod gauge;
|
|
|
13
13
|
pub mod layout;
|
|
14
14
|
pub mod line_gauge;
|
|
15
15
|
pub mod list;
|
|
16
|
+
pub mod list_state;
|
|
16
17
|
pub mod overlay;
|
|
17
18
|
pub mod paragraph;
|
|
18
19
|
pub mod ratatui_logo;
|
|
19
20
|
pub mod ratatui_mascot;
|
|
20
21
|
pub mod scrollbar;
|
|
22
|
+
pub mod scrollbar_state;
|
|
21
23
|
pub mod sparkline;
|
|
22
24
|
pub mod table;
|
|
25
|
+
pub mod table_state;
|
|
23
26
|
pub mod tabs;
|
|
@@ -12,7 +12,8 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
12
12
|
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for layers"))?;
|
|
13
13
|
|
|
14
14
|
for i in 0..layers_array.len() {
|
|
15
|
-
let index = isize::try_from(i)
|
|
15
|
+
let index = isize::try_from(i)
|
|
16
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
16
17
|
let layer: Value = layers_array.entry(index)?;
|
|
17
18
|
if let Err(e) = render_node(frame, area, layer) {
|
|
18
19
|
eprintln!("Error rendering overlay layer {i}: {e:?}");
|
|
@@ -12,7 +12,7 @@ use ratatui::{
|
|
|
12
12
|
|
|
13
13
|
use crate::text::parse_text;
|
|
14
14
|
|
|
15
|
-
fn create_paragraph
|
|
15
|
+
fn create_paragraph(node: Value, bump: &Bump) -> Result<Paragraph<'_>, Error> {
|
|
16
16
|
let text_val: Value = node.funcall("text", ())?;
|
|
17
17
|
let style_val: Value = node.funcall("style", ())?;
|
|
18
18
|
let block_val: Value = node.funcall("block", ())?;
|
|
@@ -1,27 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
use magnus::Value;
|
|
6
|
+
use ratatui::{
|
|
7
|
+
layout::Rect,
|
|
8
|
+
widgets::{RatatuiLogo, RatatuiLogoSize},
|
|
9
|
+
Frame,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
pub fn render(frame: &mut Frame, area: Rect, _node: Value) {
|
|
5
13
|
// RatatuiLogo does not support custom styling (it has fixed colors).
|
|
6
14
|
// It requires a size argument.
|
|
7
15
|
let widget = RatatuiLogo::new(RatatuiLogoSize::Small);
|
|
8
16
|
frame.render_widget(widget, area);
|
|
9
|
-
Ok(())
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
#[cfg(test)]
|
|
13
20
|
mod tests {
|
|
14
|
-
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
|
15
21
|
use super::*;
|
|
22
|
+
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
|
16
23
|
|
|
17
24
|
#[test]
|
|
18
25
|
fn test_render() {
|
|
19
26
|
let mut buffer = Buffer::empty(Rect::new(0, 0, 50, 20));
|
|
20
27
|
let widget = RatatuiLogo::new(RatatuiLogoSize::Small);
|
|
21
28
|
widget.render(Rect::new(0, 0, 50, 20), &mut buffer);
|
|
22
|
-
|
|
23
|
-
let content = buffer
|
|
24
|
-
|
|
29
|
+
|
|
30
|
+
let content = buffer
|
|
31
|
+
.content()
|
|
32
|
+
.iter()
|
|
33
|
+
.map(|c| c.symbol())
|
|
34
|
+
.collect::<String>();
|
|
35
|
+
|
|
25
36
|
// The logo uses block characters for rendering
|
|
26
37
|
assert!(content.contains('█'));
|
|
27
38
|
assert!(!content.trim().is_empty());
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
|
|
1
5
|
use crate::style::parse_block;
|
|
2
6
|
use bumpalo::Bump;
|
|
3
7
|
use magnus::{prelude::*, Error, Value};
|
|
4
|
-
use ratatui::{
|
|
5
|
-
layout::Rect,
|
|
6
|
-
widgets::RatatuiMascot,
|
|
7
|
-
Frame,
|
|
8
|
-
};
|
|
8
|
+
use ratatui::{layout::Rect, widgets::RatatuiMascot, Frame};
|
|
9
9
|
|
|
10
10
|
pub fn render_ratatui_mascot(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
11
11
|
let block_val: Value = node.funcall("block", ())?;
|
|
@@ -26,19 +26,26 @@ pub fn render_ratatui_mascot(frame: &mut Frame, area: Rect, node: Value) -> Resu
|
|
|
26
26
|
|
|
27
27
|
#[cfg(test)]
|
|
28
28
|
mod tests {
|
|
29
|
-
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
|
30
29
|
use super::*;
|
|
30
|
+
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
|
31
31
|
|
|
32
32
|
#[test]
|
|
33
33
|
fn test_render() {
|
|
34
34
|
let mut buffer = Buffer::empty(Rect::new(0, 0, 50, 20));
|
|
35
35
|
let widget = RatatuiMascot::new();
|
|
36
36
|
widget.render(Rect::new(0, 0, 50, 20), &mut buffer);
|
|
37
|
-
|
|
38
|
-
let content = buffer
|
|
39
|
-
|
|
37
|
+
|
|
38
|
+
let content = buffer
|
|
39
|
+
.content()
|
|
40
|
+
.iter()
|
|
41
|
+
.map(|c| c.symbol())
|
|
42
|
+
.collect::<String>();
|
|
43
|
+
|
|
40
44
|
// The mascot uses block drawing characters
|
|
41
|
-
assert!(
|
|
45
|
+
assert!(
|
|
46
|
+
content.contains("█"),
|
|
47
|
+
"Mascot rendering should contain block characters"
|
|
48
|
+
);
|
|
42
49
|
assert!(!content.trim().is_empty());
|
|
43
50
|
}
|
|
44
51
|
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
use crate::style::parse_block;
|
|
5
|
+
use crate::widgets::scrollbar_state::RubyScrollbarState;
|
|
5
6
|
use bumpalo::Bump;
|
|
6
|
-
use magnus::{prelude::*, Error, Symbol, Value};
|
|
7
|
+
use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
|
|
7
8
|
use ratatui::{
|
|
8
9
|
layout::Rect,
|
|
9
10
|
widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState},
|
|
@@ -14,7 +15,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
14
15
|
let content_length: usize = node.funcall("content_length", ())?;
|
|
15
16
|
let position: usize = node.funcall("position", ())?;
|
|
16
17
|
let orientation_sym: Symbol = node.funcall("orientation", ())?;
|
|
17
|
-
|
|
18
|
+
|
|
18
19
|
let thumb_symbol_val: Value = node.funcall("thumb_symbol", ())?;
|
|
19
20
|
let thumb_style_val: Value = node.funcall("thumb_style", ())?;
|
|
20
21
|
let track_symbol_val: Value = node.funcall("track_symbol", ())?;
|
|
@@ -32,7 +33,9 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
32
33
|
|
|
33
34
|
scrollbar = match orientation_sym.to_string().as_str() {
|
|
34
35
|
"vertical_left" => scrollbar.orientation(ScrollbarOrientation::VerticalLeft),
|
|
35
|
-
"horizontal_bottom" | "horizontal" =>
|
|
36
|
+
"horizontal_bottom" | "horizontal" => {
|
|
37
|
+
scrollbar.orientation(ScrollbarOrientation::HorizontalBottom)
|
|
38
|
+
}
|
|
36
39
|
"horizontal_top" => scrollbar.orientation(ScrollbarOrientation::HorizontalTop),
|
|
37
40
|
_ => scrollbar.orientation(ScrollbarOrientation::VerticalRight),
|
|
38
41
|
};
|
|
@@ -87,6 +90,97 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
87
90
|
Ok(())
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
/// Renders a Scrollbar with an external state object.
|
|
94
|
+
///
|
|
95
|
+
/// The State object is the single source of truth for position and `content_length`.
|
|
96
|
+
/// Widget properties (`position`, `content_length`) are ignored.
|
|
97
|
+
pub fn render_stateful(
|
|
98
|
+
frame: &mut Frame,
|
|
99
|
+
area: Rect,
|
|
100
|
+
node: Value,
|
|
101
|
+
state_wrapper: Value,
|
|
102
|
+
) -> Result<(), Error> {
|
|
103
|
+
// Extract the RubyScrollbarState wrapper
|
|
104
|
+
let state: &RubyScrollbarState = TryConvert::try_convert(state_wrapper)?;
|
|
105
|
+
|
|
106
|
+
let orientation_sym: Symbol = node.funcall("orientation", ())?;
|
|
107
|
+
let thumb_symbol_val: Value = node.funcall("thumb_symbol", ())?;
|
|
108
|
+
let thumb_style_val: Value = node.funcall("thumb_style", ())?;
|
|
109
|
+
let track_symbol_val: Value = node.funcall("track_symbol", ())?;
|
|
110
|
+
let track_style_val: Value = node.funcall("track_style", ())?;
|
|
111
|
+
let begin_symbol_val: Value = node.funcall("begin_symbol", ())?;
|
|
112
|
+
let begin_style_val: Value = node.funcall("begin_style", ())?;
|
|
113
|
+
let end_symbol_val: Value = node.funcall("end_symbol", ())?;
|
|
114
|
+
let end_style_val: Value = node.funcall("end_style", ())?;
|
|
115
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
116
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
117
|
+
|
|
118
|
+
let mut scrollbar = Scrollbar::default();
|
|
119
|
+
|
|
120
|
+
scrollbar = match orientation_sym.to_string().as_str() {
|
|
121
|
+
"vertical_left" => scrollbar.orientation(ScrollbarOrientation::VerticalLeft),
|
|
122
|
+
"horizontal_bottom" | "horizontal" => {
|
|
123
|
+
scrollbar.orientation(ScrollbarOrientation::HorizontalBottom)
|
|
124
|
+
}
|
|
125
|
+
"horizontal_top" => scrollbar.orientation(ScrollbarOrientation::HorizontalTop),
|
|
126
|
+
_ => scrollbar.orientation(ScrollbarOrientation::VerticalRight),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Hoisted strings to extend lifetime
|
|
130
|
+
let thumb_str: String;
|
|
131
|
+
let track_str: String;
|
|
132
|
+
let begin_str: String;
|
|
133
|
+
let end_str: String;
|
|
134
|
+
|
|
135
|
+
if !thumb_symbol_val.is_nil() {
|
|
136
|
+
thumb_str = thumb_symbol_val.funcall("to_s", ())?;
|
|
137
|
+
scrollbar = scrollbar.thumb_symbol(&thumb_str);
|
|
138
|
+
}
|
|
139
|
+
if !thumb_style_val.is_nil() {
|
|
140
|
+
scrollbar = scrollbar.thumb_style(crate::style::parse_style(thumb_style_val)?);
|
|
141
|
+
}
|
|
142
|
+
if !track_symbol_val.is_nil() {
|
|
143
|
+
track_str = track_symbol_val.funcall("to_s", ())?;
|
|
144
|
+
scrollbar = scrollbar.track_symbol(Some(&track_str));
|
|
145
|
+
}
|
|
146
|
+
if !track_style_val.is_nil() {
|
|
147
|
+
scrollbar = scrollbar.track_style(crate::style::parse_style(track_style_val)?);
|
|
148
|
+
}
|
|
149
|
+
if !begin_symbol_val.is_nil() {
|
|
150
|
+
begin_str = begin_symbol_val.funcall("to_s", ())?;
|
|
151
|
+
scrollbar = scrollbar.begin_symbol(Some(&begin_str));
|
|
152
|
+
}
|
|
153
|
+
if !begin_style_val.is_nil() {
|
|
154
|
+
scrollbar = scrollbar.begin_style(crate::style::parse_style(begin_style_val)?);
|
|
155
|
+
}
|
|
156
|
+
if !end_symbol_val.is_nil() {
|
|
157
|
+
end_str = end_symbol_val.funcall("to_s", ())?;
|
|
158
|
+
scrollbar = scrollbar.end_symbol(Some(&end_str));
|
|
159
|
+
}
|
|
160
|
+
if !end_style_val.is_nil() {
|
|
161
|
+
scrollbar = scrollbar.end_style(crate::style::parse_style(end_style_val)?);
|
|
162
|
+
}
|
|
163
|
+
if !style_val.is_nil() {
|
|
164
|
+
scrollbar = scrollbar.style(crate::style::parse_style(style_val)?);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Borrow the inner ScrollbarState, render, and release the borrow immediately
|
|
168
|
+
{
|
|
169
|
+
let mut inner_state = state.borrow_mut();
|
|
170
|
+
if block_val.is_nil() {
|
|
171
|
+
frame.render_stateful_widget(scrollbar, area, &mut inner_state);
|
|
172
|
+
} else {
|
|
173
|
+
let bump = Bump::new();
|
|
174
|
+
let block = parse_block(block_val, &bump)?;
|
|
175
|
+
let inner_area = block.inner(area);
|
|
176
|
+
frame.render_widget(block, area);
|
|
177
|
+
frame.render_stateful_widget(scrollbar, inner_area, &mut inner_state);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
Ok(())
|
|
182
|
+
}
|
|
183
|
+
|
|
90
184
|
#[cfg(test)]
|
|
91
185
|
mod tests {
|
|
92
186
|
use super::*;
|