use std::cell::Cell;
use std::cell::RefCell;
use std::cmp::max;
use std::cmp::min;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::collections::HashSet;
use std::io::BufWriter;
use std::io::Result;
use std::io::Write;
use std::ops::Add;
use std::ops::Sub;
use termion::color::Bg;
use termion::color::Fg;
use termion::cursor::Goto;
use termion::cursor::Hide;
use termion::cursor::Show;
use termion::terminal_size;
use gui::BBox;
use gui::Cap;
use gui::Id;
use gui::Object;
use gui::Renderable;
use gui::Renderer;
use gui::Widget as _;
use crate::colors::Color;
use crate::colors::Colors;
use crate::tasks::Task;
use crate::text;
use crate::text::Cursor;
use crate::text::DisplayWidth as _;
use crate::text::Width;
use crate::LINE_END;
use super::detail_dialog::DetailDialog;
use super::detail_dialog::DetailDialogData;
use super::event::Ids;
use super::in_out::InOut;
use super::in_out::InOutArea;
use super::tab_bar::TabBar;
use super::tag_dialog::SetUnsetTag;
use super::tag_dialog::TagDialog;
use super::task_list_box::TaskListBox;
use super::termui::TermUi;
const TASK_LIST_MARGIN_X: u16 = 3;
const TASK_LIST_MARGIN_Y: u16 = 2;
const TASK_SPACE: u16 = 2;
const TAG_SPACE: u16 = 2;
const TAB_TITLE_WIDTH: u16 = 30;
const DETAIL_DIALOG_MIN_W: u16 = 40;
const DETAIL_DIALOG_MIN_H: u16 = 20;
const DETAIL_DIALOG_MARGIN_X: u16 = 2;
const DETAIL_DIALOG_MARGIN_Y: u16 = 1;
const TAG_DIALOG_MARGIN_X: u16 = 2;
const TAG_DIALOG_MARGIN_Y: u16 = 1;
const TAG_DIALOG_MIN_W: u16 = 40;
const TAG_DIALOG_MIN_H: u16 = 20;
const SAVED_TEXT: &str = " Saved ";
const SEARCH_TEXT: &str = " Search ";
const ERROR_TEXT: &str = " Error ";
const INPUT_TEXT: &str = " > ";
fn window_start<T, U>(start: T, size: U, cursor: T) -> T
where
T: Copy + Ord + Add<U, Output = T> + Sub<U, Output = T>,
U: Copy + From<usize>,
{
if cursor <= start {
cursor
} else if cursor >= start + size {
cursor - size + U::from(1)
} else {
start
}
}
fn align_center(string: impl Into<String>, width: usize) -> String {
let mut string = string.into();
let length = string.len();
match length.cmp(&width) {
Ordering::Greater => {
string.replace_range(width - 3..length, "...");
},
Ordering::Less => {
let pad_right = (width - length) / 2;
let pad_left = width - length - pad_right;
let pad_right = " ".repeat(pad_right);
let pad_left = " ".repeat(pad_left);
string.insert_str(0, &pad_right);
string.push_str(&pad_left);
},
Ordering::Equal => (),
}
string
}
fn clip(x: u16, y: u16, string: &str, bbox: BBox) -> &str {
let w = bbox.w;
let h = bbox.h;
if y < h {
text::clip(string, Width::from(usize::from(w.saturating_sub(x))))
} else {
""
}
}
struct ClippingWriter<W>
where
W: Write,
{
writer: RefCell<W>,
bbox: Cell<BBox>,
terminal_size: Cell<Option<(u16, u16)>>,
}
impl<W> ClippingWriter<W>
where
W: Write,
{
fn new(writer: W) -> Self {
Self {
writer: RefCell::new(writer),
bbox: Default::default(),
terminal_size: Default::default(),
}
}
fn restrict(&self, bbox: BBox) {
self.bbox.set(bbox)
}
fn current_bbox(&self) -> BBox {
let bbox = self.bbox.get();
let tsize = self.terminal_size.get().unwrap();
BBox {
x: bbox.x,
y: bbox.y,
w: min(bbox.w, tsize.0.saturating_sub(bbox.x)),
h: min(bbox.h, tsize.1.saturating_sub(bbox.y)),
}
}
fn write<S>(&self, x: u16, y: u16, fg: Color, bg: Color, string: S) -> Result<()>
where
S: AsRef<str>,
{
let string = clip(x, y, string.as_ref(), self.current_bbox());
if !string.is_empty() {
let x = self.bbox.get().x + x + 1;
let y = self.bbox.get().y + y + 1;
write!(
self.writer.borrow_mut(),
"{}{}{}{}",
Goto(x, y),
Fg(fg.as_term_color()),
Bg(bg.as_term_color()),
string,
)?
}
Ok(())
}
fn fill_line(&self, x: u16, y: u16, w: u16, color: Color) -> Result<()> {
static SPACES: &str = concat!(
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
);
let bbox = self.current_bbox();
let w = min(bbox.w.saturating_sub(x), w);
let x = bbox.x + x + 1;
let y = bbox.y + y + 1;
let mut x = x;
let mut w = w;
while w > 0 {
let cells = min(SPACES.len(), w.into());
let s = SPACES.get(..cells).unwrap();
let () = write!(
self.writer.borrow_mut(),
"{}{}{}{}",
Goto(x, y),
Fg(color.as_term_color()),
Bg(color.as_term_color()),
s,
)?;
x += cells as u16;
w -= cells as u16;
}
Ok(())
}
fn goto(&self, x: u16, y: u16) -> Result<()> {
let x = self.bbox.get().x + x + 1;
let y = self.bbox.get().y + y + 1;
write!(self.writer.borrow_mut(), "{}", Goto(x, y))
}
fn flush(&self) -> Result<()> {
self.writer.borrow_mut().flush()
}
fn hide(&self) -> Result<()> {
write!(self.writer.borrow_mut(), "{Hide}")
}
fn show(&self) -> Result<()> {
write!(self.writer.borrow_mut(), "{Show}")
}
}
#[derive(Default)]
struct OffsetData {
pub offset: usize,
}
fn displayable_tasks(bbox: BBox) -> usize {
((bbox.h.saturating_sub(TASK_LIST_MARGIN_Y)) / TASK_SPACE) as usize
}
fn displayable_tags(bbox: BBox) -> usize {
((bbox.h.saturating_sub(2 * TAG_DIALOG_MARGIN_Y)) / TAG_SPACE) as usize
}
fn displayable_tabs(width: u16) -> usize {
(width / TAB_TITLE_WIDTH) as usize
}
pub struct TermRenderer<W>
where
W: Write,
{
writer: ClippingWriter<BufWriter<W>>,
data: RefCell<HashMap<Id, OffsetData>>,
colors: Colors,
to_render: Option<HashSet<Id>>,
rendering: Cell<Option<Id>>,
}
impl<W> TermRenderer<W>
where
W: Write,
{
pub fn new(writer: W, colors: Colors) -> Result<Self> {
let writer = ClippingWriter::new(BufWriter::new(writer));
Ok(TermRenderer {
writer,
data: Default::default(),
colors,
to_render: None,
rendering: Cell::new(None),
})
}
pub(crate) fn set_ids(&mut self, ids: Option<Ids>) {
self.to_render = ids.map(HashSet::from);
}
fn query_terminal_size() -> (u16, u16) {
match terminal_size() {
Ok(size) => size,
Err(err) => panic!("Retrieving terminal size failed: {err}"),
}
}
fn render_term_ui(&self, _ui: &TermUi, _bbox: BBox) -> Result<()> {
Ok(())
}
fn render_tab_bar(&self, tab_bar: &TabBar, cap: &dyn Cap, bbox: BBox) -> Result<()> {
let mut map = self.data.borrow_mut();
let data = map.entry(tab_bar.id()).or_default();
let mut x = 1;
let w = bbox.w.saturating_sub(1);
let tabs = tab_bar.iter(cap).len();
let count = displayable_tabs(w.saturating_sub(1));
let selection = tab_bar.selection(cap);
let offset = window_start(data.offset, count, selection);
if offset > 0 {
let fg = self.colors.more_tasks_fg;
let bg = self.colors.more_tasks_bg;
self.writer.write(0, 0, fg, bg, "<")?;
} else {
let fg = self.colors.unselected_tab_fg;
let bg = self.colors.unselected_tab_bg;
self.writer.write(0, 0, fg, bg, " ")?;
}
if tabs > offset + count {
let fg = self.colors.more_tasks_fg;
let bg = self.colors.more_tasks_bg;
self.writer.write(w, 0, fg, bg, ">")?;
} else {
let fg = self.colors.unselected_tab_fg;
let bg = self.colors.unselected_tab_bg;
self.writer.write(w, 0, fg, bg, " ")?;
}
for (i, tab) in tab_bar.iter(cap).enumerate().skip(offset).take(count) {
let (fg, bg) = if i == selection {
(self.colors.selected_tab_fg, self.colors.selected_tab_bg)
} else {
(self.colors.unselected_tab_fg, self.colors.unselected_tab_bg)
};
let title = align_center(tab.clone(), TAB_TITLE_WIDTH as usize - 4);
let padded = format!(" {title} ");
self.writer.write(x, 0, fg, bg, padded)?;
x += TAB_TITLE_WIDTH;
}
if x < w {
let pad = " ".repeat((w - x) as usize);
let fg = self.colors.unselected_tab_fg;
let bg = self.colors.unselected_tab_bg;
self.writer.write(x, 0, fg, bg, pad)?
}
data.offset = offset;
Ok(())
}
fn render_task_list_line(
&self,
task: &Task,
tagged: bool,
selected: bool,
y: u16,
w: u16,
) -> Result<()> {
let (state, state_fg, state_bg) = if !tagged {
(
"[ ]",
self.colors.task_not_started_fg,
self.colors.task_not_started_bg,
)
} else {
("[X]", self.colors.task_done_fg, self.colors.task_done_bg)
};
let (task_fg, task_bg) = if selected {
(self.colors.selected_task_fg, self.colors.selected_task_bg)
} else {
(
self.colors.unselected_task_fg,
self.colors.unselected_task_bg,
)
};
let mut x = 0;
let () = self
.writer
.fill_line(0, y, TASK_LIST_MARGIN_X, self.colors.unselected_task_bg)?;
x += TASK_LIST_MARGIN_X;
self.writer.write(x, y, state_fg, state_bg, state)?;
x += state.len() as u16;
let details = if task.details().is_empty() {
" "
} else {
" * "
};
self.writer.write(
x,
y,
self.colors.unselected_task_fg,
self.colors.unselected_task_bg,
details,
)?;
x += details.len() as u16;
self.writer.write(x, y, task_fg, task_bg, task.summary())?;
x += task.summary().display_width().as_usize() as u16;
let () = self
.writer
.fill_line(x, y, w, self.colors.unselected_task_bg)?;
Ok(())
}
fn render_task_list_box(&self, task_list: &TaskListBox, cap: &dyn Cap, bbox: BBox) -> Result<()> {
let mut map = self.data.borrow_mut();
let data = map.entry(task_list.id()).or_default();
let mut cursor = None;
let view = task_list.view(cap);
let count = displayable_tasks(bbox);
let selection = task_list.selection(cap);
let offset = window_start(data.offset, count, selection);
let () = view.iter(|iter| {
let mut tasks = iter.enumerate().skip(offset).take(count);
(0..bbox.h).try_for_each(|y| {
if y < TASK_LIST_MARGIN_Y
|| y > bbox.h - TASK_LIST_MARGIN_Y
|| (y - TASK_LIST_MARGIN_Y) % TAG_SPACE != 0
{
self
.writer
.fill_line(0, y, bbox.w, self.colors.unselected_task_bg)
} else if let Some((i, task)) = tasks.next() {
let tagged = task_list
.toggle_tag(cap)
.map(|toggle_tag| task.has_tag(&toggle_tag))
.unwrap_or(false);
let () = self.render_task_list_line(task, tagged, i == selection, y, bbox.w)?;
if i == selection && cap.is_focused(task_list.id()) {
cursor = Some((TASK_LIST_MARGIN_X + 6, y));
}
Ok(())
} else {
self
.writer
.fill_line(0, y, bbox.w, self.colors.unselected_task_bg)
}
})
})?;
if let Some((x, y)) = cursor {
self.writer.goto(x, y)?;
}
data.offset = offset;
Ok(())
}
fn render_detail_dialog_line<'s>(
&self,
line: Option<&'s str>,
y: u16,
w: u16,
) -> Result<(Width, Option<&'s str>)> {
let fg = self.colors.detail_dialog_fg;
let bg = self.colors.detail_dialog_bg;
let mut x = 0;
let () = self
.writer
.fill_line(x, y, DETAIL_DIALOG_MARGIN_X, self.colors.detail_dialog_bg)?;
x += DETAIL_DIALOG_MARGIN_X;
let (width, rest) = if let Some(line) = line {
let (line, rest) = text::wrap(
line,
Width::from(usize::from(w.saturating_sub(2 * DETAIL_DIALOG_MARGIN_X))),
);
let width = line.display_width();
let () = self.writer.write(x, y, fg, bg, line)?;
(width, rest)
} else {
(Width::from(0), None)
};
x += width.as_usize() as u16;
let () = self
.writer
.fill_line(x, y, w, self.colors.detail_dialog_bg)?;
Ok((width, rest))
}
fn render_detail_dialog(
&self,
detail_dialog: &DetailDialog,
cap: &dyn Cap,
bbox: BBox,
) -> Result<()> {
let data = detail_dialog.data::<DetailDialogData>(cap);
let details = data.details();
let mut lines = details.as_str().split(LINE_END);
let mut line = None;
let mut selection = details.cursor();
let mut cursor = None;
(0..bbox.h).try_for_each(|y| {
if y < DETAIL_DIALOG_MARGIN_Y || y >= bbox.h - DETAIL_DIALOG_MARGIN_Y {
self
.writer
.fill_line(0, y, bbox.w, self.colors.detail_dialog_bg)
} else {
let (rendered, rest) =
self.render_detail_dialog_line(line.or_else(|| lines.next()), y, bbox.w)?;
if cursor.is_none() {
if Cursor::at_start() + rendered >= selection {
cursor = Some((
selection + Width::from(usize::from(DETAIL_DIALOG_MARGIN_X)),
y,
));
} else {
selection -= rendered;
}
if rest.is_none() {
selection -= LINE_END.display_width();
}
}
line = rest;
Ok(())
}
})?;
if let Some((x, y)) = cursor {
let () = self.writer.goto(x.as_usize() as _, y)?;
let () = self.writer.show()?;
} else if cfg!(debug_assertions) {
panic!("no cursor set")
}
Ok(())
}
fn render_tag_dialog_tag_line(
&self,
tag: &SetUnsetTag,
y: u16,
w: u16,
selected: bool,
) -> Result<()> {
let set = tag.is_set();
let (state, state_fg, state_bg) = if set {
(
"[X]",
self.colors.tag_dialog_tag_set_fg,
self.colors.tag_dialog_tag_set_bg,
)
} else {
(
"[ ]",
self.colors.tag_dialog_tag_unset_fg,
self.colors.tag_dialog_tag_unset_bg,
)
};
let (tag_fg, tag_bg) = if selected {
(
self.colors.tag_dialog_selected_tag_fg,
self.colors.tag_dialog_selected_tag_bg,
)
} else {
(self.colors.tag_dialog_fg, self.colors.tag_dialog_bg)
};
let mut x = 0;
let () = self
.writer
.fill_line(x, y, TAG_DIALOG_MARGIN_X, self.colors.tag_dialog_bg)?;
x += TAG_DIALOG_MARGIN_X;
let () = self.writer.write(x, y, state_fg, state_bg, state)?;
x += state.len() as u16;
let () = self.writer.fill_line(x, y, 1, self.colors.tag_dialog_bg)?;
x += 1;
let () = self.writer.write(x, y, tag_fg, tag_bg, tag.name())?;
let () = self
.writer
.fill_line(x + tag.name().len() as u16, y, w, self.colors.tag_dialog_bg)?;
Ok(())
}
fn render_tag_dialog(&self, tag_dialog: &TagDialog, cap: &dyn Cap, bbox: BBox) -> Result<()> {
let mut map = self.data.borrow_mut();
let data = map.entry(tag_dialog.id()).or_default();
let count = displayable_tags(bbox);
let selection = tag_dialog.selection(cap);
let offset = window_start(data.offset, count, selection);
let mut tags = tag_dialog.tags(cap).iter().enumerate().skip(offset);
(0..bbox.h).try_for_each(|y| {
if y < TAG_DIALOG_MARGIN_Y
|| y >= bbox.h - TAG_DIALOG_MARGIN_Y
|| (y - TAG_DIALOG_MARGIN_Y) % TAG_SPACE != 0
{
self
.writer
.fill_line(0, y, bbox.w, self.colors.tag_dialog_bg)
} else if let Some((i, tag)) = tags.next() {
self.render_tag_dialog_tag_line(tag, y, bbox.w, i == selection)
} else {
self
.writer
.fill_line(0, y, bbox.w, self.colors.tag_dialog_bg)
}
})?;
if cap.is_focused(tag_dialog.id()) {
let x = TAG_DIALOG_MARGIN_X + 4;
let y = TAG_DIALOG_MARGIN_Y + (selection as u16 * TAG_SPACE);
self.writer.goto(x, y)?;
}
data.offset = offset;
Ok(())
}
fn render_input_output(&self, in_out: &InOutArea, cap: &dyn Cap, bbox: BBox) -> Result<()> {
let mut x = 0;
let y = bbox.h - 1;
let (prefix, fg, bg, string) = match in_out.state(cap) {
InOut::Saved => (
SAVED_TEXT,
self.colors.in_out_success_fg,
self.colors.in_out_success_bg,
None,
),
InOut::Search(ref s) => (
SEARCH_TEXT,
self.colors.in_out_status_fg,
self.colors.in_out_status_bg,
Some(s.as_ref()),
),
InOut::Error(ref e) => (
ERROR_TEXT,
self.colors.in_out_error_fg,
self.colors.in_out_error_bg,
Some(e.as_ref()),
),
InOut::Input(ref text) => (
INPUT_TEXT,
self.colors.in_out_success_fg,
self.colors.in_out_success_bg,
Some(text.as_str()),
),
InOut::Clear => {
let _ = self.data.borrow_mut().remove(&in_out.id());
let () = self
.writer
.fill_line(x, y, bbox.w, self.colors.in_out_string_bg)?;
return Ok(())
},
};
let () = self.writer.write(x, y, fg, bg, prefix)?;
x += prefix.len() as u16;
let () = self
.writer
.fill_line(x, y, 1, self.colors.in_out_string_bg)?;
x += 1;
let cursor = if let Some(string) = string {
let fg = self.colors.in_out_string_fg;
let bg = self.colors.in_out_string_bg;
if let InOut::Input(text) = in_out.state(cap) {
debug_assert!(cap.is_focused(in_out.id()));
let mut map = self.data.borrow_mut();
let data = map.entry(in_out.id()).or_default();
let count = Width::from(bbox.w.saturating_sub(x) as usize);
let cursor = text.cursor();
let offset = window_start(
text.cursor_start() + Width::from(data.offset),
count,
cursor,
);
let string = text.substr(offset..);
data.offset = offset.as_usize();
let () = self.writer.write(x, y, fg, bg, string)?;
let cursor = x + cursor.as_usize() as u16 - offset.as_usize() as u16;
x += string.display_width().as_usize() as u16;
Some(cursor)
} else {
let () = self.writer.write(x, y, fg, bg, string)?;
x += string.display_width().as_usize() as u16;
None
}
} else {
None
};
let () = self
.writer
.fill_line(x, y, bbox.w, self.colors.in_out_string_bg)?;
if let Some(x) = cursor {
let () = self.writer.goto(x, y)?;
let () = self.writer.show()?;
}
Ok(())
}
fn render_widget(
&self,
widget: &dyn Renderable,
cap: &dyn Cap,
bbox: BBox,
render: bool,
) -> Result<BBox> {
let () = self.writer.restrict(bbox);
if let Some(ui) = widget.downcast_ref::<TermUi>() {
if render {
let () = self.render_term_ui(ui, bbox)?;
}
Ok(bbox)
} else if let Some(detail_dialog) = widget.downcast_ref::<DetailDialog>() {
let w = max(DETAIL_DIALOG_MIN_W, bbox.w / 2);
let h = max(DETAIL_DIALOG_MIN_H, bbox.h / 2);
let x = w / 2;
let y = h / 2;
let bbox = BBox { x, y, w, h };
if render {
let () = self.writer.restrict(bbox);
let () = self.render_detail_dialog(detail_dialog, cap, bbox)?;
}
Ok(bbox)
} else if let Some(tag_dialog) = widget.downcast_ref::<TagDialog>() {
let w = max(TAG_DIALOG_MIN_W, bbox.w / 2);
let h = max(TAG_DIALOG_MIN_H, bbox.h / 2);
let x = w / 2;
let y = h / 2;
let bbox = BBox { x, y, w, h };
if render {
let () = self.writer.restrict(bbox);
let () = self.render_tag_dialog(tag_dialog, cap, bbox)?;
}
Ok(bbox)
} else if let Some(in_out) = widget.downcast_ref::<InOutArea>() {
if render {
let () = self.render_input_output(in_out, cap, bbox)?;
}
Ok(bbox)
} else if let Some(tab_bar) = widget.downcast_ref::<TabBar>() {
if render {
let () = self.render_tab_bar(tab_bar, cap, bbox)?;
}
let bbox = BBox {
y: bbox.y.saturating_add(1),
h: bbox.h.saturating_sub(2),
..bbox
};
Ok(bbox)
} else if let Some(task_list) = widget.downcast_ref::<TaskListBox>() {
if render {
let () = self.render_task_list_box(task_list, cap, bbox)?;
}
Ok(bbox)
} else {
panic!("Widget {widget:?} is unknown to the renderer")
}
}
fn widget_id(widget: &dyn Renderable) -> Id {
if let Some(ui) = widget.downcast_ref::<TermUi>() {
ui.id()
} else if let Some(detail_dialog) = widget.downcast_ref::<DetailDialog>() {
detail_dialog.id()
} else if let Some(tag_dialog) = widget.downcast_ref::<TagDialog>() {
tag_dialog.id()
} else if let Some(in_out) = widget.downcast_ref::<InOutArea>() {
in_out.id()
} else if let Some(tab_bar) = widget.downcast_ref::<TabBar>() {
tab_bar.id()
} else if let Some(task_list) = widget.downcast_ref::<TaskListBox>() {
task_list.id()
} else {
panic!("Widget {widget:?} is unknown to the renderer")
}
}
}
impl<W> Renderer for TermRenderer<W>
where
W: Write,
{
fn renderable_area(&self) -> BBox {
let tsize = self.writer.terminal_size.get().unwrap();
BBox {
x: 0,
y: 0,
w: tsize.0,
h: tsize.1,
}
}
fn pre_render(&self) {
let () = self
.writer
.terminal_size
.set(Some(Self::query_terminal_size()));
let result = self.writer.hide();
if let Err(err) = result {
panic!("Pre-render failed: {err}");
}
}
fn render(&self, widget: &dyn Renderable, cap: &dyn Cap, bbox: BBox) -> BBox {
let render = if let Some(to_render) = &self.to_render {
if self.rendering.get().is_none() {
let id = Self::widget_id(widget);
if to_render.contains(&id) {
let () = self.rendering.set(Some(id));
true
} else {
false
}
} else {
true
}
} else {
true
};
let result = self.render_widget(widget, cap, bbox, render);
match result {
Ok(b) => b,
Err(err) => panic!("Rendering failed: {err}"),
}
}
fn render_done(&self, widget: &dyn Renderable, _cap: &dyn Cap, _bbox: BBox) {
if let Some(rendering) = self.rendering.get() {
let id = Self::widget_id(widget);
if rendering == id {
self.rendering.set(None)
}
}
}
fn post_render(&self) {
let result = self.writer.flush();
if let Err(err) = result {
panic!("Post-render failed: {err}");
}
let () = self.writer.terminal_size.set(None);
}
}
impl<W> Drop for TermRenderer<W>
where
W: Write,
{
fn drop(&mut self) {
let _result = self.writer.show();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "nightly")]
use unstable_test::Bencher;
#[test]
fn align_string() {
assert_eq!(align_center("", 0), "");
assert_eq!(align_center("", 1), " ");
assert_eq!(align_center("", 8), " ");
assert_eq!(align_center("a", 1), "a");
assert_eq!(align_center("a", 2), "a ");
assert_eq!(align_center("a", 3), " a ");
assert_eq!(align_center("a", 4), " a ");
assert_eq!(align_center("hello", 20), " hello ");
}
#[test]
fn crop_string() {
assert_eq!(align_center("hello", 4), "h...");
assert_eq!(align_center("hello", 5), "hello");
assert_eq!(align_center("that's a test", 8), "that'...");
}
#[test]
fn clip_string() {
let bbox = BBox {
x: 0,
y: 0,
w: 0,
h: 0,
};
assert_eq!(clip(0, 0, "hello", bbox), "");
assert_eq!(clip(1, 0, "foobar", bbox), "");
assert_eq!(clip(1, 1, "baz?", bbox), "");
let bbox = BBox {
x: 10,
y: 16,
w: 6,
h: 1,
};
assert_eq!(clip(0, 0, "hello", bbox), "hello");
assert_eq!(clip(0, 0, "hello you", bbox), "hello ");
for x in 0..5 {
for y in 0..3 {
let bbox = BBox { x, y, w: 5, h: 3 };
assert_eq!(clip(0, 2, "inside", bbox), "insid");
assert_eq!(clip(0, 3, "outside", bbox), "");
assert_eq!(clip(1, 2, "inside", bbox), "insi");
assert_eq!(clip(2, 0, "inside", bbox), "ins");
assert_eq!(clip(2, 3, "outside", bbox), "");
}
}
}
#[cfg(feature = "nightly")]
#[ignore = "test requires fully functional TTY"]
#[bench]
fn bench_ui_rendering(b: &mut Bencher) {
use std::ffi::OsString;
use std::io::stdout;
use gui::Ui;
use tempfile::TempDir;
use termion::raw::IntoRawMode as _;
use termion::screen::IntoAlternateScreen as _;
use tokio::runtime::Builder;
use crate::test::default_tasks_and_tags;
use crate::ui::Renderer as TermUiRenderer;
use crate::ui::UiData as TermUiData;
use crate::DirCap;
use crate::TaskState;
use crate::UiConfig;
use crate::UiState;
let rt = Builder::new_current_thread().build().unwrap();
let () = rt.block_on(async {
let (ui_config, task_state) = default_tasks_and_tags();
let task_state = TaskState::with_serde(task_state).unwrap();
let tasks_dir = TempDir::new().unwrap();
let mut tasks_root_cap = DirCap::for_dir(tasks_dir.path().to_path_buf())
.await
.unwrap();
let () = task_state.save(&mut tasks_root_cap).await.unwrap();
let ui_config = UiConfig::with_serde(ui_config, &task_state).unwrap();
let ui_config_dir = TempDir::new().unwrap();
let ui_config_file_name = OsString::from("notnow.json");
let ui_config_path = (
ui_config_dir.path().to_path_buf(),
ui_config_file_name.clone(),
);
let mut ui_config_dir_cap = DirCap::for_dir(ui_config_dir.path().to_path_buf())
.await
.unwrap();
let ui_config_dir_write_guard = ui_config_dir_cap.write().await.unwrap();
let mut ui_config_file_cap = ui_config_dir_write_guard.file_cap(&ui_config_file_name);
let () = ui_config.save(&mut ui_config_file_cap).await.unwrap();
let ui_state_dir = TempDir::new().unwrap();
let ui_state_file_name = OsString::from("ui-state.json");
let ui_state_path = (ui_state_dir.path().to_path_buf(), ui_state_file_name);
let tasks_root = tasks_dir.path().to_path_buf();
let task_state = TaskState::load(&tasks_root).await.unwrap();
let ui_config_file = ui_config_path.0.join(&ui_config_path.1);
let ui_state_file = ui_state_path.0.join(&ui_state_path.1);
let ui_config = UiConfig::load(&ui_config_file, &task_state).await.unwrap();
let UiConfig {
colors,
toggle_tag,
views,
} = ui_config;
let ui_state = UiState::load(&ui_state_file).await.unwrap();
let ui_config_dir_cap = DirCap::for_dir(ui_config_path.0).await.unwrap();
let ui_config_file = ui_config_path.1;
let ui_state_dir_cap = DirCap::for_dir(ui_state_path.0).await.unwrap();
let ui_state_file = ui_state_path.1;
let tasks_root_cap = DirCap::for_dir(tasks_root).await.unwrap();
let (ui, _) = Ui::new(
|| {
Box::new(TermUiData::new(
tasks_root_cap,
task_state,
(ui_config_dir_cap, ui_config_file),
(ui_state_dir_cap, ui_state_file),
colors,
toggle_tag,
))
},
|id, cap| Box::new(TermUi::new(id, cap, views, ui_state)),
);
let screen = stdout()
.lock()
.into_raw_mode()
.unwrap()
.into_alternate_screen()
.unwrap();
let renderer = TermUiRenderer::new(screen, colors).unwrap();
let () = b.iter(|| {
let () = ui.render(&renderer);
});
});
}
}