ratatui_ruby 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +14 -12
  3. data/.builds/ruby-3.3.yml +14 -12
  4. data/.builds/ruby-3.4.yml +14 -12
  5. data/.builds/ruby-4.0.0.yml +14 -12
  6. data/AGENTS.md +54 -13
  7. data/CHANGELOG.md +186 -1
  8. data/README.md +17 -15
  9. data/doc/application_architecture.md +116 -0
  10. data/doc/application_testing.md +12 -7
  11. data/doc/contributors/better_dx.md +543 -0
  12. data/doc/contributors/design/ruby_frontend.md +1 -1
  13. data/doc/contributors/developing_examples.md +203 -0
  14. data/doc/contributors/documentation_style.md +97 -0
  15. data/doc/contributors/dwim_dx.md +366 -0
  16. data/doc/contributors/example_analysis.md +82 -0
  17. data/doc/custom.css +14 -0
  18. data/doc/event_handling.md +119 -0
  19. data/doc/images/all_events.png +0 -0
  20. data/doc/images/analytics.png +0 -0
  21. data/doc/images/block_padding.png +0 -0
  22. data/doc/images/block_titles.png +0 -0
  23. data/doc/images/box_demo.png +0 -0
  24. data/doc/images/calendar_demo.png +0 -0
  25. data/doc/images/cell_demo.png +0 -0
  26. data/doc/images/chart_demo.png +0 -0
  27. data/doc/images/custom_widget.png +0 -0
  28. data/doc/images/flex_layout.png +0 -0
  29. data/doc/images/gauge_demo.png +0 -0
  30. data/doc/images/hit_test.png +0 -0
  31. data/doc/images/line_gauge_demo.png +0 -0
  32. data/doc/images/list_demo.png +0 -0
  33. data/doc/images/list_styles.png +0 -0
  34. data/doc/images/login_form.png +0 -0
  35. data/doc/images/map_demo.png +0 -0
  36. data/doc/images/mouse_events.png +0 -0
  37. data/doc/images/popup_demo.png +0 -0
  38. data/doc/images/quickstart_dsl.png +0 -0
  39. data/doc/images/quickstart_lifecycle.png +0 -0
  40. data/doc/images/ratatui_logo_demo.png +0 -0
  41. data/doc/images/readme_usage.png +0 -0
  42. data/doc/images/rich_text.png +0 -0
  43. data/doc/images/scroll_text.png +0 -0
  44. data/doc/images/scrollbar_demo.png +0 -0
  45. data/doc/images/sparkline_demo.png +0 -0
  46. data/doc/images/table_flex.png +0 -0
  47. data/doc/images/table_select.png +0 -0
  48. data/doc/images/widget_style_colors.png +0 -0
  49. data/doc/index.md +1 -0
  50. data/doc/interactive_design.md +121 -0
  51. data/doc/quickstart.md +147 -72
  52. data/examples/all_events/app.rb +169 -0
  53. data/examples/all_events/app.rbs +7 -0
  54. data/examples/all_events/test_app.rb +139 -0
  55. data/examples/analytics/app.rb +258 -0
  56. data/examples/analytics/app.rbs +7 -0
  57. data/examples/analytics/test_app.rb +132 -0
  58. data/examples/block_padding/app.rb +63 -0
  59. data/examples/block_padding/app.rbs +7 -0
  60. data/examples/block_padding/test_app.rb +31 -0
  61. data/examples/block_titles/app.rb +61 -0
  62. data/examples/block_titles/app.rbs +7 -0
  63. data/examples/block_titles/test_app.rb +34 -0
  64. data/examples/box_demo/app.rb +216 -0
  65. data/examples/box_demo/app.rbs +7 -0
  66. data/examples/box_demo/test_app.rb +88 -0
  67. data/examples/calendar_demo/app.rb +101 -0
  68. data/examples/calendar_demo/app.rbs +7 -0
  69. data/examples/calendar_demo/test_app.rb +108 -0
  70. data/examples/cell_demo/app.rb +108 -0
  71. data/examples/cell_demo/app.rbs +7 -0
  72. data/examples/cell_demo/test_app.rb +36 -0
  73. data/examples/chart_demo/app.rb +203 -0
  74. data/examples/chart_demo/app.rbs +7 -0
  75. data/examples/chart_demo/test_app.rb +102 -0
  76. data/examples/custom_widget/app.rb +51 -0
  77. data/examples/custom_widget/app.rbs +7 -0
  78. data/examples/custom_widget/test_app.rb +30 -0
  79. data/examples/flex_layout/app.rb +156 -0
  80. data/examples/flex_layout/app.rbs +7 -0
  81. data/examples/flex_layout/test_app.rb +65 -0
  82. data/examples/gauge_demo/app.rb +182 -0
  83. data/examples/gauge_demo/app.rbs +7 -0
  84. data/examples/gauge_demo/test_app.rb +120 -0
  85. data/examples/hit_test/app.rb +175 -0
  86. data/examples/hit_test/app.rbs +7 -0
  87. data/examples/hit_test/test_app.rb +102 -0
  88. data/examples/line_gauge_demo/app.rb +190 -0
  89. data/examples/line_gauge_demo/app.rbs +7 -0
  90. data/examples/line_gauge_demo/test_app.rb +129 -0
  91. data/examples/list_demo/app.rb +253 -0
  92. data/examples/list_demo/app.rbs +12 -0
  93. data/examples/list_demo/test_app.rb +237 -0
  94. data/examples/list_styles/app.rb +140 -0
  95. data/examples/list_styles/app.rbs +7 -0
  96. data/examples/list_styles/test_app.rb +157 -0
  97. data/examples/{login_form.rb → login_form/app.rb} +12 -16
  98. data/examples/login_form/app.rbs +7 -0
  99. data/examples/login_form/test_app.rb +51 -0
  100. data/examples/map_demo/app.rb +90 -0
  101. data/examples/map_demo/app.rbs +7 -0
  102. data/examples/map_demo/test_app.rb +149 -0
  103. data/examples/{mouse_events.rb → mouse_events/app.rb} +29 -27
  104. data/examples/mouse_events/app.rbs +7 -0
  105. data/examples/mouse_events/test_app.rb +53 -0
  106. data/examples/{popup_demo.rb → popup_demo/app.rb} +15 -17
  107. data/examples/popup_demo/app.rbs +7 -0
  108. data/examples/{test_popup_demo.rb → popup_demo/test_app.rb} +18 -26
  109. data/examples/quickstart_dsl/app.rb +36 -0
  110. data/examples/quickstart_dsl/app.rbs +7 -0
  111. data/examples/quickstart_dsl/test_app.rb +29 -0
  112. data/examples/quickstart_lifecycle/app.rb +39 -0
  113. data/examples/quickstart_lifecycle/app.rbs +7 -0
  114. data/examples/quickstart_lifecycle/test_app.rb +29 -0
  115. data/examples/ratatui_logo_demo/app.rb +79 -0
  116. data/examples/ratatui_logo_demo/app.rbs +7 -0
  117. data/examples/ratatui_logo_demo/test_app.rb +51 -0
  118. data/examples/ratatui_mascot_demo/app.rb +84 -0
  119. data/examples/ratatui_mascot_demo/app.rbs +7 -0
  120. data/examples/ratatui_mascot_demo/test_app.rb +47 -0
  121. data/examples/readme_usage/app.rb +29 -0
  122. data/examples/readme_usage/app.rbs +7 -0
  123. data/examples/readme_usage/test_app.rb +29 -0
  124. data/examples/rich_text/app.rb +141 -0
  125. data/examples/rich_text/app.rbs +7 -0
  126. data/examples/rich_text/test_app.rb +166 -0
  127. data/examples/scroll_text/app.rb +103 -0
  128. data/examples/scroll_text/app.rbs +7 -0
  129. data/examples/scroll_text/test_app.rb +110 -0
  130. data/examples/scrollbar_demo/app.rb +143 -0
  131. data/examples/scrollbar_demo/app.rbs +7 -0
  132. data/examples/scrollbar_demo/test_app.rb +77 -0
  133. data/examples/sparkline_demo/app.rb +240 -0
  134. data/examples/sparkline_demo/app.rbs +10 -0
  135. data/examples/sparkline_demo/test_app.rb +107 -0
  136. data/examples/table_flex/app.rb +65 -0
  137. data/examples/table_flex/app.rbs +7 -0
  138. data/examples/table_flex/test_app.rb +36 -0
  139. data/examples/table_select/app.rb +198 -0
  140. data/examples/table_select/app.rbs +7 -0
  141. data/examples/table_select/test_app.rb +180 -0
  142. data/examples/widget_style_colors/app.rb +104 -0
  143. data/examples/widget_style_colors/app.rbs +14 -0
  144. data/examples/widget_style_colors/test_app.rb +48 -0
  145. data/ext/ratatui_ruby/Cargo.lock +889 -115
  146. data/ext/ratatui_ruby/Cargo.toml +4 -3
  147. data/ext/ratatui_ruby/clippy.toml +7 -0
  148. data/ext/ratatui_ruby/extconf.rb +7 -0
  149. data/ext/ratatui_ruby/src/events.rs +218 -229
  150. data/ext/ratatui_ruby/src/lib.rs +38 -10
  151. data/ext/ratatui_ruby/src/rendering.rs +90 -10
  152. data/ext/ratatui_ruby/src/style.rs +281 -98
  153. data/ext/ratatui_ruby/src/terminal.rs +119 -25
  154. data/ext/ratatui_ruby/src/text.rs +171 -0
  155. data/ext/ratatui_ruby/src/widgets/barchart.rs +97 -24
  156. data/ext/ratatui_ruby/src/widgets/block.rs +31 -3
  157. data/ext/ratatui_ruby/src/widgets/calendar.rs +45 -44
  158. data/ext/ratatui_ruby/src/widgets/canvas.rs +46 -29
  159. data/ext/ratatui_ruby/src/widgets/chart.rs +69 -27
  160. data/ext/ratatui_ruby/src/widgets/clear.rs +3 -1
  161. data/ext/ratatui_ruby/src/widgets/gauge.rs +11 -4
  162. data/ext/ratatui_ruby/src/widgets/layout.rs +218 -15
  163. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +92 -0
  164. data/ext/ratatui_ruby/src/widgets/list.rs +91 -11
  165. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  166. data/ext/ratatui_ruby/src/widgets/overlay.rs +3 -2
  167. data/ext/ratatui_ruby/src/widgets/paragraph.rs +35 -13
  168. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +29 -0
  169. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +44 -0
  170. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +59 -7
  171. data/ext/ratatui_ruby/src/widgets/sparkline.rs +70 -6
  172. data/ext/ratatui_ruby/src/widgets/table.rs +173 -64
  173. data/ext/ratatui_ruby/src/widgets/tabs.rs +105 -5
  174. data/lib/ratatui_ruby/cell.rb +166 -0
  175. data/lib/ratatui_ruby/event/focus_gained.rb +49 -0
  176. data/lib/ratatui_ruby/event/focus_lost.rb +50 -0
  177. data/lib/ratatui_ruby/event/key.rb +211 -0
  178. data/lib/ratatui_ruby/event/mouse.rb +124 -0
  179. data/lib/ratatui_ruby/event/paste.rb +71 -0
  180. data/lib/ratatui_ruby/event/resize.rb +80 -0
  181. data/lib/ratatui_ruby/event.rb +79 -0
  182. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +45 -0
  183. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +27 -0
  184. data/lib/ratatui_ruby/schema/bar_chart.rb +228 -19
  185. data/lib/ratatui_ruby/schema/block.rb +186 -14
  186. data/lib/ratatui_ruby/schema/calendar.rb +74 -17
  187. data/lib/ratatui_ruby/schema/canvas.rb +215 -48
  188. data/lib/ratatui_ruby/schema/center.rb +49 -11
  189. data/lib/ratatui_ruby/schema/chart.rb +151 -41
  190. data/lib/ratatui_ruby/schema/clear.rb +41 -72
  191. data/lib/ratatui_ruby/schema/constraint.rb +82 -22
  192. data/lib/ratatui_ruby/schema/cursor.rb +27 -9
  193. data/lib/ratatui_ruby/schema/draw.rb +53 -0
  194. data/lib/ratatui_ruby/schema/gauge.rb +59 -15
  195. data/lib/ratatui_ruby/schema/layout.rb +95 -13
  196. data/lib/ratatui_ruby/schema/line_gauge.rb +78 -0
  197. data/lib/ratatui_ruby/schema/list.rb +93 -19
  198. data/lib/ratatui_ruby/schema/overlay.rb +34 -8
  199. data/lib/ratatui_ruby/schema/paragraph.rb +87 -30
  200. data/lib/ratatui_ruby/schema/ratatui_logo.rb +25 -0
  201. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +29 -0
  202. data/lib/ratatui_ruby/schema/rect.rb +64 -15
  203. data/lib/ratatui_ruby/schema/scrollbar.rb +132 -24
  204. data/lib/ratatui_ruby/schema/shape/label.rb +66 -0
  205. data/lib/ratatui_ruby/schema/sparkline.rb +122 -15
  206. data/lib/ratatui_ruby/schema/style.rb +49 -21
  207. data/lib/ratatui_ruby/schema/table.rb +119 -21
  208. data/lib/ratatui_ruby/schema/tabs.rb +75 -13
  209. data/lib/ratatui_ruby/schema/text.rb +90 -0
  210. data/lib/ratatui_ruby/session.rb +146 -0
  211. data/lib/ratatui_ruby/test_helper.rb +156 -13
  212. data/lib/ratatui_ruby/version.rb +1 -1
  213. data/lib/ratatui_ruby.rb +143 -23
  214. data/sig/ratatui_ruby/event.rbs +69 -0
  215. data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -1
  216. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +16 -0
  217. data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +13 -0
  218. data/sig/ratatui_ruby/schema/bar_chart.rbs +20 -2
  219. data/sig/ratatui_ruby/schema/block.rbs +5 -4
  220. data/sig/ratatui_ruby/schema/calendar.rbs +6 -2
  221. data/sig/ratatui_ruby/schema/canvas.rbs +52 -39
  222. data/sig/ratatui_ruby/schema/center.rbs +3 -3
  223. data/sig/ratatui_ruby/schema/chart.rbs +8 -5
  224. data/sig/ratatui_ruby/schema/constraint.rbs +8 -5
  225. data/sig/ratatui_ruby/schema/cursor.rbs +1 -1
  226. data/sig/ratatui_ruby/schema/draw.rbs +23 -0
  227. data/sig/ratatui_ruby/schema/gauge.rbs +4 -2
  228. data/sig/ratatui_ruby/schema/layout.rbs +11 -1
  229. data/sig/ratatui_ruby/schema/line_gauge.rbs +16 -0
  230. data/sig/ratatui_ruby/schema/list.rbs +5 -1
  231. data/sig/ratatui_ruby/schema/paragraph.rbs +4 -1
  232. data/{lib/ratatui_ruby/output.rb → sig/ratatui_ruby/schema/ratatui_logo.rbs} +3 -2
  233. data/sig/ratatui_ruby/{buffer.rbs → schema/ratatui_mascot.rbs} +4 -3
  234. data/sig/ratatui_ruby/schema/rect.rbs +2 -1
  235. data/sig/ratatui_ruby/schema/scrollbar.rbs +18 -2
  236. data/sig/ratatui_ruby/schema/sparkline.rbs +6 -2
  237. data/sig/ratatui_ruby/schema/table.rbs +8 -1
  238. data/sig/ratatui_ruby/schema/tabs.rbs +5 -1
  239. data/sig/ratatui_ruby/schema/text.rbs +22 -0
  240. data/tasks/resources/build.yml.erb +13 -11
  241. data/tasks/terminal_preview/app_screenshot.rb +35 -0
  242. data/tasks/terminal_preview/crash_report.rb +54 -0
  243. data/tasks/terminal_preview/example_app.rb +25 -0
  244. data/tasks/terminal_preview/launcher_script.rb +48 -0
  245. data/tasks/terminal_preview/preview_collection.rb +60 -0
  246. data/tasks/terminal_preview/preview_timing.rb +22 -0
  247. data/tasks/terminal_preview/safety_confirmation.rb +58 -0
  248. data/tasks/terminal_preview/saved_screenshot.rb +55 -0
  249. data/tasks/terminal_preview/system_appearance.rb +11 -0
  250. data/tasks/terminal_preview/terminal_window.rb +138 -0
  251. data/tasks/terminal_preview/window_id.rb +14 -0
  252. data/tasks/terminal_preview.rake +28 -0
  253. data/tasks/test.rake +1 -1
  254. metadata +174 -53
  255. data/doc/images/examples-analytics.rb.png +0 -0
  256. data/doc/images/examples-box_demo.rb.png +0 -0
  257. data/doc/images/examples-calendar_demo.rb.png +0 -0
  258. data/doc/images/examples-chart_demo.rb.png +0 -0
  259. data/doc/images/examples-custom_widget.rb.png +0 -0
  260. data/doc/images/examples-dashboard.rb.png +0 -0
  261. data/doc/images/examples-list_styles.rb.png +0 -0
  262. data/doc/images/examples-login_form.rb.png +0 -0
  263. data/doc/images/examples-map_demo.rb.png +0 -0
  264. data/doc/images/examples-mouse_events.rb.png +0 -0
  265. data/doc/images/examples-popup_demo.rb.gif +0 -0
  266. data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
  267. data/doc/images/examples-scroll_text.rb.png +0 -0
  268. data/doc/images/examples-scrollbar_demo.rb.png +0 -0
  269. data/doc/images/examples-stock_ticker.rb.png +0 -0
  270. data/doc/images/examples-system_monitor.rb.png +0 -0
  271. data/doc/images/examples-table_select.rb.png +0 -0
  272. data/examples/analytics.rb +0 -88
  273. data/examples/box_demo.rb +0 -71
  274. data/examples/calendar_demo.rb +0 -55
  275. data/examples/chart_demo.rb +0 -84
  276. data/examples/custom_widget.rb +0 -43
  277. data/examples/dashboard.rb +0 -72
  278. data/examples/list_styles.rb +0 -66
  279. data/examples/map_demo.rb +0 -58
  280. data/examples/quickstart_dsl.rb +0 -30
  281. data/examples/quickstart_lifecycle.rb +0 -40
  282. data/examples/readme_usage.rb +0 -21
  283. data/examples/scroll_text.rb +0 -74
  284. data/examples/scrollbar_demo.rb +0 -75
  285. data/examples/stock_ticker.rb +0 -93
  286. data/examples/system_monitor.rb +0 -94
  287. data/examples/table_select.rb +0 -70
  288. data/examples/test_analytics.rb +0 -65
  289. data/examples/test_box_demo.rb +0 -38
  290. data/examples/test_calendar_demo.rb +0 -66
  291. data/examples/test_dashboard.rb +0 -38
  292. data/examples/test_list_styles.rb +0 -61
  293. data/examples/test_login_form.rb +0 -63
  294. data/examples/test_map_demo.rb +0 -100
  295. data/examples/test_scroll_text.rb +0 -130
  296. data/examples/test_stock_ticker.rb +0 -39
  297. data/examples/test_system_monitor.rb +0 -40
  298. data/examples/test_table_select.rb +0 -37
  299. data/ext/ratatui_ruby/src/buffer.rs +0 -54
  300. data/lib/ratatui_ruby/dsl.rb +0 -64
