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