1use ratatui::Frame;
2use ratatui::crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
3use ratatui::layout::{Constraint, Direction, Layout, Position, Rect};
4use ratatui::style::{Modifier, Style};
5use ratatui::widgets::Paragraph;
6
7use crate::components::event_state::EventState;
8use crate::components::events::{AppEvent, AppTx, InputEvent, SortTarget};
9use crate::components::file_list::{SortField, SortOrder};
10use crate::components::hint_row::HintRow;
11use crate::components::key_picker::{KeyPicker, PickerOutcome};
12use crate::components::panel::{ModalSpec, modal_chrome};
13use crate::components::sortable::{SortState, is_blank_property, property_key};
14use crate::settings::themes::Theme;
15
16#[derive(Clone, Copy, PartialEq)]
18enum Row {
19 Field,
20 Order,
21 Key,
22 GroupDirs,
23}
24
25pub struct SortDialog {
31 target: SortTarget,
32 pub(crate) field: SortField,
33 pub(crate) order: SortOrder,
34 group_dirs: Option<bool>,
36 allows_property: bool,
38 unsorted: bool,
41 rows: Vec<Row>,
42 selected: usize,
43 picker: KeyPicker,
44 row_rects: Vec<Rect>,
46 key_hints: HintRow,
48 row_hints: HintRow,
50}
51
52impl SortDialog {
53 pub fn new(target: SortTarget, state: SortState, allows_property: bool) -> Self {
56 let SortState {
57 field,
58 order,
59 group_dirs,
60 } = state;
61 let key = property_key(&field).unwrap_or_default().to_string();
62 let mut d = Self {
63 target,
64 field,
65 order,
66 group_dirs,
67 allows_property,
68 unsorted: false,
69 rows: Vec::new(),
70 selected: 0,
71 picker: KeyPicker::new(&key),
72 row_rects: Vec::new(),
73 key_hints: HintRow::new(&[
74 (KeyCode::Enter, "Enter", "Apply"),
75 (KeyCode::Esc, "Esc", "Close"),
76 ])
77 .with_prefix("type a key · "),
78 row_hints: if target == SortTarget::Sidebar {
79 HintRow::new(&[
80 (KeyCode::Null, "Space", "Toggle"),
81 (KeyCode::Char('s'), "s", "Save default"),
82 (KeyCode::Esc, "Esc", "Close"),
83 ])
84 } else {
85 HintRow::new(&[
86 (KeyCode::Null, "↑↓", "Move"),
87 (KeyCode::Null, "Space", "Toggle"),
88 (KeyCode::Esc, "Enter/Esc", "Close"),
89 ])
90 },
91 };
92 d.rebuild_rows();
93 d
94 }
95
96 fn rebuild_rows(&mut self) {
98 let mut rows = vec![Row::Field, Row::Order];
99 if matches!(self.field, SortField::Property(_)) {
100 rows.push(Row::Key);
101 }
102 if self.group_dirs.is_some() {
103 rows.push(Row::GroupDirs);
104 }
105 self.rows = rows;
106 self.selected = self.selected.min(self.rows.len() - 1);
107 }
108
109 pub fn unsorted(mut self, unsorted: bool) -> Self {
111 self.unsorted = unsorted;
112 self
113 }
114
115 pub(crate) fn set_keys(&mut self, keys: Vec<String>) {
116 self.picker.set_keys(keys);
117 }
118
119 #[cfg(test)]
120 pub(crate) fn key_value(&self) -> &str {
121 self.picker.value()
122 }
123
124 #[cfg(test)]
125 pub(crate) fn row_origin(&self, i: usize) -> Option<(u16, u16)> {
126 self.row_rects.get(i).map(|r| (r.x, r.y))
127 }
128
129 #[cfg(test)]
130 pub(crate) fn row_count(&self) -> usize {
131 self.rows.len()
132 }
133
134 fn emit(&self, tx: &AppTx, persist: bool) {
137 if is_blank_property(&self.field) {
139 return;
140 }
141 tx.send(AppEvent::SortChanged {
142 target: self.target,
143 state: SortState {
144 field: self.field.clone(),
145 order: self.order,
146 group_dirs: self.group_dirs,
147 },
148 persist,
149 })
150 .ok();
151 }
152
153 fn commit_typed_key(&mut self) {
156 let typed = self.picker.value().trim();
157 if matches!(self.field, SortField::Property(_)) && !typed.is_empty() {
158 self.field = SortField::Property(typed.to_string());
159 }
160 }
161
162 fn toggle_selected(&mut self, tx: &AppTx) {
163 match self.rows[self.selected] {
164 Row::Field if self.unsorted => {}
167 Row::Field => {
168 self.field = self.field.cycle(self.allows_property);
169 self.rebuild_rows();
170 }
171 Row::Order => self.order = self.order.toggle(),
172 Row::Key => {}
173 Row::GroupDirs => self.group_dirs = self.group_dirs.map(|g| !g),
174 }
175 if self.unsorted {
178 self.unsorted = false;
179 self.field = SortField::Name;
180 self.rebuild_rows();
181 }
182 self.commit_typed_key();
183 self.emit(tx, false);
184 }
185
186 pub fn handle_key(&mut self, key: KeyEvent, tx: &AppTx) -> EventState {
187 if self.rows[self.selected] == Row::Key {
188 let list_open = self.picker.is_list_open();
189 let passthrough =
190 !list_open && matches!(key.code, KeyCode::Up | KeyCode::Down | KeyCode::Esc);
191 if !passthrough {
192 match self.picker.handle_key(&key) {
193 PickerOutcome::Accepted(k) => {
194 self.field = SortField::Property(k);
195 self.emit(tx, false);
196 }
197 PickerOutcome::Submit => {
198 let typed = self.picker.value().trim().to_string();
199 if !typed.is_empty() && self.field != SortField::Property(typed.clone()) {
200 self.field = SortField::Property(typed);
201 self.emit(tx, false);
202 } else {
203 tx.send(AppEvent::CloseOverlay).ok();
204 }
205 }
206 PickerOutcome::Cancel => {
207 tx.send(AppEvent::CloseOverlay).ok();
208 }
209 _ => {}
210 }
211 return EventState::Consumed;
212 }
213 }
214 match key.code {
215 KeyCode::Up => {
216 self.selected = self.selected.saturating_sub(1);
217 }
218 KeyCode::Down => {
219 self.selected = (self.selected + 1).min(self.rows.len() - 1);
220 }
221 KeyCode::Char(' ') | KeyCode::Left | KeyCode::Right => {
222 self.toggle_selected(tx);
223 }
224 KeyCode::Char('s') if self.target == SortTarget::Sidebar => {
225 self.emit(tx, true);
226 }
227 KeyCode::Enter | KeyCode::Esc => {
228 tx.send(AppEvent::CloseOverlay).ok();
229 }
230 _ => {}
231 }
232 EventState::Consumed
233 }
234
235 pub fn handle_mouse(&mut self, ev: &MouseEvent, tx: &AppTx) -> EventState {
236 if !matches!(ev.kind, MouseEventKind::Down(MouseButton::Left)) {
237 return EventState::Consumed;
238 }
239
240 let key_row = self.rows.iter().position(|r| *r == Row::Key);
241 if key_row.is_some() {
242 match self.picker.handle_click(ev.column, ev.row) {
243 PickerOutcome::Accepted(k) => {
244 self.field = SortField::Property(k);
245 self.emit(tx, false);
246 return EventState::Consumed;
247 }
248 PickerOutcome::Consumed => {
249 if let Some(i) = key_row {
250 self.selected = i;
251 }
252 return EventState::Consumed;
253 }
254 _ => {}
255 }
256 }
257 let hints = if self.rows[self.selected] == Row::Key {
260 &self.key_hints
261 } else {
262 &self.row_hints
263 };
264 if let Some(key) = hints.hit(ev) {
265 return self.handle_key(key, tx);
266 }
267 let pos = Position {
268 x: ev.column,
269 y: ev.row,
270 };
271 if let Some(i) = self.row_rects.iter().position(|r| r.contains(pos)) {
272 self.selected = i;
273 if self.rows[i] != Row::Key {
274 self.toggle_selected(tx);
275 }
276 }
277 EventState::Consumed
278 }
279
280 fn row_label(&self, row: Row) -> (String, String) {
281 match row {
282 Row::Field => (
283 "Sort by".to_string(),
284 match &self.field {
285 _ if self.unsorted => "Unsorted".to_string(),
286 SortField::Name => "Name".to_string(),
287 SortField::Title => "Title".to_string(),
288 SortField::Property(_) => "Property".to_string(),
289 },
290 ),
291 Row::Key => ("Key".to_string(), String::new()),
292 Row::Order => (
293 "Order".to_string(),
294 match self.order {
295 SortOrder::Ascending => "Ascending \u{2191}".to_string(),
296 SortOrder::Descending => "Descending \u{2193}".to_string(),
297 },
298 ),
299 Row::GroupDirs => (
300 "Group directories".to_string(),
301 if self.group_dirs == Some(true) {
302 "On"
303 } else {
304 "Off"
305 }
306 .to_string(),
307 ),
308 }
309 }
310}
311
312const OUTER_WIDTH: u16 = 50;
314
315impl crate::components::Component for SortDialog {
316 fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
317 match event {
318 InputEvent::Key(key) => self.handle_key(*key, tx),
319 InputEvent::Mouse(m) => self.handle_mouse(m, tx),
320 _ => EventState::NotConsumed,
321 }
322 }
323
324 fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, _focused: bool) {
325 let list_open = self.rows[self.selected] == Row::Key && self.picker.is_list_open();
327 let outer_height = self.rows.len() as u16 + 3 + if list_open { 5 } else { 0 };
328 let popup = super::fixed_centered_rect(OUTER_WIDTH, outer_height, rect);
329 let inner = modal_chrome(
330 f,
331 popup,
332 theme,
333 ModalSpec {
334 title: Some(" Sort "),
335 border: Some(Style::default().fg(theme.fg.to_ratatui())),
336 ..Default::default()
337 },
338 );
339 if inner.height < 2 {
340 return;
341 }
342
343 let chunks = Layout::default()
347 .direction(Direction::Vertical)
348 .constraints([Constraint::Min(1), Constraint::Length(1)])
349 .split(inner);
350 let body = chunks[0];
351 let footer_area = chunks[1];
352
353 let bg = theme.bg_panel.to_ratatui();
354 let fg = theme.fg.to_ratatui();
355 let gray = theme.gray.to_ratatui();
356 let fg_sel = theme.selection_fg.to_ratatui();
357 let bg_sel = theme.selection_bg.to_ratatui();
358
359 self.row_rects.clear();
360 let mut key_field = None;
361 for (i, &row) in self.rows.iter().enumerate() {
362 let y = body.y + i as u16;
363 if y >= body.y + body.height {
364 break;
365 }
366 let (label, value) = self.row_label(row);
367 let selected = i == self.selected;
368 let style = if selected {
369 Style::default()
370 .fg(fg_sel)
371 .bg(bg_sel)
372 .add_modifier(Modifier::BOLD)
373 } else {
374 Style::default().fg(fg).bg(bg)
375 };
376 let marker = if selected { ">" } else { " " };
377 let row_rect = Rect {
378 x: body.x,
379 y,
380 width: body.width,
381 height: 1,
382 };
383 f.render_widget(
384 Paragraph::new(format!(" {marker} {label:<20}{value}")).style(style),
385 row_rect,
386 );
387 self.row_rects.push(row_rect);
388 if row == Row::Key {
389 let off = 3 + 20;
390 key_field = Some((
391 Rect {
392 x: row_rect.x + off.min(row_rect.width),
393 y,
394 width: row_rect.width.saturating_sub(off),
395 height: 1,
396 },
397 selected,
398 ));
399 }
400 }
401
402 let key_selected = self.rows[self.selected] == Row::Key;
403 let hints = if key_selected {
404 &mut self.key_hints
405 } else {
406 &mut self.row_hints
407 };
408 hints.render(f, footer_area, Style::default().fg(gray).bg(bg), theme);
409
410 if let Some((field, focused)) = key_field {
412 self.picker.render(f, field, inner, theme, focused);
413 }
414 }
415}
416
417impl_dialog!(SortDialog);
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422 use crate::components::events::SortTarget;
423 use crate::components::file_list::{SortField, SortOrder};
424 use crate::components::sortable::SortState;
425 use ratatui::crossterm::event::{KeyCode, KeyEvent};
426 use tokio::sync::mpsc::unbounded_channel;
427
428 fn key(code: KeyCode) -> KeyEvent {
429 KeyEvent::from(code)
430 }
431
432 fn state(field: SortField, group_dirs: Option<bool>) -> SortState {
433 SortState {
434 field,
435 order: SortOrder::Ascending,
436 group_dirs,
437 }
438 }
439
440 fn sidebar_dialog() -> SortDialog {
441 SortDialog::new(
442 SortTarget::Sidebar,
443 state(SortField::Name, Some(false)),
444 false,
445 )
446 }
447
448 #[test]
449 fn empty_property_field_emits_nothing() {
450 let mut d = SortDialog::new(SortTarget::Query, state(SortField::Name, None), true);
451 let (tx, mut rx) = unbounded_channel();
452 d.handle_key(key(KeyCode::Char(' ')), &tx); assert!(rx.try_recv().is_ok());
454 d.handle_key(key(KeyCode::Char(' ')), &tx); assert_eq!(d.field, SortField::Property(String::new()));
456 assert!(rx.try_recv().is_err(), "no event for an empty key");
457 }
458
459 #[test]
460 fn space_toggles_field_and_emits_change() {
461 let mut d = sidebar_dialog();
462 let (tx, mut rx) = unbounded_channel();
463 d.handle_key(key(KeyCode::Char(' ')), &tx);
464 assert_eq!(d.field, SortField::Title);
465 let evt = rx.try_recv().expect("a SortChanged event");
466 match evt {
467 AppEvent::SortChanged {
468 target,
469 state,
470 persist,
471 } => {
472 assert_eq!(target, SortTarget::Sidebar);
473 assert_eq!(state.field, SortField::Title);
474 assert_eq!(state.order, SortOrder::Ascending);
475 assert_eq!(state.group_dirs, Some(false));
476 assert!(!persist, "a plain toggle is not a save");
477 }
478 other => panic!("expected SortChanged, got {other:?}"),
479 }
480 }
481
482 #[test]
483 fn down_then_space_toggles_order() {
484 let mut d = sidebar_dialog();
485 let (tx, mut rx) = unbounded_channel();
486 d.handle_key(key(KeyCode::Down), &tx);
487 assert!(rx.try_recv().is_err(), "navigation alone emits nothing");
488 d.handle_key(key(KeyCode::Char(' ')), &tx);
489 assert_eq!(d.order, SortOrder::Descending);
490 assert!(matches!(rx.try_recv(), Ok(AppEvent::SortChanged { .. })));
491 }
492
493 #[test]
494 fn group_row_present_only_with_group_dirs() {
495 let sidebar = sidebar_dialog();
496 assert_eq!(sidebar.row_count(), 3);
497 let query = SortDialog::new(SortTarget::Query, state(SortField::Name, None), true);
498 assert_eq!(query.row_count(), 2);
499 }
500
501 #[test]
502 fn s_saves_default_for_sidebar_only() {
503 let mut d = sidebar_dialog();
504 let (tx, mut rx) = unbounded_channel();
505 d.handle_key(key(KeyCode::Char('s')), &tx);
506 assert!(
507 matches!(
508 rx.try_recv(),
509 Ok(AppEvent::SortChanged { persist: true, .. })
510 ),
511 "s on the sidebar emits a persisting SortChanged"
512 );
513
514 let mut q = SortDialog::new(SortTarget::Query, state(SortField::Name, None), true);
515 let (tx2, mut rx2) = unbounded_channel();
516 q.handle_key(key(KeyCode::Char('s')), &tx2);
517 assert!(rx2.try_recv().is_err(), "query target has no save-default");
518 }
519
520 #[test]
521 fn enter_and_esc_close_overlay() {
522 for code in [KeyCode::Enter, KeyCode::Esc] {
523 let mut d = sidebar_dialog();
524 let (tx, mut rx) = unbounded_channel();
525 d.handle_key(key(code), &tx);
526 assert!(matches!(rx.try_recv(), Ok(AppEvent::CloseOverlay)));
527 }
528 }
529
530 use crate::components::events::InputEvent;
531 use crate::settings::themes::Theme;
532 use ratatui::{Terminal, backend::TestBackend};
533
534 fn query_dialog(field: SortField) -> SortDialog {
535 SortDialog::new(SortTarget::Query, state(field, None), true)
536 }
537
538 fn draw(d: &mut SortDialog) {
539 use crate::components::Component;
540 let theme = Theme::gruvbox_dark();
541 let mut t = Terminal::new(TestBackend::new(80, 24)).unwrap();
542 t.draw(|f| d.render(f, f.area(), &theme, true)).unwrap();
543 }
544
545 fn mouse(col: u16, row: u16) -> ratatui::crossterm::event::MouseEvent {
546 match crate::test_support::mouse_down_at(col, row) {
547 InputEvent::Mouse(m) => m,
548 _ => unreachable!(),
549 }
550 }
551
552 #[test]
553 fn query_cycle_reaches_property_and_waits_for_a_key() {
554 let mut d = query_dialog(SortField::Title);
555 let (tx, mut rx) = unbounded_channel();
556 d.handle_key(key(KeyCode::Char(' ')), &tx);
557 assert_eq!(d.field, SortField::Property(String::new()));
558 assert!(rx.try_recv().is_err(), "no emit before a key is chosen");
559 assert_eq!(d.row_count(), 3, "Key row appears");
560 }
561
562 #[test]
563 fn accepting_a_key_emits_property_sort() {
564 let mut d = query_dialog(SortField::Property(String::new()));
565 d.set_keys(vec!["due".into(), "status".into()]);
566 let (tx, mut rx) = unbounded_channel();
567 d.handle_key(key(KeyCode::Down), &tx); d.handle_key(key(KeyCode::Down), &tx); for c in "du".chars() {
570 d.handle_key(key(KeyCode::Char(c)), &tx);
571 }
572 d.handle_key(key(KeyCode::Enter), &tx); match rx.try_recv() {
574 Ok(AppEvent::SortChanged { state, .. }) => {
575 assert_eq!(state.field, SortField::Property("due".into()))
576 }
577 other => panic!("expected SortChanged, got {other:?}"),
578 }
579 }
580
581 #[test]
584 fn typed_key_applies_when_toggling_another_row() {
585 let mut d = query_dialog(SortField::Property(String::new()));
586 let (tx, mut rx) = unbounded_channel();
587 d.handle_key(key(KeyCode::Down), &tx); d.handle_key(key(KeyCode::Down), &tx); for c in "due".chars() {
590 d.handle_key(key(KeyCode::Char(c)), &tx);
591 }
592 d.handle_key(key(KeyCode::Up), &tx); d.handle_key(key(KeyCode::Char(' ')), &tx);
594 match rx.try_recv() {
595 Ok(AppEvent::SortChanged { state, .. }) => {
596 assert_eq!(state.field, SortField::Property("due".into()));
597 assert_eq!(state.order, SortOrder::Descending);
598 }
599 other => panic!("expected SortChanged, got {other:?}"),
600 }
601 }
602
603 #[test]
604 fn opening_with_a_property_sort_preselects_it() {
605 let d = query_dialog(SortField::Property("due".into()));
606 assert_eq!(d.row_count(), 3);
607 assert_eq!(d.key_value(), "due");
608 }
609
610 #[test]
611 fn sidebar_never_cycles_to_property() {
612 let mut d = sidebar_dialog();
613 let (tx, _rx) = unbounded_channel();
614 d.handle_key(key(KeyCode::Char(' ')), &tx); d.handle_key(key(KeyCode::Char(' ')), &tx); assert_eq!(d.field, SortField::Name);
617 }
618
619 #[test]
620 fn click_on_a_row_selects_and_toggles_it() {
621 let mut d = sidebar_dialog();
622 draw(&mut d);
623 let (tx, mut rx) = unbounded_channel();
624 let (x, y) = d.row_origin(1).expect("Order row rendered");
625 d.handle_mouse(&mouse(x + 2, y), &tx);
626 assert_eq!(d.order, SortOrder::Descending);
627 assert!(matches!(rx.try_recv(), Ok(AppEvent::SortChanged { .. })));
628 }
629
630 #[test]
631 fn click_outside_sort_modal_does_nothing() {
632 let mut d = sidebar_dialog();
633 draw(&mut d);
634 let (tx, mut rx) = unbounded_channel();
635 d.handle_mouse(&mouse(0, 0), &tx);
636 assert!(rx.try_recv().is_err());
637 assert_eq!(d.field, SortField::Name);
638 }
639
640 #[test]
641 fn enter_on_key_row_with_list_closed_closes_overlay() {
642 let mut d = query_dialog(SortField::Property("due".into()));
643 let (tx, mut rx) = unbounded_channel();
644 d.handle_key(key(KeyCode::Down), &tx);
645 d.handle_key(key(KeyCode::Down), &tx);
646 d.handle_key(key(KeyCode::Enter), &tx);
647 assert!(matches!(rx.try_recv(), Ok(AppEvent::CloseOverlay)));
648 }
649
650 #[test]
652 fn browser_dialog_cycles_to_property_without_group_row() {
653 let mut d = SortDialog::new(SortTarget::Browser, state(SortField::Title, None), true);
654 assert_eq!(d.row_count(), 2, "no group row without group_dirs");
655 let (tx, mut rx) = unbounded_channel();
656 d.handle_key(key(KeyCode::Char(' ')), &tx);
657 assert_eq!(d.field, SortField::Property(String::new()));
658 d.handle_key(key(KeyCode::Char('s')), &tx);
659 assert!(rx.try_recv().is_err(), "browser has no save-default");
660 }
661
662 #[test]
665 fn sidebar_dialog_cycles_to_property_with_group_row() {
666 let mut d = SortDialog::new(
667 SortTarget::Sidebar,
668 state(SortField::Title, Some(true)),
669 true,
670 );
671 assert_eq!(d.row_count(), 3);
672 let (tx, _rx) = unbounded_channel();
673 d.handle_key(key(KeyCode::Char(' ')), &tx);
674 assert_eq!(d.field, SortField::Property(String::new()));
675 assert_eq!(d.row_count(), 4, "field, order, key and group rows");
676 }
677
678 #[test]
680 fn disallowed_property_never_cycles_to_property() {
681 let mut d = SortDialog::new(SortTarget::Query, state(SortField::Name, None), false);
682 let (tx, _rx) = unbounded_channel();
683 d.handle_key(key(KeyCode::Char(' ')), &tx);
684 d.handle_key(key(KeyCode::Char(' ')), &tx);
685 assert_eq!(d.field, SortField::Name);
686 }
687
688 fn finder_dialog() -> SortDialog {
689 SortDialog::new(SortTarget::Browser, state(SortField::Name, None), true).unsorted(true)
690 }
691
692 fn emitted(rx: &mut tokio::sync::mpsc::UnboundedReceiver<AppEvent>) -> SortState {
693 match rx.try_recv() {
694 Ok(AppEvent::SortChanged { state, .. }) => state,
695 other => panic!("expected SortChanged, got {other:?}"),
696 }
697 }
698
699 #[test]
702 fn unsorted_list_shows_unsorted_until_a_pick() {
703 let d = finder_dialog();
704 assert_eq!(d.row_label(Row::Field).1, "Unsorted");
705 assert_eq!(sidebar_dialog().row_label(Row::Field).1, "Name");
706 }
707
708 #[test]
710 fn first_field_toggle_while_unsorted_picks_name_ascending() {
711 let mut d = finder_dialog();
712 let (tx, mut rx) = unbounded_channel();
713 d.handle_key(key(KeyCode::Char(' ')), &tx);
714 let st = emitted(&mut rx);
715 assert_eq!(
716 (st.field, st.order),
717 (SortField::Name, SortOrder::Ascending)
718 );
719 assert_eq!(d.row_label(Row::Field).1, "Name");
720 d.handle_key(key(KeyCode::Char(' ')), &tx);
721 assert_eq!(emitted(&mut rx).field, SortField::Title, "then cycles");
722 }
723
724 #[test]
726 fn order_toggle_while_unsorted_picks_name_descending() {
727 let mut d = finder_dialog();
728 let (tx, mut rx) = unbounded_channel();
729 d.handle_key(key(KeyCode::Down), &tx);
730 d.handle_key(key(KeyCode::Char(' ')), &tx);
731 let st = emitted(&mut rx);
732 assert_eq!(
733 (st.field, st.order),
734 (SortField::Name, SortOrder::Descending)
735 );
736 assert_eq!(d.row_label(Row::Field).1, "Name");
737 }
738}