1use super::{adapter::DatasetAdapter, scroll::ScrollState};
6
7#[derive(Debug, Clone)]
23pub struct RowDetailView {
24 row_index: usize,
26 fields: Vec<(String, String)>,
28 scroll: ScrollState,
30 display_width: u16,
32 display_height: u16,
34}
35
36impl RowDetailView {
37 pub fn new(adapter: &DatasetAdapter, row_index: usize) -> Option<Self> {
46 Self::with_dimensions(adapter, row_index, 80, 24)
47 }
48
49 pub fn with_dimensions(
51 adapter: &DatasetAdapter,
52 row_index: usize,
53 width: u16,
54 height: u16,
55 ) -> Option<Self> {
56 if row_index >= adapter.row_count() {
57 return None;
58 }
59
60 let fields: Vec<(String, String)> = (0..adapter.column_count())
62 .filter_map(|col| {
63 let name = adapter.field_name(col)?.to_string();
64 let value = adapter
65 .get_cell(row_index, col)
66 .ok()
67 .flatten()
68 .unwrap_or_else(|| "NULL".to_string());
69 Some((name, value))
70 })
71 .collect();
72
73 let total_lines = calculate_total_lines(&fields, width);
75 let visible_lines = height.saturating_sub(2) as usize; let scroll = ScrollState::new(total_lines, visible_lines);
78
79 Some(Self {
80 row_index,
81 fields,
82 scroll,
83 display_width: width,
84 display_height: height,
85 })
86 }
87
88 pub fn row_index(&self) -> usize {
90 self.row_index
91 }
92
93 pub fn field_count(&self) -> usize {
95 self.fields.len()
96 }
97
98 pub fn is_empty(&self) -> bool {
100 self.fields.is_empty()
101 }
102
103 pub fn field_value(&self, name: &str) -> Option<&str> {
105 self.fields
106 .iter()
107 .find(|(n, _)| n == name)
108 .map(|(_, v)| v.as_str())
109 }
110
111 pub fn field_by_index(&self, index: usize) -> Option<(&str, &str)> {
113 self.fields
114 .get(index)
115 .map(|(n, v)| (n.as_str(), v.as_str()))
116 }
117
118 pub fn scroll_down(&mut self) {
122 self.scroll.scroll_down();
123 }
124
125 pub fn scroll_up(&mut self) {
127 self.scroll.scroll_up();
128 }
129
130 pub fn page_down(&mut self) {
132 self.scroll.page_down();
133 }
134
135 pub fn page_up(&mut self) {
137 self.scroll.page_up();
138 }
139
140 pub fn scroll_offset(&self) -> usize {
142 self.scroll.offset()
143 }
144
145 pub fn render_lines(&self) -> Vec<String> {
147 let max_width = self.display_width.saturating_sub(4) as usize; let mut all_lines = Vec::new();
149
150 all_lines.push(format!("Row {}", self.row_index));
152 all_lines.push(String::new());
153
154 for (name, value) in &self.fields {
156 all_lines.push(format!("{name}:"));
158
159 let wrapped = wrap_text(value, max_width);
161 for line in wrapped {
162 all_lines.push(format!(" {line}"));
163 }
164
165 all_lines.push(String::new());
167 }
168
169 let start = self.scroll.offset();
171 let visible = self.display_height.saturating_sub(2) as usize;
172 let end = (start + visible).min(all_lines.len());
173
174 all_lines[start..end].to_vec()
175 }
176
177 pub fn render(&self) -> String {
179 self.render_lines().join("\n")
180 }
181}
182
183fn calculate_total_lines(fields: &[(String, String)], width: u16) -> usize {
185 let max_width = width.saturating_sub(4) as usize;
186
187 fields
188 .iter()
189 .map(|(_, value)| {
190 1 + wrap_text(value, max_width).len() + 1
192 })
193 .sum::<usize>()
194 .saturating_add(2) }
196
197fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
199 if max_width == 0 {
200 return vec![text.to_string()];
201 }
202
203 let mut lines = Vec::new();
204
205 for line in text.lines() {
206 if line.is_empty() {
207 lines.push(String::new());
208 continue;
209 }
210
211 let chars: Vec<char> = line.chars().collect();
212 let mut start = 0;
213
214 while start < chars.len() {
215 let end = (start + max_width).min(chars.len());
216 let segment: String = chars[start..end].iter().collect();
217 lines.push(segment);
218 start = end;
219 }
220 }
221
222 if lines.is_empty() {
223 lines.push(String::new());
224 }
225
226 lines
227}
228
229#[cfg(test)]
230mod tests {
231 use std::sync::Arc;
232
233 use arrow::{
234 array::{Float32Array, Int32Array, RecordBatch, StringArray},
235 datatypes::{DataType, Field, Schema},
236 };
237
238 use super::*;
239
240 fn create_test_adapter() -> DatasetAdapter {
241 let schema = Arc::new(Schema::new(vec![
242 Field::new("id", DataType::Utf8, false),
243 Field::new("description", DataType::Utf8, false),
244 Field::new("value", DataType::Int32, false),
245 Field::new("score", DataType::Float32, false),
246 ]));
247
248 let ids = vec!["row_0", "row_1", "row_2"];
249 let descriptions = vec![
250 "Short description",
251 "This is a much longer description that will need to be wrapped across multiple lines when displayed in the detail view",
252 "Another row",
253 ];
254 let values = vec![100, 200, 300];
255 let scores = vec![0.95_f32, 0.87, 0.99];
256
257 let batch = RecordBatch::try_new(
258 schema.clone(),
259 vec![
260 Arc::new(StringArray::from(ids)),
261 Arc::new(StringArray::from(descriptions)),
262 Arc::new(Int32Array::from(values)),
263 Arc::new(Float32Array::from(scores)),
264 ],
265 )
266 .unwrap();
267
268 DatasetAdapter::from_batches(vec![batch], schema).unwrap()
269 }
270
271 #[test]
272 fn f_detail_new() {
273 let adapter = create_test_adapter();
274 let detail = RowDetailView::new(&adapter, 0);
275 assert!(detail.is_some());
276 }
277
278 #[test]
279 fn f_detail_out_of_bounds() {
280 let adapter = create_test_adapter();
281 let detail = RowDetailView::new(&adapter, 100);
282 assert!(detail.is_none());
283 }
284
285 #[test]
286 fn f_detail_row_index() {
287 let adapter = create_test_adapter();
288 let detail = RowDetailView::new(&adapter, 1).unwrap();
289 assert_eq!(detail.row_index(), 1);
290 }
291
292 #[test]
293 fn f_detail_field_count() {
294 let adapter = create_test_adapter();
295 let detail = RowDetailView::new(&adapter, 0).unwrap();
296 assert_eq!(detail.field_count(), 4);
297 }
298
299 #[test]
300 fn f_detail_field_value_by_name() {
301 let adapter = create_test_adapter();
302 let detail = RowDetailView::new(&adapter, 0).unwrap();
303 let value = detail.field_value("id");
304 assert_eq!(value, Some("row_0"));
305 }
306
307 #[test]
308 fn f_detail_field_value_not_found() {
309 let adapter = create_test_adapter();
310 let detail = RowDetailView::new(&adapter, 0).unwrap();
311 let value = detail.field_value("nonexistent");
312 assert!(value.is_none());
313 }
314
315 #[test]
316 fn f_detail_field_by_index() {
317 let adapter = create_test_adapter();
318 let detail = RowDetailView::new(&adapter, 0).unwrap();
319 let (name, value) = detail.field_by_index(0).unwrap();
320 assert_eq!(name, "id");
321 assert_eq!(value, "row_0");
322 }
323
324 #[test]
325 fn f_detail_field_by_index_out_of_bounds() {
326 let adapter = create_test_adapter();
327 let detail = RowDetailView::new(&adapter, 0).unwrap();
328 assert!(detail.field_by_index(100).is_none());
329 }
330
331 #[test]
332 fn f_detail_render_lines() {
333 let adapter = create_test_adapter();
334 let detail = RowDetailView::new(&adapter, 0).unwrap();
335 let lines = detail.render_lines();
336
337 assert!(!lines.is_empty());
338 assert!(lines[0].contains("Row 0"));
339 }
340
341 #[test]
342 fn f_detail_render() {
343 let adapter = create_test_adapter();
344 let detail = RowDetailView::new(&adapter, 0).unwrap();
345 let rendered = detail.render();
346
347 assert!(rendered.contains("Row 0"));
348 assert!(rendered.contains("id:"));
349 assert!(rendered.contains("row_0"));
350 }
351
352 #[test]
353 fn f_detail_scroll_down() {
354 let adapter = create_test_adapter();
355 let mut detail = RowDetailView::new(&adapter, 1).unwrap();
356 let initial = detail.scroll_offset();
357 detail.scroll_down();
358 assert!(detail.scroll_offset() >= initial);
360 }
361
362 #[test]
363 fn f_detail_scroll_up() {
364 let adapter = create_test_adapter();
365 let mut detail = RowDetailView::with_dimensions(&adapter, 1, 40, 10).unwrap();
366 detail.scroll_down();
367 detail.scroll_down();
368 detail.scroll_down();
369 let after_down = detail.scroll_offset();
370 detail.scroll_up();
371 assert!(detail.scroll_offset() <= after_down);
372 }
373
374 #[test]
375 fn f_detail_is_empty() {
376 let adapter = create_test_adapter();
377 let detail = RowDetailView::new(&adapter, 0).unwrap();
378 assert!(!detail.is_empty());
379 }
380
381 #[test]
382 fn f_detail_clone() {
383 let adapter = create_test_adapter();
384 let detail = RowDetailView::new(&adapter, 0).unwrap();
385 let cloned = detail.clone();
386 assert_eq!(detail.row_index(), cloned.row_index());
387 assert_eq!(detail.field_count(), cloned.field_count());
388 }
389
390 #[test]
391 fn f_wrap_text_short() {
392 let wrapped = wrap_text("hello", 20);
393 assert_eq!(wrapped, vec!["hello"]);
394 }
395
396 #[test]
397 fn f_wrap_text_long() {
398 let text = "This is a long line that needs wrapping";
399 let wrapped = wrap_text(text, 10);
400 assert!(wrapped.len() > 1);
401 for line in &wrapped {
402 assert!(line.chars().count() <= 10);
403 }
404 }
405
406 #[test]
407 fn f_wrap_text_multiline() {
408 let text = "Line one\nLine two";
409 let wrapped = wrap_text(text, 50);
410 assert_eq!(wrapped.len(), 2);
411 }
412
413 #[test]
414 fn f_wrap_text_empty() {
415 let wrapped = wrap_text("", 20);
416 assert_eq!(wrapped.len(), 1);
417 }
418
419 #[test]
420 fn f_wrap_text_zero_width() {
421 let wrapped = wrap_text("hello", 0);
422 assert_eq!(wrapped, vec!["hello"]);
423 }
424
425 #[test]
426 fn f_calculate_total_lines() {
427 let fields = vec![
428 ("name".to_string(), "value".to_string()),
429 ("other".to_string(), "data".to_string()),
430 ];
431 let total = calculate_total_lines(&fields, 80);
432 assert!(total >= 8);
434 }
435
436 #[test]
437 fn f_detail_page_down() {
438 let adapter = create_test_adapter();
439 let mut detail = RowDetailView::with_dimensions(&adapter, 1, 40, 5).unwrap();
440 let initial = detail.scroll_offset();
441 detail.page_down();
442 assert!(detail.scroll_offset() >= initial);
444 }
445
446 #[test]
447 fn f_detail_page_up() {
448 let adapter = create_test_adapter();
449 let mut detail = RowDetailView::with_dimensions(&adapter, 1, 40, 5).unwrap();
450 detail.page_down();
452 detail.page_down();
453 let after_down = detail.scroll_offset();
454 detail.page_up();
456 assert!(detail.scroll_offset() <= after_down);
458 }
459
460 #[test]
461 fn f_wrap_text_with_empty_line() {
462 let text = "First\n\nThird";
463 let wrapped = wrap_text(text, 50);
464 assert_eq!(wrapped.len(), 3);
465 assert_eq!(wrapped[1], "");
466 }
467}