@@ -4,7 +4,7 @@
4
4
  use crate::rendering::render_node;
5
5
  use magnus::{prelude::*, Error, Symbol, Value};
6
6
  use ratatui::{
7
- layout::{Constraint, Direction, Layout, Rect},
7
+ layout::{Constraint, Direction, Flex, Layout, Rect},
8
8
  Frame,
9
9
  };
10
10
 
@@ -18,27 +18,34 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
18
18
  let constraints_val: Value = node.funcall("constraints", ())?;
19
19
  let constraints_array = magnus::RArray::from_value(constraints_val);
20
20
 
21
+ let flex_sym: Symbol = node.funcall("flex", ())?;
22
+
21
23
  let direction = if direction_sym.to_string() == "vertical" {
22
24
  Direction::Vertical
23
25
  } else {
24
26
  Direction::Horizontal
25
27
  };
26
28
 
29
+ let flex = match flex_sym.to_string().as_str() {
30
+ "start" => Flex::Start,
31
+ "center" => Flex::Center,
32
+ "end" => Flex::End,
33
+ "space_between" => Flex::SpaceBetween,
34
+ "space_around" => Flex::SpaceAround,
35
+ "space_evenly" => Flex::SpaceEvenly,
36
+ _ => Flex::Legacy,
37
+ };
38
+
27
39
  let len = children_array.len();
