use crossterm::event::{Event as CTEvent, KeyCode};
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::mpsc::Sender;
use tui::backend::Backend;
use tui::buffer::Buffer;
use tui::layout::Rect;
use tui::style::{Color, Style};
use tui::terminal::Frame;
use tui::text::Text;
use tui::widgets::{Block, Borders, Clear, Paragraph, Widget, Wrap};
use uuid::Uuid;
use super::toast::ToastEvent;
use crate::decoders::Decoders;
use crate::search;
use crate::session::events::SessionEvent;
use crate::session::*;
use crate::ui::commands;
use crate::ui::views::{self, View};
pub enum UiEvent
{
Redraw,
Crossterm(CTEvent),
Toast(ToastEvent),
SessionEvent(Box<SessionEvent>),
}
pub struct ProxideUi<B: Backend>
{
pub context: UiContext,
pub ui_stack: Vec<Box<dyn View<B>>>,
pub toasts: Vec<Toast>,
pub input_command: Option<commands::CommandState<B>>,
}
pub struct Toast
{
uuid: Uuid,
text: String,
error: bool,
}
pub struct Runtime
{
pub decoders: Decoders,
pub search_index: Rc<RefCell<search::SearchIndex>>,
pub tx: Sender<UiEvent>,
}
pub struct UiContext
{
pub data: Session,
pub runtime: Runtime,
pub size: Rect,
}
pub enum HandleResult<B: Backend>
{
Update,
Quit,
PushView(Box<dyn View<B>>),
ExitView,
ExitCommand(Option<Box<HandleResult<B>>>),
}
impl<B: Backend> ProxideUi<B>
{
pub fn new(session: Session, tx: Sender<UiEvent>, decoders: Decoders, size: Rect) -> Self
{
Self {
context: UiContext {
runtime: Runtime {
search_index: Rc::new(RefCell::new(search::SearchIndex::new(
&session, &decoders,
))),
decoders,
tx,
},
data: session,
size,
},
ui_stack: vec![Box::new(views::MainView::default())],
toasts: vec![],
input_command: None,
}
}
pub fn handle(&mut self, e: UiEvent) -> Option<HandleResult<B>>
{
match e {
UiEvent::Redraw => unreachable!("This is handled by the parent loop"),
UiEvent::SessionEvent(e) => {
let index_request = match &*e {
SessionEvent::MessageDone(msg) => Some(search::IndexRequest::Message {
request: msg.uuid,
part: msg.part,
}),
_ => None,
};
if let Some(ixreq) = index_request {
self.context.runtime.search_index.borrow_mut().index(
&self.context.data,
&self.context.runtime.decoders,
ixreq,
);
}
let mut results_iter = self.context.data.handle(*e).into_iter().map(|change| {
self.ui_stack
.last_mut()
.unwrap()
.on_change(&self.context, &change)
});
match results_iter.any(|b| b) {
true => Some(HandleResult::Update),
false => None,
}
}
UiEvent::Crossterm(e) => self.on_input(&e, self.context.size),
UiEvent::Toast(e) => {
match e {
ToastEvent::Show { uuid, text, error } => {
self.toasts.push(Toast { uuid, text, error })
}
ToastEvent::Close { uuid } => {
self.toasts.retain(|t| t.uuid != uuid);
}
}
Some(HandleResult::Update)
}
}
}
fn handle_result(&mut self, result: HandleResult<B>) -> Option<HandleResult<B>>
{
match result {
r @ HandleResult::Update | r @ HandleResult::Quit => return Some(r),
HandleResult::PushView(v) => {
self.ui_stack.push(v);
}
HandleResult::ExitView => {
self.ui_stack.pop();
}
HandleResult::ExitCommand(cmd) => {
self.input_command = None;
return cmd.and_then(|r| self.handle_result(*r));
}
}
Some(HandleResult::Update)
}
fn on_input(&mut self, e: &CTEvent, size: Rect) -> Option<HandleResult<B>>
{
let result = if let Some(cmd) = &mut self.input_command {
cmd.on_input(&mut self.context, e)
} else {
self.ui_stack
.last_mut()
.unwrap()
.on_input(&self.context, e, size)
};
if let Some(result) = result {
return self.handle_result(result);
}
Some(match e {
CTEvent::Resize(width, height) => {
self.context.size = Rect {
x: 0,
y: 0,
width: *width,
height: *height,
};
HandleResult::Update
}
CTEvent::Key(key) => match key.code {
KeyCode::Char(':') => {
self.input_command = Some(commands::CommandState {
help: "Enter command".to_string(),
prompt: ":".to_string(),
input: Default::default(),
text_cursor: 0,
display_cursor: 0,
executable: Box::new(commands::ColonCommand),
});
HandleResult::Update
}
KeyCode::Char('Q') => HandleResult::Quit,
KeyCode::Esc => {
if self.ui_stack.len() > 1 {
self.ui_stack.pop();
}
HandleResult::Update
}
_ => return None,
},
_ => return None,
})
}
pub fn draw(&mut self, terminal: &mut tui::Terminal<B>) -> std::io::Result<()>
{
terminal.hide_cursor()?;
terminal.draw(|f| self.draw_views(f))?;
if let Some(cmd) = &self.input_command {
let x = cmd.cursor();
terminal.set_cursor(x + 2, terminal.size()?.height - 1)?;
terminal.show_cursor()?;
}
Ok(())
}
pub fn draw_views(&mut self, f: &mut Frame<B>)
{
let chunk = f.size();
let view_chunk = Rect {
x: 0,
y: 0,
width: chunk.width,
height: chunk.height - 2,
};
let mut idx = self.ui_stack.len() - 1;
while self.ui_stack[idx].transparent() {
idx -= 1;
}
for i in idx..self.ui_stack.len() {
self.ui_stack[i].draw(&self.context, f, view_chunk);
}
let help_text = if let Some(cmd) = &self.input_command {
format!("{}\n{}{}", cmd.help, cmd.prompt, cmd.input)
} else {
let view = self.ui_stack.last_mut().unwrap();
view.help_text(&self.context, self.context.size)
};
let text_chunk = Rect {
x: 1,
y: chunk.height - 2,
width: chunk.width - 2,
height: 2,
};
f.render_widget(
Paragraph::new(Text::raw(&help_text)).wrap(Wrap { trim: false }),
text_chunk,
);
let mut offset = 1;
for t in &self.toasts {
offset = t.draw(offset, f);
}
}
}
impl Toast
{
fn draw<B: Backend>(&self, offset: u16, f: &mut Frame<B>) -> u16
{
let screen = f.size();
let lines: Vec<_> = self.text.split('\n').collect();
let max_line = lines.iter().fold(0, |max, line| max.max(line.len()));
let mut rect = Rect {
x: 0,
y: offset,
width: (max_line + 2) as u16,
height: (lines.len() + 2) as u16,
};
rect.x = screen.width - rect.width - 2;
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(match self.error {
true => Color::LightRed,
false => Color::Reset,
}));
f.render_widget(Clear, rect);
f.render_widget(
Paragraph::new(Text::raw(&self.text))
.block(block)
.wrap(Wrap { trim: false }),
rect,
);
offset + rect.height
}
}
pub struct ProxideTable<T>
{
phantom: std::marker::PhantomData<T>,
}
impl<T> Default for ProxideTable<T>
{
fn default() -> Self
{
Self {
phantom: std::marker::PhantomData::<T>,
}
}
}
impl std::fmt::Display for Status
{
fn fmt(&self, w: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error>
{
match self {
Status::InProgress => write!(w, ".."),
Status::Succeeded => write!(w, "OK"),
Status::Failed => write!(w, "Fail"),
}
}
}
pub struct TextLine<'a>(pub &'a str);
impl<'a> Widget for TextLine<'a>
{
fn render(self, area: Rect, buf: &mut Buffer)
{
buf.set_stringn(
area.x,
area.y,
self.0,
area.width as usize,
Style::default(),
);
}
}