ratatui_ruby 0.5.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 +6 -0
- data/CHANGELOG.md +44 -7
- data/README.md +11 -4
- data/REUSE.toml +2 -7
- data/doc/application_architecture.md +84 -10
- data/doc/application_testing.md +75 -29
- data/doc/contributors/design/ruby_frontend.md +39 -3
- data/doc/contributors/design/rust_backend.md +1 -0
- data/doc/contributors/developing_examples.md +129 -44
- 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/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/quickstart.md +69 -76
- data/doc/terminal_limitations.md +92 -0
- data/examples/app_all_events/README.md +45 -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 +8 -8
- data/examples/app_all_events/view/controls_view.rb +8 -6
- data/examples/app_all_events/view/counts_view.rb +12 -8
- data/examples/app_all_events/view/live_view.rb +8 -7
- data/examples/app_all_events/view/log_view.rb +10 -15
- data/examples/app_color_picker/README.md +84 -44
- 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 +47 -0
- data/examples/app_login_form/app.rb +2 -3
- 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/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 +8 -2
- 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 +49 -0
- data/examples/widget_barchart_demo/app.rb +5 -5
- 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/widget_calendar_demo/README.md +39 -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 +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 +2 -2
- 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_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_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 +46 -8
- 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 +106 -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 +113 -1
- data/ext/ratatui_ruby/src/lib.rs +34 -4
- 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 +1 -1
- data/ext/ratatui_ruby/src/widgets/barchart.rs +24 -6
- 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 +113 -1
- data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -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 +96 -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/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/style.rb +24 -4
- data/lib/ratatui_ruby/schema/table.rb +21 -3
- 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 +65 -0
- data/lib/ratatui_ruby/session.rb +22 -7
- 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 +65 -358
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +42 -19
- 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/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 +13 -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 +9 -7
- data/tasks/autodoc.rake +11 -5
- data/tasks/bump/changelog.rb +3 -3
- data/tasks/bump/links.rb +67 -0
- data/tasks/sourcehut.rake +61 -21
- data/tasks/terminal_preview/app_screenshot.rb +13 -3
- data/tasks/terminal_preview/saved_screenshot.rb +4 -3
- metadata +111 -37
- 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/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/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/bump/comparison_links.rb +0 -41
- /data/doc/images/{app_map_demo.png → widget_map_demo.png} +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
use magnus::value::ReprValue;
|
|
5
|
-
use magnus::Error;
|
|
5
|
+
use magnus::{Error, Module};
|
|
6
6
|
use ratatui::{
|
|
7
7
|
backend::{CrosstermBackend, TestBackend},
|
|
8
8
|
Terminal,
|
|
@@ -21,28 +21,32 @@ pub fn init_terminal(focus_events: bool, bracketed_paste: bool) -> Result<(), Er
|
|
|
21
21
|
let ruby = magnus::Ruby::get().unwrap();
|
|
22
22
|
let mut term_lock = TERMINAL.lock().unwrap();
|
|
23
23
|
if term_lock.is_none() {
|
|
24
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
25
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
26
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
27
|
+
|
|
24
28
|
ratatui::crossterm::terminal::enable_raw_mode()
|
|
25
|
-
.map_err(|e| Error::new(
|
|
29
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
26
30
|
let mut stdout = io::stdout();
|
|
27
31
|
ratatui::crossterm::execute!(
|
|
28
32
|
stdout,
|
|
29
33
|
ratatui::crossterm::terminal::EnterAlternateScreen,
|
|
30
34
|
ratatui::crossterm::event::EnableMouseCapture
|
|
31
35
|
)
|
|
32
|
-
.map_err(|e| Error::new(
|
|
36
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
33
37
|
|
|
34
38
|
if focus_events {
|
|
35
39
|
ratatui::crossterm::execute!(stdout, ratatui::crossterm::event::EnableFocusChange)
|
|
36
|
-
.map_err(|e| Error::new(
|
|
40
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
37
41
|
}
|
|
38
42
|
if bracketed_paste {
|
|
39
43
|
ratatui::crossterm::execute!(stdout, ratatui::crossterm::event::EnableBracketedPaste)
|
|
40
|
-
.map_err(|e| Error::new(
|
|
44
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
let backend = CrosstermBackend::new(stdout);
|
|
44
|
-
let terminal =
|
|
45
|
-
.map_err(|e| Error::new(
|
|
48
|
+
let terminal =
|
|
49
|
+
Terminal::new(backend).map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
46
50
|
*term_lock = Some(TerminalWrapper::Crossterm(terminal));
|
|
47
51
|
}
|
|
48
52
|
Ok(())
|
|
@@ -52,8 +56,10 @@ pub fn init_test_terminal(width: u16, height: u16) -> Result<(), Error> {
|
|
|
52
56
|
let ruby = magnus::Ruby::get().unwrap();
|
|
53
57
|
let mut term_lock = TERMINAL.lock().unwrap();
|
|
54
58
|
let backend = TestBackend::new(width, height);
|
|
55
|
-
let
|
|
56
|
-
|
|
59
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
60
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
61
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
62
|
+
let terminal = Terminal::new(backend).map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
57
63
|
*term_lock = Some(TerminalWrapper::Test(terminal));
|
|
58
64
|
Ok(())
|
|
59
65
|
}
|
|
@@ -93,8 +99,11 @@ pub fn get_buffer_content() -> Result<String, Error> {
|
|
|
93
99
|
}
|
|
94
100
|
Ok(result)
|
|
95
101
|
} else {
|
|
102
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
103
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
104
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
96
105
|
Err(Error::new(
|
|
97
|
-
|
|
106
|
+
error_class,
|
|
98
107
|
"Terminal is not initialized as TestBackend",
|
|
99
108
|
))
|
|
100
109
|
}
|
|
@@ -104,13 +113,19 @@ pub fn get_cursor_position() -> Result<Option<(u16, u16)>, Error> {
|
|
|
104
113
|
let ruby = magnus::Ruby::get().unwrap();
|
|
105
114
|
let mut term_lock = TERMINAL.lock().unwrap();
|
|
106
115
|
if let Some(TerminalWrapper::Test(terminal)) = term_lock.as_mut() {
|
|
116
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
117
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
118
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
107
119
|
let pos = terminal
|
|
108
120
|
.get_cursor_position()
|
|
109
|
-
.map_err(|e| Error::new(
|
|
121
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
110
122
|
Ok(Some(pos.into()))
|
|
111
123
|
} else {
|
|
124
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
125
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
126
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
112
127
|
Err(Error::new(
|
|
113
|
-
|
|
128
|
+
error_class,
|
|
114
129
|
"Terminal is not initialized as TestBackend",
|
|
115
130
|
))
|
|
116
131
|
}
|
|
@@ -125,7 +140,10 @@ pub fn resize_terminal(width: u16, height: u16) -> Result<(), Error> {
|
|
|
125
140
|
TerminalWrapper::Test(terminal) => {
|
|
126
141
|
terminal.backend_mut().resize(width, height);
|
|
127
142
|
if let Err(e) = terminal.resize(ratatui::layout::Rect::new(0, 0, width, height)) {
|
|
128
|
-
|
|
143
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
144
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
145
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
146
|
+
return Err(Error::new(error_class, e.to_string()));
|
|
129
147
|
}
|
|
130
148
|
}
|
|
131
149
|
}
|
|
@@ -148,14 +166,20 @@ pub fn get_cell_at(x: u16, y: u16) -> Result<magnus::RHash, Error> {
|
|
|
148
166
|
hash.aset("modifiers", modifiers_to_value(cell.modifier))?;
|
|
149
167
|
Ok(hash)
|
|
150
168
|
} else {
|
|
169
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
170
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
171
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
151
172
|
Err(Error::new(
|
|
152
|
-
|
|
173
|
+
error_class,
|
|
153
174
|
format!("Coordinates ({x}, {y}) out of bounds"),
|
|
154
175
|
))
|
|
155
176
|
}
|
|
156
177
|
} else {
|
|
178
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
179
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
180
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
157
181
|
Err(Error::new(
|
|
158
|
-
|
|
182
|
+
error_class,
|
|
159
183
|
"Terminal is not initialized as TestBackend",
|
|
160
184
|
))
|
|
161
185
|
}
|
|
@@ -90,7 +90,7 @@ pub fn parse_text(value: Value) -> Result<Vec<Line<'static>>, Error> {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/// Parses a Ruby Span object into a ratatui Span.
|
|
93
|
-
fn parse_span(value: Value) -> Result<Span<'static>, Error> {
|
|
93
|
+
pub fn parse_span(value: Value) -> Result<Span<'static>, Error> {
|
|
94
94
|
let ruby = magnus::Ruby::get().unwrap();
|
|
95
95
|
|
|
96
96
|
// Get class name
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
use crate::style::{parse_bar_set, parse_block, parse_style};
|
|
5
|
+
use crate::text::{parse_line, parse_span};
|
|
5
6
|
use bumpalo::Bump;
|
|
6
7
|
use magnus::{prelude::*, Error, RArray, Symbol, Value};
|
|
7
8
|
use ratatui::{
|
|
@@ -11,6 +12,7 @@ use ratatui::{
|
|
|
11
12
|
Frame,
|
|
12
13
|
};
|
|
13
14
|
|
|
15
|
+
#[allow(clippy::too_many_lines)]
|
|
14
16
|
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
15
17
|
let bump = Bump::new();
|
|
16
18
|
let data_val: Value = node.funcall("data", ())?;
|
|
@@ -66,16 +68,32 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
66
68
|
|
|
67
69
|
let label_val: Value = bar_obj.funcall("label", ())?;
|
|
68
70
|
if !label_val.is_nil() {
|
|
69
|
-
|
|
70
|
-
let
|
|
71
|
-
|
|
71
|
+
// Try to parse as Line (rich text)
|
|
72
|
+
if let Ok(line) = parse_line(label_val) {
|
|
73
|
+
bar = bar.label(line);
|
|
74
|
+
} else if let Ok(span) = parse_span(label_val) {
|
|
75
|
+
bar = bar.label(Line::from(vec![span]));
|
|
76
|
+
} else {
|
|
77
|
+
// Fallback to string
|
|
78
|
+
let s: String = label_val.funcall("to_s", ())?;
|
|
79
|
+
let s_ref = bump.alloc_str(&s) as &str;
|
|
80
|
+
bar = bar.label(Line::from(s_ref));
|
|
81
|
+
}
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
let text_val: Value = bar_obj.funcall("text_value", ())?;
|
|
75
85
|
if !text_val.is_nil() {
|
|
76
|
-
|
|
77
|
-
let
|
|
78
|
-
|
|
86
|
+
// Try to parse as Line (rich text)
|
|
87
|
+
if let Ok(line) = parse_line(text_val) {
|
|
88
|
+
bar = bar.text_value(line);
|
|
89
|
+
} else if let Ok(span) = parse_span(text_val) {
|
|
90
|
+
bar = bar.text_value(Line::from(vec![span]));
|
|
91
|
+
} else {
|
|
92
|
+
// Fallback to string
|
|
93
|
+
let s: String = text_val.funcall("to_s", ())?;
|
|
94
|
+
let s_ref = bump.alloc_str(&s) as &str;
|
|
95
|
+
bar = bar.text_value(s_ref);
|
|
96
|
+
}
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
let style_val: Value = bar_obj.funcall("style", ())?;
|
|
@@ -2,6 +2,7 @@
|
|
|
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_span;
|
|
5
6
|
use bumpalo::Bump;
|
|
6
7
|
use magnus::{prelude::*, Error, Value};
|
|
7
8
|
use ratatui::{layout::Rect, widgets::Gauge, Frame};
|
|
@@ -17,8 +18,14 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
17
18
|
let mut gauge = Gauge::default().ratio(ratio).use_unicode(use_unicode);
|
|
18
19
|
|
|
19
20
|
if !label_val.is_nil() {
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
// Try to parse as a Span (rich text)
|
|
22
|
+
if let Ok(span) = parse_span(label_val) {
|
|
23
|
+
gauge = gauge.label(span);
|
|
24
|
+
} else {
|
|
25
|
+
// Fallback to string
|
|
26
|
+
let label_str: String = label_val.funcall("to_s", ())?;
|
|
27
|
+
gauge = gauge.label(label_str);
|
|
28
|
+
}
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
if !style_val.is_nil() {
|
|
@@ -2,6 +2,7 @@
|
|
|
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_span;
|
|
5
6
|
use bumpalo::Bump;
|
|
6
7
|
use magnus::{prelude::*, Error, Value};
|
|
7
8
|
use ratatui::{layout::Rect, widgets::LineGauge, Frame};
|
|
@@ -23,8 +24,14 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
23
24
|
.unfilled_symbol(&unfilled_symbol_val);
|
|
24
25
|
|
|
25
26
|
if !label_val.is_nil() {
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
// Try to parse as a Span (rich text)
|
|
28
|
+
if let Ok(span) = parse_span(label_val) {
|
|
29
|
+
gauge = gauge.label(span);
|
|
30
|
+
} else {
|
|
31
|
+
// Fallback to string
|
|
32
|
+
let label_str: String = label_val.funcall("to_s", ())?;
|
|
33
|
+
gauge = gauge.label(label_str);
|
|
34
|
+
}
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
if !style_val.is_nil() {
|
|
@@ -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,11 +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
34
|
let index = isize::try_from(i)
|
|
33
35
|
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
34
|
-
let
|
|
36
|
+
let item_val: Value = items_array.entry(index)?;
|
|
37
|
+
let item = parse_list_item(item_val)?;
|
|
35
38
|
items.push(item);
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -47,6 +50,12 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
47
50
|
state.select(Some(index));
|
|
48
51
|
}
|
|
49
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
|
+
|
|
50
59
|
let mut list = List::new(items);
|
|
51
60
|
|
|
52
61
|
let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
|
|
@@ -101,6 +110,173 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
101
110
|
Ok(())
|
|
102
111
|
}
|
|
103
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
|
+
|
|
104
280
|
#[cfg(test)]
|
|
105
281
|
mod tests {
|
|
106
282
|
use super::*;
|
|
@@ -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;
|