28
40
  if len > 0 {
29
41
  let mut ratatui_constraints = Vec::new();
30
42
 
31
43
  if let Some(arr) = constraints_array {
32
44
  for i in 0..arr.len() {
33
- let constraint_obj: Value = arr.entry(i as isize)?;
34
- let type_sym: Symbol = constraint_obj.funcall("type", ())?;
35
- let value: u16 = constraint_obj.funcall("value", ())?;
36
-
37
- match type_sym.to_string().as_str() {
38
- "length" => ratatui_constraints.push(Constraint::Length(value)),
39
- "percentage" => ratatui_constraints.push(Constraint::Percentage(value)),
40
- "min" => ratatui_constraints.push(Constraint::Min(value)),
41
- _ => {}
45
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
46
+ let constraint_obj: Value = arr.entry(index)?;
47
+ if let Ok(constraint) = parse_constraint(constraint_obj) {
48
+ ratatui_constraints.push(constraint);
42
49
  }
43
50
  }
44
51
  }
@@ -46,29 +53,152 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
46
53
  // If constraints don't match children, adjust or default
47
54
  if ratatui_constraints.len() != len {
48
55
  ratatui_constraints = (0..len)
49
- .map(|_| Constraint::Percentage(100 / (len as u16).max(1)))
56
+ .map(|_| Constraint::Percentage(100 / u16::try_from(len).unwrap_or(u16::MAX).max(1)))
50
57
  .collect();
51
58
  }
52
59
 
53
60
  let chunks = Layout::default()
54
61
  .direction(direction)
62
+ .flex(flex)
55
63
  .constraints(ratatui_constraints)
56
64
  .split(area);
57
65
 
58
66
  for i in 0..len {
59
- let child: Value = children_array.entry(i as isize)?;
67
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
68
+ let child: Value = children_array.entry(index)?;
60
69
  if let Err(e) = render_node(frame, chunks[i], child) {
61
- eprintln!("Error rendering child {}: {:?}", i, e);
70
+ eprintln!("Error rendering child {i}: {e:?}");
62
71
  }
63
72
  }
64
73
  }
65
74
  Ok(())
66
75
  }
