ratatui_ruby 0.5.0 → 0.7.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 +10 -4
- data/CHANGELOG.md +79 -7
- data/README.md +37 -5
- data/REUSE.toml +2 -7
- data/doc/application_architecture.md +96 -22
- data/doc/application_testing.md +76 -30
- data/doc/contributors/architectural_overhaul/chat_conversations.md +4952 -0
- data/doc/contributors/architectural_overhaul/implementation_plan.md +60 -0
- data/doc/contributors/architectural_overhaul/task.md +37 -0
- data/doc/contributors/design/ruby_frontend.md +288 -56
- data/doc/contributors/design/rust_backend.md +349 -54
- data/doc/contributors/developing_examples.md +134 -49
- data/doc/contributors/index.md +7 -5
- data/doc/contributors/v1.0.0_blockers.md +1729 -0
- data/doc/event_handling.md +11 -3
- 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_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_list_demo.png +0 -0
- data/doc/images/widget_overlay_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_sparkline_demo.png +0 -0
- data/doc/images/widget_table_demo.png +0 -0
- data/doc/images/widget_tabs_demo.png +0 -0
- data/doc/images/widget_text_width.png +0 -0
- data/doc/index.md +11 -6
- data/doc/interactive_design.md +2 -2
- data/doc/quickstart.md +127 -165
- data/doc/terminal_limitations.md +92 -0
- data/doc/v0.7.0_migration.md +236 -0
- data/doc/why.md +93 -0
- data/examples/app_all_events/README.md +47 -27
- data/examples/app_all_events/app.rb +38 -35
- data/examples/app_all_events/model/app_model.rb +157 -0
- data/examples/app_all_events/model/event_entry.rb +17 -0
- data/examples/app_all_events/model/msg.rb +37 -0
- data/examples/app_all_events/update.rb +73 -0
- data/examples/app_all_events/view/app_view.rb +9 -9
- data/examples/app_all_events/view/controls_view.rb +9 -7
- data/examples/app_all_events/view/counts_view.rb +13 -9
- data/examples/app_all_events/view/live_view.rb +9 -8
- data/examples/app_all_events/view/log_view.rb +11 -16
- data/examples/app_color_picker/README.md +84 -42
- data/examples/app_color_picker/app.rb +24 -62
- data/examples/app_color_picker/controls.rb +90 -0
- data/examples/app_color_picker/copy_dialog.rb +45 -49
- data/examples/app_color_picker/export_pane.rb +126 -0
- data/examples/app_color_picker/input.rb +99 -67
- data/examples/app_color_picker/main_container.rb +178 -0
- data/examples/app_color_picker/palette.rb +55 -26
- data/examples/app_login_form/README.md +49 -0
- data/examples/app_login_form/app.rb +2 -3
- data/examples/app_stateful_interaction/README.md +33 -0
- data/examples/app_stateful_interaction/app.rb +272 -0
- data/examples/timeout_demo.rb +43 -0
- data/examples/verify_quickstart_dsl/README.md +49 -0
- data/examples/verify_quickstart_dsl/app.rb +2 -0
- data/examples/verify_quickstart_layout/README.md +71 -0
- data/examples/verify_quickstart_layout/app.rb +2 -0
- data/examples/verify_quickstart_lifecycle/README.md +56 -0
- data/examples/verify_quickstart_lifecycle/app.rb +10 -4
- data/examples/verify_readme_usage/README.md +43 -0
- data/examples/verify_readme_usage/app.rb +8 -2
- data/examples/widget_barchart_demo/README.md +50 -0
- data/examples/widget_barchart_demo/app.rb +5 -5
- data/examples/widget_block_demo/README.md +36 -0
- data/examples/widget_block_demo/app.rb +256 -0
- data/examples/widget_box_demo/README.md +45 -0
- data/examples/widget_calendar_demo/README.md +39 -0
- data/examples/widget_calendar_demo/app.rb +5 -1
- 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 +31 -24
- 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 +7 -2
- data/examples/widget_gauge_demo/README.md +41 -0
- data/examples/widget_layout_split/README.md +44 -0
- data/examples/widget_line_gauge_demo/README.md +41 -0
- data/examples/widget_list_demo/README.md +49 -0
- data/examples/widget_list_demo/app.rb +91 -107
- data/examples/widget_map_demo/README.md +39 -0
- data/examples/{app_map_demo → widget_map_demo}/app.rb +4 -4
- data/examples/widget_overlay_demo/README.md +36 -0
- data/examples/widget_overlay_demo/app.rb +248 -0
- data/examples/widget_popup_demo/README.md +36 -0
- data/examples/widget_ratatui_logo_demo/README.md +34 -0
- data/examples/widget_ratatui_logo_demo/app.rb +1 -1
- data/examples/widget_ratatui_mascot_demo/README.md +34 -0
- data/examples/widget_rect/README.md +38 -0
- data/examples/widget_render/README.md +37 -0
- data/examples/widget_render/app.rb +3 -3
- data/examples/widget_rich_text/README.md +35 -0
- data/examples/widget_rich_text/app.rb +62 -33
- data/examples/widget_scroll_text/README.md +37 -0
- data/examples/widget_scroll_text/app.rb +0 -1
- data/examples/widget_scrollbar_demo/README.md +37 -0
- data/examples/widget_sparkline_demo/README.md +42 -0
- data/examples/widget_sparkline_demo/app.rb +4 -3
- data/examples/widget_style_colors/README.md +34 -0
- data/examples/widget_table_demo/README.md +48 -0
- data/examples/{app_table_select → widget_table_demo}/app.rb +65 -12
- data/examples/widget_tabs_demo/README.md +41 -0
- data/examples/widget_tabs_demo/app.rb +15 -1
- data/examples/widget_text_width/README.md +35 -0
- data/examples/widget_text_width/app.rb +113 -0
- data/exe/.gitkeep +0 -0
- data/ext/ratatui_ruby/Cargo.lock +11 -4
- data/ext/ratatui_ruby/Cargo.toml +2 -1
- data/ext/ratatui_ruby/src/events.rs +238 -26
- data/ext/ratatui_ruby/src/frame.rs +116 -3
- data/ext/ratatui_ruby/src/lib.rs +37 -6
- data/ext/ratatui_ruby/src/rendering.rs +22 -21
- data/ext/ratatui_ruby/src/string_width.rs +101 -0
- data/ext/ratatui_ruby/src/terminal.rs +39 -15
- data/ext/ratatui_ruby/src/text.rs +13 -4
- data/ext/ratatui_ruby/src/widgets/barchart.rs +24 -6
- data/ext/ratatui_ruby/src/widgets/canvas.rs +5 -5
- data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
- data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
- data/ext/ratatui_ruby/src/widgets/list.rs +179 -3
- 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/scrollbar.rs +93 -1
- data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
- data/ext/ratatui_ruby/src/widgets/table.rs +191 -34
- data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
- data/lib/ratatui_ruby/buffer/cell.rb +168 -0
- data/lib/ratatui_ruby/buffer.rb +15 -0
- data/lib/ratatui_ruby/cell.rb +4 -4
- 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 +111 -51
- data/lib/ratatui_ruby/event/mouse.rb +3 -3
- data/lib/ratatui_ruby/event/paste.rb +1 -1
- data/lib/ratatui_ruby/frame.rb +100 -4
- data/lib/ratatui_ruby/layout/constraint.rb +95 -0
- data/lib/ratatui_ruby/layout/layout.rb +106 -0
- data/lib/ratatui_ruby/layout/rect.rb +118 -0
- data/lib/ratatui_ruby/layout.rb +19 -0
- data/lib/ratatui_ruby/list_state.rb +88 -0
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
- data/lib/ratatui_ruby/schema/cursor.rb +5 -0
- data/lib/ratatui_ruby/schema/gauge.rb +3 -1
- data/lib/ratatui_ruby/schema/layout.rb +1 -1
- data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
- data/lib/ratatui_ruby/schema/list.rb +25 -4
- data/lib/ratatui_ruby/schema/list_item.rb +41 -0
- data/lib/ratatui_ruby/schema/rect.rb +43 -0
- data/lib/ratatui_ruby/schema/row.rb +66 -0
- data/lib/ratatui_ruby/schema/style.rb +24 -4
- data/lib/ratatui_ruby/schema/table.rb +29 -11
- data/lib/ratatui_ruby/schema/text.rb +96 -3
- data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
- data/lib/ratatui_ruby/style/style.rb +81 -0
- data/lib/ratatui_ruby/style.rb +15 -0
- 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 +414 -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 +65 -358
- data/lib/ratatui_ruby/tui/buffer_factories.rb +20 -0
- data/lib/ratatui_ruby/tui/canvas_factories.rb +44 -0
- data/lib/ratatui_ruby/tui/core.rb +38 -0
- data/lib/ratatui_ruby/tui/layout_factories.rb +74 -0
- data/lib/ratatui_ruby/tui/state_factories.rb +33 -0
- data/lib/ratatui_ruby/tui/style_factories.rb +20 -0
- data/lib/ratatui_ruby/tui/text_factories.rb +44 -0
- data/lib/ratatui_ruby/tui/widget_factories.rb +195 -0
- data/lib/ratatui_ruby/tui.rb +75 -0
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +47 -0
- data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +25 -0
- data/lib/ratatui_ruby/widgets/bar_chart.rb +239 -0
- data/lib/ratatui_ruby/widgets/block.rb +192 -0
- data/lib/ratatui_ruby/widgets/calendar.rb +84 -0
- data/lib/ratatui_ruby/widgets/canvas.rb +231 -0
- data/lib/ratatui_ruby/widgets/cell.rb +47 -0
- data/lib/ratatui_ruby/widgets/center.rb +59 -0
- data/lib/ratatui_ruby/widgets/chart.rb +185 -0
- data/lib/ratatui_ruby/widgets/clear.rb +54 -0
- data/lib/ratatui_ruby/widgets/cursor.rb +42 -0
- data/lib/ratatui_ruby/widgets/gauge.rb +72 -0
- data/lib/ratatui_ruby/widgets/line_gauge.rb +80 -0
- data/lib/ratatui_ruby/widgets/list.rb +127 -0
- data/lib/ratatui_ruby/widgets/list_item.rb +43 -0
- data/lib/ratatui_ruby/widgets/overlay.rb +43 -0
- data/lib/ratatui_ruby/widgets/paragraph.rb +99 -0
- data/lib/ratatui_ruby/widgets/ratatui_logo.rb +31 -0
- data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +36 -0
- data/lib/ratatui_ruby/widgets/row.rb +68 -0
- data/lib/ratatui_ruby/widgets/scrollbar.rb +143 -0
- data/lib/ratatui_ruby/widgets/shape/label.rb +68 -0
- data/lib/ratatui_ruby/widgets/sparkline.rb +134 -0
- data/lib/ratatui_ruby/widgets/table.rb +141 -0
- data/lib/ratatui_ruby/widgets/tabs.rb +85 -0
- data/lib/ratatui_ruby/widgets.rb +40 -0
- data/lib/ratatui_ruby.rb +64 -57
- data/sig/examples/app_all_events/view.rbs +1 -1
- data/sig/examples/app_all_events/view_state.rbs +1 -1
- data/sig/examples/app_stateful_interaction/app.rbs +33 -0
- data/sig/examples/widget_block_demo/app.rbs +32 -0
- data/sig/examples/{app_map_demo → widget_map_demo}/app.rbs +2 -2
- data/sig/examples/{app_table_select → widget_table_demo}/app.rbs +2 -2
- data/sig/examples/{widget_table_flex → widget_text_width}/app.rbs +2 -3
- data/sig/ratatui_ruby/event.rbs +11 -1
- data/sig/ratatui_ruby/frame.rbs +2 -0
- data/sig/ratatui_ruby/list_state.rbs +13 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -2
- data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
- data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
- 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/row.rbs +22 -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 +9 -6
- data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
- data/sig/ratatui_ruby/session.rbs +41 -48
- 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/sig/ratatui_ruby/tui/buffer_factories.rbs +10 -0
- data/sig/ratatui_ruby/tui/canvas_factories.rbs +14 -0
- data/sig/ratatui_ruby/tui/core.rbs +14 -0
- data/sig/ratatui_ruby/tui/layout_factories.rbs +19 -0
- data/sig/ratatui_ruby/tui/state_factories.rbs +12 -0
- data/sig/ratatui_ruby/tui/style_factories.rbs +10 -0
- data/sig/ratatui_ruby/tui/text_factories.rbs +14 -0
- data/sig/ratatui_ruby/tui/widget_factories.rbs +39 -0
- data/sig/ratatui_ruby/tui.rbs +19 -0
- data/tasks/autodoc/examples.rb +79 -0
- data/tasks/autodoc.rake +7 -35
- data/tasks/bump/changelog.rb +3 -3
- data/tasks/bump/links.rb +67 -0
- data/tasks/sourcehut.rake +64 -21
- data/tasks/terminal_preview/app_screenshot.rb +13 -3
- data/tasks/terminal_preview/saved_screenshot.rb +4 -3
- metadata +169 -48
- data/doc/contributors/dwim_dx.md +0 -366
- data/doc/images/app_analytics.png +0 -0
- data/doc/images/app_custom_widget.png +0 -0
- data/doc/images/app_mouse_events.png +0 -0
- data/doc/images/app_table_select.png +0 -0
- data/doc/images/widget_block_padding.png +0 -0
- data/doc/images/widget_block_titles.png +0 -0
- data/doc/images/widget_list_styles.png +0 -0
- data/doc/images/widget_table_flex.png +0 -0
- data/examples/app_all_events/model/events.rb +0 -180
- data/examples/app_all_events/model/highlight.rb +0 -57
- data/examples/app_all_events/test/snapshots/after_focus_lost.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_focus_regained.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_key_a.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_mouse_click.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_multiple_events.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_paste.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_right_click.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/initial_state.txt +0 -24
- data/examples/app_all_events/view_state.rb +0 -42
- data/examples/app_color_picker/scene.rb +0 -201
- data/examples/widget_block_padding/app.rb +0 -67
- data/examples/widget_block_titles/app.rb +0 -69
- data/examples/widget_list_styles/app.rb +0 -141
- data/examples/widget_table_flex/app.rb +0 -95
- data/lib/ratatui_ruby/session/autodoc.rb +0 -417
- data/lib/ratatui_ruby/session.rb +0 -163
- data/sig/examples/widget_block_padding/app.rbs +0 -11
- data/sig/examples/widget_block_titles/app.rbs +0 -11
- data/sig/examples/widget_list_styles/app.rbs +0 -11
- data/tasks/autodoc/inventory.rb +0 -61
- data/tasks/autodoc/notice.rb +0 -26
- data/tasks/autodoc/rbs.rb +0 -38
- data/tasks/autodoc/rdoc.rb +0 -45
- data/tasks/bump/comparison_links.rb +0 -41
- /data/doc/images/{app_map_demo.png → widget_map_demo.png} +0 -0
|
@@ -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;
|
|
@@ -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},
|
|
@@ -89,6 +90,97 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
89
90
|
Ok(())
|
|
90
91
|
}
|
|
91
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
|
+
|
|
92
184
|
#[cfg(test)]
|
|
93
185
|
mod tests {
|
|
94
186
|
use super::*;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
//! `ScrollbarState` wrapper for exposing Ratatui's `ScrollbarState` to Ruby.
|
|
5
|
+
//!
|
|
6
|
+
//! This module provides `RubyScrollbarState`, a Magnus-wrapped struct that holds
|
|
7
|
+
//! a `RefCell<ScrollbarState>` for interior mutability during stateful rendering.
|
|
8
|
+
|
|
9
|
+
use magnus::{function, method, prelude::*, Error, Module, Ruby};
|
|
10
|
+
use ratatui::widgets::ScrollbarState;
|
|
11
|
+
use std::cell::RefCell;
|
|
12
|
+
|
|
13
|
+
/// A wrapper around Ratatui's `ScrollbarState` exposed to Ruby.
|
|
14
|
+
///
|
|
15
|
+
/// Ratatui's `ScrollbarState` doesn't expose getters for `position`, `content_length`,
|
|
16
|
+
/// or `viewport_content_length`. We track these values internally.
|
|
17
|
+
#[magnus::wrap(class = "RatatuiRuby::ScrollbarState")]
|
|
18
|
+
pub struct RubyScrollbarState {
|
|
19
|
+
inner: RefCell<ScrollbarState>,
|
|
20
|
+
/// We store these values ourselves since Ratatui's `ScrollbarState`
|
|
21
|
+
/// doesn't expose getters for them.
|
|
22
|
+
position_val: RefCell<usize>,
|
|
23
|
+
content_len: RefCell<usize>,
|
|
24
|
+
viewport_len: RefCell<usize>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl RubyScrollbarState {
|
|
28
|
+
/// Creates a new `RubyScrollbarState` with the given content length.
|
|
29
|
+
pub fn new(content_length: usize) -> Self {
|
|
30
|
+
Self {
|
|
31
|
+
inner: RefCell::new(ScrollbarState::new(content_length)),
|
|
32
|
+
position_val: RefCell::new(0),
|
|
33
|
+
content_len: RefCell::new(content_length),
|
|
34
|
+
viewport_len: RefCell::new(0),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Returns the current scroll position.
|
|
39
|
+
pub fn position(&self) -> usize {
|
|
40
|
+
*self.position_val.borrow()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Sets the current scroll position.
|
|
44
|
+
pub fn set_position(&self, position: usize) {
|
|
45
|
+
*self.position_val.borrow_mut() = position;
|
|
46
|
+
let mut state = self.inner.borrow_mut();
|
|
47
|
+
*state = state.position(position);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Returns the total content length.
|
|
51
|
+
pub fn content_length(&self) -> usize {
|
|
52
|
+
*self.content_len.borrow()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Sets the total content length.
|
|
56
|
+
pub fn set_content_length(&self, length: usize) {
|
|
57
|
+
*self.content_len.borrow_mut() = length;
|
|
58
|
+
let mut state = self.inner.borrow_mut();
|
|
59
|
+
*state = state.content_length(length);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Returns the viewport content length.
|
|
63
|
+
pub fn viewport_content_length(&self) -> usize {
|
|
64
|
+
*self.viewport_len.borrow()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Sets the viewport content length.
|
|
68
|
+
pub fn set_viewport_content_length(&self, length: usize) {
|
|
69
|
+
*self.viewport_len.borrow_mut() = length;
|
|
70
|
+
let mut state = self.inner.borrow_mut();
|
|
71
|
+
*state = state.viewport_content_length(length);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Scrolls to the first position.
|
|
75
|
+
pub fn first(&self) {
|
|
76
|
+
*self.position_val.borrow_mut() = 0;
|
|
77
|
+
self.inner.borrow_mut().first();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Scrolls to the last position.
|
|
81
|
+
pub fn last(&self) {
|
|
82
|
+
let content_len = *self.content_len.borrow();
|
|
83
|
+
let new_pos = content_len.saturating_sub(1);
|
|
84
|
+
*self.position_val.borrow_mut() = new_pos;
|
|
85
|
+
self.inner.borrow_mut().last();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Scrolls to the next position.
|
|
89
|
+
pub fn next(&self) {
|
|
90
|
+
let content_len = *self.content_len.borrow();
|
|
91
|
+
let current = *self.position_val.borrow();
|
|
92
|
+
let new_pos = (current + 1).min(content_len.saturating_sub(1));
|
|
93
|
+
*self.position_val.borrow_mut() = new_pos;
|
|
94
|
+
self.inner.borrow_mut().next();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Scrolls to the previous position.
|
|
98
|
+
pub fn prev(&self) {
|
|
99
|
+
let current = *self.position_val.borrow();
|
|
100
|
+
let new_pos = current.saturating_sub(1);
|
|
101
|
+
*self.position_val.borrow_mut() = new_pos;
|
|
102
|
+
self.inner.borrow_mut().prev();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Borrows the inner `ScrollbarState` mutably for rendering.
|
|
106
|
+
pub fn borrow_mut(&self) -> std::cell::RefMut<'_, ScrollbarState> {
|
|
107
|
+
self.inner.borrow_mut()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Registers the `ScrollbarState` class with Ruby.
|
|
112
|
+
pub fn register(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
|
|
113
|
+
let class = module.define_class("ScrollbarState", ruby.class_object())?;
|
|
114
|
+
class.define_singleton_method("new", function!(RubyScrollbarState::new, 1))?;
|
|
115
|
+
class.define_method("position", method!(RubyScrollbarState::position, 0))?;
|
|
116
|
+
class.define_method("position=", method!(RubyScrollbarState::set_position, 1))?;
|
|
117
|
+
class.define_method(
|
|
118
|
+
"content_length",
|
|
119
|
+
method!(RubyScrollbarState::content_length, 0),
|
|
120
|
+
)?;
|
|
121
|
+
class.define_method(
|
|
122
|
+
"content_length=",
|
|
123
|
+
method!(RubyScrollbarState::set_content_length, 1),
|
|
124
|
+
)?;
|
|
125
|
+
class.define_method(
|
|
126
|
+
"viewport_content_length",
|
|
127
|
+
method!(RubyScrollbarState::viewport_content_length, 0),
|
|
128
|
+
)?;
|
|
129
|
+
class.define_method(
|
|
130
|
+
"viewport_content_length=",
|
|
131
|
+
method!(RubyScrollbarState::set_viewport_content_length, 1),
|
|
132
|
+
)?;
|
|
133
|
+
class.define_method("first", method!(RubyScrollbarState::first, 0))?;
|
|
134
|
+
class.define_method("last", method!(RubyScrollbarState::last, 0))?;
|
|
135
|
+
class.define_method("next", method!(RubyScrollbarState::next, 0))?;
|
|
136
|
+
class.define_method("prev", method!(RubyScrollbarState::prev, 0))?;
|
|
137
|
+
Ok(())
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[cfg(test)]
|
|
141
|
+
mod tests {
|
|
142
|
+
use super::*;
|
|
143
|
+
|
|
144
|
+
#[test]
|
|
145
|
+
fn test_new_with_content_length() {
|
|
146
|
+
let state = RubyScrollbarState::new(100);
|
|
147
|
+
assert_eq!(state.content_length(), 100);
|
|
148
|
+
assert_eq!(state.position(), 0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[test]
|
|
152
|
+
fn test_position_navigation() {
|
|
153
|
+
let state = RubyScrollbarState::new(10);
|
|
154
|
+
state.next();
|
|
155
|
+
assert_eq!(state.position(), 1);
|
|
156
|
+
state.prev();
|
|
157
|
+
assert_eq!(state.position(), 0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#[test]
|
|
161
|
+
fn test_first_and_last() {
|
|
162
|
+
let state = RubyScrollbarState::new(10);
|
|
163
|
+
state.set_position(5);
|
|
164
|
+
state.first();
|
|
165
|
+
assert_eq!(state.position(), 0);
|
|
166
|
+
state.last();
|
|
167
|
+
assert_eq!(state.position(), 9);
|
|
168
|
+
}
|
|
169
|
+
}
|