diff --git a/meli/src/notifications.rs b/meli/src/notifications.rs index a2060af1..11163471 100644 --- a/meli/src/notifications.rs +++ b/meli/src/notifications.rs @@ -24,6 +24,8 @@ use std::process::{Command, Stdio}; #[cfg(all(target_os = "linux", feature = "dbus-notifications"))] pub use dbus::*; +use melib::{utils::datetime, UnixTimestamp}; +use smallvec::SmallVec; use super::*; @@ -294,3 +296,174 @@ fn update_xbiff(path: &str) -> Result<()> { } Ok(()) } + +#[derive(Debug)] +/// On-screen-display messages. +pub struct DisplayMessage { + pub timestamp: UnixTimestamp, + pub msg: String, +} + +#[derive(Debug)] +/// Show notifications on [`Screen`]. +pub struct DisplayMessageBox { + messages: SmallVec<[DisplayMessage; 8]>, + pub expiration_start: Option, + pub active: bool, + dirty: bool, + pub initialised: bool, + pub pos: usize, + cached_area: Area, + id: ComponentId, +} + +impl DisplayMessageBox { + pub fn new(sc: &Screen) -> Box { + Box::new(Self { + messages: SmallVec::new(), + expiration_start: None, + pos: 0, + active: false, + dirty: false, + initialised: false, + cached_area: sc.area().into_empty(), + id: ComponentId::default(), + }) + } + + #[inline] + pub fn cached_area(&self) -> Area { + self.cached_area + } +} + +impl std::ops::Deref for DisplayMessageBox { + type Target = SmallVec<[DisplayMessage; 8]>; + + fn deref(&self) -> &Self::Target { + &self.messages + } +} + +impl std::ops::DerefMut for DisplayMessageBox { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.messages + } +} + +impl std::fmt::Display for DisplayMessageBox { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "") + } +} + +impl Component for DisplayMessageBox { + fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { + if let Some(DisplayMessage { + ref timestamp, + ref msg, + .. + }) = self.messages.get(self.pos) + { + let noto_colors = crate::conf::value(context, "status.notification"); + use crate::melib::text_processing::{Reflow, TextProcessing}; + + let box_width = area.width() / 3; + if box_width < 10 { + self.set_dirty(false); + return; + } + + let msg_lines = msg.split_lines_reflow(Reflow::All, Some(box_width)); + let width = msg_lines + .iter() + .map(|line| line.grapheme_len() + 4) + .max() + .unwrap_or(0); + + if width == 0 { + self.set_dirty(false); + return; + } + + self.cached_area = area.place_inside( + (width, area.height().min(msg_lines.len() + 4)), + false, + false, + ); + let box_displ_area = create_box(grid, self.cached_area); + for row in grid.bounds_iter(box_displ_area) { + for c in row { + grid[c] + .set_ch(' ') + .set_fg(noto_colors.fg) + .set_bg(noto_colors.bg) + .set_attrs(noto_colors.attrs); + } + } + let mut lines_no = 0; + for (idx, line) in msg_lines + .into_iter() + .chain(Some(String::new())) + .chain(Some(datetime::timestamp_to_string(*timestamp, None, false))) + .enumerate() + { + let (_, y) = grid.write_string( + &line, + noto_colors.fg, + noto_colors.bg, + noto_colors.attrs, + box_displ_area.skip_rows(idx), + Some(0), + ); + lines_no += 1 + y; + } + + if self.messages.len() > 1 { + grid.write_string( + &if self.pos == 0 { + format!( + "Next: {}", + context.settings.shortcuts.general.info_message_next + ) + } else if self.pos + 1 == self.len() { + format!( + "Prev: {}", + context.settings.shortcuts.general.info_message_previous + ) + } else { + format!( + "Prev: {} Next: {}", + context.settings.shortcuts.general.info_message_previous, + context.settings.shortcuts.general.info_message_next + ) + }, + noto_colors.fg, + noto_colors.bg, + noto_colors.attrs, + box_displ_area.skip_rows(lines_no), + Some(0), + ); + } + } else { + self.cached_area = area.into_empty(); + } + self.set_dirty(false); + } + + fn process_event(&mut self, _event: &mut UIEvent, _context: &mut Context) -> bool { + false + } + + fn is_dirty(&self) -> bool { + self.dirty + } + + fn set_dirty(&mut self, value: bool) { + self.dirty = value; + } + + fn id(&self) -> ComponentId { + self.id + } +} diff --git a/meli/src/state.rs b/meli/src/state.rs index bd541424..38f536e2 100644 --- a/meli/src/state.rs +++ b/meli/src/state.rs @@ -48,13 +48,13 @@ use indexmap::{IndexMap, IndexSet}; use melib::{ backends::{AccountHash, BackendEvent, BackendEventConsumer, Backends, RefreshEvent}, utils::datetime, - UnixTimestamp, }; use smallvec::SmallVec; use super::*; use crate::{ jobs::JobExecutor, + notifications::{DisplayMessage, DisplayMessageBox}, terminal::{get_events, Screen, Tty}, }; @@ -285,20 +285,7 @@ pub struct State { component_tree: IndexMap, pub context: Box, timer: thread::JoinHandle<()>, - - display_messages: SmallVec<[DisplayMessage; 8]>, - display_messages_expiration_start: Option, - display_messages_active: bool, - display_messages_dirty: bool, - display_messages_initialised: bool, - display_messages_pos: usize, - //display_messages_area: Area, -} - -#[derive(Debug)] -struct DisplayMessage { - timestamp: UnixTimestamp, - msg: String, + message_box: Box, } impl Drop for State { @@ -422,6 +409,7 @@ impl State { } else { Screen::draw_horizontal_segment_no_color }); + let message_box = DisplayMessageBox::new(&screen); let mut s = State { screen, child: None, @@ -431,12 +419,7 @@ impl State { component_tree: IndexMap::default(), timer, draw_rate_limit: RateLimit::new(1, 3, job_executor.clone()), - display_messages: SmallVec::new(), - display_messages_expiration_start: None, - display_messages_pos: 0, - display_messages_active: false, - display_messages_dirty: false, - display_messages_initialised: false, + message_box, context: Box::new(Context { accounts, settings, @@ -535,8 +518,8 @@ impl State { pub fn update_size(&mut self) { self.screen.update_size(); self.rcv_event(UIEvent::Resize); - self.display_messages_dirty = true; - self.display_messages_initialised = false; + self.message_box.set_dirty(true); + self.message_box.initialised = false; // Invalidate dirty areas. self.context.dirty_areas.clear(); @@ -553,17 +536,18 @@ impl State { } let mut areas: smallvec::SmallVec<[Area; 8]> = self.context.dirty_areas.drain(0..).collect(); - if self.display_messages_active { + if self.message_box.active { let now = datetime::now(); if self - .display_messages_expiration_start + .message_box + .expiration_start .map(|t| t + 5 < now) .unwrap_or(false) { - self.display_messages_active = false; - self.display_messages_dirty = true; - self.display_messages_initialised = false; - self.display_messages_expiration_start = None; + self.message_box.active = false; + self.message_box.set_dirty(true); + self.message_box.initialised = false; + self.message_box.expiration_start = None; areas.push(self.screen.area()); } } @@ -571,16 +555,21 @@ impl State { /* Sort by x_start, ie upper_left corner's x coordinate */ areas.sort_by(|a, b| a.upper_left().0.partial_cmp(&b.upper_left().0).unwrap()); - if self.display_messages_active { + if self.message_box.active { /* Check if any dirty area intersects with the area occupied by * floating notification box */ - //let (displ_top, displ_bot) = self.display_messages_area; - //for &((top_x, top_y), (bottom_x, bottom_y)) in &areas { - // self.display_messages_dirty |= !(bottom_y < displ_top.1 - // || displ_bot.1 < top_y - // || bottom_x < displ_top.0 - // || displ_bot.0 < top_x); - //} + let displ = self.message_box.cached_area(); + let (displ_top, displ_bot) = (displ.upper_left(), displ.bottom_right()); + let mut is_dirty = self.message_box.is_dirty(); + for a in &areas { + let (top_x, top_y) = a.upper_left(); + let (bottom_x, bottom_y) = a.bottom_right(); + is_dirty |= !(bottom_y < displ_top.1 + || displ_bot.1 < top_y + || bottom_x < displ_top.0 + || displ_bot.0 < top_x); + } + self.message_box.set_dirty(is_dirty); } /* draw each dirty area */ let rows = self.screen.area().height(); @@ -617,143 +606,52 @@ impl State { } } - if self.display_messages_dirty && self.display_messages_active { - //if let Some(DisplayMessage { - // ref timestamp, - // ref msg, - // .. - //}) = self.display_messages.get(self.display_messages_pos) - //{ - // if !self.display_messages_initialised { - // { - // /* Clear area previously occupied by floating - // * notification box */ - // //let displ_area = self.display_messages_area; - // //for y in get_y(displ_area.upper_left()).. - // // =get_y(displ_area.bottom_right()) { - // // (self.screen.tty().draw_fn())( - // // self.screen.grid_mut(), - // // self.screen.tty_mut().stdout_mut(), - // // get_x(displ_area.upper_left()), - // // get_x(displ_area.bottom_right()), - // // y, - // // ); - // //} - // } - // let noto_colors = crate::conf::value(&self.context, - // "status.notification"); use - // crate::melib::text_processing::{Reflow, TextProcessing}; - - // let msg_lines = - // msg.split_lines_reflow(Reflow::All, - // Some(self.screen.area().width() / 3)); let width = - // msg_lines .iter() - // .map(|line| line.grapheme_len() + 4) - // .max() - // .unwrap_or(0); - - // let displ_area = self.screen.area().place_inside( - // ( - // width, - // std::cmp::min(self.screen.area().height(), msg_lines.len() + - // 4), ), - // false, - // false, - // ); - // /* - // let box_displ_area = create_box(&mut self.screen.overlay_grid, - // displ_area); for row in - // self.screen.overlay_grid.bounds_iter(box_displ_area) { - // for c in row { - // self.screen.overlay_grid[c] - // .set_ch(' ') - // .set_fg(noto_colors.fg) - // .set_bg(noto_colors.bg) - // .set_attrs(noto_colors.attrs); - // } - // } - // let ((x, mut y), box_displ_area_bottom_right) = box_displ_area; - // for line in msg_lines - // .into_iter() - // .chain(Some(String::new())) - // .chain(Some(datetime::timestamp_to_string(*timestamp, None, - // false))) { - // self.screen.overlay_grid.write_string( - // &line, - // noto_colors.fg, - // noto_colors.bg, - // noto_colors.attrs, - // ((x, y), box_displ_area_bottom_right), - // Some(x), - // ); - // y += 1; - // } - - // if self.display_messages.len() > 1 { - // self.screen.overlay_grid.write_string( - // &if self.display_messages_pos == 0 { - // format!( - // "Next: {}", - // - // self.context.settings.shortcuts.general.info_message_next - // ) - // } else if self.display_messages_pos + 1 == - // self.display_messages.len() { format!( - // "Prev: {}", - // self.context - // .settings - // .shortcuts - // .general - // .info_message_previous - // ) - // } else { - // format!( - // "Prev: {} Next: {}", - // self.context - // .settings - // .shortcuts - // .general - // .info_message_previous, - // - // self.context.settings.shortcuts.general.info_message_next - // ) - // }, - // noto_colors.fg, - // noto_colors.bg, - // noto_colors.attrs, - // ((x, y), box_displ_area_bottom_right), - // Some(x), - // ); - // } - // self.display_messages_area = displ_area; - // */ - // } - // //for y in get_y(self.display_messages_area.upper_left()) - // // ..=get_y(self.display_messages_area.bottom_right()) - // //{ - // // (self.screen.tty().draw_fn())( - // // &mut self.screen.overlay_grid, - // // self.screen.tty_mut().stdout_mut(), - // // get_x(self.display_messages_area.upper_left()), - // // get_x(self.display_messages_area.bottom_right()), - // // y, - // // ); - // //} - //} - self.display_messages_dirty = false; - } else if self.display_messages_dirty { + if self.message_box.is_dirty() && self.message_box.active { + if !self.message_box.is_empty() { + if !self.message_box.initialised { + { + /* Clear area previously occupied by floating + * notification box */ + if self.message_box.cached_area().generation() + == self.screen.area().generation() + { + for row in self + .screen + .grid() + .bounds_iter(self.message_box.cached_area()) + { + self.screen + .draw(row.cols().start, row.cols().end, row.row_index()); + } + } + } + } + let area = self.screen.area(); + self.message_box + .draw(self.screen.overlay_grid_mut(), area, &mut self.context); + for row in self + .screen + .overlay_grid() + .bounds_iter(self.message_box.cached_area()) + { + self.screen + .draw_overlay(row.cols().start, row.cols().end, row.row_index()); + } + } + self.message_box.set_dirty(false); + } else if self.message_box.is_dirty() { /* Clear area previously occupied by floating notification box */ - //let displ_area = self.display_messages_area; - //for y in get_y(displ_area.upper_left())..=get_y(displ_area.bottom_right()) { - // (self.screen.tty().draw_fn())( - // self.screen.grid_mut(), - // self.screen.tty_mut().stdout_mut(), - // get_x(displ_area.upper_left()), - // get_x(displ_area.bottom_right()), - // y, - // ); - //} - self.display_messages_dirty = false; + if self.message_box.cached_area().generation() == self.screen.area().generation() { + for row in self + .screen + .grid() + .bounds_iter(self.message_box.cached_area()) + { + self.screen + .draw(row.cols().start, row.cols().end, row.row_index()); + } + } + self.message_box.set_dirty(false); } if !self.overlay.is_empty() { @@ -999,8 +897,8 @@ impl State { /// The application's main loop sends `UIEvents` to state via this method. pub fn rcv_event(&mut self, mut event: UIEvent) { if let UIEvent::Input(_) = event { - if self.display_messages_expiration_start.is_none() { - self.display_messages_expiration_start = Some(datetime::now()); + if self.message_box.expiration_start.is_none() { + self.message_box.expiration_start = Some(datetime::now()); } } @@ -1160,36 +1058,36 @@ impl State { .general .info_message_previous => { - self.display_messages_expiration_start = Some(datetime::now()); - self.display_messages_active = true; - self.display_messages_initialised = false; - self.display_messages_dirty = true; - self.display_messages_pos = self.display_messages_pos.saturating_sub(1); + self.message_box.expiration_start = Some(datetime::now()); + self.message_box.active = true; + self.message_box.initialised = false; + self.message_box.set_dirty(true); + self.message_box.pos = self.message_box.pos.saturating_sub(1); return; } UIEvent::Input(ref key) if *key == self.context.settings.shortcuts.general.info_message_next => { - self.display_messages_expiration_start = Some(datetime::now()); - self.display_messages_active = true; - self.display_messages_initialised = false; - self.display_messages_dirty = true; - self.display_messages_pos = std::cmp::min( - self.display_messages.len().saturating_sub(1), - self.display_messages_pos + 1, + self.message_box.expiration_start = Some(datetime::now()); + self.message_box.active = true; + self.message_box.initialised = false; + self.message_box.set_dirty(true); + self.message_box.pos = std::cmp::min( + self.message_box.len().saturating_sub(1), + self.message_box.pos + 1, ); return; } UIEvent::StatusEvent(StatusEvent::DisplayMessage(ref msg)) => { - self.display_messages.push(DisplayMessage { + self.message_box.push(DisplayMessage { timestamp: datetime::now(), msg: msg.clone(), }); - self.display_messages_active = true; - self.display_messages_initialised = false; - self.display_messages_dirty = true; - self.display_messages_expiration_start = None; - self.display_messages_pos = self.display_messages.len() - 1; + self.message_box.active = true; + self.message_box.initialised = false; + self.message_box.set_dirty(true); + self.message_box.expiration_start = None; + self.message_box.pos = self.message_box.len() - 1; self.redraw(); } UIEvent::FinishedUIDialog(ref id, ref mut results) if self.overlay.contains_key(id) => { diff --git a/meli/src/terminal/cells.rs b/meli/src/terminal/cells.rs index 13562556..2a9a0882 100644 --- a/meli/src/terminal/cells.rs +++ b/meli/src/terminal/cells.rs @@ -1386,6 +1386,11 @@ impl RowIterator { self.row } + #[inline] + pub fn cols(&self) -> std::ops::Range { + self.col.clone() + } + pub const fn empty(generation: ScreenGeneration) -> Self { Self { row: 0, diff --git a/meli/src/terminal/screen.rs b/meli/src/terminal/screen.rs index ad4cea2d..858efc7c 100644 --- a/meli/src/terminal/screen.rs +++ b/meli/src/terminal/screen.rs @@ -261,6 +261,20 @@ impl Screen { (self.display.draw_horizontal_segment_fn)(&mut self.grid, stdout, x_start, x_end, y); } + #[inline] + pub fn draw_overlay(&mut self, x_start: usize, x_end: usize, y: usize) { + let Some(stdout) = self.display.stdout.as_mut() else { + return; + }; + (self.display.draw_horizontal_segment_fn)( + &mut self.overlay_grid, + stdout, + x_start, + x_end, + y, + ); + } + /// On `SIGWNICH` the `State` redraws itself according to the new /// terminal size. pub fn update_size(&mut self) {