diff --git a/src/bin.rs b/src/bin.rs index bafd223f..b800441a 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -66,7 +66,7 @@ fn main() { let menu = Entity { component: Box::new(AccountMenu::new(&state.context.accounts)), }; - let listing = MailListing::new(); + let listing = CompactListing::new(); let b = Entity { component: Box::new(listing), }; diff --git a/ui/src/components/mail/listing/compact.rs b/ui/src/components/mail/listing/compact.rs index cc59a28c..0ccebd72 100644 --- a/ui/src/components/mail/listing/compact.rs +++ b/ui/src/components/mail/listing/compact.rs @@ -289,34 +289,22 @@ impl Component for CompactListing { /* Draw the entire list */ self.draw_list(grid, area, context); } else { - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); if self.length == 0 && self.dirty { clear_area(grid, area); context.dirty_areas.push_back(area); } - /* Render the mail body in a pager, basically copy what HSplit does */ - let total_rows = get_y(bottom_right) - get_y(upper_left); - let pager_ratio = context.runtime_settings.pager.pager_ratio; - let bottom_entity_rows = (pager_ratio * total_rows) / 100; - - if bottom_entity_rows > total_rows { - clear_area(grid, area); - context.dirty_areas.push_back(area); - return; - } - let mid = get_y(upper_left) + total_rows - bottom_entity_rows; + /* Render the mail body in a pager */ if !self.dirty { if let Some(v) = self.view.as_mut() { - v.draw(grid, (set_y(upper_left, mid + 1), bottom_right), context); + v.draw(grid, area, context); } return; } self.view = Some(ThreadView::new(self.cursor_pos)); self.view.as_mut().unwrap().draw( grid, - (set_y(upper_left, mid + 1), bottom_right), + area, context, ); self.dirty = false; @@ -446,10 +434,15 @@ impl Component for CompactListing { }, _ => {} } + if let Some(ref mut v) = self.view { + v.process_event(event, context); + } } fn is_dirty(&self) -> bool { - true + self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false) } fn set_dirty(&mut self) { + self.view.as_mut().map(|p| p.set_dirty()); + self.dirty = true; } } diff --git a/ui/src/components/mail/listing/mod.rs b/ui/src/components/mail/listing/mod.rs index 5dc6dba5..f6672d18 100644 --- a/ui/src/components/mail/listing/mod.rs +++ b/ui/src/components/mail/listing/mod.rs @@ -21,7 +21,6 @@ use super::*; -//use melib::mailbox::backends::BackendOp; mod compact; pub use self::compact::*; @@ -779,6 +778,7 @@ impl Component for MailListing { self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false) } fn set_dirty(&mut self) { + self.view.as_mut().map(|p| p.set_dirty()); self.dirty = true; } } diff --git a/ui/src/components/mail/view/envelope.rs b/ui/src/components/mail/view/envelope.rs index 9f3d7c71..ac5d73da 100644 --- a/ui/src/components/mail/view/envelope.rs +++ b/ui/src/components/mail/view/envelope.rs @@ -301,7 +301,7 @@ impl Component for EnvelopeView { } else { self.pager.as_mut().map(|p| p.cursor_pos()) }; - self.pager = Some(Pager::from_buf(&buf, cursor_pos)); + self.pager = Some(Pager::from_buf(buf.split_newlines(), cursor_pos)); } }; self.dirty = false; diff --git a/ui/src/components/mail/view/html.rs b/ui/src/components/mail/view/html.rs index 9ccbbf7a..1fdc5875 100644 --- a/ui/src/components/mail/view/html.rs +++ b/ui/src/components/mail/view/html.rs @@ -49,7 +49,7 @@ impl HtmlView { )); let buf = MailView::plain_text_to_buf(&display_text, true); - let pager = Pager::from_buf(&buf, None); + let pager = Pager::from_buf(buf.split_newlines(), None); HtmlView { pager, bytes } } } diff --git a/ui/src/components/mail/view/mod.rs b/ui/src/components/mail/view/mod.rs index db0113cd..ccbfe7ce 100644 --- a/ui/src/components/mail/view/mod.rs +++ b/ui/src/components/mail/view/mod.rs @@ -329,7 +329,7 @@ impl Component for MailView { } else { self.pager.as_mut().map(|p| p.cursor_pos()) }; - self.pager = Some(Pager::from_buf(&buf, cursor_pos)); + self.pager = Some(Pager::from_buf(buf.split_newlines(), cursor_pos)); } }; self.dirty = false; diff --git a/ui/src/components/mail/view/thread.rs b/ui/src/components/mail/view/thread.rs index 4c0c96a4..eedc1038 100644 --- a/ui/src/components/mail/view/thread.rs +++ b/ui/src/components/mail/view/thread.rs @@ -21,10 +21,23 @@ use super::*; +#[derive(Debug)] +struct ThreadEntry { + index: (usize, usize, usize), + indentation: usize, + content: CellBuffer, + + msg_idx: usize, + expanded: bool, +} + + +#[derive(Debug)] pub struct ThreadView { dirty: bool, coordinates: (usize, usize, usize), - + pager: Pager, + entries: Vec, } impl ThreadView { @@ -33,6 +46,77 @@ impl ThreadView { ThreadView { dirty: true, coordinates, + pager: Pager::default(), + entries: Vec::new(), + } + } + + fn make_entry(&mut self, context: &mut Context, i: (usize, usize, usize)) -> ThreadEntry { + let (ind, idx, order) = i; + let mailbox = &context.accounts[self.coordinates.0][self.coordinates.1] + .as_ref() + .unwrap(); + let container = &mailbox.threads.containers()[idx]; + let msg_idx = if let Some(i) = container.message() { + i + } else { + mailbox.threads.containers()[ + container.first_child().unwrap() + ].message().unwrap() + }; + let envelope: &Envelope = &mailbox.collection[msg_idx]; + let op = context.accounts[self.coordinates.0].backend.operation(envelope.hash()); + let body = envelope.body(op); + + let mut body_text: String = " \n".repeat(6); + body_text.push_str(&String::from_utf8_lossy(&decode_rec(&body, None))); + let mut buf = CellBuffer::from(&body_text).split_newlines(); + + + let date = format!("Date: {}\n", envelope.date_as_str()); + let from = format!("From: {}\n", envelope.from_to_string()); + let message_id = &format!("Message-ID: <{}>\n\n", envelope.message_id_raw()); + let mut width = [date.len(), from.len(), message_id.len(), buf.size().0].iter().map(|&v| v).max().unwrap_or(1); + let height = buf.size().1; + if width > buf.size().0 { + buf.resize(width, height, Cell::default()); + width -= 1; + } else { + width = buf.size().0 - 1; + } + + write_string_to_grid( + &date, + &mut buf, + Color::Byte(33), + Color::Default, + ((0, 0), (width, height)), + true, + ); + write_string_to_grid( + &from, + &mut buf, + Color::Byte(33), + Color::Default, + ((0, 1), (width, height)), + true, + ); + write_string_to_grid( + &message_id, + &mut buf, + Color::Byte(33), + Color::Default, + ((0, 2), (width, height)), + true, + ); + + ThreadEntry { + index: (ind, idx, order), + indentation: ind, + content: buf, + + msg_idx, + expanded: false, } } } @@ -48,91 +132,90 @@ impl Component for ThreadView { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); - let mailbox = &mut context.accounts[self.coordinates.0][self.coordinates.1].as_ref().unwrap(); - let threads = &mailbox.threads; - let container = &threads.containers()[threads.root_set()[self.coordinates.2]]; - let i = if let Some(i) = container.message() { - i - } else { - threads.containers()[ - container.first_child().unwrap() - ].message().unwrap() + clear_area(grid, area); + let mut stack: Vec<(usize, usize)> = Vec::with_capacity(32); + + let y = + { + let mailbox = &mut context.accounts[self.coordinates.0][self.coordinates.1].as_ref().unwrap(); + let threads = &mailbox.threads; + let container = &threads.containers()[threads.root_set()[self.coordinates.2]]; + let i = if let Some(i) = container.message() { + i + } else { + threads.containers()[ + container.first_child().unwrap() + ].message().unwrap() + }; + let envelope: &Envelope = &mailbox.collection[i]; + + let (x, y) = write_string_to_grid( + &envelope.subject(), + grid, + Color::Byte(33), + Color::Default, + area, + true, + ); + for x in x..=get_x(bottom_right) { + grid[(x, y)].set_ch(' '); + grid[(x, y)].set_bg(Color::Default); + grid[(x, y)].set_fg(Color::Default); + } + if container.message().is_some() { + stack.push((0, threads.root_set()[self.coordinates.2])); + } else { + stack.push((1, container.first_child().unwrap())); + } + y }; - let envelope: &Envelope = &mailbox.collection[i]; - let (x, y) = write_string_to_grid( - &format!("Date: {}", envelope.date_as_str()), - grid, - Color::Byte(33), - Color::Default, - area, - true, - ); - for x in x..=get_x(bottom_right) { - grid[(x, y)].set_ch(' '); - grid[(x, y)].set_bg(Color::Default); - grid[(x, y)].set_fg(Color::Default); + + if self.dirty { + + let mut line = 0; + while let Some((ind, idx)) = stack.pop() { + let entry = self.make_entry(context, (ind, idx, line)); + self.entries.push(entry); + line += 1; + let mailbox = &mut context.accounts[self.coordinates.0][self.coordinates.1].as_ref().unwrap(); + let threads = &mailbox.threads; + let container = &threads.containers()[idx]; + if let Some(i) = container.next_sibling() { + stack.push((ind, i)); + } + if let Some(i) = container.first_child() { + stack.push((ind + 1, i)); + } + } + let height = self.entries.iter().map(|x| x.content.size().1).sum(); + let width = self.entries.iter().map(|x| x.content.size().0).max().unwrap_or(0); + let mut content = CellBuffer::new(width, height, Cell::default()); + + let mut y = 0; + + let (cols_dest, rows_dest) = content.size(); + for e in &self.entries { + let (cols_src, rows_src) = e.content.size(); + y = copy_area(&mut content, &e.content, ((0, y), (cols_dest - 1, rows_dest - 1)), ((0, 0), (cols_src - 1, rows_src - 1))).1; + for i in 0..cols_dest { + content[(i, y)].set_ch(HORZ_BOUNDARY); + } + } + self.pager = Pager::from_buf(content, None); + self.dirty = false; } - let (x, y) = write_string_to_grid( - &format!("From: {}", envelope.from_to_string()), - grid, - Color::Byte(33), - Color::Default, - (set_y(upper_left, y + 1), bottom_right), - true, - ); - for x in x..=get_x(bottom_right) { - grid[(x, y)].set_ch(' '); - grid[(x, y)].set_bg(Color::Default); - grid[(x, y)].set_fg(Color::Default); - } - let (x, y) = write_string_to_grid( - &format!("To: {}", envelope.to_to_string()), - grid, - Color::Byte(33), - Color::Default, - (set_y(upper_left, y + 1), bottom_right), - true, - ); - for x in x..=get_x(bottom_right) { - grid[(x, y)].set_ch(' '); - grid[(x, y)].set_bg(Color::Default); - grid[(x, y)].set_fg(Color::Default); - } - let (x, y) = write_string_to_grid( - &format!("Subject: {}", envelope.subject()), - grid, - Color::Byte(33), - Color::Default, - (set_y(upper_left, y + 1), bottom_right), - true, - ); - for x in x..=get_x(bottom_right) { - grid[(x, y)].set_ch(' '); - grid[(x, y)].set_bg(Color::Default); - grid[(x, y)].set_fg(Color::Default); - } - let (x, y) = write_string_to_grid( - &format!("Message-ID: <{}>", envelope.message_id_raw()), - grid, - Color::Byte(33), - Color::Default, - (set_y(upper_left, y + 1), bottom_right), - true, - ); - for x in x..=get_x(bottom_right) { - grid[(x, y)].set_ch(' '); - grid[(x, y)].set_bg(Color::Default); - grid[(x, y)].set_fg(Color::Default); - } - clear_area(grid, (set_y(upper_left, y + 1), set_y(bottom_right, y + 2))); + + self.pager.draw(grid, (set_y(upper_left, y + 1), bottom_right), context); + context .dirty_areas - .push_back((upper_left, set_y(bottom_right, y + 1))); + .push_back(area); } - fn process_event(&mut self, _event: &UIEvent, _context: &mut Context) { + fn process_event(&mut self, event: &UIEvent, context: &mut Context) { + self.pager.process_event(event, context); } fn is_dirty(&self) -> bool { - self.dirty + self.dirty || self.pager.is_dirty() } fn set_dirty(&mut self) { self.dirty = true; diff --git a/ui/src/components/mod.rs b/ui/src/components/mod.rs index e8c8d89e..6a5839a8 100644 --- a/ui/src/components/mod.rs +++ b/ui/src/components/mod.rs @@ -102,14 +102,15 @@ pub fn copy_area_with_break( grid_src: &CellBuffer, dest: Area, src: Area, -) { +) -> Pos { if !is_valid_area!(dest) || !is_valid_area!(src) { eprintln!( "BUG: Invalid areas in copy_area:\n src: {:?}\n dest: {:?}", src, dest ); - return; + return upper_left!(dest); } + let mut ret = bottom_right!(dest); let mut src_x = get_x(upper_left!(src)); let mut src_y = get_y(upper_left!(src)); @@ -119,6 +120,7 @@ pub fn copy_area_with_break( src_y += 1; src_x = 0; if src_y >= get_y(bottom_right!(src)) { + ret.1 = y; break 'y_; } continue 'y_; @@ -131,29 +133,33 @@ pub fn copy_area_with_break( src_x = 0; if src_y >= get_y(bottom_right!(src)) { //clear_area(grid_dest, ((get_x(upper_left!(dest)), y), bottom_right!(dest))); + ret.1 = y; break 'y_; } break 'x_; } } } + ret } /// Copy a source `Area` to a destination. -pub fn copy_area(grid_dest: &mut CellBuffer, grid_src: &CellBuffer, dest: Area, src: Area) { +pub fn copy_area(grid_dest: &mut CellBuffer, grid_src: &CellBuffer, dest: Area, src: Area) -> Pos { if !is_valid_area!(dest) || !is_valid_area!(src) { eprintln!( "BUG: Invalid areas in copy_area:\n src: {:?}\n dest: {:?}", src, dest ); - return; + return upper_left!(dest); } + + let mut ret = bottom_right!(dest); let mut src_x = get_x(upper_left!(src)); let mut src_y = get_y(upper_left!(src)); let (cols, rows) = grid_src.size(); if src_x >= cols || src_y >= rows { eprintln!("DEBUG: src area outside of grid_src in copy_area",); - return; + return upper_left!(dest); } for y in get_y(upper_left!(dest))..=get_y(bottom_right!(dest)) { @@ -170,10 +176,12 @@ pub fn copy_area(grid_dest: &mut CellBuffer, grid_src: &CellBuffer, dest: Area, grid_dest, ((get_x(upper_left!(dest)), y), bottom_right!(dest)), ); + ret.1 = y; break; } src_y += 1; } + ret } /// Change foreground and background colors in an `Area` @@ -211,8 +219,7 @@ fn write_string_to_grid( eprintln!(" Invalid area with string {} and area {:?}", s, area); return (x, y); } - for l in s.lines() { - 'char: for c in l.chars() { + 'char: for c in s.chars() { grid[(x, y)].set_ch(c); grid[(x, y)].set_fg(fg_color); grid[(x, y)].set_bg(bg_color); @@ -228,7 +235,6 @@ fn write_string_to_grid( break 'char; } } - } } (x, y) } diff --git a/ui/src/components/utilities.rs b/ui/src/components/utilities.rs index 41363b22..7aad252a 100644 --- a/ui/src/components/utilities.rs +++ b/ui/src/components/utilities.rs @@ -182,21 +182,23 @@ impl Component for VSplit { } } +#[derive(Debug)] +enum PagerMovement { + PageUp, + PageDown, +} + /// A pager for text. /// `Pager` holds its own content in its own `CellBuffer` and when `draw` is called, it draws the /// current view of the text. It is responsible for scrolling etc. +#[derive(Default, Debug)] pub struct Pager { cursor_pos: usize, height: usize, width: usize, dirty: bool, content: CellBuffer, -} - -impl Default for Pager { - fn default() -> Self { - Pager::from_str("", None) - } + movement: Option, } impl fmt::Display for Pager { @@ -258,6 +260,7 @@ impl Pager { width: width, dirty: true, content: content, + .. Default::default() } } pub fn from_str(s: &str, cursor_pos: Option) -> Self { @@ -272,29 +275,18 @@ impl Pager { width, dirty: true, content, + .. Default::default() } } - pub fn from_buf(buf: &CellBuffer, cursor_pos: Option) -> Self { - let lines: Vec<&[Cell]> = buf.split(|cell| cell.ch() == '\n').collect(); - let height = lines.len(); - let width = lines.iter().map(|l| l.len()).max().unwrap_or(0) + 1; - let mut content = CellBuffer::new(width, height, Cell::with_char(' ')); - { - let mut x; - let c_slice: &mut [Cell] = &mut content; - for (y, l) in lines.iter().enumerate() { - let y_r = y * width; - x = l.len() + y_r; - c_slice[y_r..x].copy_from_slice(l); - c_slice[x].set_ch('\n'); - } - } + pub fn from_buf(content: CellBuffer, cursor_pos: Option) -> Self { + let (width, height) = content.size(); Pager { cursor_pos: cursor_pos.unwrap_or(0), height, width, dirty: true, content, + .. Default::default() } } pub fn print_string(content: &mut CellBuffer, s: &str) { @@ -328,15 +320,37 @@ impl Component for Pager { } self.dirty = false; - if self.cursor_pos > 0 && self.cursor_pos + 1 + height!(area) > self.height { - self.cursor_pos = self.cursor_pos.saturating_sub(1); - return; + + let height = height!(area); + if let Some(mvm) = self.movement.take() { + // TODO: Spend some time on this + match mvm { + PagerMovement::PageUp => { + self.cursor_pos = self.cursor_pos.saturating_sub(height); + }, + PagerMovement::PageDown => { + if self.cursor_pos + 2*height + 1 < self.height { + self.cursor_pos += height; + } else { + self.cursor_pos = self.height.saturating_sub(height).saturating_sub(1); + } + }, + } + } + + if self.height > height { + if self.cursor_pos > 0 && self.cursor_pos + height >= self.height { + self.cursor_pos = self.height.saturating_sub(height).saturating_sub(2); + //return; + } + } else { + self.cursor_pos = 0; } if self.height == 0 || self.height == self.cursor_pos || self.width == 0 { return; } - + clear_area(grid, area); //let pager_context: usize = context.settings.pager.pager_context; //let pager_stop: bool = context.settings.pager.pager_stop; @@ -364,6 +378,14 @@ impl Component for Pager { self.dirty = true; } } + UIEventType::Input(Key::PageUp) => { + self.movement = Some(PagerMovement::PageUp); + self.dirty = true; + } + UIEventType::Input(Key::PageDown) => { + self.movement = Some(PagerMovement::PageDown); + self.dirty = true; + } UIEventType::ChangeMode(UIMode::Normal) => { self.dirty = true; } diff --git a/ui/src/types/cells.rs b/ui/src/types/cells.rs index 551e8b2b..596fb2b1 100644 --- a/ui/src/types/cells.rs +++ b/ui/src/types/cells.rs @@ -97,14 +97,24 @@ pub trait CellAccessor: HasSize { /// /// The first index, `Cellbuffer[y]`, corresponds to a row, and thus the y-axis. The second /// index, `Cellbuffer[y][x]`, corresponds to a column within a row and thus the x-axis. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq)] pub struct CellBuffer { cols: usize, rows: usize, buf: Vec, } +impl fmt::Debug for CellBuffer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CellBuffer {{ cols: {}, rows: {}, buf: {} cells", self.cols, self.rows, self.buf.len()) + } +} + impl CellBuffer { + pub fn set_cols(&mut self, new_cols: usize) { + self.cols = new_cols; + } + /// Constructs a new `CellBuffer` with the given number of columns and rows, using the given /// `cell` as a blank. pub fn new(cols: usize, rows: usize, cell: Cell) -> CellBuffer { @@ -130,6 +140,24 @@ impl CellBuffer { self.cols = newcols; self.rows = newrows; } + + pub fn split_newlines(self) -> Self { + let lines: Vec<&[Cell]> = self.split(|cell| cell.ch() == '\n').collect(); + let height = lines.len(); + let width = lines.iter().map(|l| l.len()).max().unwrap_or(0) + 1; + let mut content = CellBuffer::new(width, height, Cell::with_char(' ')); + { + let mut x; + let c_slice: &mut [Cell] = &mut content; + for (y, l) in lines.iter().enumerate() { + let y_r = y * width; + x = l.len() + y_r; + c_slice[y_r..x].copy_from_slice(l); + c_slice[x].set_ch('\n'); + } + } + content + } } impl HasSize for CellBuffer {