67
76
 
77
+ pub fn parse_constraint(value: Value) -> Result<Constraint, Error> {
78
+ let type_sym: Symbol = value.funcall("type", ())?;
79
+ let value_obj: Value = value.funcall("value", ())?;
80
+
81
+ match type_sym.to_string().as_str() {
82
+ "length" => {
83
+ let val = u16::try_convert(value_obj)?;
84
+ Ok(Constraint::Length(val))
85
+ }
86
+ "percentage" => {
87
+ let val = u16::try_convert(value_obj)?;
88
+ Ok(Constraint::Percentage(val))
89
+ }
90
+ "min" => {
91
+ let val = u16::try_convert(value_obj)?;
92
+ Ok(Constraint::Min(val))
93
+ }
94
+ "max" => {
95
+ let val = u16::try_convert(value_obj)?;
96
+ Ok(Constraint::Max(val))
97
+ }
98
+ "fill" => {
99
+ let val = u16::try_convert(value_obj)?;
100
+ Ok(Constraint::Fill(val))
101
+ }
102
+ "ratio" => {
103
+ if let Some(arr) = magnus::RArray::from_value(value_obj) {
104
+ if arr.len() == 2 {
105
+ let n = u32::try_convert(arr.entry(0)?)?;
106
+ let d = u32::try_convert(arr.entry(1)?)?;
107
+ return Ok(Constraint::Ratio(n, d));
108
+ }
109
+ }
110
+ // Fallback or error for invalid ratio?
111
+ // For now, let's treat it as Min(0) or similar, or error.
112
+ // But to match previous behavior (which ignored invalid), we just return 0 length or something?
113
+ // Check previous logic: it ignored it (`_ => {}`).
114
+ // But we need to return a Constraint. Use Length(0) as safe fallback if unmatched?
115
+ // Actually, let's error if strictly required, but existing logic pushed nothing if mismatched.
116
+ // If we push nothing, we can't return a Constraint.
117
+ // Let's assume input is valid for now or return a default.
118
+ Ok(Constraint::Length(0))
119
+ }
120
+ _ => Ok(Constraint::Length(0)), // Default fallback
121
+ }
122
+ }
123
+
124
+ /// Splits an area into multiple rectangles based on constraints.
125
+ /// This is a pure calculation helper for hit testing.
126
+ ///
127
+ /// # Arguments
128
+ /// * `area` - A Ruby Hash or Rect with :x, :y, :width, :height keys
129
+ /// * `direction` - Symbol :vertical or :horizontal
130
+ /// * `constraints` - Array of Constraint objects
131
+ /// * `flex` - Symbol for flex mode
132
+ ///
133
+ /// # Returns
134
+ /// An array of Ruby Hashes representing Rect objects
135
+ pub fn split_layout(
136
+ area: Value,
137
+ direction: Symbol,
138
+ constraints: magnus::RArray,
139
+ flex: Symbol,
140
+ ) -> Result<magnus::RArray, Error> {
141
+ let ruby = magnus::Ruby::get().unwrap();
142
+
143
+ // Parse area from Hash or Rect-like object
144
+ let x: u16 = area.funcall("x", ())?;
145
+ let y: u16 = area.funcall("y", ())?;
146
+ let width: u16 = area.funcall("width", ())?;
147
+ let height: u16 = area.funcall("height", ())?;
148
+ let rect = Rect::new(x, y, width, height);
149
+
150
+ // Parse direction
151
+ let dir = if direction.to_string() == "vertical" {
152
+ Direction::Vertical
153
+ } else {
154
+ Direction::Horizontal
155
+ };
156
+
157
+ // Parse flex
158
+ let flex_mode = match flex.to_string().as_str() {
159
+ "start" => Flex::Start,
160
+ "center" => Flex::Center,
161
+ "end" => Flex::End,
162
+ "space_between" => Flex::SpaceBetween,
163
+ "space_around" => Flex::SpaceAround,
164
+ "space_evenly" => Flex::SpaceEvenly,
165
+ _ => Flex::Legacy,
166
+ };
167
+
168
+ // Parse constraints
169
+ let mut ratatui_constraints = Vec::new();
170
+ for i in 0..constraints.len() {
171
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
172
+ let constraint_obj: Value = constraints.entry(index)?;
173
+ if let Ok(constraint) = parse_constraint(constraint_obj) {
174
+ ratatui_constraints.push(constraint);
175
+ }
176
+ }
177
+
178
+ // Compute layout
179
+ let chunks = Layout::default()
180
+ .direction(dir)
181
+ .flex(flex_mode)
182
+ .constraints(ratatui_constraints)
183
+ .split(rect);
184
+
185
+ // Convert to Ruby array of Hashes
186
+ let result = ruby.ary_new_capa(chunks.len());
187
+ for chunk in chunks.iter() {
188
+ let hash = ruby.hash_new();
189
+ hash.aset(ruby.sym_new("x"), chunk.x)?;
190
+ hash.aset(ruby.sym_new("y"), chunk.y)?;
191
+ hash.aset(ruby.sym_new("width"), chunk.width)?;
192
+ hash.aset(ruby.sym_new("height"), chunk.height)?;
193
+ result.push(hash)?;
194
+ }
195
+
196
+ Ok(result)
197
+ }
198
+
68
199
  #[cfg(test)]
