1use crate::widgets::{AmountWidget, EditMode, SelectOption, SelectWidget, WidgetKind};
21
22const COL_COUNT: usize = 6;
23
24pub const COL_FROM: usize = 0;
25pub const COL_TO: usize = 1;
26pub const COL_FROM_COMM: usize = 2;
27pub const COL_TO_COMM: usize = 3;
28pub const COL_VALUE: usize = 4;
29pub const COL_TO_AMOUNT: usize = 5;
30
31pub struct SplitRowPrefill<'a> {
33 pub from: &'a str,
34 pub to: &'a str,
35 pub from_commodity: &'a str,
36 pub to_commodity: &'a str,
37 pub value: &'a str,
38 pub to_amount: Option<&'a str>,
39}
40
41#[derive(Debug)]
43pub struct SplitRow {
44 pub from: SelectWidget,
45 pub to: SelectWidget,
46 pub from_commodity: SelectWidget,
47 pub to_commodity: SelectWidget,
48 pub value: AmountWidget,
49 pub to_amount: AmountWidget,
50}
51
52impl SplitRow {
53 fn new(mode: EditMode, accounts: &[SelectOption], commodities: &[SelectOption]) -> Self {
56 let mut row = Self {
57 from: SelectWidget::new(),
58 to: SelectWidget::new(),
59 from_commodity: SelectWidget::new(),
60 to_commodity: SelectWidget::new(),
61 value: AmountWidget::new(mode),
62 to_amount: AmountWidget::new(mode),
63 };
64 row.from.set_options(accounts.to_vec());
65 row.to.set_options(accounts.to_vec());
66 row.from_commodity.set_options(commodities.to_vec());
67 row.to_commodity.set_options(commodities.to_vec());
68 row
69 }
70
71 #[must_use]
73 pub fn col_kind(col: usize) -> WidgetKind {
74 match col {
75 COL_FROM | COL_TO | COL_FROM_COMM | COL_TO_COMM => WidgetKind::Select,
76 _ => WidgetKind::Amount,
77 }
78 }
79
80 pub(super) fn select_mut(&mut self, col: usize) -> Option<&mut SelectWidget> {
81 match col {
82 COL_FROM => Some(&mut self.from),
83 COL_TO => Some(&mut self.to),
84 COL_FROM_COMM => Some(&mut self.from_commodity),
85 COL_TO_COMM => Some(&mut self.to_commodity),
86 _ => None,
87 }
88 }
89
90 pub(super) fn amount_mut(&mut self, col: usize) -> Option<&mut AmountWidget> {
91 match col {
92 COL_VALUE => Some(&mut self.value),
93 COL_TO_AMOUNT => Some(&mut self.to_amount),
94 _ => None,
95 }
96 }
97}
98
99pub enum FocusedSubWidget<'a> {
101 Select(&'a mut SelectWidget),
102 Amount(&'a mut AmountWidget),
103}
104
105#[derive(Debug)]
110pub struct SplitsWidget {
111 rows: Vec<SplitRow>,
112 pub row_focus: usize,
113 pub col_focus: usize,
114 mode: EditMode,
115 account_options: Vec<SelectOption>,
118 commodity_options: Vec<SelectOption>,
119}
120
121impl SplitsWidget {
122 #[must_use]
124 pub fn new(mode: EditMode) -> Self {
125 Self {
126 rows: vec![SplitRow::new(mode, &[], &[])],
127 row_focus: 0,
128 col_focus: 0,
129 mode,
130 account_options: Vec::new(),
131 commodity_options: Vec::new(),
132 }
133 }
134
135 #[must_use]
137 pub fn rows(&self) -> &[SplitRow] {
138 &self.rows
139 }
140
141 #[must_use]
143 pub fn focused_subwidget_kind(&self) -> WidgetKind {
144 SplitRow::col_kind(self.col_focus)
145 }
146
147 pub fn focused_subwidget_mut(&mut self) -> Option<FocusedSubWidget<'_>> {
149 let row = self.rows.get_mut(self.row_focus)?;
150 match self.col_focus {
151 COL_FROM | COL_TO | COL_FROM_COMM | COL_TO_COMM => {
152 row.select_mut(self.col_focus).map(FocusedSubWidget::Select)
153 }
154 _ => row.amount_mut(self.col_focus).map(FocusedSubWidget::Amount),
155 }
156 }
157
158 pub fn advance_cell(&mut self) {
162 if self.col_focus + 1 < COL_COUNT {
163 self.col_focus += 1;
164 } else if self.row_focus + 1 < self.rows.len() {
165 self.row_focus += 1;
166 self.col_focus = 0;
167 }
168 }
169
170 pub fn retreat_cell(&mut self) {
173 if self.col_focus > 0 {
174 self.col_focus -= 1;
175 } else if self.row_focus > 0 {
176 self.row_focus -= 1;
177 self.col_focus = COL_COUNT - 1;
178 }
179 }
180
181 #[must_use]
184 pub fn at_first_cell(&self) -> bool {
185 self.row_focus == 0 && self.col_focus == 0
186 }
187
188 #[must_use]
191 pub fn at_last_cell(&self) -> bool {
192 self.row_focus + 1 == self.rows.len() && self.col_focus + 1 == COL_COUNT
193 }
194
195 pub fn next_row(&mut self) {
197 if !self.rows.is_empty() {
198 self.row_focus = (self.row_focus + 1) % self.rows.len();
199 }
200 }
201
202 pub fn prev_row(&mut self) {
204 let len = self.rows.len();
205 if len > 0 {
206 self.row_focus = (self.row_focus + len - 1) % len;
207 }
208 }
209
210 pub fn add_row(&mut self) {
213 let insert_at = self.row_focus + 1;
214 let row = SplitRow::new(self.mode, &self.account_options, &self.commodity_options);
215 self.rows.insert(insert_at, row);
216 self.row_focus = insert_at;
217 }
218
219 pub fn remove_row(&mut self) {
221 if self.rows.len() <= 1 {
222 return;
223 }
224 self.rows.remove(self.row_focus);
225 if self.row_focus >= self.rows.len() {
226 self.row_focus = self.rows.len() - 1;
227 }
228 }
229
230 pub fn set_account_options(&mut self, options: Vec<SelectOption>) {
233 self.account_options = options;
234 for row in &mut self.rows {
235 row.from
236 .set_options_preserving_id(self.account_options.clone());
237 row.to
238 .set_options_preserving_id(self.account_options.clone());
239 }
240 }
241
242 pub fn set_commodity_options(&mut self, options: Vec<SelectOption>) {
245 self.commodity_options = options;
246 for row in &mut self.rows {
247 row.from_commodity
248 .set_options_preserving_id(self.commodity_options.clone());
249 row.to_commodity
250 .set_options_preserving_id(self.commodity_options.clone());
251 }
252 }
253
254 pub fn apply_prefill(&mut self, rows: &[SplitRowPrefill<'_>]) {
263 self.rows.clear();
264 self.row_focus = 0;
265 self.col_focus = 0;
266 for data in rows {
267 let make1 = |id: &str| {
268 vec![SelectOption {
269 id: id.to_string(),
270 label: id.to_string(),
271 }]
272 };
273 let mut row = SplitRow {
274 from: SelectWidget::new(),
275 to: SelectWidget::new(),
276 from_commodity: SelectWidget::new(),
277 to_commodity: SelectWidget::new(),
278 value: AmountWidget::with_value(self.mode, data.value),
279 to_amount: AmountWidget::with_value(self.mode, data.to_amount.unwrap_or("")),
280 };
281 row.from.set_options(make1(data.from));
282 row.to.set_options(make1(data.to));
283 row.from_commodity.set_options(make1(data.from_commodity));
284 row.to_commodity.set_options(make1(data.to_commodity));
285 self.rows.push(row);
286 }
287 if self.rows.is_empty() {
288 self.rows.push(SplitRow::new(
289 self.mode,
290 &self.account_options,
291 &self.commodity_options,
292 ));
293 }
294 }
295
296 #[must_use]
298 pub fn display(&self) -> String {
299 let n = self.rows.len();
300 format!(
301 "{n} split(s) [row {}, col {}]",
302 self.row_focus, self.col_focus
303 )
304 }
305
306 #[must_use]
310 pub fn render_lines(&self) -> Vec<String> {
311 self.rows
312 .iter()
313 .enumerate()
314 .map(|(i, row)| self.render_row(i, row))
315 .collect()
316 }
317
318 fn render_row(&self, idx: usize, row: &SplitRow) -> String {
319 let rmark = if idx == self.row_focus { "▸" } else { " " };
320 let from = self.cell(idx, COL_FROM, select_label(&row.from));
321 let to = self.cell(idx, COL_TO, select_label(&row.to));
322 let fc = self.cell(idx, COL_FROM_COMM, select_label(&row.from_commodity));
323 let tc = self.cell(idx, COL_TO_COMM, select_label(&row.to_commodity));
324 let value = self.cell(idx, COL_VALUE, amount_text(&row.value));
325 let conv = if row.from_commodity.value() != row.to_commodity.value() {
326 format!(
327 " → {}",
328 self.cell(idx, COL_TO_AMOUNT, amount_text(&row.to_amount))
329 )
330 } else {
331 String::new()
332 };
333 format!(
334 "{rmark}{}. {from} → {to} [{fc}/{tc}] val={value}{conv}",
335 idx + 1
336 )
337 }
338
339 fn cell(&self, idx: usize, col: usize, content: String) -> String {
340 if idx == self.row_focus && col == self.col_focus {
341 format!("[{content}]")
342 } else {
343 content
344 }
345 }
346}
347
348fn select_label(sw: &SelectWidget) -> String {
349 let label = sw.display();
350 if label.is_empty() {
351 "?".to_string()
352 } else {
353 label.to_string()
354 }
355}
356
357fn amount_text(aw: &AmountWidget) -> String {
358 let v = aw.value();
359 if v.is_empty() {
360 "0".to_string()
361 } else {
362 v.to_string()
363 }
364}
365
366#[cfg(test)]
367mod tests;