Skip to main content

matchmaker/render/
mod.rs

1mod dynamic;
2mod state;
3
4use crossterm::event::{MouseButton, MouseEventKind};
5pub use dynamic::*;
6pub use state::*;
7// ------------------------------
8
9use std::io::Write;
10
11use log::{info, warn};
12use ratatui::Frame;
13use ratatui::layout::{Position, Rect};
14use tokio::sync::mpsc;
15
16#[cfg(feature = "bracketed-paste")]
17use crate::PasteHandler;
18use crate::action::{Action, ActionExt};
19use crate::config::{CursorSetting, ExitConfig, RowConnectionStyle};
20use crate::event::EventSender;
21use crate::message::{Event, Interrupt, RenderCommand};
22use crate::tui::Tui;
23use crate::ui::{DisplayUI, InputUI, OverlayUI, PickerUI, PreviewUI, ResultsUI, UI};
24use crate::{ActionAliaser, ActionExtHandler, Initializer, MatchError, SSS, Selection};
25
26fn apply_aliases<T: SSS, S: Selection, A: ActionExt>(
27    buffer: &mut Vec<RenderCommand<A>>,
28    aliaser: &mut ActionAliaser<T, S, A>,
29    dispatcher: &mut MMState<'_, '_, T, S>,
30) {
31    let mut out = Vec::new();
32
33    for cmd in buffer.drain(..) {
34        match cmd {
35            RenderCommand::Action(a) => out.extend(
36                aliaser(a, dispatcher)
37                    .into_iter()
38                    .map(RenderCommand::Action),
39            ),
40            other => out.push(other),
41        }
42    }
43
44    *buffer = out;
45}
46
47#[allow(clippy::too_many_arguments)]
48pub(crate) async fn render_loop<'a, W: Write, T: SSS, S: Selection, A: ActionExt>(
49    mut ui: UI,
50    mut picker_ui: PickerUI<'a, T, S>,
51    mut footer_ui: DisplayUI,
52    mut preview_ui: Option<PreviewUI>,
53    mut tui: Tui<W>,
54
55    mut overlay_ui: Option<OverlayUI<A>>,
56    exit_config: ExitConfig,
57
58    mut render_rx: mpsc::UnboundedReceiver<RenderCommand<A>>,
59    controller_tx: EventSender,
60
61    dynamic_handlers: DynamicHandlers<T, S>,
62    mut ext_handler: Option<ActionExtHandler<T, S, A>>,
63    mut ext_aliaser: Option<ActionAliaser<T, S, A>>,
64    initializer: Option<Initializer<T, S>>,
65    #[cfg(feature = "bracketed-paste")] //
66    mut paste_handler: Option<PasteHandler<T, S>>,
67) -> Result<Vec<S>, MatchError> {
68    let mut state = State::new();
69
70    if let Some(handler) = initializer {
71        handler(&mut state.dispatcher(
72            &mut ui,
73            &mut picker_ui,
74            &mut footer_ui,
75            &mut preview_ui,
76            &controller_tx,
77        ));
78    }
79
80    let mut click = Click::None;
81
82    // place the initial command in the state where the preview listener can access
83    if let Some(ref p) = preview_ui {
84        state.update_preview(p.get_initial_command());
85    }
86
87    let mut buffer = Vec::with_capacity(256);
88
89    while render_rx.recv_many(&mut buffer, 256).await > 0 {
90        if state.iterations == 0 {
91            log::debug!("Render loop started");
92        }
93        let mut did_pause = false;
94        let mut did_exit = false;
95        let mut did_resize = false;
96
97        // todo: why exactly can we not borrow the picker_ui mutably?
98        if let Some(aliaser) = &mut ext_aliaser {
99            apply_aliases(
100                &mut buffer,
101                aliaser,
102                &mut state.dispatcher(
103                    &mut ui,
104                    &mut picker_ui,
105                    &mut footer_ui,
106                    &mut preview_ui,
107                    &controller_tx,
108                ),
109            )
110            // effects could be moved out for efficiency, but it seems more logical to add them as they come so that we can trigger interrupts
111        };
112
113        if state.should_quit {
114            log::debug!("Exiting due to should_quit");
115            let ret = picker_ui.selector.output().collect::<Vec<S>>();
116            return if picker_ui.selector.is_disabled()
117                && let Some((_, item)) = get_current(&picker_ui)
118            {
119                Ok(vec![item])
120            } else if ret.is_empty() {
121                Err(MatchError::Abort(0))
122            } else {
123                Ok(ret)
124            };
125        } else if state.should_quit_nomatch {
126            log::debug!("Exiting due to should_quit_no_match");
127            return Err(MatchError::NoMatch);
128        }
129
130        for event in buffer.drain(..) {
131            state.clear_interrupt();
132
133            if !matches!(event, RenderCommand::Tick) {
134                info!("Recieved {event:?}");
135            } else {
136                // log::trace!("Recieved {event:?}");
137            }
138
139            match event {
140                #[cfg(feature = "bracketed-paste")]
141                RenderCommand::Paste(content) => {
142                    if let Some(handler) = &mut paste_handler {
143                        let content = {
144                            handler(
145                                content,
146                                &state.dispatcher(
147                                    &mut ui,
148                                    &mut picker_ui,
149                                    &mut footer_ui,
150                                    &mut preview_ui,
151                                    &controller_tx,
152                                ),
153                            )
154                        };
155                        if !content.is_empty() {
156                            if let Some(x) = overlay_ui.as_mut()
157                                && x.index().is_some()
158                            {
159                                for c in content.chars() {
160                                    x.handle_input(c);
161                                }
162                            } else {
163                                picker_ui.input.push_str(&content);
164                            }
165                        }
166                    }
167                }
168                RenderCommand::Resize(area) => {
169                    tui.resize(area);
170                    ui.area = area;
171                }
172                RenderCommand::Refresh => {
173                    tui.redraw();
174                }
175                RenderCommand::HeaderTable(columns) => {
176                    picker_ui.header.header_table(columns);
177                }
178                RenderCommand::Mouse(mouse) => {
179                    // we could also impl this in the aliasing step
180                    let pos = Position::from((mouse.column, mouse.row));
181                    let [preview, input, status, result] = state.layout;
182
183                    match mouse.kind {
184                        MouseEventKind::Down(MouseButton::Left) => {
185                            // todo: clickable column headers, clickable results, also, grouping?
186                            if result.contains(pos) {
187                                click = Click::ResultPos(mouse.row - result.top());
188                            } else if input.contains(pos) {
189                                // The X offset of the start of the visible text relative to the terminal
190                                let text_start_x = input.x
191                                    + picker_ui.input.prompt.width() as u16
192                                    + picker_ui.input.config.border.left();
193
194                                if pos.x >= text_start_x {
195                                    let visual_offset = pos.x - text_start_x;
196                                    picker_ui.input.set_at_visual_offset(visual_offset);
197                                } else {
198                                    picker_ui.input.set(None, 0);
199                                }
200                            } else if status.contains(pos) {
201                                // todo
202                            }
203                        }
204                        MouseEventKind::ScrollDown => {
205                            if preview.contains(pos) {
206                                if let Some(p) = preview_ui.as_mut() {
207                                    p.down(1)
208                                }
209                            } else {
210                                picker_ui.results.cursor_next()
211                            }
212                        }
213                        MouseEventKind::ScrollUp => {
214                            if preview.contains(pos) {
215                                if let Some(p) = preview_ui.as_mut() {
216                                    p.up(1)
217                                }
218                            } else {
219                                picker_ui.results.cursor_prev()
220                            }
221                        }
222                        MouseEventKind::ScrollLeft => {
223                            // todo
224                        }
225                        MouseEventKind::ScrollRight => {
226                            // todo
227                        }
228                        // Drag tracking: todo
229                        _ => {}
230                    }
231                }
232                RenderCommand::QuitEmpty => {
233                    return Ok(vec![]);
234                }
235                RenderCommand::Action(action) => {
236                    if let Some(x) = overlay_ui.as_mut() {
237                        if match action {
238                            Action::Char(c) => x.handle_input(c),
239                            _ => x.handle_action(&action),
240                        } {
241                            continue;
242                        }
243                    }
244                    let PickerUI {
245                        input,
246                        results,
247                        worker,
248                        selector: selections,
249                        ..
250                    } = &mut picker_ui;
251                    match action {
252                        Action::Select => {
253                            if let Some(item) = worker.get_nth(results.index()) {
254                                selections.sel(item);
255                            }
256                        }
257                        Action::Deselect => {
258                            if let Some(item) = worker.get_nth(results.index()) {
259                                selections.desel(item);
260                            }
261                        }
262                        Action::Toggle => {
263                            if let Some(item) = worker.get_nth(results.index()) {
264                                selections.toggle(item);
265                            }
266                        }
267                        Action::CycleAll => {
268                            selections.cycle_all_bg(worker.raw_results());
269                        }
270                        Action::ClearSelections => {
271                            selections.clear();
272                        }
273                        Action::Accept => {
274                            let ret = if selections.is_empty() {
275                                if let Some(item) = get_current(&picker_ui) {
276                                    vec![item.1]
277                                } else if exit_config.allow_empty {
278                                    vec![]
279                                } else {
280                                    continue;
281                                }
282                            } else {
283                                selections.output().collect::<Vec<S>>()
284                            };
285                            return Ok(ret);
286                        }
287                        Action::Quit(code) => {
288                            return Err(MatchError::Abort(code));
289                        }
290
291                        // Results
292                        Action::ToggleWrap => {
293                            results.wrap(!results.is_wrap());
294                        }
295                        Action::Up(x) | Action::Down(x) => {
296                            let next = matches!(action, Action::Down(_)) ^ results.reverse();
297                            for _ in 0..x.into() {
298                                if next {
299                                    results.cursor_next();
300                                } else {
301                                    results.cursor_prev();
302                                }
303                            }
304                        }
305                        Action::Pos(pos) => {
306                            let pos = if pos >= 0 {
307                                pos as u32
308                            } else {
309                                results.status.matched_count.saturating_sub((-pos) as u32)
310                            };
311                            results.cursor_jump(pos);
312                        }
313                        Action::QueryPos(pos) => {
314                            let pos = if pos >= 0 {
315                                pos as u16
316                            } else {
317                                (input.len() as u16).saturating_sub((-pos) as u16)
318                            };
319                            input.set(None, pos);
320                        }
321                        Action::HScroll(n) | Action::VScroll(n) => {
322                            if let Some(p) = &mut preview_ui
323                                && !p.config.wrap
324                                && false
325                            // track mouse location?
326                            {
327                                p.scroll(true, n);
328                            } else {
329                                results.current_scroll(n, matches!(action, Action::HScroll(_)));
330                            }
331                        }
332                        Action::PageDown | Action::PageUp => {
333                            let x = results.height();
334                            let next = matches!(action, Action::Down(_)) ^ results.reverse();
335                            for _ in 0..x.into() {
336                                if next {
337                                    results.cursor_next();
338                                } else {
339                                    results.cursor_prev();
340                                }
341                            }
342                        }
343
344                        // Preview Navigation
345                        Action::PreviewUp(n) => {
346                            if let Some(p) = preview_ui.as_mut() {
347                                p.up(n)
348                            }
349                        }
350                        Action::PreviewDown(n) => {
351                            if let Some(p) = preview_ui.as_mut() {
352                                p.down(n)
353                            }
354                        }
355                        Action::PreviewHalfPageUp => {
356                            let n = (ui.area.height + 1) / 2;
357                            if let Some(p) = preview_ui.as_mut() {
358                                p.down(n)
359                            }
360                        }
361                        Action::PreviewHalfPageDown => {
362                            let n = (ui.area.height + 1) / 2;
363                            if let Some(p) = preview_ui.as_mut() {
364                                p.down(n)
365                            }
366                        }
367
368                        Action::PreviewHScroll(x) | Action::PreviewScroll(x) => {
369                            if let Some(p) = preview_ui.as_mut() {
370                                p.scroll(matches!(action, Action::PreviewHScroll(_)), x);
371                            }
372                        }
373                        Action::PreviewJump => {
374                            // todo
375                        }
376
377                        // Preview
378                        // this sometimes aborts the viewer on some files, why?
379                        Action::CyclePreview => {
380                            if let Some(p) = preview_ui.as_mut() {
381                                p.cycle_layout();
382                                if !p.command().is_empty() {
383                                    state.update_preview(p.command());
384                                }
385                            }
386                        }
387
388                        Action::Preview(context) => {
389                            if let Some(p) = preview_ui.as_mut() {
390                                if !state.update_preview(context.as_str()) {
391                                    p.toggle_show()
392                                } else {
393                                    p.show(true);
394                                }
395                            };
396                        }
397                        Action::Help(context) => {
398                            if let Some(p) = preview_ui.as_mut() {
399                                // empty payload signifies help
400                                if !state.update_preview_set(context) {
401                                    state.update_preview_unset()
402                                } else {
403                                    p.show(true);
404                                }
405                            };
406                        }
407                        Action::SetPreview(idx) => {
408                            if let Some(p) = preview_ui.as_mut() {
409                                if let Some(idx) = idx {
410                                    p.set_layout(idx);
411                                } else {
412                                    state.update_preview(p.command());
413                                }
414                            }
415                        }
416                        Action::SwitchPreview(idx) => {
417                            if let Some(p) = preview_ui.as_mut() {
418                                if let Some(idx) = idx {
419                                    if !p.set_layout(idx) && !state.update_preview(p.command()) {
420                                        p.toggle_show();
421                                    }
422                                } else {
423                                    p.toggle_show()
424                                }
425                            }
426                        }
427                        Action::TogglePreviewWrap => {
428                            if let Some(p) = preview_ui.as_mut() {
429                                p.wrap(!p.is_wrap());
430                            }
431                        }
432
433                        // Programmable
434                        Action::Execute(payload) => {
435                            state.set_interrupt(Interrupt::Execute, payload);
436                        }
437                        Action::ExecuteSilent(payload) => {
438                            state.set_interrupt(Interrupt::ExecuteSilent, payload);
439                        }
440                        Action::Become(payload) => {
441                            state.set_interrupt(Interrupt::Become, payload);
442                        }
443                        Action::Reload(payload) => {
444                            state.set_interrupt(Interrupt::Reload, payload);
445                        }
446                        Action::Print(payload) => {
447                            state.set_interrupt(Interrupt::Print, payload);
448                        }
449
450                        // Columns
451                        Action::SwitchColumn(col_name) => {
452                            if worker.columns.iter().any(|c| *c.name == col_name) {
453                                input.prepare_column_change();
454                                input.push_str(&format!("%{} ", col_name));
455                            } else {
456                                log::warn!("Column {} not found in worker columns", col_name);
457                            }
458                        }
459                        Action::NextColumn | Action::PrevColumn => {
460                            let cursor_byte = input.byte_index(input.cursor() as usize);
461                            let active_col_name = worker.query.active_column(cursor_byte);
462                            let active_idx = active_col_name.and_then(|name| {
463                                worker.columns.iter().position(|c| c.name == *name)
464                            });
465
466                            let num_columns = worker.columns.len();
467                            if num_columns > 0 {
468                                input.prepare_column_change();
469
470                                let mut next_idx = match action {
471                                    Action::NextColumn => active_idx.map(|x| x + 1).unwrap_or(0),
472                                    Action::PrevColumn => active_idx
473                                        .map(|x| (x + num_columns - 1) % num_columns)
474                                        .unwrap_or(num_columns - 1),
475                                    _ => unreachable!(),
476                                } % num_columns;
477
478                                loop {
479                                    if next_idx < results.hidden_columns.len()
480                                        && results.hidden_columns[next_idx]
481                                    {
482                                        next_idx = match action {
483                                            Action::NextColumn => (next_idx + 1) % num_columns,
484                                            Action::PrevColumn => {
485                                                (next_idx + num_columns - 1) % num_columns
486                                            }
487                                            _ => unreachable!(),
488                                        };
489                                    } else {
490                                        break;
491                                    }
492                                }
493
494                                let col_name = &worker.columns[next_idx].name;
495                                input.push_str(&format!("%{} ", col_name));
496                            }
497                        }
498
499                        Action::ToggleColumn(col_name) => {
500                            let index = if let Some(name) = col_name {
501                                worker.columns.iter().position(|c| *c.name == name)
502                            } else {
503                                let cursor_byte = input.byte_index(input.cursor() as usize);
504                                Some(worker.query.active_column_index(cursor_byte))
505                            };
506
507                            if let Some(idx) = index {
508                                if idx >= results.hidden_columns.len() {
509                                    results.hidden_columns.resize(idx + 1, false);
510                                }
511                                results.hidden_columns[idx] = !results.hidden_columns[idx];
512                            }
513                        }
514
515                        Action::ShowColumn(col_name) => {
516                            if let Some(name) = col_name {
517                                if let Some(idx) =
518                                    worker.columns.iter().position(|c| *c.name == name)
519                                {
520                                    if idx < results.hidden_columns.len() {
521                                        results.hidden_columns[idx] = false;
522                                    }
523                                }
524                            } else {
525                                for val in results.hidden_columns.iter_mut() {
526                                    *val = false;
527                                }
528                            }
529                        }
530
531                        Action::ScrollLeft => {}
532                        Action::ScrollRight => {}
533
534                        // Edit
535                        Action::SetQuery(context) => {
536                            input.set(context, u16::MAX);
537                        }
538                        Action::ForwardChar => input.forward_char(),
539                        Action::BackwardChar => input.backward_char(),
540                        Action::ForwardWord => input.forward_word(),
541                        Action::BackwardWord => input.backward_word(),
542                        Action::DeleteChar => input.delete(),
543                        Action::DeleteWord => input.delete_word(),
544                        Action::DeleteLineStart => input.delete_line_start(),
545                        Action::DeleteLineEnd => input.delete_line_end(),
546                        Action::Cancel => input.cancel(),
547
548                        // Other
549                        Action::Redraw => {
550                            tui.redraw();
551                        }
552                        Action::Overlay(index) => {
553                            if let Some(x) = overlay_ui.as_mut() {
554                                x.enable(index, &ui.area);
555                                tui.redraw();
556                            };
557                        }
558                        Action::Custom(e) => {
559                            if let Some(handler) = &mut ext_handler {
560                                handler(
561                                    e,
562                                    &mut state.dispatcher(
563                                        &mut ui,
564                                        &mut picker_ui,
565                                        &mut footer_ui,
566                                        &mut preview_ui,
567                                        &controller_tx,
568                                    ),
569                                );
570                            }
571                        }
572                        Action::Char(c) => picker_ui.input.push_char(c),
573                    }
574                }
575                _ => {}
576            }
577
578            let interrupt = state.interrupt();
579
580            match interrupt {
581                Interrupt::None => continue,
582                Interrupt::Execute => {
583                    if controller_tx.send(Event::Pause).is_err() {
584                        break;
585                    }
586                    tui.enter_execute();
587                    did_exit = true;
588                    did_pause = true;
589                }
590                Interrupt::Reload => {
591                    picker_ui.worker.restart(false);
592                    state.synced = [false; 2];
593                }
594                Interrupt::Become => {
595                    tui.exit();
596                }
597                _ => {}
598            }
599            // Apply interrupt effect
600            {
601                let mut dispatcher = state.dispatcher(
602                    &mut ui,
603                    &mut picker_ui,
604                    &mut footer_ui,
605                    &mut preview_ui,
606                    &controller_tx,
607                );
608                for h in dynamic_handlers.1.get(interrupt) {
609                    h(&mut dispatcher);
610                }
611
612                if matches!(interrupt, Interrupt::Become) {
613                    return Err(MatchError::Become(state.payload().clone()));
614                }
615            }
616
617            if state.should_quit {
618                log::debug!("Exiting due to should_quit");
619                let ret = picker_ui.selector.output().collect::<Vec<S>>();
620                return if picker_ui.selector.is_disabled()
621                    && let Some((_, item)) = get_current(&picker_ui)
622                {
623                    Ok(vec![item])
624                } else if ret.is_empty() {
625                    Err(MatchError::Abort(0))
626                } else {
627                    Ok(ret)
628                };
629            } else if state.should_quit_nomatch {
630                log::debug!("Exiting due to should_quit_nomatch");
631                return Err(MatchError::NoMatch);
632            }
633        }
634
635        // debug!("{state:?}");
636
637        // ------------- update state + render ------------------------
638        if state.filtering {
639            picker_ui.update();
640        } else {
641            // nothing
642        }
643        // process exit conditions
644        if exit_config.select_1
645            && picker_ui.results.status.matched_count == 1
646            && let Some((_, item)) = get_current(&picker_ui)
647        {
648            return Ok(vec![item]);
649        }
650
651        // resume tui
652        if did_exit {
653            tui.return_execute()
654                .map_err(|e| MatchError::TUIError(e.to_string()))?;
655            tui.redraw();
656        }
657
658        let mut overlay_ui_ref = overlay_ui.as_mut();
659        let mut cursor_y_offset = 0;
660
661        tui.terminal
662            .draw(|frame| {
663                let mut area = frame.area();
664
665                render_ui(frame, &mut area, &ui);
666
667                let mut _area = area;
668
669                let full_width_footer = footer_ui.single()
670                    && footer_ui.config.row_connection_style == RowConnectionStyle::Full;
671
672                let mut footer =
673                    if full_width_footer || preview_ui.as_ref().is_none_or(|p| !p.visible()) {
674                        split(&mut _area, footer_ui.height(), picker_ui.reverse())
675                    } else {
676                        Rect::default()
677                    };
678
679                let [preview, picker_area, footer] = if let Some(preview_ui) = preview_ui.as_mut()
680                    && preview_ui.visible()
681                    && let Some(setting) = preview_ui.setting()
682                {
683                    let layout = &setting.layout;
684
685                    let [preview, mut picker_area] = layout.split(_area);
686
687                    if state.iterations == 0 && picker_area.width <= 5 {
688                        warn!("UI too narrow, hiding preview");
689                        preview_ui.show(false);
690
691                        [Rect::default(), _area, footer]
692                    } else {
693                        if !full_width_footer {
694                            footer =
695                                split(&mut picker_area, footer_ui.height(), picker_ui.reverse());
696                        }
697
698                        [preview, picker_area, footer]
699                    }
700                } else {
701                    [Rect::default(), _area, footer]
702                };
703
704                let [input, status, header, results] = picker_ui.layout(picker_area);
705
706                // compare and save dimensions
707                did_resize = state.update_layout([preview, input, status, results]);
708
709                if did_resize {
710                    picker_ui.results.update_dimensions(&results);
711                    picker_ui.input.update_width(input.width);
712                    footer_ui.update_width(
713                        if footer_ui.config.row_connection_style == RowConnectionStyle::Capped {
714                            area.width
715                        } else {
716                            footer.width
717                        },
718                    );
719                    picker_ui.header.update_width(header.width);
720                    // although these only want update when the whole ui change
721                    ui.update_dimensions(area);
722                    if let Some(x) = overlay_ui_ref.as_deref_mut() {
723                        x.update_dimensions(&area);
724                    }
725                };
726
727                cursor_y_offset = render_input(frame, input, &mut picker_ui.input).y;
728                render_status(frame, status, &picker_ui.results, ui.area.width);
729                render_results(frame, results, &mut picker_ui, &mut click);
730                render_display(frame, header, &mut picker_ui.header, &picker_ui.results);
731                render_display(frame, footer, &mut footer_ui, &picker_ui.results);
732                if let Some(preview_ui) = preview_ui.as_mut()
733                    && preview_ui.visible()
734                {
735                    state.update_preview_visible(preview_ui);
736                    if did_resize {
737                        preview_ui.update_dimensions(&preview);
738                    }
739                    render_preview(frame, preview, preview_ui);
740                }
741                if let Some(x) = overlay_ui_ref {
742                    x.draw(frame);
743                }
744            })
745            .map_err(|e| MatchError::TUIError(e.to_string()))?;
746
747        // useful to clear artifacts
748        if did_resize && tui.config.redraw_on_resize && !did_exit {
749            tui.redraw();
750            tui.cursor_y_offset = Some(cursor_y_offset)
751        }
752        buffer.clear();
753
754        // note: the remainder could be scoped by a conditional on having run?
755        // ====== Event handling ==========
756        state.update(&picker_ui, &overlay_ui);
757        let events = state.events();
758
759        // ---- Invoke handlers -------
760        let mut dispatcher = state.dispatcher(
761            &mut ui,
762            &mut picker_ui,
763            &mut footer_ui,
764            &mut preview_ui,
765            &controller_tx,
766        );
767        // if let Some((signal, handler)) = signal_handler &&
768        // let s = signal.load(std::sync::atomic::Ordering::Acquire) &&
769        // s > 0
770        // {
771        //     handler(s, &mut dispatcher);
772        //     signal.store(0, std::sync::atomic::Ordering::Release);
773        // };
774
775        // ping handlers with events
776        for e in events.iter() {
777            for h in dynamic_handlers.0.get(e) {
778                h(&mut dispatcher, &e)
779            }
780        }
781
782        // ------------------------------
783        // send events into controller
784        for e in events.iter() {
785            controller_tx
786                .send(e)
787                .unwrap_or_else(|err| eprintln!("send failed: {:?}", err));
788        }
789        // =================================
790
791        if did_pause {
792            log::debug!("Waiting for ack response to pause");
793            if controller_tx.send(Event::Resume).is_err() {
794                break;
795            };
796            // due to control flow, this does nothing, but is anyhow a useful safeguard to guarantee the pause
797            while let Some(msg) = render_rx.recv().await {
798                if matches!(msg, RenderCommand::Ack) {
799                    log::debug!("Recieved ack response to pause");
800                    break;
801                }
802            }
803        }
804
805        click.process(&mut buffer);
806    }
807
808    Err(MatchError::EventLoopClosed)
809}
810
811// ------------------------- HELPERS ----------------------------
812
813pub enum Click {
814    None,
815    ResultPos(u16),
816    ResultIdx(u32),
817}
818
819impl Click {
820    fn process<A: ActionExt>(&mut self, buffer: &mut Vec<RenderCommand<A>>) {
821        match self {
822            Self::ResultIdx(u) => {
823                buffer.push(RenderCommand::Action(Action::Pos(*u as i32)));
824            }
825            _ => {
826                // todo
827            }
828        }
829        *self = Click::None
830    }
831}
832
833fn render_preview(frame: &mut Frame, area: Rect, ui: &mut PreviewUI) {
834    // if ui.view.changed() {
835    // doesn't work, use resize
836    //     frame.render_widget(Clear, area);
837    // } else {
838    //     let widget = ui.make_preview();
839    //     frame.render_widget(widget, area);
840    // }
841    assert!(ui.visible()); // don't call if not visible.
842    let widget = ui.make_preview();
843    frame.render_widget(widget, area);
844}
845
846fn render_results<T: SSS, S: Selection>(
847    frame: &mut Frame,
848    mut area: Rect,
849    ui: &mut PickerUI<T, S>,
850    click: &mut Click,
851) {
852    let cap = matches!(
853        ui.results.config.row_connection_style,
854        RowConnectionStyle::Capped
855    );
856    let (widget, table_width) = ui.make_table(click);
857
858    if cap {
859        area.width = area.width.min(table_width);
860    }
861
862    frame.render_widget(widget, area);
863}
864
865/// Returns the offset of the cursor against the drawing area
866fn render_input(frame: &mut Frame, area: Rect, ui: &mut InputUI) -> Position {
867    ui.scroll_to_cursor();
868    let widget = ui.make_input();
869    let p = ui.cursor_offset(&area);
870    if let CursorSetting::Default = ui.config.cursor {
871        frame.set_cursor_position(p)
872    };
873
874    frame.render_widget(widget, area);
875
876    p
877}
878
879fn render_status(frame: &mut Frame, area: Rect, ui: &ResultsUI, full_width: u16) {
880    if ui.status_config.show {
881        let widget = ui.make_status(full_width);
882        frame.render_widget(widget, area);
883    }
884}
885
886fn render_display(frame: &mut Frame, area: Rect, ui: &mut DisplayUI, results_ui: &ResultsUI) {
887    if !ui.show {
888        return;
889    }
890    let widget = ui.make_display(
891        results_ui.indentation() as u16,
892        results_ui.widths().to_vec(),
893        results_ui.config.column_spacing.0,
894    );
895
896    frame.render_widget(widget, area);
897
898    if ui.single() {
899        let widget = ui.make_full_width_row(results_ui.indentation() as u16);
900        frame.render_widget(widget, area);
901    }
902}
903
904fn render_ui(frame: &mut Frame, area: &mut Rect, ui: &UI) {
905    let widget = ui.make_ui();
906    frame.render_widget(widget, *area);
907    *area = ui.inner_area(area);
908}
909
910fn split(rect: &mut Rect, height: u16, cut_top: bool) -> Rect {
911    let h = height.min(rect.height);
912
913    if cut_top {
914        let offshoot = Rect {
915            x: rect.x,
916            y: rect.y,
917            width: rect.width,
918            height: h,
919        };
920
921        rect.y += h;
922        rect.height -= h;
923
924        offshoot
925    } else {
926        let offshoot = Rect {
927            x: rect.x,
928            y: rect.y + rect.height - h,
929            width: rect.width,
930            height: h,
931        };
932
933        rect.height -= h;
934
935        offshoot
936    }
937}
938
939// -----------------------------------------------------------------------------------
940
941#[cfg(test)]
942mod test {}
943
944// #[cfg(test)]
945// async fn send_every_second(tx: mpsc::UnboundedSender<RenderCommand>) {
946//     let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
947
948//     loop {
949//         interval.tick().await;
950//         if tx.send(RenderCommand::quit()).is_err() {
951//             break;
952//         }
953//     }
954// }