69
200
  mod tests {
70
-
71
- use ratatui::layout::{Constraint, Direction, Layout, Rect};
201
+ use ratatui::layout::{Constraint, Direction, Flex, Layout, Rect};
72
202
 
73
203
  #[test]
74
204
  fn test_layout_logic() {
@@ -80,4 +210,77 @@ mod tests {
80
210
  assert_eq!(chunks.len(), 2);
81
211
  assert_eq!(chunks[0].height, 50);
82
212
  }
213
+
214
+ #[test]
215
+ fn test_fill_constraint() {
216
+ let area = Rect::new(0, 0, 100, 10);
217
+ let chunks = Layout::default()
218
+ .direction(Direction::Horizontal)
219
+ .constraints([Constraint::Fill(1), Constraint::Fill(3)])
220
+ .split(area);
221
+ assert_eq!(chunks.len(), 2);
222
+ assert_eq!(chunks[0].width, 25);
223
+ assert_eq!(chunks[1].width, 75);
224
+ }
225
+
226
+ #[test]
227
+ fn test_flex_space_between() {
228
+ let area = Rect::new(0, 0, 100, 10);
229
+ let chunks = Layout::default()
230
+ .direction(Direction::Horizontal)
231
+ .flex(Flex::SpaceBetween)
232
+ .constraints([
233
+ Constraint::Length(10),
234
+ Constraint::Length(10),
235
+ Constraint::Length(10),
236
+ ])
237
+ .split(area);
238
+ assert_eq!(chunks.len(), 3);
239
+ assert_eq!(chunks[0].x, 0);
240
+ assert_eq!(chunks[1].x, 45);
241
+ assert_eq!(chunks[2].x, 90);
242
+ }
243
+
244
+ #[test]
245
+ fn test_flex_space_evenly() {
246
+ let area = Rect::new(0, 0, 100, 10);
247
+ let chunks = Layout::default()
248
+ .direction(Direction::Horizontal)
249
+ .flex(Flex::SpaceEvenly)
250
+ .constraints([
251
+ Constraint::Length(10),
252
+ Constraint::Length(10),
253
+ Constraint::Length(10),
254
+ ])
255
+ .split(area);
256
+ assert_eq!(chunks.len(), 3);
257
+ assert_eq!(chunks[0].x, 18);
258
+ assert_eq!(chunks[1].x, 45);
259
+ assert_eq!(chunks[2].x, 73);
260
+ }
261
+
262
+ #[test]
263
+ fn test_flex_center() {
264
+ let area = Rect::new(0, 0, 100, 10);
265
+ let chunks = Layout::default()
266
+ .direction(Direction::Horizontal)
267
+ .flex(Flex::Center)
268
+ .constraints([Constraint::Length(20)])
269
+ .split(area);
270
+ assert_eq!(chunks.len(), 1);
271
+ assert_eq!(chunks[0].x, 40);
272
+ assert_eq!(chunks[0].width, 20);
273
+ }
274
+
275
+ #[test]
276
+ fn test_max_constraint() {
277
+ let area = Rect::new(0, 0, 100, 10);
278
+ let chunks = Layout::default()
279
+ .direction(Direction::Horizontal)
280
+ .constraints([Constraint::Max(30), Constraint::Fill(1)])
281
+ .split(area);
282
+ assert_eq!(chunks.len(), 2);
283
+ assert_eq!(chunks[0].width, 30);
284
+ assert_eq!(chunks[1].width, 70);
285
+ }
83
286
  }
@@ -0,0 +1,92 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ use crate::style::{parse_block, parse_style};
5
+ use bumpalo::Bump;
6
+ use magnus::{prelude::*, Error, Value};
7
+ use ratatui::{layout::Rect, widgets::LineGauge, Frame};
8
+
9
+ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
10
+ let bump = Bump::new();
11
+ let ratio: f64 = node.funcall("ratio", ())?;
12
+ let label_val: Value = node.funcall("label", ())?;
13
+ let style_val: Value = node.funcall("style", ())?;
14
+ let filled_style_val: Value = node.funcall("filled_style", ())?;
15
+ let unfilled_style_val: Value = node.funcall("unfilled_style", ())?;
16
+ let block_val: Value = node.funcall("block", ())?;
17
+ let filled_symbol_val: String = node.funcall("filled_symbol", ())?;
18
+ let unfilled_symbol_val: String = node.funcall("unfilled_symbol", ())?;
19
+
20
+ let mut gauge = LineGauge::default()
21
+ .ratio(ratio)
22
+ .filled_symbol(&filled_symbol_val)
23
+ .unfilled_symbol(&unfilled_symbol_val);
24
+
25
+ if !label_val.is_nil() {
26
+ let label_str: String = label_val.funcall("to_s", ())?;
27
+ gauge = gauge.label(label_str);
28
+ }
29
+
30
+ if !style_val.is_nil() {
31
+ gauge = gauge.style(parse_style(style_val)?);
32
+ }
33
+
34
+ if !filled_style_val.is_nil() {
35
+ let parsed_style = parse_style(filled_style_val)?;
36
+ gauge = gauge.filled_style(parsed_style);
37
+ }
38
+
39
+ if !unfilled_style_val.is_nil() {
40
+ let parsed_style = parse_style(unfilled_style_val)?;
41
+ gauge = gauge.unfilled_style(parsed_style);
42
+ }
43
+
44
+ if !block_val.is_nil() {
45
+ gauge = gauge.block(parse_block(block_val, &bump)?);
46
+ }
47
+
48
+ frame.render_widget(gauge, area);
49
+ Ok(())
50
+ }
51
+
52
+ #[cfg(test)]
53
+ mod tests {
54
+ use super::*;
55
+ use ratatui::buffer::Buffer;
56
+ use ratatui::widgets::{LineGauge, Widget};
57
+
58
+ #[test]
59
+ fn test_line_gauge_rendering() {
60
+ let gauge = LineGauge::default().ratio(0.5).label("50%");
61
+ let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
62
+ gauge.render(Rect::new(0, 0, 20, 1), &mut buf);
63
+ // LineGauge renders filled and unfilled characters
64
+ assert!(buf.content().iter().any(|c| c.symbol() != " "));
65
+ // Should contain label
66
+ assert!(buf.content().iter().any(|c| c.symbol() == "5"));
67
+ assert!(buf.content().iter().any(|c| c.symbol() == "%"));
68
+ }
69
+
70
+ #[test]
71
+ fn test_line_gauge_zero_ratio() {
72
+ let gauge = LineGauge::default().ratio(0.0);
73
+ let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
74
+ gauge.render(Rect::new(0, 0, 10, 1), &mut buf);
75
+ // At zero ratio, should mostly show unfilled character
76
+ let unfilled_count = buf
77
+ .content()
78
+ .iter()
79
+ .filter(|c| c.symbol() == "░" || c.symbol() == " ")
80
+ .count();
81
+ assert!(unfilled_count > 0);
82
+ }
83
+
84
+ #[test]
85
+ fn test_line_gauge_full_ratio() {
86
+ let gauge = LineGauge::default().ratio(1.0).label("100%");
87
+ let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
88
+ gauge.render(Rect::new(0, 0, 20, 1), &mut buf);
89
+ // At full ratio, should show some non-space characters (filled or partial)
90
+ assert!(buf.content().iter().any(|c| c.symbol() != " "));
91
+ }
92
+ }
@@ -2,14 +2,17 @@
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
4
  use crate::style::{parse_block, parse_style};
