Skip to main content

modalkit_ratatui/
screen.rs

1//! # Tabbed window layout
2//!
3//! ## Overview
4//!
5//! This widget can be used by consumers to create a tabbed window layout containing horizontal and
6//! vertical splits. It builds on top of [CommandBarState] and [WindowLayoutState] to accomplish
7//! this, both of which can also be used on their own if something different is needed.
8use std::borrow::Cow;
9use std::iter::Iterator;
10use std::marker::PhantomData;
11
12use serde::{Deserialize, Serialize};
13
14use ratatui::{
15    buffer::Buffer,
16    layout::Rect,
17    style::{Color, Modifier as StyleModifier, Style},
18    text::{Line, Span},
19    widgets::{BorderType, StatefulWidget, Tabs, Widget},
20};
21
22use super::{
23    cmdbar::{CommandBar, CommandBarState},
24    util::{rect_down, rect_zero_height},
25    windows::{WindowActions, WindowLayout, WindowLayoutRoot, WindowLayoutState},
26    TerminalCursor,
27    Window,
28    WindowOps,
29};
30
31use modalkit::actions::*;
32use modalkit::errors::{EditResult, UIError, UIResult};
33use modalkit::prelude::*;
34use modalkit::ui::FocusList;
35
36use modalkit::editing::{
37    application::{ApplicationInfo, EmptyInfo},
38    completion::CompletionList,
39    context::EditContext,
40    store::Store,
41};
42
43const MAX_COMPL_BARH: usize = 10;
44const GAP_COMPL_COL: usize = 2;
45
46/// Pop-up hover menu displaying completions.
47#[derive(Default)]
48struct CompletionMenu {
49    cursor: (u16, u16),
50}
51
52impl CompletionMenu {
53    fn new(cursor: (u16, u16)) -> Self {
54        CompletionMenu { cursor }
55    }
56}
57
58impl StatefulWidget for CompletionMenu {
59    type State = CompletionList;
60
61    fn render(self, area: Rect, buffer: &mut Buffer, state: &mut CompletionList) {
62        if area.height <= 1 {
63            // Not enough space to render a menu above or below the cursor.
64            return;
65        }
66
67        let len = state.candidates.len();
68
69        let top = area.top();
70        let bot = area.bottom();
71        let (cx, cy) = self.cursor;
72
73        let above = cy.saturating_sub(top) as usize;
74        let below = bot.saturating_sub(cy).saturating_sub(1) as usize;
75
76        let right = area.right();
77        let space = right.saturating_sub(cx).saturating_sub(1) as usize;
78        let maxw = state.candidates.iter().map(|s| s.len()).max().unwrap_or(0).min(space);
79        let style = Style::reset().add_modifier(StyleModifier::REVERSED);
80        let style_sel = style.bg(Color::Yellow).fg(Color::Black);
81
82        let x = if state.start.y == state.cursor.y {
83            let diff = state.cursor.x.saturating_sub(state.start.x);
84            cx.saturating_sub(diff as u16)
85        } else {
86            cx
87        };
88
89        let mut draw = |y: u16, idx: usize, s: &str| {
90            let sel = matches!(state.selected, Some(i) if i == idx);
91            let style = if sel { style_sel } else { style };
92            let slen = s.len();
93
94            let (x, _) = buffer.set_stringn(x, y, s, space, style);
95            let start = (maxw - slen) as u16;
96
97            for off in 0..start {
98                buffer.set_stringn(x + off, y, " ", 1, style);
99            }
100        };
101
102        let candidates = state.candidates.iter().enumerate();
103
104        if len <= below || below >= above {
105            // We bias towards a drop-down menu.
106            let height = len.min(below);
107            let page = if let Some(selected) = state.selected {
108                selected / height
109            } else {
110                0
111            };
112
113            for (y, (idx, s)) in candidates.skip(page * height).take(height).enumerate() {
114                let y = cy + y as u16 + 1;
115                draw(y, idx, s);
116            }
117        } else {
118            // Draw a menu above the line.
119            let height = len.min(above);
120            let page = if let Some(selected) = state.selected {
121                selected / height
122            } else {
123                0
124            };
125
126            let n = (len - page * height).min(height);
127
128            for (y, (idx, s)) in candidates.skip(page * height).take(height).enumerate() {
129                let y = cy.saturating_sub((n - y) as u16);
130                draw(y, idx, s);
131            }
132        }
133    }
134}
135
136/// Row and columns information for the completion bar.
137#[derive(Default)]
138struct CompletionBar {
139    colw: u16,
140    cols: u16,
141    rows: u16,
142}
143
144impl CompletionBar {
145    fn new(list: &CompletionList, width: u16) -> Self {
146        match list.display {
147            CompletionDisplay::None => CompletionBar::default(),
148            CompletionDisplay::List => CompletionBar::default(),
149            CompletionDisplay::Bar => {
150                let len = list.candidates.len();
151                let width = width as usize;
152
153                if len == 0 {
154                    return CompletionBar::default();
155                }
156
157                let cmax = list.candidates.iter().map(|s| s.len()).max().unwrap_or(0);
158                let cmax = cmax.clamp(1, width);
159                let colw = (cmax + GAP_COMPL_COL).min(width);
160                let cols = (width / colw).max(1);
161                let rows = (len / cols).clamp(1, MAX_COMPL_BARH);
162
163                CompletionBar {
164                    colw: colw as u16,
165                    cols: cols as u16,
166                    rows: rows as u16,
167                }
168            },
169        }
170    }
171}
172
173impl StatefulWidget for CompletionBar {
174    type State = CompletionList;
175
176    fn render(self, area: Rect, buffer: &mut Buffer, state: &mut CompletionList) {
177        if area.height == 0 {
178            return;
179        }
180
181        let mut iter = state.candidates.iter();
182        let maxw = (self.colw as usize).saturating_sub(GAP_COMPL_COL);
183        let style = Style::default();
184
185        for x in 0..self.cols {
186            for y in 0..self.rows {
187                let item = match iter.next() {
188                    Some(item) => item.as_str(),
189                    None => return,
190                };
191
192                let x = area.x + x * self.colw;
193                let y = area.y + y;
194                buffer.set_stringn(x, y, item, maxw, style);
195            }
196        }
197    }
198}
199
200trait TabActions<C, S, I>
201where
202    I: ApplicationInfo,
203{
204    /// Close one or more tabs, and all of their [Windows](Window).
205    fn tab_close(
206        &mut self,
207        target: &TabTarget,
208        flags: CloseFlags,
209        ctx: &C,
210        store: &mut S,
211    ) -> UIResult<EditInfo, I>;
212
213    /// Extract the currently focused [Window] from the currently focused tab, and place it in a
214    /// new tab.
215    fn tab_extract(
216        &mut self,
217        change: &FocusChange,
218        side: &MoveDir1D,
219        ctx: &C,
220        store: &mut S,
221    ) -> UIResult<EditInfo, I>;
222
223    /// Switch focus to another tab.
224    fn tab_focus(&mut self, change: &FocusChange, ctx: &C, store: &mut S) -> UIResult<EditInfo, I>;
225
226    /// Move the current tab to another position.
227    fn tab_move(&mut self, change: &FocusChange, ctx: &C, store: &mut S) -> UIResult<EditInfo, I>;
228
229    /// Open a new tab after the tab targeted by [FocusChange].
230    fn tab_open(
231        &mut self,
232        target: &OpenTarget<I::WindowId>,
233        change: &FocusChange,
234        ctx: &C,
235        store: &mut S,
236    ) -> UIResult<EditInfo, I>;
237}
238
239fn bold<'a>(s: String) -> Span<'a> {
240    Span::styled(s, Style::default().add_modifier(StyleModifier::BOLD))
241}
242
243/// A description of open tabs.
244#[derive(Clone, Debug, Deserialize, Serialize)]
245#[serde(bound(deserialize = "I::WindowId: Deserialize<'de>"))]
246#[serde(bound(serialize = "I::WindowId: Serialize"))]
247pub struct TabbedLayoutDescription<I: ApplicationInfo> {
248    /// The description of the window layout for each tab.
249    pub tabs: Vec<WindowLayoutRoot<I>>,
250    /// The index of the last focused tab
251    pub focused: usize,
252}
253
254impl<I: ApplicationInfo> TabbedLayoutDescription<I> {
255    /// Create a new collection of tabs from this description.
256    pub fn to_layout<W: Window<I>>(
257        self,
258        area: Option<Rect>,
259        store: &mut Store<I>,
260    ) -> UIResult<FocusList<WindowLayoutState<W, I>>, I> {
261        let mut tabs = self
262            .tabs
263            .into_iter()
264            .map(|desc| desc.to_layout(area, store))
265            .collect::<UIResult<Vec<_>, I>>()
266            .map(FocusList::new)?;
267
268        // Count starts at 1
269        let change = FocusChange::Offset(Count::Exact(self.focused + 1), true);
270        let ctx = EditContext::default();
271        tabs.focus(&change, &ctx);
272
273        Ok(tabs)
274    }
275}
276
277/// Controls which part of the [ScreenState] is currently receiving user input.
278#[derive(Clone, Copy, Debug, Eq, PartialEq)]
279pub enum CurrentFocus {
280    /// Focus on the [CommandBarState].
281    Command,
282
283    /// Focus on the [WindowLayoutState].
284    Window,
285}
286
287/// Persistent state for [Screen].
288pub struct ScreenState<W, I = EmptyInfo>
289where
290    W: Window<I>,
291    I: ApplicationInfo,
292{
293    focused: CurrentFocus,
294    cmdbar: CommandBarState<I>,
295    tabs: FocusList<WindowLayoutState<W, I>>,
296
297    messages: Vec<(String, Style)>,
298    last_message: bool,
299}
300
301impl<W, I> ScreenState<W, I>
302where
303    W: Window<I>,
304    I: ApplicationInfo,
305{
306    /// Create state for a [Screen] widget.
307    pub fn new(win: W, cmdbar: CommandBarState<I>) -> Self {
308        let tab = WindowLayoutState::new(win);
309        let tabs = FocusList::from(tab);
310
311        Self::from_list(tabs, cmdbar)
312    }
313
314    /// Create state for a [Screen] widget from an existing set of tabs.
315    pub fn from_list(tabs: FocusList<WindowLayoutState<W, I>>, cmdbar: CommandBarState<I>) -> Self {
316        ScreenState {
317            focused: CurrentFocus::Window,
318            cmdbar,
319            tabs,
320
321            messages: vec![],
322            last_message: false,
323        }
324    }
325
326    /// Get a description of the open tabs and their window layouts.
327    pub fn as_description(&self) -> TabbedLayoutDescription<I> {
328        TabbedLayoutDescription {
329            tabs: self.tabs.iter().map(WindowLayoutState::as_description).collect(),
330            focused: self.tabs.pos(),
331        }
332    }
333
334    /// Push a new error or status message.
335    pub fn push_message<T: ToString>(&mut self, msg: T, style: Style) {
336        self.messages.push((msg.to_string(), style));
337        self.last_message = true;
338    }
339
340    /// Push an error message with a red foreground.
341    pub fn push_error<T: ToString>(&mut self, msg: T) {
342        let style = Style::default().fg(Color::Red);
343
344        self.push_message(msg, style);
345    }
346
347    /// Push an info message with a default [Style].
348    pub fn push_info<T: Into<String>>(&mut self, msg: T) {
349        let style = Style::default();
350
351        self.push_message(msg.into(), style);
352    }
353
354    /// Clear the displayed error or status message.
355    pub fn clear_message(&mut self) {
356        self.last_message = false;
357    }
358
359    fn focus_command(
360        &mut self,
361        prompt: &str,
362        ct: CommandType,
363        act: &Action<I>,
364        ctx: &EditContext,
365    ) -> EditResult<EditInfo, I> {
366        self.focused = CurrentFocus::Command;
367        self.cmdbar.reset();
368        self.cmdbar.set_type(prompt, ct, act, ctx);
369        self.clear_message();
370
371        Ok(None)
372    }
373
374    fn focus_window(&mut self) -> EditResult<EditInfo, I> {
375        self.focused = CurrentFocus::Window;
376        self.cmdbar.reset();
377
378        Ok(None)
379    }
380
381    /// Perform a command bar action.
382    pub fn command_bar(
383        &mut self,
384        act: &CommandBarAction<I>,
385        ctx: &EditContext,
386    ) -> EditResult<EditInfo, I> {
387        match act {
388            CommandBarAction::Focus(s, ct, act) => self.focus_command(s, *ct, act, ctx),
389            CommandBarAction::Unfocus => self.focus_window(),
390        }
391    }
392
393    /// Get a reference to the window layout for the current tab.
394    pub fn current_tab(&self) -> UIResult<&WindowLayoutState<W, I>, I> {
395        self.tabs.get().ok_or(UIError::NoTab)
396    }
397
398    /// Get a mutable reference to the window layout for the current tab.
399    pub fn current_tab_mut(&mut self) -> UIResult<&mut WindowLayoutState<W, I>, I> {
400        self.tabs.get_mut().ok_or(UIError::NoTab)
401    }
402
403    /// Get a reference to the currently focused window.
404    pub fn current_window(&self) -> Option<&W> {
405        self.tabs.get().and_then(WindowLayoutState::get)
406    }
407
408    /// Get a mutable reference to the currently focused window.
409    pub fn current_window_mut(&mut self) -> UIResult<&mut W, I> {
410        self.current_tab_mut()?.get_mut().ok_or(UIError::NoWindow)
411    }
412
413    /// Get the maximum valid tab index.
414    fn _max_idx(&self) -> usize {
415        self.tabs.len().saturating_sub(1)
416    }
417}
418
419impl<W, I> TabActions<EditContext, Store<I>, I> for ScreenState<W, I>
420where
421    W: Window<I>,
422    I: ApplicationInfo,
423{
424    fn tab_close(
425        &mut self,
426        target: &TabTarget,
427        flags: CloseFlags,
428        ctx: &EditContext,
429        store: &mut Store<I>,
430    ) -> UIResult<EditInfo, I> {
431        let mut filter = |tab: &mut WindowLayoutState<W, I>| -> UIResult<(), I> {
432            let _ = tab.window_close(&WindowTarget::All, flags, ctx, store);
433
434            if tab.windows() == 0 {
435                return Ok(());
436            }
437
438            let msg = "Could not close all windows in tab";
439            let err = UIError::Failure(msg.into());
440
441            return Err(err);
442        };
443
444        self.tabs.try_close(target, &mut filter, ctx)?;
445
446        Ok(None)
447    }
448
449    fn tab_extract(
450        &mut self,
451        change: &FocusChange,
452        side: &MoveDir1D,
453        ctx: &EditContext,
454        _: &mut Store<I>,
455    ) -> UIResult<EditInfo, I> {
456        if self.windows() <= 1 {
457            return Ok(Some(InfoMessage::from("Already one window")));
458        }
459
460        let tab = self.current_tab_mut()?.extract();
461
462        let (idx, side) = if let Some((idx, _)) = self.tabs.target(change, ctx) {
463            (idx, *side)
464        } else {
465            (self._max_idx(), MoveDir1D::Next)
466        };
467
468        self.tabs.insert(idx, side, tab);
469
470        return Ok(None);
471    }
472
473    fn tab_focus(
474        &mut self,
475        change: &FocusChange,
476        ctx: &EditContext,
477        _: &mut Store<I>,
478    ) -> UIResult<EditInfo, I> {
479        self.tabs.focus(change, ctx);
480
481        Ok(None)
482    }
483
484    fn tab_move(
485        &mut self,
486        change: &FocusChange,
487        ctx: &EditContext,
488        _: &mut Store<I>,
489    ) -> UIResult<EditInfo, I> {
490        self.tabs.transfer(change, ctx);
491
492        return Ok(None);
493    }
494
495    fn tab_open(
496        &mut self,
497        target: &OpenTarget<I::WindowId>,
498        change: &FocusChange,
499        ctx: &EditContext,
500        store: &mut Store<I>,
501    ) -> UIResult<EditInfo, I> {
502        let (idx, side) =
503            self.tabs.target(change, ctx).unwrap_or((self.tabs.pos(), MoveDir1D::Next));
504        let tab = self.current_tab_mut()?.from_target(target, ctx, store)?;
505
506        self.tabs.insert(idx, side, tab);
507
508        return Ok(None);
509    }
510}
511
512impl<W, I> TabCount for ScreenState<W, I>
513where
514    W: Window<I>,
515    I: ApplicationInfo,
516{
517    fn tabs(&self) -> usize {
518        self.tabs.len()
519    }
520}
521
522impl<W, I> TabContainer<EditContext, Store<I>, I> for ScreenState<W, I>
523where
524    W: Window<I>,
525    I: ApplicationInfo,
526{
527    fn tab_command(
528        &mut self,
529        act: &TabAction<I>,
530        ctx: &EditContext,
531        store: &mut Store<I>,
532    ) -> UIResult<EditInfo, I> {
533        match act {
534            TabAction::Close(target, flags) => self.tab_close(target, *flags, ctx, store),
535            TabAction::Extract(target, side) => self.tab_extract(target, side, ctx, store),
536            TabAction::Focus(change) => self.tab_focus(change, ctx, store),
537            TabAction::Move(change) => self.tab_move(change, ctx, store),
538            TabAction::Open(target, change) => self.tab_open(target, change, ctx, store),
539            act => {
540                let msg = format!("unknown tab action: {act:?}");
541                return Err(UIError::Unimplemented(msg));
542            },
543        }
544    }
545}
546
547impl<W, I> WindowCount for ScreenState<W, I>
548where
549    W: Window<I>,
550    I: ApplicationInfo,
551{
552    fn windows(&self) -> usize {
553        self.tabs.get().map(WindowCount::windows).unwrap_or(0)
554    }
555}
556
557impl<W, I> WindowContainer<EditContext, Store<I>, I> for ScreenState<W, I>
558where
559    W: Window<I>,
560    I: ApplicationInfo,
561{
562    fn window_command(
563        &mut self,
564        act: &WindowAction<I>,
565        ctx: &EditContext,
566        store: &mut Store<I>,
567    ) -> UIResult<EditInfo, I> {
568        let tab = self.current_tab_mut()?;
569        let ret = tab.window_command(act, ctx, store);
570
571        if tab.windows() == 0 {
572            self.tabs.remove_current();
573        }
574
575        ret
576    }
577}
578
579macro_rules! delegate_focus {
580    ($s: expr, $id: ident => $invoke: expr) => {
581        match $s.focused {
582            CurrentFocus::Command => {
583                let $id = &mut $s.cmdbar;
584                $invoke
585            },
586            CurrentFocus::Window => {
587                if let Ok($id) = $s.current_window_mut() {
588                    $invoke
589                } else {
590                    Ok(Default::default())
591                }
592            },
593        }
594    };
595}
596
597impl<W, I> Editable<EditContext, Store<I>, I> for ScreenState<W, I>
598where
599    W: Window<I> + Editable<EditContext, Store<I>, I>,
600    I: ApplicationInfo,
601{
602    fn editor_command(
603        &mut self,
604        act: &EditorAction,
605        ctx: &EditContext,
606        store: &mut Store<I>,
607    ) -> EditResult<EditInfo, I> {
608        delegate_focus!(self, f => f.editor_command(act, ctx, store))
609    }
610}
611
612impl<W, I> TerminalCursor for ScreenState<W, I>
613where
614    W: Window<I> + TerminalCursor,
615    I: ApplicationInfo,
616{
617    fn get_term_cursor(&self) -> Option<(u16, u16)> {
618        match self.focused {
619            CurrentFocus::Command => self.cmdbar.get_term_cursor(),
620            CurrentFocus::Window => {
621                if let Some(w) = self.current_window() {
622                    w.get_term_cursor()
623                } else {
624                    None
625                }
626            },
627        }
628    }
629}
630
631impl<W, C, I> Jumpable<C, I> for ScreenState<W, I>
632where
633    W: Window<I> + Jumpable<C, I>,
634    I: ApplicationInfo,
635{
636    fn jump(
637        &mut self,
638        list: PositionList,
639        dir: MoveDir1D,
640        count: usize,
641        ctx: &C,
642    ) -> UIResult<usize, I> {
643        self.current_tab_mut()?.jump(list, dir, count, ctx)
644    }
645}
646
647impl<W, I> Promptable<EditContext, Store<I>, I> for ScreenState<W, I>
648where
649    W: Window<I> + Promptable<EditContext, Store<I>, I>,
650    I: ApplicationInfo,
651{
652    fn prompt(
653        &mut self,
654        act: &PromptAction,
655        ctx: &EditContext,
656        store: &mut Store<I>,
657    ) -> EditResult<Vec<(Action<I>, EditContext)>, I> {
658        delegate_focus!(self, f => f.prompt(act, ctx, store))
659    }
660}
661
662impl<W, I> Scrollable<EditContext, Store<I>, I> for ScreenState<W, I>
663where
664    W: Window<I> + Scrollable<EditContext, Store<I>, I>,
665    I: ApplicationInfo,
666{
667    fn scroll(
668        &mut self,
669        style: &ScrollStyle,
670        ctx: &EditContext,
671        store: &mut Store<I>,
672    ) -> EditResult<EditInfo, I> {
673        delegate_focus!(self, f => f.scroll(style, ctx, store))
674    }
675}
676
677impl<W, C, I> Searchable<C, Store<I>, I> for ScreenState<W, I>
678where
679    W: Window<I> + Searchable<C, Store<I>, I>,
680    I: ApplicationInfo,
681{
682    fn search(
683        &mut self,
684        dir: MoveDirMod,
685        count: Count,
686        ctx: &C,
687        store: &mut Store<I>,
688    ) -> UIResult<EditInfo, I> {
689        self.current_window_mut()?.search(dir, count, ctx, store)
690    }
691}
692
693/// Widget for displaying a tabbed window layout with a command bar.
694pub struct Screen<'a, W, I = EmptyInfo>
695where
696    W: Window<I>,
697    I: ApplicationInfo,
698{
699    store: &'a mut Store<I>,
700    showdialog: Vec<Span<'a>>,
701    showmode: Option<Span<'a>>,
702
703    borders: bool,
704    border_style: Style,
705    border_style_focused: Style,
706    border_type: BorderType,
707    cmdbar_style: Style,
708    cmdbar_prompt_style: Option<Style>,
709    tab_style: Style,
710    tab_style_focused: Style,
711    divider: Span<'a>,
712    focused: bool,
713
714    _p: PhantomData<(W, I)>,
715}
716
717impl<'a, W, I> Screen<'a, W, I>
718where
719    W: Window<I>,
720    I: ApplicationInfo,
721{
722    /// Create a new widget.
723    pub fn new(store: &'a mut Store<I>) -> Self {
724        Screen {
725            store,
726            showdialog: Vec::new(),
727            showmode: None,
728            borders: false,
729            border_style: Style::default(),
730            border_style_focused: Style::default(),
731            border_type: BorderType::Plain,
732            cmdbar_style: Style::default(),
733            cmdbar_prompt_style: None,
734            tab_style: Style::default(),
735            tab_style_focused: Style::default(),
736            divider: Span::raw("|"),
737            focused: true,
738            _p: PhantomData,
739        }
740    }
741
742    /// What [Style] should be used when drawing borders.
743    pub fn border_style(mut self, style: Style) -> Self {
744        self.border_style = style;
745        self
746    }
747
748    /// What [Style] should be used when drawing the border of the selected window.
749    pub fn border_style_focused(mut self, style: Style) -> Self {
750        self.border_style_focused = style;
751        self
752    }
753
754    /// What characters should be used when drawing borders.
755    pub fn border_type(mut self, border_type: BorderType) -> Self {
756        self.border_type = border_type;
757        self
758    }
759
760    /// Indicate whether to draw borders around windows.
761    pub fn borders(mut self, borders: bool) -> Self {
762        self.borders = borders;
763        self
764    }
765
766    /// What [Style] should be used when drawing borders.
767    pub fn cmdbar_style(mut self, style: Style) -> Self {
768        self.cmdbar_style = style;
769        self
770    }
771
772    /// What [Style] should be used when drawing the border of the selected window.
773    pub fn cmdbar_prompt_style(mut self, style: Style) -> Self {
774        self.cmdbar_prompt_style = Some(style);
775        self
776    }
777
778    /// What [Style] should be used for tab names.
779    pub fn tab_style(mut self, style: Style) -> Self {
780        self.tab_style = style;
781        self
782    }
783
784    /// What [Style] should be used for the focused tab name.
785    pub fn tab_style_focused(mut self, style: Style) -> Self {
786        self.tab_style_focused = style;
787        self
788    }
789
790    /// Set the divider [Span] to place in between tab names.
791    ///
792    /// This defaults to an unstyled "|".
793    pub fn divider(mut self, divider: impl Into<Span<'a>>) -> Self {
794        self.divider = divider.into();
795        self
796    }
797
798    /// Indicates whether the terminal window is currently focused.
799    pub fn focus(mut self, focused: bool) -> Self {
800        self.focused = focused;
801        self
802    }
803
804    /// Show the message from an interactive dialog.
805    pub fn show_dialog(mut self, dialog: Vec<Cow<'a, str>>) -> Self {
806        self.showdialog = dialog.into_iter().map(Span::raw).collect();
807        self
808    }
809
810    /// Set the mode string to display.
811    pub fn show_mode(mut self, mode: Option<String>) -> Self {
812        self.showmode = mode.map(bold);
813        self
814    }
815}
816
817impl<W, I> StatefulWidget for Screen<'_, W, I>
818where
819    W: Window<I>,
820    I: ApplicationInfo,
821{
822    type State = ScreenState<W, I>;
823
824    fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
825        if area.height == 0 {
826            return;
827        }
828
829        let focused = state.focused;
830
831        let mut compls = match focused {
832            CurrentFocus::Command => state.cmdbar.get_completions(),
833            CurrentFocus::Window => state.current_window().and_then(WindowOps::get_completions),
834        };
835
836        // Determine whether we need to show the tab bar.
837        let ntabs = state.tabs.len();
838        let tabh = if ntabs > 1 && area.height > 2 { 1 } else { 0 };
839
840        // Determine whether we need to show the completion bar, and how big it should be.
841        let cbar = compls
842            .as_ref()
843            .map(|l| CompletionBar::new(l, area.width))
844            .unwrap_or_default();
845        let mut barh = cbar.rows;
846
847        // Calculate height for dialog message.
848        let dialog = std::mem::take(&mut self.showdialog);
849        let cmdh = match dialog.len() {
850            0 => 1,
851            n => {
852                // If we have a dialog message, we'll skip showing a completion bar.
853                barh = 0;
854                (n as u16).clamp(1, area.height)
855            },
856        };
857
858        // The rest of the space goes to showing the open windows and the command bar.
859        let winh = area.height.saturating_sub(tabh).saturating_sub(barh).saturating_sub(cmdh);
860
861        let init = rect_zero_height(area);
862        let tabarea = rect_down(init, tabh);
863        let winarea = rect_down(tabarea, winh);
864        let bararea = rect_down(winarea, barh);
865        let cmdarea = rect_down(bararea, cmdh);
866
867        let titles: Vec<Line> = state
868            .tabs
869            .iter()
870            .map(|tab| {
871                let mut spans = vec![];
872                let n = tab.windows();
873
874                if n > 1 {
875                    spans.push(Span::from(format!("{n} ")));
876                }
877
878                if let Some(w) = tab.get() {
879                    let mut title = w.get_tab_title(self.store).spans;
880
881                    spans.append(&mut title);
882                } else {
883                    spans.push(Span::from("[No Name]"));
884                }
885
886                Line::from(spans)
887            })
888            .collect();
889
890        Tabs::new(titles)
891            .style(self.tab_style)
892            .highlight_style(self.tab_style_focused)
893            .divider(self.divider)
894            .select(state.tabs.pos())
895            .render(tabarea, buf);
896
897        if let Ok(tab) = state.current_tab_mut() {
898            WindowLayout::new(self.store)
899                .focus(self.focused && focused == CurrentFocus::Window)
900                .border_style(self.border_style)
901                .border_style_focused(self.border_style_focused)
902                .border_type(self.border_type)
903                .borders(self.borders)
904                .render(winarea, buf, tab);
905        }
906
907        if !dialog.is_empty() {
908            let iter = dialog.into_iter().take(cmdarea.height as usize);
909
910            for (i, line) in iter.enumerate() {
911                let y = cmdarea.y + i as u16;
912                buf.set_span(0, y, &line, cmdarea.width);
913            }
914
915            return;
916        }
917
918        let status = if self.showmode.is_some() || !state.last_message {
919            state.last_message = false;
920            self.showmode
921        } else if let Some((s, style)) = state.messages.last() {
922            Some(Span::styled(s, *style))
923        } else {
924            None
925        };
926
927        CommandBar::new()
928            .focus(focused == CurrentFocus::Command)
929            .status(status)
930            .style(self.cmdbar_style)
931            .prompt_style(self.cmdbar_prompt_style.unwrap_or(self.cmdbar_style))
932            .render(cmdarea, buf, &mut state.cmdbar);
933
934        // Render completion list last so it's drawn on top of the windows.
935        if let Some(ref mut completions) = compls {
936            match completions.display {
937                CompletionDisplay::None => {},
938                CompletionDisplay::Bar => {
939                    cbar.render(bararea, buf, completions);
940                },
941                CompletionDisplay::List => {
942                    if let Some(cursor) = state.get_term_cursor() {
943                        CompletionMenu::new(cursor).render(winarea, buf, completions);
944                    }
945                },
946            }
947        }
948    }
949}