@@ -807,7 +807,7 @@ fn render_create_event_modal( |
| 807 | 807 | return; |
| 808 | 808 | } |
| 809 | 809 | |
| 810 | | - let modal = create_modal_area(area); |
| 810 | + let modal = create_modal_area(area, form); |
| 811 | 811 | fill_rect(buf, modal, styles.panel); |
| 812 | 812 | draw_border(buf, modal, styles.border, BorderCharacters::normal()); |
| 813 | 813 | |
@@ -825,29 +825,55 @@ fn render_create_event_modal( |
| 825 | 825 | styles.title, |
| 826 | 826 | ); |
| 827 | 827 | 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); |
| 828 | 830 | |
| 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 { |
| 831 | 833 | break; |
| 832 | 834 | } |
| 833 | 835 | |
| 834 | | - let marker = if row.focused { ">" } else { " " }; |
| 835 | | - write_padded_left(buf, y, content.x, 1, marker, styles.label); |
| 836 | 836 | let label_x = content.x.saturating_add(2); |
| 837 | | - write_padded_left(buf, y, label_x, label_width, row.label, styles.label); |
| 838 | | - |
| 839 | 837 | 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; |
| 849 | 846 | } |
| 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 | + }; |
| 850 | 874 | } |
| 875 | + |
| 876 | + y = y.saturating_add(row_height); |
| 851 | 877 | } |
| 852 | 878 | |
| 853 | 879 | if let Some(error) = form.error() { |
@@ -861,18 +887,19 @@ fn render_create_event_modal( |
| 861 | 887 | footer_y, |
| 862 | 888 | content.x, |
| 863 | 889 | content.width, |
| 864 | | - "Tab fields | Ctrl-S save | Esc cancel", |
| 890 | + "Tab/Up/Down fields | Ctrl-S save | Esc cancel", |
| 865 | 891 | styles.footer, |
| 866 | 892 | ); |
| 867 | 893 | } |
| 868 | 894 | |
| 869 | | -fn create_modal_area(area: Rect) -> Rect { |
| 895 | +fn create_modal_area(area: Rect, form: &CreateEventForm) -> Rect { |
| 870 | 896 | if area.width < 52 || area.height < 16 { |
| 871 | 897 | return area; |
| 872 | 898 | } |
| 873 | 899 | |
| 874 | 900 | 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); |
| 876 | 903 | Rect::new( |
| 877 | 904 | area.x + area.width.saturating_sub(width) / 2, |
| 878 | 905 | area.y + area.height.saturating_sub(height) / 2, |
@@ -881,6 +908,40 @@ fn create_modal_area(area: Rect) -> Rect { |
| 881 | 908 | ) |
| 882 | 909 | } |
| 883 | 910 | |
| 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 | + |
| 884 | 945 | fn render_agenda_panel(agenda: &DayAgenda, area: Rect, buf: &mut Buffer, styles: DayViewStyles) { |
| 885 | 946 | render_panel(area, "Agenda", buf, styles); |
| 886 | 947 | |
@@ -1643,6 +1704,36 @@ fn write_toggle_value( |
| 1643 | 1704 | } |
| 1644 | 1705 | } |
| 1645 | 1706 | |
| 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 | + |
| 1646 | 1737 | const fn inset_rect(area: Rect) -> Rect { |
| 1647 | 1738 | Rect::new( |
| 1648 | 1739 | area.x.saturating_add(1), |
@@ -2012,7 +2103,7 @@ mod tests { |
| 2012 | 2103 | |
| 2013 | 2104 | let area = Rect::new(0, 0, 84, 26); |
| 2014 | 2105 | 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")); |
| 2016 | 2107 | let content = inset_rect(modal); |
| 2017 | 2108 | let row_y = content.y.saturating_add(2); |
| 2018 | 2109 | let label_x = content.x.saturating_add(2); |
@@ -2035,6 +2126,48 @@ mod tests { |
| 2035 | 2126 | assert_styled_text(&buffer, value_x + 4, row_y + 8, "5m", Color::Gray); |
| 2036 | 2127 | } |
| 2037 | 2128 | |
| 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 | + |
| 2038 | 2171 | #[test] |
| 2039 | 2172 | fn create_modal_uses_full_screen_area_when_tight() { |
| 2040 | 2173 | let mut app = AppState::new(date(2026, Month::April, 23)); |