5
- use magnus::{prelude::*, Error, TryConvert, Value};
5
+ use bumpalo::Bump;
6
+ use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
6
7
  use ratatui::{
7
8
  layout::Rect,
8
- widgets::{List, ListState},
9
+ text::Line,
10
+ widgets::{HighlightSpacing, List, ListState},
9
11
  Frame,
10
12
  };
11
13
 
12
14
  pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
15
+ let bump = Bump::new();
13
16
  let ruby = magnus::Ruby::get().unwrap();
14
17
  let items_val: Value = node.funcall("items", ())?;
15
18
  let items_array = magnus::RArray::from_value(items_val)
@@ -18,19 +21,23 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
18
21
  let style_val: Value = node.funcall("style", ())?;
19
22
  let highlight_style_val: Value = node.funcall("highlight_style", ())?;
20
23
  let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
24
+ let repeat_highlight_symbol_val: Value = node.funcall("repeat_highlight_symbol", ())?;
25
+ let highlight_spacing_sym: Symbol = node.funcall("highlight_spacing", ())?;
26
+ let direction_val: Value = node.funcall("direction", ())?;
27
+ let scroll_padding_val: Value = node.funcall("scroll_padding", ())?;
21
28
  let block_val: Value = node.funcall("block", ())?;
22
29
 
