diff --git a/cove-tui/src/chat/tree.rs b/cove-tui/src/chat/tree.rs index d3a53fa..ed753cf 100644 --- a/cove-tui/src/chat/tree.rs +++ b/cove-tui/src/chat/tree.rs @@ -4,14 +4,18 @@ mod layout; use std::marker::PhantomData; +use chrono::{DateTime, Utc}; use crossterm::event::{KeyCode, KeyEvent}; use crossterm::style::{ContentStyle, Stylize}; use toss::frame::{Frame, Pos, Size}; use crate::store::{Msg, MsgStore}; -use self::blocks::{BlockContent, Blocks}; -use self::constants::{INDENT, INDENT_WIDTH, TIME_WIDTH}; +use self::blocks::{BlockBody, Blocks}; +use self::constants::{ + after_indent, style_indent, style_indent_inverted, style_placeholder, style_time, + style_time_inverted, INDENT, INDENT_WIDTH, PLACEHOLDER, TIME_EMPTY, TIME_FORMAT, TIME_WIDTH, +}; use super::Cursor; @@ -29,23 +33,42 @@ impl TreeView { } } - fn render_indentation(&mut self, frame: &mut Frame, pos: Pos, indent: usize, cursor: bool) { + fn render_time(frame: &mut Frame, x: i32, y: i32, time: Option>, cursor: bool) { + let pos = Pos::new(x, y); + + let style = if cursor { + style_time_inverted() + } else { + style_time() + }; + + if let Some(time) = time { + let time = format!("{}", time.format(TIME_FORMAT)); + frame.write(pos, &time, style); + } else { + frame.write(pos, TIME_EMPTY, style); + } + } + + fn render_indent(frame: &mut Frame, x: i32, y: i32, indent: usize, cursor: bool) { for i in 0..indent { - let x = TIME_WIDTH + INDENT_WIDTH * i; - let pos = Pos::new(pos.x + x as i32, pos.y); + let pos = Pos::new(x + after_indent(i), y); + let style = if cursor { - ContentStyle::default().black().on_white() + style_indent_inverted() } else { - ContentStyle::default() + style_indent() }; + frame.write(pos, INDENT, style); } } fn render_layout(&mut self, frame: &mut Frame, pos: Pos, size: Size, layout: &Blocks) { for block in &layout.blocks { - match &block.content { - BlockContent::Msg(msg) => { + // Draw rest of block + match &block.body { + BlockBody::Msg(msg) => { let nick_width = frame.width(&msg.nick) as i32; for (i, line) in msg.lines.iter().enumerate() { let y = pos.y + block.line + i as i32; @@ -53,29 +76,26 @@ impl TreeView { continue; } - self.render_indentation( - frame, - Pos::new(pos.x, y), - block.indent, - block.cursor, - ); + Self::render_indent(frame, pos.x, y, block.indent, block.cursor); let after_indent = pos.x + (TIME_WIDTH + INDENT_WIDTH * block.indent) as i32; if i == 0 { - let time = format!("{}", msg.time.format("%H:%M")); - frame.write(Pos::new(pos.x, y), &time, ContentStyle::default()); + Self::render_time(frame, pos.x, y, block.time, block.cursor); let nick = format!("[{}]", msg.nick); frame.write(Pos::new(after_indent, y), &nick, ContentStyle::default()); + } else { + Self::render_time(frame, pos.x, y, None, block.cursor); } let msg_x = after_indent + 1 + nick_width + 2; frame.write(Pos::new(msg_x, y), line, ContentStyle::default()); } } - BlockContent::Placeholder => { - self.render_indentation(frame, pos, block.indent, block.cursor); - let x = pos.x + (TIME_WIDTH + INDENT_WIDTH * block.indent) as i32; + BlockBody::Placeholder => { let y = pos.y + block.line; - frame.write(Pos::new(x, y), "[...]", ContentStyle::default()); + Self::render_time(frame, pos.x, y, block.time, block.cursor); + Self::render_indent(frame, pos.x, y, block.indent, block.cursor); + let pos = Pos::new(pos.x + after_indent(block.indent), y); + frame.write(pos, PLACEHOLDER, style_placeholder()); } } } diff --git a/cove-tui/src/chat/tree/blocks.rs b/cove-tui/src/chat/tree/blocks.rs index bd8ef1c..7cd46fd 100644 --- a/cove-tui/src/chat/tree/blocks.rs +++ b/cove-tui/src/chat/tree/blocks.rs @@ -5,50 +5,61 @@ use chrono::{DateTime, Utc}; use crate::chat::Cursor; pub struct Block { + pub id: I, pub line: i32, pub height: i32, - pub id: I, - pub indent: usize, pub cursor: bool, - pub content: BlockContent, + pub time: Option>, + pub indent: usize, + pub body: BlockBody, } impl Block { - pub fn placeholder(id: I, indent: usize) -> Self { + pub fn msg( + id: I, + indent: usize, + time: DateTime, + nick: String, + lines: Vec, + ) -> Self { Self { - line: 0, - height: 1, id, + line: 0, + height: lines.len() as i32, indent, + time: Some(time), cursor: false, - content: BlockContent::Placeholder, + body: BlockBody::Msg(MsgBlock { nick, lines }), } } + + pub fn placeholder(id: I, indent: usize) -> Self { + Self { + id, + line: 0, + height: 1, + indent, + time: None, + cursor: false, + body: BlockBody::Placeholder, + } + } + + pub fn time(mut self, time: DateTime) -> Self { + self.time = Some(time); + self + } } -pub enum BlockContent { +pub enum BlockBody { Msg(MsgBlock), Placeholder, } pub struct MsgBlock { - pub time: DateTime, pub nick: String, pub lines: Vec, } -impl MsgBlock { - pub fn into_block(self, id: I, indent: usize) -> Block { - Block { - line: 0, - height: self.lines.len() as i32, - id, - indent, - cursor: false, - content: BlockContent::Msg(self), - } - } -} - /// Pre-layouted messages as a sequence of blocks. /// /// These blocks are straightforward to render, but also provide a level of diff --git a/cove-tui/src/chat/tree/constants.rs b/cove-tui/src/chat/tree/constants.rs index d59df33..c028fcb 100644 --- a/cove-tui/src/chat/tree/constants.rs +++ b/cove-tui/src/chat/tree/constants.rs @@ -1,6 +1,8 @@ use crossterm::style::{ContentStyle, Stylize}; +use toss::frame::Frame; pub const TIME_FORMAT: &str = "%H:%M "; +pub const TIME_EMPTY: &str = " "; pub const TIME_WIDTH: usize = 6; pub fn style_time() -> ContentStyle { @@ -15,9 +17,26 @@ pub const INDENT: &str = "│ "; pub const INDENT_WIDTH: usize = 2; pub fn style_indent() -> ContentStyle { - ContentStyle::default().grey() + ContentStyle::default().dark_grey() } pub fn style_indent_inverted() -> ContentStyle { ContentStyle::default().black().on_white() } + +pub const PLACEHOLDER: &str = "[...]"; + +pub fn style_placeholder() -> ContentStyle { + ContentStyle::default().dark_grey() +} + +// Something like this should fit: [+, 1234 more] +pub const MIN_CONTENT_WIDTH: usize = 14; + +pub fn after_indent(indent: usize) -> i32 { + (TIME_WIDTH + indent * INDENT_WIDTH) as i32 +} + +pub fn after_nick(frame: &mut Frame, indent: usize, nick: &str) -> i32 { + after_indent(indent) + 1 + frame.width(nick) as i32 + 2 +} diff --git a/cove-tui/src/chat/tree/layout.rs b/cove-tui/src/chat/tree/layout.rs index d1a9771..aaa0d4c 100644 --- a/cove-tui/src/chat/tree/layout.rs +++ b/cove-tui/src/chat/tree/layout.rs @@ -3,32 +3,30 @@ use toss::frame::{Frame, Size}; use crate::chat::Cursor; use crate::store::{Msg, MsgStore, Tree}; -use super::blocks::{Block, Blocks, MsgBlock}; -use super::constants::{INDENT_WIDTH, TIME_WIDTH}; +use super::blocks::{Block, Blocks}; +use super::constants::{self, MIN_CONTENT_WIDTH}; use super::TreeView; impl TreeView { fn msg_to_block( &mut self, - msg: &M, - indent: usize, frame: &mut Frame, size: Size, + msg: &M, + indent: usize, ) -> Block { let nick = msg.nick(); let content = msg.content(); - let used_width = TIME_WIDTH + INDENT_WIDTH * indent + 1 + frame.width(&nick) + 2; - let rest_width = size.width as usize - used_width; - - let lines = toss::split_at_indices(&content, &frame.wrap(&content, rest_width)); - let lines = lines.into_iter().map(|s| s.to_string()).collect::>(); - MsgBlock { - time: msg.time(), - nick, - lines, + let content_width = size.width as i32 - constants::after_nick(frame, indent, &nick); + if content_width < MIN_CONTENT_WIDTH as i32 { + Block::placeholder(msg.id(), indent).time(msg.time()) + } else { + let content_width = content_width as usize; + let lines = toss::split_at_indices(&content, &frame.wrap(&content, content_width)); + let lines = lines.into_iter().map(|s| s.to_string()).collect::>(); + Block::msg(msg.id(), indent, msg.time(), nick, lines) } - .into_block(msg.id(), indent) } fn layout_subtree( @@ -41,7 +39,7 @@ impl TreeView { layout: &mut Blocks, ) { let block = if let Some(msg) = tree.msg(id) { - self.msg_to_block(msg, indent, frame, size) + self.msg_to_block(frame, size, msg, indent) } else { Block::placeholder(id.clone(), indent) };