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