use std::borrow::Cow;
use std::iter::Iterator;
use std::marker::PhantomData;
use serde::{Deserialize, Serialize};
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier as StyleModifier, Style},
text::{Line, Span},
widgets::{BorderType, StatefulWidget, Tabs, Widget},
};
use super::{
cmdbar::{CommandBar, CommandBarState},
util::{rect_down, rect_zero_height},
windows::{WindowActions, WindowLayout, WindowLayoutRoot, WindowLayoutState},
TerminalCursor,
Window,
WindowOps,
};
use modalkit::actions::*;
use modalkit::errors::{EditResult, UIError, UIResult};
use modalkit::prelude::*;
use modalkit::ui::FocusList;
use modalkit::editing::{
application::{ApplicationInfo, EmptyInfo},
completion::CompletionList,
context::EditContext,
store::Store,
};
const MAX_COMPL_BARH: usize = 10;
const GAP_COMPL_COL: usize = 2;
#[derive(Default)]
struct CompletionMenu {
cursor: (u16, u16),
}
impl CompletionMenu {
fn new(cursor: (u16, u16)) -> Self {
CompletionMenu { cursor }
}
}
impl StatefulWidget for CompletionMenu {
type State = CompletionList;
fn render(self, area: Rect, buffer: &mut Buffer, state: &mut CompletionList) {
if area.height <= 1 {
return;
}
let len = state.candidates.len();
let top = area.top();
let bot = area.bottom();
let (cx, cy) = self.cursor;
let above = cy.saturating_sub(top) as usize;
let below = bot.saturating_sub(cy).saturating_sub(1) as usize;
let right = area.right();
let space = right.saturating_sub(cx).saturating_sub(1) as usize;
let maxw = state.candidates.iter().map(|s| s.len()).max().unwrap_or(0).min(space);
let style = Style::reset().add_modifier(StyleModifier::REVERSED);
let style_sel = style.bg(Color::Yellow).fg(Color::Black);
let x = if state.start.y == state.cursor.y {
let diff = state.cursor.x.saturating_sub(state.start.x);
cx.saturating_sub(diff as u16)
} else {
cx
};
let mut draw = |y: u16, idx: usize, s: &str| {
let sel = matches!(state.selected, Some(i) if i == idx);
let style = if sel { style_sel } else { style };
let slen = s.len();
let (x, _) = buffer.set_stringn(x, y, s, space, style);
let start = (maxw - slen) as u16;
for off in 0..start {
buffer.set_stringn(x + off, y, " ", 1, style);
}
};
let candidates = state.candidates.iter().enumerate();
if len <= below || below >= above {
let height = len.min(below);
let page = if let Some(selected) = state.selected {
selected / height
} else {
0
};
for (y, (idx, s)) in candidates.skip(page * height).take(height).enumerate() {
let y = cy + y as u16 + 1;
draw(y, idx, s);
}
} else {
let height = len.min(above);
let page = if let Some(selected) = state.selected {
selected / height
} else {
0
};
let n = (len - page * height).min(height);
for (y, (idx, s)) in candidates.skip(page * height).take(height).enumerate() {
let y = cy.saturating_sub((n - y) as u16);
draw(y, idx, s);
}
}
}
}
#[derive(Default)]
struct CompletionBar {
colw: u16,
cols: u16,
rows: u16,
}
impl CompletionBar {
fn new(list: &CompletionList, width: u16) -> Self {
match list.display {
CompletionDisplay::None => CompletionBar::default(),
CompletionDisplay::List => CompletionBar::default(),
CompletionDisplay::Bar => {
let len = list.candidates.len();
let width = width as usize;
if len == 0 {
return CompletionBar::default();
}
let cmax = list.candidates.iter().map(|s| s.len()).max().unwrap_or(0);
let cmax = cmax.clamp(1, width);
let colw = (cmax + GAP_COMPL_COL).min(width);
let cols = (width / colw).max(1);
let rows = (len / cols).clamp(1, MAX_COMPL_BARH);
CompletionBar {
colw: colw as u16,
cols: cols as u16,
rows: rows as u16,
}
},
}
}
}
impl StatefulWidget for CompletionBar {
type State = CompletionList;
fn render(self, area: Rect, buffer: &mut Buffer, state: &mut CompletionList) {
if area.height == 0 {
return;
}
let mut iter = state.candidates.iter();
let maxw = (self.colw as usize).saturating_sub(GAP_COMPL_COL);
let style = Style::default();
for x in 0..self.cols {
for y in 0..self.rows {
let item = match iter.next() {
Some(item) => item.as_str(),
None => return,
};
let x = area.x + x * self.colw;
let y = area.y + y;
buffer.set_stringn(x, y, item, maxw, style);
}
}
}
}
trait TabActions<C, S, I>
where
I: ApplicationInfo,
{
fn tab_close(
&mut self,
target: &TabTarget,
flags: CloseFlags,
ctx: &C,
store: &mut S,
) -> UIResult<EditInfo, I>;
fn tab_extract(
&mut self,
change: &FocusChange,
side: &MoveDir1D,
ctx: &C,
store: &mut S,
) -> UIResult<EditInfo, I>;
fn tab_focus(&mut self, change: &FocusChange, ctx: &C, store: &mut S) -> UIResult<EditInfo, I>;
fn tab_move(&mut self, change: &FocusChange, ctx: &C, store: &mut S) -> UIResult<EditInfo, I>;
fn tab_open(
&mut self,
target: &OpenTarget<I::WindowId>,
change: &FocusChange,
ctx: &C,
store: &mut S,
) -> UIResult<EditInfo, I>;
}
fn bold<'a>(s: String) -> Span<'a> {
Span::styled(s, Style::default().add_modifier(StyleModifier::BOLD))
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(bound(deserialize = "I::WindowId: Deserialize<'de>"))]
#[serde(bound(serialize = "I::WindowId: Serialize"))]
pub struct TabbedLayoutDescription<I: ApplicationInfo> {
pub tabs: Vec<WindowLayoutRoot<I>>,
pub focused: usize,
}
impl<I: ApplicationInfo> TabbedLayoutDescription<I> {
pub fn to_layout<W: Window<I>>(
self,
area: Option<Rect>,
store: &mut Store<I>,
) -> UIResult<FocusList<WindowLayoutState<W, I>>, I> {
let mut tabs = self
.tabs
.into_iter()
.map(|desc| desc.to_layout(area, store))
.collect::<UIResult<Vec<_>, I>>()
.map(FocusList::new)?;
let change = FocusChange::Offset(Count::Exact(self.focused + 1), true);
let ctx = EditContext::default();
tabs.focus(&change, &ctx);
Ok(tabs)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CurrentFocus {
Command,
Window,
}
pub struct ScreenState<W, I = EmptyInfo>
where
W: Window<I>,
I: ApplicationInfo,
{
focused: CurrentFocus,
cmdbar: CommandBarState<I>,
tabs: FocusList<WindowLayoutState<W, I>>,
messages: Vec<(String, Style)>,
last_message: bool,
}
impl<W, I> ScreenState<W, I>
where
W: Window<I>,
I: ApplicationInfo,
{
pub fn new(win: W, cmdbar: CommandBarState<I>) -> Self {
let tab = WindowLayoutState::new(win);
let tabs = FocusList::from(tab);
Self::from_list(tabs, cmdbar)
}
pub fn from_list(tabs: FocusList<WindowLayoutState<W, I>>, cmdbar: CommandBarState<I>) -> Self {
ScreenState {
focused: CurrentFocus::Window,
cmdbar,
tabs,
messages: vec![],
last_message: false,
}
}
pub fn as_description(&self) -> TabbedLayoutDescription<I> {
TabbedLayoutDescription {
tabs: self.tabs.iter().map(WindowLayoutState::as_description).collect(),
focused: self.tabs.pos(),
}
}
pub fn push_message<T: ToString>(&mut self, msg: T, style: Style) {
self.messages.push((msg.to_string(), style));
self.last_message = true;
}
pub fn push_error<T: ToString>(&mut self, msg: T) {
let style = Style::default().fg(Color::Red);
self.push_message(msg, style);
}
pub fn push_info<T: Into<String>>(&mut self, msg: T) {
let style = Style::default();
self.push_message(msg.into(), style);
}
pub fn clear_message(&mut self) {
self.last_message = false;
}
fn focus_command(
&mut self,
prompt: &str,
ct: CommandType,
act: &Action<I>,
ctx: &EditContext,
) -> EditResult<EditInfo, I> {
self.focused = CurrentFocus::Command;
self.cmdbar.reset();
self.cmdbar.set_type(prompt, ct, act, ctx);
self.clear_message();
Ok(None)
}
fn focus_window(&mut self) -> EditResult<EditInfo, I> {
self.focused = CurrentFocus::Window;
self.cmdbar.reset();
Ok(None)
}
pub fn command_bar(
&mut self,
act: &CommandBarAction<I>,
ctx: &EditContext,
) -> EditResult<EditInfo, I> {
match act {
CommandBarAction::Focus(s, ct, act) => self.focus_command(s, *ct, act, ctx),
CommandBarAction::Unfocus => self.focus_window(),
}
}
pub fn current_tab(&self) -> UIResult<&WindowLayoutState<W, I>, I> {
self.tabs.get().ok_or(UIError::NoTab)
}
pub fn current_tab_mut(&mut self) -> UIResult<&mut WindowLayoutState<W, I>, I> {
self.tabs.get_mut().ok_or(UIError::NoTab)
}
pub fn current_window(&self) -> Option<&W> {
self.tabs.get().and_then(WindowLayoutState::get)
}
pub fn current_window_mut(&mut self) -> UIResult<&mut W, I> {
self.current_tab_mut()?.get_mut().ok_or(UIError::NoWindow)
}
fn _max_idx(&self) -> usize {
self.tabs.len().saturating_sub(1)
}
}
impl<W, I> TabActions<EditContext, Store<I>, I> for ScreenState<W, I>
where
W: Window<I>,
I: ApplicationInfo,
{
fn tab_close(
&mut self,
target: &TabTarget,
flags: CloseFlags,
ctx: &EditContext,
store: &mut Store<I>,
) -> UIResult<EditInfo, I> {
let mut filter = |tab: &mut WindowLayoutState<W, I>| -> UIResult<(), I> {
let _ = tab.window_close(&WindowTarget::All, flags, ctx, store);
if tab.windows() == 0 {
return Ok(());
}
let msg = "Could not close all windows in tab";
let err = UIError::Failure(msg.into());
return Err(err);
};
self.tabs.try_close(target, &mut filter, ctx)?;
Ok(None)
}
fn tab_extract(
&mut self,
change: &FocusChange,
side: &MoveDir1D,
ctx: &EditContext,
_: &mut Store<I>,
) -> UIResult<EditInfo, I> {
if self.windows() <= 1 {
return Ok(Some(InfoMessage::from("Already one window")));
}
let tab = self.current_tab_mut()?.extract();
let (idx, side) = if let Some((idx, _)) = self.tabs.target(change, ctx) {
(idx, *side)
} else {
(self._max_idx(), MoveDir1D::Next)
};
self.tabs.insert(idx, side, tab);
return Ok(None);
}
fn tab_focus(
&mut self,
change: &FocusChange,
ctx: &EditContext,
_: &mut Store<I>,
) -> UIResult<EditInfo, I> {
self.tabs.focus(change, ctx);
Ok(None)
}
fn tab_move(
&mut self,
change: &FocusChange,
ctx: &EditContext,
_: &mut Store<I>,
) -> UIResult<EditInfo, I> {
self.tabs.transfer(change, ctx);
return Ok(None);
}
fn tab_open(
&mut self,
target: &OpenTarget<I::WindowId>,
change: &FocusChange,
ctx: &EditContext,
store: &mut Store<I>,
) -> UIResult<EditInfo, I> {
let (idx, side) =
self.tabs.target(change, ctx).unwrap_or((self.tabs.pos(), MoveDir1D::Next));
let tab = self.current_tab_mut()?.from_target(target, ctx, store)?;
self.tabs.insert(idx, side, tab);
return Ok(None);
}
}
impl<W, I> TabCount for ScreenState<W, I>
where
W: Window<I>,
I: ApplicationInfo,
{
fn tabs(&self) -> usize {
self.tabs.len()
}
}
impl<W, I> TabContainer<EditContext, Store<I>, I> for ScreenState<W, I>
where
W: Window<I>,
I: ApplicationInfo,
{
fn tab_command(
&mut self,
act: &TabAction<I>,
ctx: &EditContext,
store: &mut Store<I>,
) -> UIResult<EditInfo, I> {
match act {
TabAction::Close(target, flags) => self.tab_close(target, *flags, ctx, store),
TabAction::Extract(target, side) => self.tab_extract(target, side, ctx, store),
TabAction::Focus(change) => self.tab_focus(change, ctx, store),
TabAction::Move(change) => self.tab_move(change, ctx, store),
TabAction::Open(target, change) => self.tab_open(target, change, ctx, store),
act => {
let msg = format!("unknown tab action: {act:?}");
return Err(UIError::Unimplemented(msg));
},
}
}
}
impl<W, I> WindowCount for ScreenState<W, I>
where
W: Window<I>,
I: ApplicationInfo,
{
fn windows(&self) -> usize {
self.tabs.get().map(WindowCount::windows).unwrap_or(0)
}
}
impl<W, I> WindowContainer<EditContext, Store<I>, I> for ScreenState<W, I>
where
W: Window<I>,
I: ApplicationInfo,
{
fn window_command(
&mut self,
act: &WindowAction<I>,
ctx: &EditContext,
store: &mut Store<I>,
) -> UIResult<EditInfo, I> {
let tab = self.current_tab_mut()?;
let ret = tab.window_command(act, ctx, store);
if tab.windows() == 0 {
self.tabs.remove_current();
}
ret
}
}
macro_rules! delegate_focus {
($s: expr, $id: ident => $invoke: expr) => {
match $s.focused {
CurrentFocus::Command => {
let $id = &mut $s.cmdbar;
$invoke
},
CurrentFocus::Window => {
if let Ok($id) = $s.current_window_mut() {
$invoke
} else {
Ok(Default::default())
}
},
}
};
}
impl<W, I> Editable<EditContext, Store<I>, I> for ScreenState<W, I>
where
W: Window<I> + Editable<EditContext, Store<I>, I>,
I: ApplicationInfo,
{
fn editor_command(
&mut self,
act: &EditorAction,
ctx: &EditContext,
store: &mut Store<I>,
) -> EditResult<EditInfo, I> {
delegate_focus!(self, f => f.editor_command(act, ctx, store))
}
}
impl<W, I> TerminalCursor for ScreenState<W, I>
where
W: Window<I> + TerminalCursor,
I: ApplicationInfo,
{
fn get_term_cursor(&self) -> Option<(u16, u16)> {
match self.focused {
CurrentFocus::Command => self.cmdbar.get_term_cursor(),
CurrentFocus::Window => {
if let Some(w) = self.current_window() {
w.get_term_cursor()
} else {
None
}
},
}
}
}
impl<W, C, I> Jumpable<C, I> for ScreenState<W, I>
where
W: Window<I> + Jumpable<C, I>,
I: ApplicationInfo,
{
fn jump(
&mut self,
list: PositionList,
dir: MoveDir1D,
count: usize,
ctx: &C,
) -> UIResult<usize, I> {
self.current_tab_mut()?.jump(list, dir, count, ctx)
}
}
impl<W, I> Promptable<EditContext, Store<I>, I> for ScreenState<W, I>
where
W: Window<I> + Promptable<EditContext, Store<I>, I>,
I: ApplicationInfo,
{
fn prompt(
&mut self,
act: &PromptAction,
ctx: &EditContext,
store: &mut Store<I>,
) -> EditResult<Vec<(Action<I>, EditContext)>, I> {
delegate_focus!(self, f => f.prompt(act, ctx, store))
}
}
impl<W, I> Scrollable<EditContext, Store<I>, I> for ScreenState<W, I>
where
W: Window<I> + Scrollable<EditContext, Store<I>, I>,
I: ApplicationInfo,
{
fn scroll(
&mut self,
style: &ScrollStyle,
ctx: &EditContext,
store: &mut Store<I>,
) -> EditResult<EditInfo, I> {
delegate_focus!(self, f => f.scroll(style, ctx, store))
}
}
impl<W, C, I> Searchable<C, Store<I>, I> for ScreenState<W, I>
where
W: Window<I> + Searchable<C, Store<I>, I>,
I: ApplicationInfo,
{
fn search(
&mut self,
dir: MoveDirMod,
count: Count,
ctx: &C,
store: &mut Store<I>,
) -> UIResult<EditInfo, I> {
self.current_window_mut()?.search(dir, count, ctx, store)
}
}
pub struct Screen<'a, W, I = EmptyInfo>
where
W: Window<I>,
I: ApplicationInfo,
{
store: &'a mut Store<I>,
showdialog: Vec<Span<'a>>,
showmode: Option<Span<'a>>,
borders: bool,
border_style: Style,
border_style_focused: Style,
border_type: BorderType,
cmdbar_style: Style,
cmdbar_prompt_style: Option<Style>,
tab_style: Style,
tab_style_focused: Style,
divider: Span<'a>,
focused: bool,
_p: PhantomData<(W, I)>,
}
impl<'a, W, I> Screen<'a, W, I>
where
W: Window<I>,
I: ApplicationInfo,
{
pub fn new(store: &'a mut Store<I>) -> Self {
Screen {
store,
showdialog: Vec::new(),
showmode: None,
borders: false,
border_style: Style::default(),
border_style_focused: Style::default(),
border_type: BorderType::Plain,
cmdbar_style: Style::default(),
cmdbar_prompt_style: None,
tab_style: Style::default(),
tab_style_focused: Style::default(),
divider: Span::raw("|"),
focused: true,
_p: PhantomData,
}
}
pub fn border_style(mut self, style: Style) -> Self {
self.border_style = style;
self
}
pub fn border_style_focused(mut self, style: Style) -> Self {
self.border_style_focused = style;
self
}
pub fn border_type(mut self, border_type: BorderType) -> Self {
self.border_type = border_type;
self
}
pub fn borders(mut self, borders: bool) -> Self {
self.borders = borders;
self
}
pub fn cmdbar_style(mut self, style: Style) -> Self {
self.cmdbar_style = style;
self
}
pub fn cmdbar_prompt_style(mut self, style: Style) -> Self {
self.cmdbar_prompt_style = Some(style);
self
}
pub fn tab_style(mut self, style: Style) -> Self {
self.tab_style = style;
self
}
pub fn tab_style_focused(mut self, style: Style) -> Self {
self.tab_style_focused = style;
self
}
pub fn divider(mut self, divider: impl Into<Span<'a>>) -> Self {
self.divider = divider.into();
self
}
pub fn focus(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
pub fn show_dialog(mut self, dialog: Vec<Cow<'a, str>>) -> Self {
self.showdialog = dialog.into_iter().map(Span::raw).collect();
self
}
pub fn show_mode(mut self, mode: Option<String>) -> Self {
self.showmode = mode.map(bold);
self
}
}
impl<W, I> StatefulWidget for Screen<'_, W, I>
where
W: Window<I>,
I: ApplicationInfo,
{
type State = ScreenState<W, I>;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
if area.height == 0 {
return;
}
let focused = state.focused;
let mut compls = match focused {
CurrentFocus::Command => state.cmdbar.get_completions(),
CurrentFocus::Window => state.current_window().and_then(WindowOps::get_completions),
};
let ntabs = state.tabs.len();
let tabh = if ntabs > 1 && area.height > 2 { 1 } else { 0 };
let cbar = compls
.as_ref()
.map(|l| CompletionBar::new(l, area.width))
.unwrap_or_default();
let mut barh = cbar.rows;
let dialog = std::mem::take(&mut self.showdialog);
let cmdh = match dialog.len() {
0 => 1,
n => {
barh = 0;
(n as u16).clamp(1, area.height)
},
};
let winh = area.height.saturating_sub(tabh).saturating_sub(barh).saturating_sub(cmdh);
let init = rect_zero_height(area);
let tabarea = rect_down(init, tabh);
let winarea = rect_down(tabarea, winh);
let bararea = rect_down(winarea, barh);
let cmdarea = rect_down(bararea, cmdh);
let titles: Vec<Line> = state
.tabs
.iter()
.map(|tab| {
let mut spans = vec![];
let n = tab.windows();
if n > 1 {
spans.push(Span::from(format!("{n} ")));
}
if let Some(w) = tab.get() {
let mut title = w.get_tab_title(self.store).spans;
spans.append(&mut title);
} else {
spans.push(Span::from("[No Name]"));
}
Line::from(spans)
})
.collect();
Tabs::new(titles)
.style(self.tab_style)
.highlight_style(self.tab_style_focused)
.divider(self.divider)
.select(state.tabs.pos())
.render(tabarea, buf);
if let Ok(tab) = state.current_tab_mut() {
WindowLayout::new(self.store)
.focus(self.focused && focused == CurrentFocus::Window)
.border_style(self.border_style)
.border_style_focused(self.border_style_focused)
.border_type(self.border_type)
.borders(self.borders)
.render(winarea, buf, tab);
}
if !dialog.is_empty() {
let iter = dialog.into_iter().take(cmdarea.height as usize);
for (i, line) in iter.enumerate() {
let y = cmdarea.y + i as u16;
buf.set_span(0, y, &line, cmdarea.width);
}
return;
}
let status = if self.showmode.is_some() || !state.last_message {
state.last_message = false;
self.showmode
} else if let Some((s, style)) = state.messages.last() {
Some(Span::styled(s, *style))
} else {
None
};
CommandBar::new()
.focus(focused == CurrentFocus::Command)
.status(status)
.style(self.cmdbar_style)
.prompt_style(self.cmdbar_prompt_style.unwrap_or(self.cmdbar_style))
.render(cmdarea, buf, &mut state.cmdbar);
if let Some(ref mut completions) = compls {
match completions.display {
CompletionDisplay::None => {},
CompletionDisplay::Bar => {
cbar.render(bararea, buf, completions);
},
CompletionDisplay::List => {
if let Some(cursor) = state.get_term_cursor() {
CompletionMenu::new(cursor).render(winarea, buf, completions);
}
},
}
}
}
}