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, OverlayUI, PickerUI, PreviewUI, QueryUI, 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_payload(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.query.push_str(&content);
164 }
165 }
166 }
167 }
168 RenderCommand::Resize(area) => {
169 tui.resize(area);
170 ui.update_dimensions(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 + picker_ui.query.left();
191
192 if pos.x >= text_start_x {
193 let visual_offset = pos.x - text_start_x;
194 picker_ui.query.set_at_visual_offset(visual_offset);
195 } else {
196 picker_ui.query.set(None, 0);
197 }
198 } else if status.contains(pos) {
199 }
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 }
223 MouseEventKind::ScrollRight => {
224 }
226 _ => {}
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 query: 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 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 {
325 p.scroll(true, n);
326 } else {
327 results.current_scroll(n, matches!(action, Action::HScroll(_)));
328 }
329 }
330 Action::HalfPageDown | Action::HalfPageUp => {
331 let x = (results.height() + 1) / 2;
332 let next = matches!(action, Action::HalfPageDown) ^ 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 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 | Action::PreviewHalfPageDown => {
354 if let Some(p) = preview_ui.as_mut() {
355 let n = (p.area.height + 1) / 2;
356
357 if matches!(action, Action::PreviewHalfPageUp) {
358 p.up(n)
359 } else {
360 p.down(n)
361 }
362 }
363 }
364
365 Action::PreviewHScroll(x) | Action::PreviewScroll(x) => {
366 if let Some(p) = preview_ui.as_mut() {
367 p.scroll(matches!(action, Action::PreviewHScroll(_)), x);
368 }
369 }
370 Action::PreviewJump => {
371 }
373
374 Action::CyclePreview => {
377 if let Some(p) = preview_ui.as_mut() {
378 p.cycle_layout();
379 if !p.command().is_empty() {
380 state.update_preview_payload(p.command());
381 }
382 }
383 }
384
385 Action::Preview(context) => {
386 if let Some(p) = preview_ui.as_mut() {
387 if !state.update_preview_payload(context.as_str()) {
388 p.toggle_show()
389 } else {
390 p.show(true);
391 }
392 };
393 }
394 Action::Help(context) => {
395 if let Some(p) = preview_ui.as_mut() {
396 if !state.update_preview_set(context) {
398 state.update_preview_unset()
399 } else {
400 p.show(true);
401 }
402 };
403 }
404 Action::SetPreview(idx) => {
405 if let Some(p) = preview_ui.as_mut() {
406 if let Some(idx) = idx {
407 p.set_layout(idx);
408 } else {
409 state.update_preview_payload(p.command());
410 }
411 }
412 }
413 Action::SwitchPreview(idx) => {
414 if let Some(p) = preview_ui.as_mut() {
415 if let Some(idx) = idx {
416 if !p.set_layout(idx)
417 && !state.update_preview_payload(p.command())
418 {
419 p.toggle_show();
420 }
421 } else {
422 p.toggle_show()
423 }
424 }
425 }
426 Action::TogglePreviewWrap => {
427 if let Some(p) = preview_ui.as_mut() {
428 p.wrap(!p.is_wrap());
429 }
430 }
431
432 Action::Execute(payload) => {
434 state.set_interrupt(Interrupt::Execute, payload);
435 }
436 Action::ExecuteSilent(payload) => {
437 state.set_interrupt(Interrupt::ExecuteSilent, payload);
438 }
439 Action::Store(payload) => {
440 state.store_payload = payload;
441 }
442 Action::Become(payload) => {
443 state.set_interrupt(Interrupt::Become, payload);
444 }
445 Action::Reload(payload) => {
446 state.set_interrupt(Interrupt::Reload, payload);
447 }
448 Action::Print(payload) => {
449 state.set_interrupt(Interrupt::Print, payload);
450 }
451
452 Action::SwitchColumn(col_name) => {
454 if worker.query.active_column_name(input.str_at_cursor()) != col_name
455 && worker.columns.iter().any(|c| *c.name == col_name)
456 {
457 input.prepare_column_change();
458 input.push_str(&format!("%{} ", col_name));
459 } else {
460 log::warn!("Column {} not found in worker columns", col_name);
461 }
462 }
463 Action::NextColumn | Action::PrevColumn => {
464 let cursor_byte = input.byte_index(input.cursor() as usize);
465 let active_idx = worker.query.active_column_index(cursor_byte);
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 + 1,
473 Action::PrevColumn => {
474 active_idx + num_columns - 1 % num_columns
475 }
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 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 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.query.push_char(c),
574
575 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 {
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 if state.filtering {
644 picker_ui.update();
645 } else {
646 }
648 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 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);
672
673 let mut _area = area;
674
675 let full_width_footer = footer_ui.is_single_column()
676 && footer_ui.config.row_connection == RowConnectionStyle::Full;
677
678 let mut footer =
679 if full_width_footer || preview_ui.as_ref().is_none_or(|p| !p.visible()) {
680 split(&mut _area, footer_ui.height(), picker_ui.reverse())
681 } else {
682 Rect::default()
683 };
684
685 let [preview, picker_area, footer] = if let Some(preview_ui) = preview_ui.as_mut()
686 && preview_ui.visible()
687 && let Some(setting) = preview_ui.setting()
688 {
689 let layout = &setting.layout;
690
691 let [preview, mut picker_area] = layout.split(_area);
692
693 if state.iterations == 0 && picker_area.width <= 5 {
694 warn!("UI too narrow, hiding preview");
695 preview_ui.show(false);
696
697 [Rect::default(), _area, footer]
698 } else {
699 if !full_width_footer {
700 footer =
701 split(&mut picker_area, footer_ui.height(), picker_ui.reverse());
702 }
703
704 [preview, picker_area, footer]
705 }
706 } else {
707 [Rect::default(), _area, footer]
708 };
709
710 let [input, status, header, results] = picker_ui.layout(picker_area);
711
712 did_resize = state.update_layout([preview, input, status, results]);
714
715 if did_resize {
716 picker_ui.results.update_dimensions(&results);
717 picker_ui.query.update_width(input.width);
718 footer_ui.update_width(
719 if footer_ui.config.row_connection == RowConnectionStyle::Capped {
720 area.width
721 } else {
722 footer.width
723 },
724 );
725 picker_ui.header.update_width(header.width);
726 ui.update_dimensions(area);
728 if let Some(x) = overlay_ui_ref.as_deref_mut() {
729 x.update_dimensions(&area);
730 }
731 };
732
733 cursor_y_offset = render_input(frame, input, &mut picker_ui.query).y;
734 render_status(frame, status, &picker_ui.results, ui.area().width);
735 render_results(frame, results, &mut picker_ui, &mut click);
736 render_display(frame, header, &mut picker_ui.header, &picker_ui.results);
737 render_display(frame, footer, &mut footer_ui, &picker_ui.results);
738 if let Some(preview_ui) = preview_ui.as_mut() {
739 state.update_preview_visible(preview_ui);
740 if preview_ui.visible() {
741 if did_resize {
742 preview_ui.update_dimensions(&preview);
743 }
744 render_preview(frame, preview, preview_ui);
745 }
746 }
747 if let Some(x) = overlay_ui_ref {
748 x.draw(frame);
749 }
750 })
751 .map_err(|e| MatchError::TUIError(e.to_string()))?;
752
753 if did_resize && tui.config.redraw_on_resize && !did_exit {
755 tui.redraw();
756 tui.cursor_y_offset = Some(cursor_y_offset)
757 }
758 buffer.clear();
759
760 state.update(&picker_ui, &overlay_ui);
763 let events = state.events();
764
765 let mut dispatcher = state.dispatcher(
767 &mut ui,
768 &mut picker_ui,
769 &mut footer_ui,
770 &mut preview_ui,
771 &controller_tx,
772 );
773 for e in events.iter() {
783 for h in dynamic_handlers.0.get(e) {
784 h(&mut dispatcher, &e)
785 }
786 }
787
788 for e in events.iter() {
791 controller_tx
792 .send(e)
793 .unwrap_or_else(|err| eprintln!("send failed: {:?}", err));
794 }
795 if did_pause {
798 log::debug!("Waiting for ack response to pause");
799 if controller_tx.send(Event::Resume).is_err() {
800 break;
801 };
802 while let Some(msg) = render_rx.recv().await {
804 if matches!(msg, RenderCommand::Ack) {
805 log::debug!("Recieved ack response to pause");
806 break;
807 }
808 }
809 }
810
811 click.process(&mut buffer);
812 }
813
814 Err(MatchError::EventLoopClosed)
815}
816
817pub enum Click {
820 None,
821 ResultPos(u16),
822 ResultIdx(u32),
823}
824
825impl Click {
826 fn process<A: ActionExt>(&mut self, buffer: &mut Vec<RenderCommand<A>>) {
827 match self {
828 Self::ResultIdx(u) => {
829 buffer.push(RenderCommand::Action(Action::Pos(*u as i32)));
830 }
831 _ => {
832 }
834 }
835 *self = Click::None
836 }
837}
838
839fn render_preview(frame: &mut Frame, area: Rect, ui: &mut PreviewUI) {
840 assert!(ui.visible()); let widget = ui.make_preview();
849 frame.render_widget(widget, area);
850}
851
852fn render_results<T: SSS, S: Selection>(
853 frame: &mut Frame,
854 mut area: Rect,
855 ui: &mut PickerUI<T, S>,
856 click: &mut Click,
857) {
858 let cap = matches!(ui.results.config.row_connection, RowConnectionStyle::Capped);
859 let (widget, table_width) = ui.make_table(click);
860
861 if cap {
862 area.width = area.width.min(table_width);
863 }
864
865 frame.render_widget(widget, area);
866}
867
868fn render_input(frame: &mut Frame, area: Rect, ui: &mut QueryUI) -> Position {
870 ui.scroll_to_cursor();
871 let widget = ui.make_input();
872 let p = ui.cursor_offset(&area);
873 if let CursorSetting::Default = ui.config.cursor {
874 frame.set_cursor_position(p)
875 };
876
877 frame.render_widget(widget, area);
878
879 p
880}
881
882fn render_status(frame: &mut Frame, area: Rect, ui: &ResultsUI, full_width: u16) {
883 if ui.status_config.show {
884 let widget = ui.make_status(full_width);
885 frame.render_widget(widget, area);
886 }
887}
888
889fn render_display(frame: &mut Frame, area: Rect, ui: &mut DisplayUI, results_ui: &ResultsUI) {
890 if !ui.show {
891 return;
892 }
893 let mut widths = results_ui.widths().to_vec();
894 widths
895 .get_mut(0)
896 .map(|w| *w += results_ui.indentation() as u16);
897
898 let widget = ui.make_display(
899 results_ui.indentation() as u16,
900 widths,
901 results_ui.config.column_spacing.0,
902 );
903
904 frame.render_widget(widget, area);
905
906 if ui.is_single_column() {
907 let widget = ui.make_full_width_row(results_ui.indentation() as u16);
908 frame.render_widget(widget, area);
909 }
910}
911
912fn render_ui(frame: &mut Frame, area: &mut Rect, ui: &UI) {
914 let widget = ui.make_ui();
915 frame.render_widget(widget, *area);
916 *area = ui.compute_area(area);
917}
918
919fn split(rect: &mut Rect, height: u16, cut_top: bool) -> Rect {
920 let h = height.min(rect.height);
921
922 if cut_top {
923 let offshoot = Rect {
924 x: rect.x,
925 y: rect.y,
926 width: rect.width,
927 height: h,
928 };
929
930 rect.y += h;
931 rect.height -= h;
932
933 offshoot
934 } else {
935 let offshoot = Rect {
936 x: rect.x,
937 y: rect.y + rect.height - h,
938 width: rect.width,
939 height: h,
940 };
941
942 rect.height -= h;
943
944 offshoot
945 }
946}
947
948#[cfg(test)]
951mod test {}
952
953