Re-add on-screen message display

Introduce new DisplayMessageBox struct that handles the rendering of
notifications/notices on the overlay screen.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
This commit is contained in:
Manos Pitsidianakis 2023-11-04 13:21:38 +02:00
parent 0e3a0c4b70
commit 84f3641ec1
No known key found for this signature in database
GPG Key ID: 7729C7707F7E09D0
4 changed files with 283 additions and 193 deletions

View File

@ -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<UnixTimestamp>,
pub active: bool,
dirty: bool,
pub initialised: bool,
pub pos: usize,
cached_area: Area,
id: ComponentId,
}
impl DisplayMessageBox {
pub fn new(sc: &Screen<Tty>) -> Box<Self> {
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
}
}

View File

@ -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<ComponentId, ComponentPath>,
pub context: Box<Context>,
timer: thread::JoinHandle<()>,
display_messages: SmallVec<[DisplayMessage; 8]>,
display_messages_expiration_start: Option<UnixTimestamp>,
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<DisplayMessageBox>,
}
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) => {

View File

@ -1386,6 +1386,11 @@ impl RowIterator {
self.row
}
#[inline]
pub fn cols(&self) -> std::ops::Range<usize> {
self.col.clone()
}
pub const fn empty(generation: ScreenGeneration) -> Self {
Self {
row: 0,

View File

@ -261,6 +261,20 @@ impl Screen<Tty> {
(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) {