Restructured how UI works so there's views
This commit is contained in:
parent
ba5d07db91
commit
c76c2eaf5c
7 changed files with 273 additions and 76 deletions
|
@ -5,6 +5,12 @@
|
|||
//! extensions. Although its primary purpose is to be used in panorama, it should be usable for
|
||||
//! general-purpose IMAP usage. See the [client][crate::client] module for more information on how
|
||||
//! to get started with a client quickly.
|
||||
//!
|
||||
//! RFCs:
|
||||
//!
|
||||
//! - RFC3501 (IMAP4) : work-in-progress
|
||||
//! - RFC2177 (IDLE) : implemented
|
||||
//! - RFC5256 (SORT / THREAD) : planned
|
||||
|
||||
#[macro_use]
|
||||
extern crate anyhow;
|
||||
|
|
|
@ -99,7 +99,7 @@ fn run_ui(
|
|||
let localset = LocalSet::new();
|
||||
|
||||
localset.spawn_local(async {
|
||||
ui::run_ui(stdout, exit_tx, mail2ui_rx)
|
||||
ui::run_ui2(stdout, exit_tx, mail2ui_rx)
|
||||
.unwrap_or_else(report_err)
|
||||
.await;
|
||||
});
|
||||
|
|
|
@ -27,7 +27,6 @@ impl HandlesInput for ColonPrompt {
|
|||
let KeyEvent { code, .. } = evt;
|
||||
match code {
|
||||
KeyCode::Esc => return Ok(InputResult::Pop),
|
||||
// KeyCode::Char('q') => return Ok(InputResult::Pop),
|
||||
KeyCode::Char(c) => {
|
||||
let mut b = [0; 2];
|
||||
self.value += c.encode_utf8(&mut b);
|
||||
|
|
|
@ -43,10 +43,6 @@ impl HandlesInput for BaseInputHandler {
|
|||
KeyCode::Char(':') => {
|
||||
let colon_prompt = Box::new(ColonPrompt::init(term));
|
||||
return Ok(InputResult::Push(colon_prompt));
|
||||
// let rect = term.size()?;
|
||||
// term.set_cursor(1, rect.height - 1)?;
|
||||
// term.show_cursor()?;
|
||||
// colon_prompt = Some(ColonPrompt::default());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
@ -17,10 +17,10 @@ use tui::{
|
|||
|
||||
use crate::mail::EmailMetadata;
|
||||
|
||||
use super::FrameType;
|
||||
use super::{FrameType, HandlesInput, Window, UI};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MailTabState {
|
||||
#[derive(Default, Debug)]
|
||||
pub struct MailView {
|
||||
pub folders: Vec<String>,
|
||||
pub message_uids: Vec<u32>,
|
||||
pub message_map: HashMap<u32, EmailMetadata>,
|
||||
|
@ -29,71 +29,20 @@ pub struct MailTabState {
|
|||
pub change: Arc<AtomicI8>,
|
||||
}
|
||||
|
||||
fn humanize_timestamp(date: DateTime<Local>) -> String {
|
||||
let now = Local::now();
|
||||
let diff = now - date;
|
||||
impl HandlesInput for MailView {}
|
||||
|
||||
if diff < Duration::days(1) {
|
||||
HumanTime::from(date).to_string()
|
||||
} else if date.year() == now.year() {
|
||||
date.format("%b %e %T").to_string()
|
||||
} else {
|
||||
date.to_rfc2822()
|
||||
}
|
||||
impl Window for MailView {
|
||||
fn name(&self) -> String {
|
||||
String::from("email")
|
||||
}
|
||||
|
||||
impl MailTabState {
|
||||
pub fn move_down(&mut self) {
|
||||
if self.message_uids.is_empty() {
|
||||
return;
|
||||
}
|
||||
let len = self.message_uids.len();
|
||||
if let Some(selected) = self.message_list.selected() {
|
||||
if selected + 1 < len {
|
||||
self.message_list.select(Some(selected + 1));
|
||||
}
|
||||
} else {
|
||||
self.message_list.select(Some(0));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_up(&mut self) {
|
||||
if self.message_uids.is_empty() {
|
||||
return;
|
||||
}
|
||||
let len = self.message_uids.len();
|
||||
if let Some(selected) = self.message_list.selected() {
|
||||
if selected >= 1 {
|
||||
self.message_list.select(Some(selected - 1));
|
||||
}
|
||||
} else {
|
||||
self.message_list.select(Some(len - 1));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self, f: &mut FrameType, area: Rect) {
|
||||
fn draw(&self, f: FrameType, area: Rect, ui: &UI) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.margin(0)
|
||||
.constraints([Constraint::Length(20), Constraint::Max(5000)])
|
||||
.split(area);
|
||||
|
||||
// make the change
|
||||
if self
|
||||
.change
|
||||
.compare_exchange(-1, 0, Ordering::Relaxed, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
self.move_up();
|
||||
}
|
||||
if self
|
||||
.change
|
||||
.compare_exchange(1, 0, Ordering::Relaxed, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
self.move_down();
|
||||
}
|
||||
|
||||
// folder list
|
||||
let items = self
|
||||
.folders
|
||||
|
@ -155,6 +104,67 @@ impl MailTabState {
|
|||
.highlight_style(Style::default().bg(Color::DarkGray));
|
||||
|
||||
f.render_widget(dirlist, chunks[0]);
|
||||
f.render_stateful_widget(table, chunks[1], &mut self.message_list);
|
||||
f.render_widget(table, chunks[1]);
|
||||
}
|
||||
}
|
||||
|
||||
fn humanize_timestamp(date: DateTime<Local>) -> String {
|
||||
let now = Local::now();
|
||||
let diff = now - date;
|
||||
|
||||
if diff < Duration::days(1) {
|
||||
HumanTime::from(date).to_string()
|
||||
} else if date.year() == now.year() {
|
||||
date.format("%b %e %T").to_string()
|
||||
} else {
|
||||
date.to_rfc2822()
|
||||
}
|
||||
}
|
||||
|
||||
impl MailView {
|
||||
pub fn move_down(&mut self) {
|
||||
if self.message_uids.is_empty() {
|
||||
return;
|
||||
}
|
||||
let len = self.message_uids.len();
|
||||
if let Some(selected) = self.message_list.selected() {
|
||||
if selected + 1 < len {
|
||||
self.message_list.select(Some(selected + 1));
|
||||
}
|
||||
} else {
|
||||
self.message_list.select(Some(0));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_up(&mut self) {
|
||||
if self.message_uids.is_empty() {
|
||||
return;
|
||||
}
|
||||
let len = self.message_uids.len();
|
||||
if let Some(selected) = self.message_list.selected() {
|
||||
if selected >= 1 {
|
||||
self.message_list.select(Some(selected - 1));
|
||||
}
|
||||
} else {
|
||||
self.message_list.select(Some(len - 1));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self) {
|
||||
// make the change
|
||||
if self
|
||||
.change
|
||||
.compare_exchange(-1, 0, Ordering::Relaxed, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
self.move_up();
|
||||
}
|
||||
if self
|
||||
.change
|
||||
.compare_exchange(1, 0, Ordering::Relaxed, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
self.move_down();
|
||||
}
|
||||
}
|
||||
}
|
166
src/ui/mod.rs
166
src/ui/mod.rs
|
@ -3,7 +3,7 @@
|
|||
mod colon_prompt;
|
||||
mod input;
|
||||
mod keybinds;
|
||||
mod mail_tab;
|
||||
mod mail_view;
|
||||
mod messages;
|
||||
mod windows;
|
||||
|
||||
|
@ -41,15 +41,171 @@ use crate::mail::{EmailMetadata, MailEvent};
|
|||
|
||||
use self::colon_prompt::ColonPrompt;
|
||||
use self::input::{BaseInputHandler, HandlesInput, InputResult};
|
||||
use self::mail_tab::MailTabState;
|
||||
use self::mail_view::MailView;
|
||||
pub(crate) use self::messages::*;
|
||||
use self::windows::*;
|
||||
|
||||
pub(crate) type FrameType<'a, 'b> = Frame<'a, CrosstermBackend<&'b mut Stdout>>;
|
||||
pub(crate) type FrameType<'a, 'b, 'c> = &'c mut Frame<'a, CrosstermBackend<&'b mut Stdout>>;
|
||||
pub(crate) type TermType<'a, 'b> = &'b mut Terminal<CrosstermBackend<&'a mut Stdout>>;
|
||||
|
||||
const FRAME_DURATION: Duration = Duration::from_millis(17);
|
||||
|
||||
/// Main entrypoint for the UI
|
||||
pub async fn run_ui2(
|
||||
mut stdout: Stdout,
|
||||
exit_tx: mpsc::Sender<()>,
|
||||
mut mail2ui_rx: mpsc::UnboundedReceiver<MailEvent>,
|
||||
) -> Result<()> {
|
||||
execute!(stdout, cursor::Hide, terminal::EnterAlternateScreen)?;
|
||||
terminal::enable_raw_mode()?;
|
||||
|
||||
let backend = CrosstermBackend::new(&mut stdout);
|
||||
let mut term = Terminal::new(backend)?;
|
||||
|
||||
let should_exit = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut ui = UI {
|
||||
should_exit: should_exit.clone(),
|
||||
window_layout: WindowLayout::default(),
|
||||
windows: HashMap::new(),
|
||||
page_names: HashMap::new(),
|
||||
};
|
||||
|
||||
ui.open_window(MailView::default());
|
||||
|
||||
// let mut input_states: Vec<Box<dyn HandlesInput>> = vec![];
|
||||
|
||||
while !should_exit.load(Ordering::Relaxed) {
|
||||
term.draw(|f| {
|
||||
ui.draw(f);
|
||||
})?;
|
||||
|
||||
// handle events coming from the UI
|
||||
if event::poll(FRAME_DURATION)? {
|
||||
let event = event::read()?;
|
||||
ui.process_event(event)?;
|
||||
}
|
||||
|
||||
select! {
|
||||
// got an event from the mail thread
|
||||
evt = mail2ui_rx.recv().fuse() => if let Some(evt) = evt {
|
||||
ui.process_mail_event(evt);
|
||||
},
|
||||
|
||||
// wait for approx 60fps
|
||||
_ = time::sleep(FRAME_DURATION).fuse() => {},
|
||||
}
|
||||
}
|
||||
|
||||
mem::drop(term);
|
||||
mem::drop(ui);
|
||||
|
||||
execute!(
|
||||
stdout,
|
||||
style::ResetColor,
|
||||
cursor::Show,
|
||||
terminal::LeaveAlternateScreen
|
||||
)?;
|
||||
terminal::disable_raw_mode()?;
|
||||
|
||||
exit_tx.send(()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// UI
|
||||
pub struct UI {
|
||||
should_exit: Arc<AtomicBool>,
|
||||
window_layout: WindowLayout,
|
||||
windows: HashMap<LayoutId, Box<dyn Window>>,
|
||||
page_names: HashMap<PageId, String>,
|
||||
}
|
||||
|
||||
impl UI {
|
||||
fn draw(&mut self, f: FrameType) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(0)
|
||||
.constraints([Constraint::Max(5000), Constraint::Length(1)])
|
||||
.split(f.size());
|
||||
|
||||
let pages = self.window_layout.list_pages();
|
||||
|
||||
// draw a list of pages at the bottom
|
||||
let titles = self
|
||||
.window_layout
|
||||
.list_pages()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, id)| {
|
||||
self.page_names
|
||||
.get(id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| i.to_string())
|
||||
})
|
||||
.map(|s| Spans::from(s))
|
||||
.collect();
|
||||
let tabs = Tabs::new(titles).style(Style::default().bg(Color::DarkGray));
|
||||
f.render_widget(tabs, chunks[1]);
|
||||
|
||||
// render all other windows
|
||||
let visible = self.window_layout.visible_windows(chunks[0]);
|
||||
for (layout_id, area) in visible.into_iter() {
|
||||
if let Some(window) = self.windows.get(&layout_id) {
|
||||
debug!("drawing window {:?}", window.name());
|
||||
window.draw(f, area, self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_window(&mut self, window: impl Window) {
|
||||
debug!("opened window {:?}", window.name());
|
||||
let (layout_id, page_id) = self.window_layout.new_page();
|
||||
|
||||
let window = Box::new(window);
|
||||
self.windows.insert(layout_id, window);
|
||||
}
|
||||
|
||||
/// Main entrypoint for handling any kind of event coming from the terminal
|
||||
fn process_event(&mut self, evt: Event) -> Result<()> {
|
||||
if let Event::Key(evt) = evt {
|
||||
if let KeyEvent {
|
||||
code: KeyCode::Char('q'),
|
||||
..
|
||||
} = evt
|
||||
{
|
||||
self.should_exit.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
// handle states in the state stack
|
||||
// although this is written in a for loop, every case except one should break
|
||||
// let mut should_pop = false;
|
||||
// for input_state in input_states.iter_mut().rev() {
|
||||
// match input_state.handle_key(&mut term, evt)? {
|
||||
// InputResult::Ok => break,
|
||||
// InputResult::Push(state) => {
|
||||
// input_states.push(state);
|
||||
// break;
|
||||
// }
|
||||
// InputResult::Pop => {
|
||||
// should_pop = true;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// if should_pop {
|
||||
// input_states.pop();
|
||||
// }
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_mail_event(&mut self, evt: MailEvent) {
|
||||
debug!("received mail event: {:?}", evt);
|
||||
}
|
||||
}
|
||||
|
||||
/// Main entrypoint for the UI
|
||||
pub async fn run_ui(
|
||||
mut stdout: Stdout,
|
||||
|
@ -61,7 +217,7 @@ pub async fn run_ui(
|
|||
|
||||
let backend = CrosstermBackend::new(&mut stdout);
|
||||
let mut term = Terminal::new(backend)?;
|
||||
let mut mail_tab = MailTabState::default();
|
||||
let mut mail_tab = MailView::default();
|
||||
|
||||
// state stack for handling inputs
|
||||
let should_exit = Arc::new(AtomicBool::new(false));
|
||||
|
@ -101,7 +257,7 @@ pub async fn run_ui(
|
|||
f.render_widget(tabs, chunks[0]);
|
||||
|
||||
// this is the main mail tab
|
||||
mail_tab.render(f, chunks[1]);
|
||||
// mail_tab.render(f, chunks[1]);
|
||||
|
||||
// this is the status bar
|
||||
if let Some(last_state) = input_states.last() {
|
||||
|
|
|
@ -2,13 +2,33 @@ use std::collections::{HashMap, HashSet, VecDeque};
|
|||
|
||||
use tui::layout::Rect;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||
use super::{FrameType, HandlesInput, UI};
|
||||
|
||||
pub trait Window: HandlesInput {
|
||||
// Return some kind of name
|
||||
fn name(&self) -> String;
|
||||
|
||||
// Main draw function
|
||||
fn draw(&self, f: FrameType, area: Rect, ui: &UI);
|
||||
|
||||
/// Draw function, except the window is not the actively focused one
|
||||
///
|
||||
/// By default, this just calls the regular draw function but the window may choose to perform
|
||||
/// a less intensive draw if it's known to not be active
|
||||
fn draw_inactive(&mut self, f: FrameType, area: Rect, ui: &UI) {
|
||||
self.draw(f, area, ui);
|
||||
}
|
||||
}
|
||||
|
||||
downcast_rs::impl_downcast!(Window);
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct LayoutId(usize);
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct PageId(usize);
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct WindowLayout {
|
||||
ctr: usize,
|
||||
currently_active: Option<LayoutId>,
|
||||
|
@ -31,6 +51,12 @@ impl WindowLayout {
|
|||
self.ctr += 1;
|
||||
self.pages.insert(pid, pg);
|
||||
self.page_order.push(pid);
|
||||
self.ids.insert(id, pid);
|
||||
|
||||
if let None = self.currently_active {
|
||||
self.currently_active = Some(id);
|
||||
}
|
||||
|
||||
(id, pid)
|
||||
}
|
||||
|
||||
|
@ -38,8 +64,8 @@ impl WindowLayout {
|
|||
&self.page_order
|
||||
}
|
||||
|
||||
/// Get a set of all windows visible on the current page
|
||||
pub fn visible_windows(&self) -> HashMap<LayoutId, Rect> {
|
||||
/// Get a set of all windows visible on the current page, given the size of the allotted space
|
||||
pub fn visible_windows(&self, area: Rect) -> HashMap<LayoutId, Rect> {
|
||||
let mut map = HashMap::new();
|
||||
if let Some(page) = self
|
||||
.currently_active
|
||||
|
@ -52,17 +78,21 @@ impl WindowLayout {
|
|||
|
||||
while !q.is_empty() {
|
||||
let front = q.pop_front().expect("not empty");
|
||||
// TODO: how to subdivide properly?
|
||||
map.insert(front, area);
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PageGraph {
|
||||
root: LayoutId,
|
||||
adj: HashMap<LayoutId, HashSet<(LayoutId, Dir)>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Dir {
|
||||
H,
|
||||
V,
|
||||
|
|
Loading…
Reference in a new issue