23
- let mut items = Vec::new();
30
+ let mut items: Vec<String> = Vec::new();
24
31
  for i in 0..items_array.len() {
25
- let item: String = items_array.entry(i as isize)?;
32
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
33
+ let item: String = items_array.entry(index)?;
26
34
  items.push(item);
27
35
  }
28
36
 
29
- let symbol: String = if !highlight_symbol_val.is_nil() {
30
- let s: String = String::try_convert(highlight_symbol_val)?;
31
- s
32
- } else {
37
+ let symbol: String = if highlight_symbol_val.is_nil() {
33
38
  String::new()
39
+ } else {
40
+ String::try_convert(highlight_symbol_val)?
34
41
  };
35
42
 
36
43
  let mut state = ListState::default();
@@ -41,8 +48,40 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
41
48
 
42
49
  let mut list = List::new(items);
43
50
 
51
+ let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
52
+ "always" => HighlightSpacing::Always,
53
+ "never" => HighlightSpacing::Never,
54
+ _ => HighlightSpacing::WhenSelected,
55
+ };
56
+ list = list.highlight_spacing(highlight_spacing);
57
+
44
58
  if !highlight_symbol_val.is_nil() {
45
- list = list.highlight_symbol(&symbol);
59
+ list = list.highlight_symbol(Line::from(symbol));
60
+ }
61
+
62
+ if !repeat_highlight_symbol_val.is_nil() {
63
+ let repeat: bool = TryConvert::try_convert(repeat_highlight_symbol_val)?;
64
+ list = list.repeat_highlight_symbol(repeat);
65
+ }
66
+
67
+ if !direction_val.is_nil() {
68
+ let direction_sym: magnus::Symbol = TryConvert::try_convert(direction_val)?;
69
+ let direction_str = direction_sym.name().unwrap();
70
+ match direction_str.as_ref() {
71
+ "top_to_bottom" => list = list.direction(ratatui::widgets::ListDirection::TopToBottom),
72
+ "bottom_to_top" => list = list.direction(ratatui::widgets::ListDirection::BottomToTop),
73
+ _ => {
74
+ return Err(Error::new(
75
+ ruby.exception_arg_error(),
76
+ "direction must be :top_to_bottom or :bottom_to_top",
77
+ ))
78
+ }
79
+ }
80
+ }
81
+
82
+ if !scroll_padding_val.is_nil() {
83
+ let padding: usize = TryConvert::try_convert(scroll_padding_val)?;
84
+ list = list.scroll_padding(padding);
46
85
  }
47
86
 
48
87
  if !style_val.is_nil() {
@@ -54,7 +93,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
54
93
  }
55
94
 
56
95
  if !block_val.is_nil() {
57
- list = list.block(parse_block(block_val)?);
96
+ list = list.block(parse_block(block_val, &bump)?);
58
97
  }
59
98
 
60
99
  frame.render_stateful_widget(list, area, &mut state);
@@ -71,7 +110,7 @@ mod tests {
71
110
  fn test_list_rendering() {
72
111
  let items = vec!["Item 1", "Item 2"];
73
112
  let list = List::new(items)
74
- .highlight_symbol(">> ")
113
+ .highlight_symbol(Line::from(">> "))
75
114
  .style(ratatui::style::Style::default().fg(ratatui::style::Color::White))
76
115
  .highlight_style(
77
116
  ratatui::style::Style::default()
@@ -98,4 +137,45 @@ mod tests {
98
137
  .modifier
99
138
  .contains(ratatui::style::Modifier::BOLD));
100
139
  }
140
+
141
+ #[test]
142
+ fn test_repeat_highlight_symbol() {
143
+ let items = vec!["Item 1", "Item 2"];
144
+ let list_without_repeat = List::new(items.clone()).highlight_symbol(Line::from(">> "));
145
+ let list_with_repeat = List::new(items).highlight_symbol(Line::from(">> ")).repeat_highlight_symbol(true);
146
+
147
+ let mut state = ListState::default();
148
+ state.select(Some(0));
149
+
150
+ let mut buf1 = Buffer::empty(Rect::new(0, 0, 10, 2));
151
+ use ratatui::widgets::StatefulWidget;
152
+ StatefulWidget::render(list_without_repeat, Rect::new(0, 0, 10, 2), &mut buf1, &mut state);
153
+
154
+ let mut buf2 = Buffer::empty(Rect::new(0, 0, 10, 2));
155
+ StatefulWidget::render(list_with_repeat, Rect::new(0, 0, 10, 2), &mut buf2, &mut state);
156
+
157
+ // Both should render, but the behavior might differ based on content width
158
+ let content1 = buf1.content().iter().map(|c| c.symbol()).collect::<String>();
159
+ let content2 = buf2.content().iter().map(|c| c.symbol()).collect::<String>();
160
+ assert!(!content1.is_empty());
161
+ assert!(!content2.is_empty());
162
+ }
163
+
164
+ #[test]
165
+ fn test_scroll_padding() {
166
+ let items = vec!["Item 1", "Item 2", "Item 3", "Item 4"];
167
+ let list = List::new(items).scroll_padding(1).highlight_symbol(Line::from(">> "));
168
+
169
+ let mut state = ListState::default();
170
+ state.select(Some(1));
171
+
172
+ let mut buf = Buffer::empty(Rect::new(0, 0, 15, 4));
173
+ use ratatui::widgets::StatefulWidget;
174
+ StatefulWidget::render(list, Rect::new(0, 0, 15, 4), &mut buf, &mut state);
175
+
176
+ let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
177
+ // With scroll padding, it should render but the exact behavior is handled by ratatui
178
+ assert!(!content.is_empty());
179
+ assert!(content.contains("Item"));
180
+ }
101
181
  }
@@ -11,9 +11,12 @@ pub mod clear;
11
11
  pub mod cursor;
12
12
  pub mod gauge;
13
13
  pub mod layout;
14
+ pub mod line_gauge;
14
15
  pub mod list;
15
16
  pub mod overlay;
16
17
  pub mod paragraph;
18
+ pub mod ratatui_logo;
19
+ pub mod ratatui_mascot;
17
20
  pub mod scrollbar;
18
21
  pub mod sparkline;
19
22
  pub mod table;
@@ -12,9 +12,10 @@ 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 layer: Value = layers_array.entry(i as isize)?;
15
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
16
+ let layer: Value = layers_array.entry(index)?;
16
17
  if let Err(e) = render_node(frame, area, layer) {
17
- eprintln!("Error rendering overlay layer {}: {:?}", i, e);
18
+ eprintln!("Error rendering overlay layer {i}: {e:?}");
18
19
  }
19
20
  }
