1mod dynamic;
2mod state;
3
4use crossterm::event::{MouseButton, MouseEventKind};
5pub use dynamic::*;
6pub use state::*;
7use 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")] 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 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 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 };
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 }
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 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 if result.contains(pos) {
187 click = Click::ResultPos(mouse.row - result.top());
188 } else if input.contains(pos) {
189 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 }
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 }
225 MouseEventKind::ScrollRight => {
226 }
228 _ => {}
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 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 {
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 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 }
376
377 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 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 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 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 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 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 {
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 if state.filtering {
639 picker_ui.update();
640 } else {
641 }
643 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 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 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 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 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 state.update(&picker_ui, &overlay_ui);
757 let events = state.events();
758
759 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 for e in events.iter() {
777 for h in dynamic_handlers.0.get(e) {
778 h(&mut dispatcher, &e)
779 }
780 }
781
782 for e in events.iter() {
785 controller_tx
786 .send(e)
787 .unwrap_or_else(|err| eprintln!("send failed: {:?}", err));
788 }
789 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 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
811pub 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 }
828 }
829 *self = Click::None
830 }
831}
832
833fn render_preview(frame: &mut Frame, area: Rect, ui: &mut PreviewUI) {
834 assert!(ui.visible()); 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
865fn 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#[cfg(test)]
942mod test {}
943
944