tenseleyflow/rcal / e19fcf6

Browse files

Wrap create modal notes

Authored by espadonne
SHA
e19fcf60d1009388fa4a872dfde1e7632565252f
Parents
4d1c7ff
Tree
36feb36

2 changed files

StatusFile+-
M src/app.rs 35 1
M src/tui.rs 153 20
src/app.rsmodified
@@ -250,6 +250,14 @@ impl CreateEventForm {
250250
                 self.focus_previous();
251251
                 CreateEventInputResult::Continue
252252
             }
253
+            KeyCode::Up => {
254
+                self.focus_previous();
255
+                CreateEventInputResult::Continue
256
+            }
257
+            KeyCode::Down => {
258
+                self.focus_next();
259
+                CreateEventInputResult::Continue
260
+            }
253261
             KeyCode::Backspace => {
254262
                 self.edit_text_field(|value| {
255263
                     value.pop();
@@ -355,7 +363,7 @@ impl CreateEventForm {
355363
             CreateEventField::EndDate => self.end_date.clone(),
356364
             CreateEventField::EndTime => self.end_time.clone(),
357365
             CreateEventField::Location => self.location.clone(),
358
-            CreateEventField::Notes => self.notes.replace('\n', " / "),
366
+            CreateEventField::Notes => self.notes.clone(),
359367
             CreateEventField::Reminder(index) => {
360368
                 let preset = REMINDER_PRESETS[index];
361369
                 format!("{} {}", checkbox(self.reminders[index]), preset.label)
@@ -424,6 +432,7 @@ pub struct CreateEventFormRow {
424432
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
425433
 pub enum CreateEventFormRowKind {
426434
     Text,
435
+    Multiline,
427436
     Toggle,
428437
 }
429438
 
@@ -516,6 +525,7 @@ impl CreateEventField {
516525
 
517526
     const fn kind(self) -> CreateEventFormRowKind {
518527
         match self {
528
+            Self::Notes => CreateEventFormRowKind::Multiline,
519529
             Self::AllDay | Self::Reminder(_) => CreateEventFormRowKind::Toggle,
520530
             _ => CreateEventFormRowKind::Text,
521531
         }
@@ -1096,6 +1106,30 @@ mod tests {
10961106
         );
10971107
     }
10981108
 
1109
+    #[test]
1110
+    fn create_form_up_and_down_move_between_fields() {
1111
+        let day = date(2026, Month::April, 23);
1112
+        let mut app = AppState::new(day);
1113
+        app.apply(AppAction::OpenCreate);
1114
+
1115
+        assert!(app.create_form().expect("form opens").rows()[0].focused);
1116
+
1117
+        assert_eq!(
1118
+            app.handle_create_key(key(KeyCode::Down)),
1119
+            CreateEventInputResult::Continue
1120
+        );
1121
+        let rows = app.create_form().expect("form stays open").rows();
1122
+        assert!(rows[1].focused);
1123
+        assert_eq!(app.selected_date(), day);
1124
+
1125
+        assert_eq!(
1126
+            app.handle_create_key(key(KeyCode::Up)),
1127
+            CreateEventInputResult::Continue
1128
+        );
1129
+        assert!(app.create_form().expect("form stays open").rows()[0].focused);
1130
+        assert_eq!(app.selected_date(), day);
1131
+    }
1132
+
10991133
     #[test]
11001134
     fn create_form_validates_required_title() {
11011135
         let mut form = CreateEventForm::new(
src/tui.rsmodified
@@ -807,7 +807,7 @@ fn render_create_event_modal(
807807
         return;
808808
     }
809809
 
810
-    let modal = create_modal_area(area);
810
+    let modal = create_modal_area(area, form);
811811
     fill_rect(buf, modal, styles.panel);
812812
     draw_border(buf, modal, styles.border, BorderCharacters::normal());
813813
 
@@ -825,29 +825,55 @@ fn render_create_event_modal(
825825
         styles.title,
826826
     );
827827
     let label_width = 12.min(content.width.saturating_sub(1));
828
+    let rows_bottom = content.bottom().saturating_sub(2);
829
+    let mut y = content.y.saturating_add(2);
828830
 
829
-    for (y, row) in (content.y.saturating_add(2)..).zip(form.rows()) {
830
-        if y >= content.bottom().saturating_sub(2) {
831
+    for row in form.rows() {
832
+        if y >= rows_bottom {
831833
             break;
832834
         }
833835
 
834
-        let marker = if row.focused { ">" } else { " " };
835
-        write_padded_left(buf, y, content.x, 1, marker, styles.label);
836836
         let label_x = content.x.saturating_add(2);
837
-        write_padded_left(buf, y, label_x, label_width, row.label, styles.label);
838
-
839837
         let value_x = label_x.saturating_add(label_width).saturating_add(1);
840
-        if value_x < content.right() {
841
-            let value_width = content.right() - value_x;
842
-            match row.kind {
843
-                CreateEventFormRowKind::Text => {
844
-                    write_padded_left(buf, y, value_x, value_width, &row.value, styles.value);
845
-                }
846
-                CreateEventFormRowKind::Toggle => {
847
-                    write_toggle_value(buf, y, value_x, value_width, &row.value, styles);
848
-                }
838
+        let value_width = content.right().saturating_sub(value_x);
839
+        let value_lines = create_modal_value_lines(row.kind, &row.value, value_width);
840
+        let row_height = u16::try_from(value_lines.len()).unwrap_or(u16::MAX);
841
+
842
+        for (line_index, value_line) in value_lines.iter().enumerate() {
843
+            let line_y = y.saturating_add(u16::try_from(line_index).unwrap_or(u16::MAX));
844
+            if line_y >= rows_bottom {
845
+                break;
849846
             }
847
+
848
+            let marker = if line_index == 0 && row.focused {
849
+                ">"
850
+            } else {
851
+                " "
852
+            };
853
+            let label = if line_index == 0 { row.label } else { "" };
854
+            write_padded_left(buf, line_y, content.x, 1, marker, styles.label);
855
+            write_padded_left(buf, line_y, label_x, label_width, label, styles.label);
856
+
857
+            if value_x < content.right() {
858
+                match row.kind {
859
+                    CreateEventFormRowKind::Text | CreateEventFormRowKind::Multiline => {
860
+                        write_padded_left(
861
+                            buf,
862
+                            line_y,
863
+                            value_x,
864
+                            value_width,
865
+                            value_line,
866
+                            styles.value,
867
+                        );
868
+                    }
869
+                    CreateEventFormRowKind::Toggle => {
870
+                        write_toggle_value(buf, line_y, value_x, value_width, value_line, styles);
871
+                    }
872
+                }
873
+            };
850874
         }
875
+
876
+        y = y.saturating_add(row_height);
851877
     }
852878
 
853879
     if let Some(error) = form.error() {
@@ -861,18 +887,19 @@ fn render_create_event_modal(
861887
         footer_y,
862888
         content.x,
863889
         content.width,
864
-        "Tab fields | Ctrl-S save | Esc cancel",
890
+        "Tab/Up/Down fields | Ctrl-S save | Esc cancel",
865891
         styles.footer,
866892
     );
867893
 }
868894
 
869
-fn create_modal_area(area: Rect) -> Rect {
895
+fn create_modal_area(area: Rect, form: &CreateEventForm) -> Rect {
870896
     if area.width < 52 || area.height < 16 {
871897
         return area;
872898
     }
873899
 
874900
     let width = area.width.saturating_sub(4).min(72);
875
-    let height = area.height.saturating_sub(4).min(22);
901
+    let max_height = area.height.saturating_sub(4);
902
+    let height = desired_create_modal_height(form, width).min(max_height);
876903
     Rect::new(
877904
         area.x + area.width.saturating_sub(width) / 2,
878905
         area.y + area.height.saturating_sub(height) / 2,
@@ -881,6 +908,40 @@ fn create_modal_area(area: Rect) -> Rect {
881908
     )
882909
 }
883910
 
911
+fn desired_create_modal_height(form: &CreateEventForm, modal_width: u16) -> u16 {
912
+    let content_width = modal_width.saturating_sub(2);
913
+    let value_width = create_modal_value_width(content_width);
914
+    let rows_height = form
915
+        .rows()
916
+        .into_iter()
917
+        .map(|row| create_modal_row_height(row.kind, &row.value, value_width))
918
+        .fold(0_u16, u16::saturating_add);
919
+
920
+    rows_height.saturating_add(6).max(22)
921
+}
922
+
923
+fn create_modal_value_width(content_width: u16) -> u16 {
924
+    let label_width = 12.min(content_width.saturating_sub(1));
925
+    content_width.saturating_sub(label_width.saturating_add(3))
926
+}
927
+
928
+fn create_modal_row_height(kind: CreateEventFormRowKind, value: &str, value_width: u16) -> u16 {
929
+    u16::try_from(create_modal_value_lines(kind, value, value_width).len()).unwrap_or(u16::MAX)
930
+}
931
+
932
+fn create_modal_value_lines(
933
+    kind: CreateEventFormRowKind,
934
+    value: &str,
935
+    value_width: u16,
936
+) -> Vec<String> {
937
+    match kind {
938
+        CreateEventFormRowKind::Multiline => wrap_text_lines(value, value_width),
939
+        CreateEventFormRowKind::Text | CreateEventFormRowKind::Toggle => {
940
+            vec![value.to_string()]
941
+        }
942
+    }
943
+}
944
+
884945
 fn render_agenda_panel(agenda: &DayAgenda, area: Rect, buf: &mut Buffer, styles: DayViewStyles) {
885946
     render_panel(area, "Agenda", buf, styles);
886947
 
@@ -1643,6 +1704,36 @@ fn write_toggle_value(
16431704
     }
16441705
 }
16451706
 
1707
+fn wrap_text_lines(text: &str, width: u16) -> Vec<String> {
1708
+    let width = usize::from(width);
1709
+    if width == 0 {
1710
+        return vec![String::new()];
1711
+    }
1712
+
1713
+    let mut lines = Vec::new();
1714
+    for raw_line in text.split('\n') {
1715
+        if raw_line.is_empty() {
1716
+            lines.push(String::new());
1717
+            continue;
1718
+        }
1719
+
1720
+        let mut line = String::new();
1721
+        for character in raw_line.chars() {
1722
+            if line.chars().count() == width {
1723
+                lines.push(line);
1724
+                line = String::new();
1725
+            }
1726
+            line.push(character);
1727
+        }
1728
+        lines.push(line);
1729
+    }
1730
+
1731
+    if lines.is_empty() {
1732
+        lines.push(String::new());
1733
+    }
1734
+    lines
1735
+}
1736
+
16461737
 const fn inset_rect(area: Rect) -> Rect {
16471738
     Rect::new(
16481739
         area.x.saturating_add(1),
@@ -2012,7 +2103,7 @@ mod tests {
20122103
 
20132104
         let area = Rect::new(0, 0, 84, 26);
20142105
         let buffer = render_app_buffer(&app, area.width, area.height);
2015
-        let modal = create_modal_area(area);
2106
+        let modal = create_modal_area(area, app.create_form().expect("form stays open"));
20162107
         let content = inset_rect(modal);
20172108
         let row_y = content.y.saturating_add(2);
20182109
         let label_x = content.x.saturating_add(2);
@@ -2035,6 +2126,48 @@ mod tests {
20352126
         assert_styled_text(&buffer, value_x + 4, row_y + 8, "5m", Color::Gray);
20362127
     }
20372128
 
2129
+    #[test]
2130
+    fn create_modal_wraps_long_notes_and_shifts_following_rows() {
2131
+        let selected = date(2026, Month::April, 23);
2132
+        let mut app = AppState::new(selected);
2133
+        app.apply(AppAction::OpenCreate);
2134
+        for _ in 0..7 {
2135
+            let _ = app.handle_create_key(key(KeyCode::Tab));
2136
+        }
2137
+        let notes = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
2138
+        for character in notes.chars() {
2139
+            let _ = app.handle_create_key(key(KeyCode::Char(character)));
2140
+        }
2141
+
2142
+        let area = Rect::new(0, 0, 58, 32);
2143
+        let buffer = render_app_buffer(&app, area.width, area.height);
2144
+        let modal = create_modal_area(area, app.create_form().expect("form stays open"));
2145
+        let content = inset_rect(modal);
2146
+        let row_y = content.y.saturating_add(2);
2147
+        let notes_y = row_y.saturating_add(7);
2148
+        let label_x = content.x.saturating_add(2);
2149
+        let label_width = 12.min(content.width.saturating_sub(1));
2150
+        let value_x = label_x.saturating_add(label_width).saturating_add(1);
2151
+
2152
+        assert!(modal.height > 22);
2153
+        assert_styled_text(&buffer, label_x, notes_y, "Notes", Color::White);
2154
+        assert_styled_text(
2155
+            &buffer,
2156
+            value_x,
2157
+            notes_y,
2158
+            "abcdefghijklmnopqrstuvwxyz0123456789A",
2159
+            Color::Gray,
2160
+        );
2161
+        assert_styled_text(
2162
+            &buffer,
2163
+            value_x,
2164
+            notes_y + 1,
2165
+            "BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijkl",
2166
+            Color::Gray,
2167
+        );
2168
+        assert_styled_text(&buffer, label_x, notes_y + 4, "Reminder", Color::White);
2169
+    }
2170
+
20382171
     #[test]
20392172
     fn create_modal_uses_full_screen_area_when_tight() {
20402173
         let mut app = AppState::new(date(2026, Month::April, 23));