20
21
  Ok(())
@@ -2,40 +2,44 @@
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
4
  use crate::style::{parse_block, parse_style};
5
+ use bumpalo::Bump;
5
6
  use magnus::{prelude::*, Error, Symbol, Value};
6
7
  use ratatui::{
7
- layout::{Alignment, Rect},
8
+ layout::{HorizontalAlignment, Rect},
8
9
  widgets::{Paragraph, Wrap},
9
10
  Frame,
10
11
  };
11
12
 
12
- pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
13
- let text: String = node.funcall("text", ())?;
13
+ use crate::text::parse_text;
14
+
15
+ fn create_paragraph<'a>(node: Value, bump: &'a Bump) -> Result<Paragraph<'a>, Error> {
16
+ let text_val: Value = node.funcall("text", ())?;
14
17
  let style_val: Value = node.funcall("style", ())?;
15
18
  let block_val: Value = node.funcall("block", ())?;
16
19
  let wrap: bool = node.funcall("wrap", ())?;
17
- let align_sym: Symbol = node.funcall("align", ())?;
20
+ let alignment_opt: Option<Symbol> = node.funcall("alignment", ())?;
18
21
  let scroll_val: Value = node.funcall("scroll", ())?;
19
22
 
23
+ let lines = parse_text(text_val)?;
20
24
  let style = parse_style(style_val)?;
21
- let mut paragraph = Paragraph::new(text).style(style);
25
+ let mut paragraph = Paragraph::new(lines).style(style);
22
26
 
23
27
  if !block_val.is_nil() {
24
- paragraph = paragraph.block(parse_block(block_val)?);
28
+ paragraph = paragraph.block(parse_block(block_val, bump)?);
25
29
  }
26
30
 
27
31
  if wrap {
28
32
  paragraph = paragraph.wrap(Wrap { trim: true });
29
33
  }
30
34
 
31
- match align_sym.to_string().as_str() {
32
- "center" => paragraph = paragraph.alignment(Alignment::Center),
33
- "right" => paragraph = paragraph.alignment(Alignment::Right),
34
- _ => {}
35
+ if let Some(alignment) = alignment_opt {
36
+ match alignment.to_string().as_str() {
37
+ "center" => paragraph = paragraph.alignment(HorizontalAlignment::Center),
38
+ "right" => paragraph = paragraph.alignment(HorizontalAlignment::Right),
39
+ _ => {}
40
+ }
35
41
  }
36
42
 
37
- // Apply scroll offset if provided
38
- // Ruby passes (y, x) array matching ratatui's convention
39
43
  if !scroll_val.is_nil() {
40
44
  let scroll_array: Vec<u16> = Vec::<u16>::try_convert(scroll_val)?;
41
45
  if scroll_array.len() >= 2 {
@@ -43,10 +47,28 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
43
47
  }
44
48
  }
45
49
 
50
+ Ok(paragraph)
51
+ }
52
+
53
+ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
54
+ let bump = Bump::new();
55
+ let paragraph = create_paragraph(node, &bump)?;
46
56
  frame.render_widget(paragraph, area);
47
57
  Ok(())
48
58
  }
49
59
 
60
+ pub fn line_count(node: Value, width: u16) -> Result<usize, Error> {
61
+ let bump = Bump::new();
62
+ let paragraph = create_paragraph(node, &bump)?;
63
+ Ok(paragraph.line_count(width))
64
+ }
65
+
66
+ pub fn line_width(node: Value) -> Result<usize, Error> {
67
+ let bump = Bump::new();
68
+ let paragraph = create_paragraph(node, &bump)?;
69
+ Ok(paragraph.line_width())
70
+ }
71
+
50
72
  #[cfg(test)]
51
73
  mod tests {
52
74
  use super::*;
@@ -54,7 +76,7 @@ mod tests {
54
76
 
55
77
  #[test]
56
78
  fn test_paragraph_rendering() {
57
- let p = Paragraph::new("test content").alignment(Alignment::Center);
79
+ let p = Paragraph::new("test content").alignment(HorizontalAlignment::Center);
58
80
  let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
59
81
  use ratatui::widgets::Widget;
60
82
  p.render(Rect::new(0, 0, 20, 1), &mut buf);
@@ -0,0 +1,29 @@
1
+ use magnus::{Error, Value};
2
+ use ratatui::{layout::Rect, widgets::{RatatuiLogo, RatatuiLogoSize}, Frame};
3
+
4
+ pub fn render(frame: &mut Frame, area: Rect, _node: Value) -> Result<(), Error> {
5
+ // RatatuiLogo does not support custom styling (it has fixed colors).
6
+ // It requires a size argument.
7
+ let widget = RatatuiLogo::new(RatatuiLogoSize::Small);
8
+ frame.render_widget(widget, area);
9
+ Ok(())
10
+ }
11
+
12
+ #[cfg(test)]
13
+ mod tests {
14
+ use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
15
+ use super::*;
16
+
17
+ #[test]
18
+ fn test_render() {
19
+ let mut buffer = Buffer::empty(Rect::new(0, 0, 50, 20));
20
+ let widget = RatatuiLogo::new(RatatuiLogoSize::Small);
21
+ widget.render(Rect::new(0, 0, 50, 20), &mut buffer);
22
+
23
+ let content = buffer.content().iter().map(|c| c.symbol()).collect::<String>();
24
+
25
+ // The logo uses block characters for rendering
26
+ assert!(content.contains('█'));
27
+ assert!(!content.trim().is_empty());
28
+ }
29
+ }