Layout using new algorithm and new blocks

This commit is contained in:
Joscha 2022-07-31 19:57:45 +02:00
parent 6f4d94afa5
commit ae8ec70e5e
7 changed files with 459 additions and 254 deletions

View file

@ -7,24 +7,24 @@ use crate::macros::some_or_return;
use crate::ui::widgets::BoxedWidget;
pub struct Block<I> {
id: I,
top_line: i32,
height: i32,
pub id: I,
pub top_line: i32,
pub height: i32,
/// The lines of the block that should be made visible if the block is
/// focused on. By default, the focus encompasses the entire block.
///
/// If not all of these lines can be made visible, the top of the range
/// should be preferred over the bottom.
focus: Range<i32>,
widget: BoxedWidget,
pub focus: Range<i32>,
pub widget: BoxedWidget,
}
impl<I> Block<I> {
pub fn new<W: Into<BoxedWidget>>(frame: &mut Frame, width: u16, id: I, widget: W) -> Self {
pub fn new<W: Into<BoxedWidget>>(frame: &mut Frame, id: I, widget: W) -> Self {
// Interestingly, rust-analyzer fails to deduce the type of `widget`
// here but rustc knows it's a `BoxedWidget`.
let widget = widget.into();
let size = widget.size(frame, Some(width), None);
let size = widget.size(frame, Some(frame.size().width), None);
let height = size.height.into();
Self {
id,
@ -97,15 +97,55 @@ impl<I> Blocks<I> {
self.push_back(block);
}
}
pub fn set_top_line(&mut self, line: i32) {
self.top_line = line;
if let Some(first_block) = self.blocks.front_mut() {
first_block.top_line = self.top_line;
}
for i in 1..self.blocks.len() {
self.blocks[i].top_line = self.blocks[i - 1].top_line + self.blocks[i - 1].height;
}
self.bottom_line = self
.blocks
.back()
.map(|b| b.top_line + b.height - 1)
.unwrap_or(self.top_line - 1);
}
pub fn set_bottom_line(&mut self, line: i32) {
self.bottom_line = line;
if let Some(last_block) = self.blocks.back_mut() {
last_block.top_line = self.bottom_line + 1 - last_block.height;
}
for i in (1..self.blocks.len()).rev() {
self.blocks[i - 1].top_line = self.blocks[i].top_line - self.blocks[i - 1].height;
}
self.top_line = self
.blocks
.front()
.map(|b| b.top_line)
.unwrap_or(self.bottom_line + 1)
}
}
impl<I: Eq> Blocks<I> {
pub fn recalculate_offsets(&mut self, id: I, top_line: i32) {
pub fn find(&self, id: &I) -> Option<&Block<I>> {
self.blocks.iter().find(|b| b.id == *id)
}
pub fn recalculate_offsets(&mut self, id: &I, top_line: i32) {
let idx = some_or_return!(self
.blocks
.iter()
.enumerate()
.find(|(_, b)| b.id == id)
.find(|(_, b)| b.id == *id)
.map(|(i, _)| i));
self.blocks[idx].top_line = top_line;

View file

@ -1,4 +1,7 @@
mod layout;
mod time;
mod tree_blocks;
mod widgets;
use std::sync::Arc;
@ -6,17 +9,20 @@ use async_trait::async_trait;
use crossterm::event::KeyEvent;
use parking_lot::FairMutex;
use tokio::sync::Mutex;
use toss::frame::{Frame, Size};
use toss::frame::{Frame, Pos, Size};
use toss::terminal::Terminal;
use crate::store::{Msg, MsgStore};
use crate::ui::widgets::editor::EditorState;
use crate::ui::widgets::Widget;
use self::tree_blocks::TreeBlocks;
///////////
// State //
///////////
#[derive(Debug, Clone, Copy)]
pub enum Cursor<I> {
Bottom,
Msg(I),
@ -24,6 +30,24 @@ pub enum Cursor<I> {
Pseudo(Option<I>),
}
impl<I: Eq> Cursor<I> {
pub fn refers_to(&self, id: &I) -> bool {
if let Self::Msg(own_id) = self {
own_id == id
} else {
false
}
}
pub fn refers_to_last_child_of(&self, id: &I) -> bool {
if let Self::Editor(Some(parent)) | Self::Pseudo(Some(parent)) = self {
parent == id
} else {
false
}
}
}
struct InnerTreeViewState<M: Msg, S: MsgStore<M>> {
store: S,
@ -113,5 +137,16 @@ where
async fn render(self: Box<Self>, frame: &mut Frame) {
let mut guard = self.0.lock().await;
let blocks = guard.relayout(frame).await;
let size = frame.size();
for block in blocks.into_blocks().blocks {
frame.push(
Pos::new(0, block.top_line),
Size::new(size.width, block.height as u16),
);
block.widget.render(frame).await;
frame.pop();
}
}
}

View file

@ -1,292 +1,310 @@
//! Arranging messages as blocks.
use toss::frame::{Frame, Size};
use toss::frame::Frame;
use crate::store::{Msg, MsgStore, Path, Tree};
use crate::ui::chat::blocks::Block;
use crate::ui::widgets::empty::Empty;
use crate::ui::widgets::text::Text;
use super::blocks::{Block, BlockBody, Blocks, MsgBlock};
use super::cursor::Cursor;
use super::{util, InnerTreeViewState};
use super::tree_blocks::{BlockId, Root, TreeBlocks};
use super::{widgets, Cursor, InnerTreeViewState};
impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
async fn cursor_path(&self, cursor: &Cursor<M::Id>) -> Path<M::Id> {
match cursor {
Cursor::Bottom => match self.store.last_tree_id().await {
Some(id) => Path::new(vec![id]),
None => Path::new(vec![M::last_possible_id()]),
},
Cursor::Msg(id) => self.store.path(id).await,
Cursor::Compose(lc) | Cursor::Placeholder(lc) => match &lc.after {
None => Path::new(vec![M::last_possible_id()]),
Some(id) => {
let mut path = self.store.path(id).await;
path.push(M::last_possible_id());
path
}
},
Cursor::Bottom | Cursor::Editor(None) | Cursor::Pseudo(None) => {
Path::new(vec![M::last_possible_id()])
}
Cursor::Editor(Some(parent)) | Cursor::Pseudo(Some(parent)) => {
let mut path = self.store.path(parent).await;
path.push(M::last_possible_id());
path
}
}
}
fn cursor_tree_id<'a>(
cursor: &Cursor<M::Id>,
cursor_path: &'a Path<M::Id>,
) -> Option<&'a M::Id> {
match cursor {
Cursor::Bottom => None,
Cursor::Msg(_) => Some(cursor_path.first()),
Cursor::Compose(lc) | Cursor::Placeholder(lc) => match &lc.after {
None => None,
Some(_) => Some(cursor_path.first()),
},
}
}
fn cursor_line(
last_blocks: &Blocks<M::Id>,
cursor: &Cursor<M::Id>,
cursor_path: &Path<M::Id>,
last_cursor_path: &Path<M::Id>,
size: Size,
) -> i32 {
if matches!(cursor, Cursor::Bottom) {
// Ensures that a Cursor::Bottom is always at the bottom of the
// screen. Will be scroll-clamped to the bottom later.
fn cursor_line(&self, blocks: &TreeBlocks<M::Id>) -> i32 {
if let Cursor::Bottom = self.cursor {
// The value doesn't matter as it will always be ignored.
0
} else if let Some(block) = last_blocks.find(|b| cursor.matches_block(b)) {
block.line
} else if last_cursor_path < cursor_path {
// If the cursor is bottom, the bottom marker needs to be located at
// the line below the last visible line. If it is a normal message
// cursor, it will be made visible again one way or another later.
size.height.into()
} else {
0
blocks
.blocks()
.find(&BlockId::from_cursor(&self.cursor))
.expect("cursor is visible")
.top_line
}
}
fn msg_to_block(frame: &mut Frame, indent: usize, msg: &M) -> Block<M::Id> {
let size = frame.size();
let nick = msg.nick();
let content = msg.content();
let content_width = size.width as i32 - util::after_nick(frame, indent, &nick);
if content_width < util::MIN_CONTENT_WIDTH as i32 {
Block::placeholder(Some(msg.time()), indent, msg.id())
} else {
let content_width = content_width as usize;
let breaks = frame.wrap(&content.text(), content_width);
let lines = content.split_at_indices(&breaks);
Block::msg(msg.time(), indent, msg.id(), nick, lines)
}
fn contains_cursor(&self, blocks: &TreeBlocks<M::Id>) -> bool {
blocks
.blocks()
.find(&BlockId::from_cursor(&self.cursor))
.is_some()
}
fn layout_subtree(
&self,
frame: &mut Frame,
tree: &Tree<M>,
indent: usize,
id: &M::Id,
result: &mut Blocks<M::Id>,
blocks: &mut TreeBlocks<M::Id>,
) {
let block = if let Some(msg) = tree.msg(id) {
Self::msg_to_block(frame, indent, msg)
} else {
Block::placeholder(None, indent, id.clone())
};
result.push_back(block);
// Ghost cursor in front, for positioning according to last cursor line
if self.last_cursor.refers_to(id) {
let block = Block::new(frame, BlockId::LastCursor, Empty);
blocks.blocks_mut().push_back(block);
}
// Main message body
let highlighted = self.cursor.refers_to(id);
let widget = if let Some(msg) = tree.msg(id) {
widgets::msg(highlighted, indent, msg)
} else {
widgets::msg_placeholder(highlighted, indent)
};
let block = Block::new(frame, BlockId::Msg(id.clone()), widget);
blocks.blocks_mut().push_back(block);
// Children, recursively
if let Some(children) = tree.children(id) {
for child in children {
Self::layout_subtree(frame, tree, indent + 1, child, result);
self.layout_subtree(frame, tree, indent + 1, child, blocks);
}
}
result.push_back(Block::after(indent, id.clone()))
// Trailing ghost cursor, for positioning according to last cursor line
if self.last_cursor.refers_to_last_child_of(id) {
let block = Block::new(frame, BlockId::LastCursor, Empty);
blocks.blocks_mut().push_back(block);
}
// Trailing editor or pseudomessage
if self.cursor.refers_to_last_child_of(id) {
// TODO Render proper editor or pseudocursor
let block = Block::new(frame, BlockId::Cursor, Text::new("TODO"));
blocks.blocks_mut().push_back(block);
}
}
fn layout_tree(frame: &mut Frame, tree: Tree<M>) -> Blocks<M::Id> {
let mut blocks = Blocks::new();
Self::layout_subtree(frame, &tree, 0, tree.root(), &mut blocks);
blocks.roots = Some((tree.root().clone(), tree.root().clone()));
fn layout_tree(&self, frame: &mut Frame, tree: Tree<M>) -> TreeBlocks<M::Id> {
let root = Root::Tree(tree.root().clone());
let mut blocks = TreeBlocks::new(root.clone(), root);
self.layout_subtree(frame, &tree, 0, tree.root(), &mut blocks);
blocks
}
/// Create a [`Blocks`] of the current cursor's immediate surroundings.
async fn layout_cursor_surroundings(&self, frame: &mut Frame) -> Blocks<M::Id> {
let size = frame.size();
fn layout_bottom(&self, frame: &mut Frame) -> TreeBlocks<M::Id> {
let mut blocks = TreeBlocks::new(Root::Bottom, Root::Bottom);
let cursor_path = self.cursor_path(&self.cursor).await;
let last_cursor_path = self.cursor_path(&self.last_cursor).await;
let tree_id = Self::cursor_tree_id(&self.cursor, &cursor_path);
let cursor_line = Self::cursor_line(
&self.last_blocks,
&self.cursor,
&cursor_path,
&last_cursor_path,
size,
);
if let Some(tree_id) = tree_id {
let tree = self.store.tree(tree_id).await;
let mut blocks = Self::layout_tree(frame, tree);
blocks.recalculate_offsets(|b| {
if self.cursor.matches_block(b) {
Some(cursor_line)
} else {
None
}
});
blocks
} else {
Blocks::new_bottom(cursor_line)
// Ghost cursor, for positioning according to last cursor line
if let Cursor::Editor(None) | Cursor::Pseudo(None) = self.last_cursor {
let block = Block::new(frame, BlockId::LastCursor, Empty);
blocks.blocks_mut().push_back(block);
}
// Editor or pseudomessage
if let Cursor::Editor(None) | Cursor::Pseudo(None) = self.cursor {
// TODO Render proper editor or pseudocursor
let block = Block::new(frame, BlockId::Cursor, Text::new("TODO"));
blocks.blocks_mut().push_back(block);
}
blocks
}
fn scroll_so_cursor_is_visible(blocks: &mut Blocks<M::Id>, cursor: &Cursor<M::Id>, size: Size) {
if !matches!(cursor, Cursor::Msg(_)) {
// In all other cases, there is special scrolling behaviour, so
// let's not interfere.
return;
}
let block = blocks
.find(|b| cursor.matches_block(b))
// This should never happen since we always start rendering the
// blocks from the cursor.
.expect("no cursor found");
let min_line = 0;
let max_line = size.height as i32 - block.height();
if block.line < min_line {
blocks.offset(min_line - block.line);
} else if block.line > max_line {
blocks.offset(max_line - block.line);
}
}
/// Try to obtain a [`Cursor::Msg`] pointing to the block.
fn as_msg_cursor(block: &Block<M::Id>) -> Option<Cursor<M::Id>> {
match &block.body {
BlockBody::Msg(MsgBlock { id, .. }) => Some(Cursor::Msg(id.clone())),
_ => None,
}
}
fn move_cursor_so_it_is_visible(
blocks: &mut Blocks<M::Id>,
cursor: &mut Cursor<M::Id>,
size: Size,
) {
if !matches!(cursor, Cursor::Msg(_)) {
// In all other cases, there is special scrolling behaviour, so
// let's not interfere.
return;
}
let block = blocks
.find(|b| cursor.matches_block(b))
// This should never happen since we always start rendering the
// blocks from the cursor.
.expect("no cursor found");
let min_line = 1 - block.height();
let max_line = size.height as i32 - 1;
let new_cursor = if block.line < min_line {
// Move cursor to first possible visible block
blocks
.iter()
.filter(|b| b.line >= min_line)
.find_map(Self::as_msg_cursor)
} else if block.line > max_line {
// Move cursor to last possible visible block
blocks
.iter()
.rev()
.filter(|b| b.line <= max_line)
.find_map(Self::as_msg_cursor)
} else {
None
};
if let Some(new_cursor) = new_cursor {
*cursor = new_cursor;
}
}
async fn expand_blocks_up(&self, frame: &mut Frame, blocks: &mut Blocks<M::Id>) {
while blocks.top_line > 0 {
let tree_id = if let Some((root_top, _)) = &blocks.roots {
self.store.prev_tree_id(root_top).await
} else {
self.store.last_tree_id().await
};
if let Some(tree_id) = tree_id {
let tree = self.store.tree(&tree_id).await;
blocks.prepend(Self::layout_tree(frame, tree));
} else {
break;
}
}
}
async fn expand_blocks_down(&self, frame: &mut Frame, blocks: &mut Blocks<M::Id>) {
while blocks.bottom_line < frame.size().height as i32 {
let tree_id = if let Some((_, root_bot)) = &blocks.roots {
self.store.next_tree_id(root_bot).await
} else {
// We assume that a Blocks without roots is at the bottom of the
// room's history. Therefore, there are no more messages below.
break;
};
if let Some(tree_id) = tree_id {
let tree = self.store.tree(&tree_id).await;
blocks.append(Self::layout_tree(frame, tree));
} else {
break;
}
}
}
async fn clamp_scrolling(&self, frame: &mut Frame, blocks: &mut Blocks<M::Id>) {
let size = frame.size();
async fn expand_to_top(&self, frame: &mut Frame, blocks: &mut TreeBlocks<M::Id>) {
let top_line = 0;
let bottom_line = size.height as i32 - 1;
self.expand_blocks_up(frame, blocks).await;
if blocks.top_line > top_line {
blocks.offset(top_line - blocks.top_line);
while blocks.blocks().top_line > top_line {
let top_root = blocks.top_root();
let prev_tree_id = match top_root {
Root::Bottom => self.store.last_tree_id().await,
Root::Tree(tree_id) => self.store.prev_tree_id(tree_id).await,
};
let prev_tree_id = match prev_tree_id {
Some(tree_id) => tree_id,
None => break,
};
let prev_tree = self.store.tree(&prev_tree_id).await;
blocks.prepend(self.layout_tree(frame, prev_tree));
}
self.expand_blocks_down(frame, blocks).await;
if blocks.bottom_line < bottom_line {
blocks.offset(bottom_line - blocks.bottom_line);
}
self.expand_blocks_up(frame, blocks).await;
}
pub async fn relayout(&mut self, frame: &mut Frame) {
let size = frame.size();
async fn expand_to_bottom(&self, frame: &mut Frame, blocks: &mut TreeBlocks<M::Id>) {
let bottom_line = frame.size().height as i32 - 1;
let mut blocks = self.layout_cursor_surroundings(frame).await;
while blocks.blocks().bottom_line < bottom_line {
let bottom_root = blocks.bottom_root();
let next_tree_id = match bottom_root {
Root::Bottom => break,
Root::Tree(tree_id) => self.store.prev_tree_id(tree_id).await,
};
if let Some(next_tree_id) = next_tree_id {
let next_tree = self.store.tree(&next_tree_id).await;
blocks.append(self.layout_tree(frame, next_tree));
} else {
blocks.append(self.layout_bottom(frame));
}
}
}
async fn fill_screen_and_clamp_scrolling(
&self,
frame: &mut Frame,
blocks: &mut TreeBlocks<M::Id>,
) {
let top_line = 0;
let bottom_line = frame.size().height as i32 - 1;
self.expand_to_top(frame, blocks).await;
if blocks.blocks().top_line > top_line {
blocks.blocks_mut().set_top_line(0);
}
self.expand_to_bottom(frame, blocks).await;
if blocks.blocks().bottom_line < bottom_line {
blocks.blocks_mut().set_bottom_line(bottom_line);
}
self.expand_to_top(frame, blocks).await;
}
async fn layout_last_cursor_seed(
&self,
frame: &mut Frame,
last_cursor_path: &Path<M::Id>,
) -> TreeBlocks<M::Id> {
match &self.last_cursor {
Cursor::Bottom => {
let mut blocks = self.layout_bottom(frame);
let bottom_line = frame.size().height as i32 - 1;
blocks.blocks_mut().set_bottom_line(bottom_line);
blocks
}
Cursor::Editor(None) | Cursor::Pseudo(None) => {
let mut blocks = self.layout_bottom(frame);
blocks
.blocks_mut()
.recalculate_offsets(&BlockId::LastCursor, self.last_cursor_line);
blocks
}
Cursor::Msg(_) | Cursor::Editor(Some(_)) | Cursor::Pseudo(Some(_)) => {
let root = last_cursor_path.first();
let tree = self.store.tree(root).await;
let mut blocks = self.layout_tree(frame, tree);
blocks
.blocks_mut()
.recalculate_offsets(&BlockId::LastCursor, self.last_cursor_line);
blocks
}
}
}
async fn layout_cursor_seed(
&self,
frame: &mut Frame,
last_cursor_path: &Path<M::Id>,
cursor_path: &Path<M::Id>,
) -> TreeBlocks<M::Id> {
let bottom_line = frame.size().height as i32 - 1;
match &self.cursor {
Cursor::Bottom | Cursor::Editor(None) | Cursor::Pseudo(None) => {
let mut blocks = self.layout_bottom(frame);
blocks.blocks_mut().set_bottom_line(bottom_line);
blocks
}
Cursor::Msg(_) | Cursor::Editor(Some(_)) | Cursor::Pseudo(Some(_)) => {
let root = cursor_path.first();
let tree = self.store.tree(root).await;
let mut blocks = self.layout_tree(frame, tree);
let cursor_above_last = cursor_path < last_cursor_path;
let cursor_line = if cursor_above_last { 0 } else { bottom_line };
blocks
.blocks_mut()
.recalculate_offsets(&BlockId::from_cursor(&self.cursor), cursor_line);
blocks
}
}
}
async fn layout_initial_seed(
&self,
frame: &mut Frame,
last_cursor_path: &Path<M::Id>,
cursor_path: &Path<M::Id>,
) -> TreeBlocks<M::Id> {
if let Cursor::Bottom = self.cursor {
self.layout_cursor_seed(frame, last_cursor_path, cursor_path)
.await
} else {
self.layout_last_cursor_seed(frame, last_cursor_path).await
}
}
pub async fn relayout(&mut self, frame: &mut Frame) -> TreeBlocks<M::Id> {
// The basic idea is this:
//
// First, layout a full screen of blocks around self.last_cursor, using
// self.last_cursor_line for offset positioning.
//
// Then, check if self.cursor is somewhere in these blocks. If it is, we
// now know the position of our own cursor. If it is not, it has jumped
// too far away from self.last_cursor and we'll need to render a new
// full screen of blocks around self.cursor before proceeding, using the
// cursor paths to determine the position of self.cursor on the screen.
//
// Now that we have a more-or-less accurate screen position of
// self.cursor, we can perform the actual cursor logic, i.e. make the
// cursor visible or move it so it is visible.
//
// This entire process is complicated by the different kinds of cursors.
let last_cursor_path = self.cursor_path(&self.last_cursor).await;
let cursor_path = self.cursor_path(&self.cursor).await;
let mut blocks = self
.layout_initial_seed(frame, &last_cursor_path, &cursor_path)
.await;
self.fill_screen_and_clamp_scrolling(frame, &mut blocks)
.await;
if !self.contains_cursor(&blocks) {
blocks = self
.layout_cursor_seed(frame, &last_cursor_path, &cursor_path)
.await;
self.fill_screen_and_clamp_scrolling(frame, &mut blocks)
.await;
}
if self.make_cursor_visible {
Self::scroll_so_cursor_is_visible(&mut blocks, &self.cursor, size);
// self.make_cursor_visible(&mut blocks).await; // TODO
self.fill_screen_and_clamp_scrolling(frame, &mut blocks)
.await;
} else {
// self.move_cursor_so_it_is_visible(&mut blocks); // TODO
self.fill_screen_and_clamp_scrolling(frame, &mut blocks)
.await;
}
self.clamp_scrolling(frame, &mut blocks).await;
if !self.make_cursor_visible {
Self::move_cursor_so_it_is_visible(&mut blocks, &mut self.cursor, size);
}
self.last_blocks = blocks;
self.last_cursor = self.cursor.clone();
self.last_cursor_line = self.cursor_line(&blocks);
self.make_cursor_visible = false;
blocks
}
}

View file

@ -1,8 +1,8 @@
use crossterm::style::{ContentStyle, Stylize};
use time::format_description::FormatItem;
use time::macros::format_description;
use time::OffsetDateTime;
use crate::euph::api::Time;
use crate::ui::widgets::background::Background;
use crate::ui::widgets::text::Text;
use crate::ui::widgets::BoxedWidget;
@ -18,9 +18,9 @@ fn style_inverted() -> ContentStyle {
ContentStyle::default().black().on_white()
}
pub fn widget(time: Option<Time>, highlighted: bool) -> BoxedWidget {
pub fn widget(time: Option<OffsetDateTime>, highlighted: bool) -> BoxedWidget {
let text = if let Some(time) = time {
time.0.format(TIME_FORMAT).expect("could not format time")
time.format(TIME_FORMAT).expect("could not format time")
} else {
TIME_EMPTY.to_string()
};

View file

@ -0,0 +1,88 @@
use crate::ui::chat::blocks::Blocks;
use super::Cursor;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockId<I> {
Msg(I),
Cursor,
LastCursor,
}
impl<I: Clone> BlockId<I> {
pub fn from_cursor(cursor: &Cursor<I>) -> Self {
match cursor {
Cursor::Msg(id) => Self::Msg(id.clone()),
_ => Self::Cursor,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Root<I> {
Bottom,
Tree(I),
}
pub struct TreeBlocks<I> {
blocks: Blocks<BlockId<I>>,
top_root: Root<I>,
bottom_root: Root<I>,
}
impl<I> TreeBlocks<I> {
pub fn new(top_root: Root<I>, bottom_root: Root<I>) -> Self {
Self {
blocks: Blocks::new(),
top_root,
bottom_root,
}
}
/// See [`Blocks::new_below`].
pub fn new_below(line: i32, top_root: Root<I>, bottom_root: Root<I>) -> Self {
Self {
blocks: Blocks::new_below(line),
top_root,
bottom_root,
}
}
pub fn blocks(&self) -> &Blocks<BlockId<I>> {
&self.blocks
}
pub fn blocks_mut(&mut self) -> &mut Blocks<BlockId<I>> {
&mut self.blocks
}
pub fn into_blocks(self) -> Blocks<BlockId<I>> {
self.blocks
}
pub fn top_root(&self) -> &Root<I> {
&self.top_root
}
pub fn top_root_mut(&mut self) -> &mut Root<I> {
&mut self.top_root
}
pub fn bottom_root(&self) -> &Root<I> {
&self.bottom_root
}
pub fn bottom_root_mut(&mut self) -> &mut Root<I> {
&mut self.bottom_root
}
pub fn prepend(&mut self, other: Self) {
self.blocks.prepend(other.blocks);
self.top_root = other.top_root;
}
pub fn append(&mut self, other: Self) {
self.blocks.append(other.blocks);
self.bottom_root = other.bottom_root;
}
}

View file

@ -0,0 +1,24 @@
use crate::store::Msg;
use crate::ui::widgets::join::{HJoin, Segment};
use crate::ui::widgets::padding::Padding;
use crate::ui::widgets::text::Text;
use crate::ui::widgets::BoxedWidget;
use super::time;
pub fn msg<M: Msg>(highlighted: bool, indent: usize, msg: &M) -> BoxedWidget {
HJoin::new(vec![
Segment::new(Padding::new(time::widget(Some(msg.time()), highlighted)).right(1)),
Segment::new(Padding::new(Text::new(msg.nick())).right(1)),
Segment::new(Text::new(msg.content()).wrap(true)),
])
.into()
}
pub fn msg_placeholder(highlighted: bool, indent: usize) -> BoxedWidget {
HJoin::new(vec![
Segment::new(Padding::new(time::widget(None, highlighted)).right(1)),
Segment::new(Text::new("[...]")),
])
.into()
}

View file

@ -17,8 +17,8 @@ impl Text {
}
}
pub fn wrap(mut self) -> Self {
self.wrap = true;
pub fn wrap(mut self, active: bool) -> Self {
self.wrap = active;
self
}