Remove old chat, widgets, util modules

This commit is contained in:
Joscha 2023-04-17 10:08:14 +02:00
parent e2b75d2f52
commit bc8c5968d6
27 changed files with 0 additions and 4417 deletions

View file

@ -1,11 +1,8 @@
mod chat;
mod chat2;
mod euph;
mod input;
mod rooms;
mod util;
mod util2;
mod widgets;
mod widgets2;
use std::convert::Infallible;

View file

@ -1,163 +0,0 @@
// TODO Implement thread view
// TODO Implement flat (chronological?) view
// TODO Implement message search?
mod blocks;
mod tree;
use std::sync::Arc;
use std::{fmt, io};
use async_trait::async_trait;
use parking_lot::FairMutex;
use time::OffsetDateTime;
use toss::{Frame, Size, Styled, Terminal, WidthDb};
use crate::store::{Msg, MsgStore};
use self::tree::{TreeView, TreeViewState};
use super::input::{InputEvent, KeyBindingsList};
use super::widgets::Widget;
///////////
// Trait //
///////////
pub trait ChatMsg {
fn time(&self) -> OffsetDateTime;
fn styled(&self) -> (Styled, Styled);
fn edit(nick: &str, content: &str) -> (Styled, Styled);
fn pseudo(nick: &str, content: &str) -> (Styled, Styled);
}
///////////
// State //
///////////
pub enum Mode {
Tree,
// Thread,
// Flat,
}
pub struct ChatState<M: Msg, S: MsgStore<M>> {
store: S,
mode: Mode,
tree: TreeViewState<M, S>,
// thread: ThreadView,
// flat: FlatView,
}
impl<M: Msg, S: MsgStore<M> + Clone> ChatState<M, S> {
pub fn new(store: S) -> Self {
Self {
mode: Mode::Tree,
tree: TreeViewState::new(store.clone()),
store,
}
}
}
impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
pub fn store(&self) -> &S {
&self.store
}
pub fn widget(&self, nick: String, focused: bool) -> Chat<M, S> {
match self.mode {
Mode::Tree => Chat::Tree(self.tree.widget(nick, focused)),
}
}
}
pub enum Reaction<M: Msg> {
NotHandled,
Handled,
Composed {
parent: Option<M::Id>,
content: String,
},
ComposeError(io::Error),
}
impl<M: Msg> Reaction<M> {
pub fn handled(&self) -> bool {
!matches!(self, Self::NotHandled)
}
}
impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
pub async fn list_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) {
match self.mode {
Mode::Tree => self.tree.list_key_bindings(bindings, can_compose).await,
}
}
pub async fn handle_input_event(
&mut self,
terminal: &mut Terminal,
crossterm_lock: &Arc<FairMutex<()>>,
event: &InputEvent,
can_compose: bool,
) -> Result<Reaction<M>, S::Error> {
match self.mode {
Mode::Tree => {
self.tree
.handle_input_event(terminal, crossterm_lock, event, can_compose)
.await
}
}
}
pub async fn cursor(&self) -> Option<M::Id> {
match self.mode {
Mode::Tree => self.tree.cursor().await,
}
}
/// A [`Reaction::Composed`] message was sent, either successfully or
/// unsuccessfully.
///
/// If successful, include the message's id as an argument. If unsuccessful,
/// instead pass a `None`.
pub async fn sent(&mut self, id: Option<M::Id>) {
match self.mode {
Mode::Tree => self.tree.sent(id).await,
}
}
}
////////////
// Widget //
////////////
pub enum Chat<M: Msg, S: MsgStore<M>> {
Tree(TreeView<M, S>),
}
#[async_trait]
impl<M, S> Widget for Chat<M, S>
where
M: Msg + ChatMsg + Send + Sync,
M::Id: Send + Sync,
S: MsgStore<M> + Send + Sync,
S::Error: fmt::Display,
{
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Size {
match self {
Self::Tree(tree) => tree.size(widthdb, max_width, max_height).await,
}
}
async fn render(self: Box<Self>, frame: &mut Frame) {
match *self {
Self::Tree(tree) => Box::new(tree).render(frame).await,
}
}
}

View file

@ -1,171 +0,0 @@
use std::collections::{vec_deque, VecDeque};
use std::ops::Range;
use toss::Frame;
use crate::macros::some_or_return;
use crate::ui::widgets::BoxedWidget;
pub struct Block<I> {
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.
pub focus: Range<i32>,
pub widget: BoxedWidget,
}
impl<I> Block<I> {
pub async 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 max_width = frame.size().width;
let size = widget.size(frame.widthdb(), Some(max_width), None).await;
let height = size.height.into();
Self {
id,
top_line: 0,
height,
focus: 0..height,
widget,
}
}
pub fn focus(mut self, focus: Range<i32>) -> Self {
self.focus = focus;
self
}
}
pub struct Blocks<I> {
pub blocks: VecDeque<Block<I>>,
/// The top line of the first block. Useful for prepending blocks,
/// especially to empty [`Blocks`]s.
pub top_line: i32,
/// The bottom line of the last block. Useful for appending blocks,
/// especially to empty [`Blocks`]s.
pub bottom_line: i32,
}
impl<I> Blocks<I> {
pub fn new() -> Self {
Self::new_below(0)
}
/// Create a new [`Blocks`] such that the first prepended line will be on
/// `line`.
pub fn new_below(line: i32) -> Self {
Self {
blocks: VecDeque::new(),
top_line: line + 1,
bottom_line: line,
}
}
pub fn iter(&self) -> vec_deque::Iter<'_, Block<I>> {
self.blocks.iter()
}
pub fn offset(&mut self, delta: i32) {
self.top_line += delta;
self.bottom_line += delta;
for block in &mut self.blocks {
block.top_line += delta;
}
}
pub fn push_front(&mut self, mut block: Block<I>) {
self.top_line -= block.height;
block.top_line = self.top_line;
self.blocks.push_front(block);
}
pub fn push_back(&mut self, mut block: Block<I>) {
block.top_line = self.bottom_line + 1;
self.bottom_line += block.height;
self.blocks.push_back(block);
}
pub fn prepend(&mut self, mut layout: Self) {
while let Some(block) = layout.blocks.pop_back() {
self.push_front(block);
}
}
pub fn append(&mut self, mut layout: Self) {
while let Some(block) = layout.blocks.pop_front() {
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 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)
.map(|(i, _)| i));
self.blocks[idx].top_line = top_line;
// Propagate changes to top
for i in (0..idx).rev() {
self.blocks[i].top_line = self.blocks[i + 1].top_line - self.blocks[i].height;
}
self.top_line = self.blocks.front().expect("blocks nonempty").top_line;
// Propagate changes to bottom
for i in (idx + 1)..self.blocks.len() {
self.blocks[i].top_line = self.blocks[i - 1].top_line + self.blocks[i - 1].height;
}
let bottom = self.blocks.back().expect("blocks nonempty");
self.bottom_line = bottom.top_line + bottom.height - 1;
}
}

View file

@ -1,458 +0,0 @@
// TODO Focusing on sub-trees
mod cursor;
mod layout;
mod tree_blocks;
mod widgets;
use std::collections::HashSet;
use std::fmt;
use std::sync::Arc;
use async_trait::async_trait;
use parking_lot::FairMutex;
use tokio::sync::Mutex;
use toss::{Frame, Pos, Size, Terminal, WidthDb};
use crate::macros::logging_unwrap;
use crate::store::{Msg, MsgStore};
use crate::ui::input::{key, InputEvent, KeyBindingsList};
use crate::ui::util;
use crate::ui::widgets::editor::EditorState;
use crate::ui::widgets::Widget;
use self::cursor::Cursor;
use super::{ChatMsg, Reaction};
///////////
// State //
///////////
enum Correction {
MakeCursorVisible,
MoveCursorToVisibleArea,
CenterCursor,
}
struct InnerTreeViewState<M: Msg, S: MsgStore<M>> {
store: S,
last_cursor: Cursor<M::Id>,
last_cursor_line: i32,
last_visible_msgs: Vec<M::Id>,
cursor: Cursor<M::Id>,
editor: EditorState,
/// Scroll the view on the next render. Positive values scroll up and
/// negative values scroll down.
scroll: i32,
correction: Option<Correction>,
folded: HashSet<M::Id>,
}
impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
fn new(store: S) -> Self {
Self {
store,
last_cursor: Cursor::Bottom,
last_cursor_line: 0,
last_visible_msgs: vec![],
cursor: Cursor::Bottom,
editor: EditorState::new(),
scroll: 0,
correction: None,
folded: HashSet::new(),
}
}
pub fn list_movement_key_bindings(&self, bindings: &mut KeyBindingsList) {
bindings.binding("j/k, ↓/↑", "move cursor up/down");
bindings.binding("J/K, ctrl+↓/↑", "move cursor to prev/next sibling");
bindings.binding("p/P", "move cursor to parent/root");
bindings.binding("h/l, ←/→", "move cursor chronologically");
bindings.binding("H/L, ctrl+←/→", "move cursor to prev/next unseen message");
bindings.binding("g, home", "move cursor to top");
bindings.binding("G, end", "move cursor to bottom");
bindings.binding("ctrl+y/e", "scroll up/down a line");
bindings.binding("ctrl+u/d", "scroll up/down half a screen");
bindings.binding("ctrl+b/f, page up/down", "scroll up/down one screen");
bindings.binding("z", "center cursor on screen");
// TODO Bindings inspired by vim's ()/[]/{} bindings?
}
async fn handle_movement_input_event(
&mut self,
frame: &mut Frame,
event: &InputEvent,
) -> Result<bool, S::Error> {
let chat_height = frame.size().height - 3;
match event {
key!('k') | key!(Up) => self.move_cursor_up().await?,
key!('j') | key!(Down) => self.move_cursor_down().await?,
key!('K') | key!(Ctrl + Up) => self.move_cursor_up_sibling().await?,
key!('J') | key!(Ctrl + Down) => self.move_cursor_down_sibling().await?,
key!('p') => self.move_cursor_to_parent().await?,
key!('P') => self.move_cursor_to_root().await?,
key!('h') | key!(Left) => self.move_cursor_older().await?,
key!('l') | key!(Right) => self.move_cursor_newer().await?,
key!('H') | key!(Ctrl + Left) => self.move_cursor_older_unseen().await?,
key!('L') | key!(Ctrl + Right) => self.move_cursor_newer_unseen().await?,
key!('g') | key!(Home) => self.move_cursor_to_top().await?,
key!('G') | key!(End) => self.move_cursor_to_bottom().await,
key!(Ctrl + 'y') => self.scroll_up(1),
key!(Ctrl + 'e') => self.scroll_down(1),
key!(Ctrl + 'u') => self.scroll_up((chat_height / 2).into()),
key!(Ctrl + 'd') => self.scroll_down((chat_height / 2).into()),
key!(Ctrl + 'b') | key!(PageUp) => self.scroll_up(chat_height.saturating_sub(1).into()),
key!(Ctrl + 'f') | key!(PageDown) => {
self.scroll_down(chat_height.saturating_sub(1).into())
}
key!('z') => self.center_cursor(),
_ => return Ok(false),
}
Ok(true)
}
pub fn list_action_key_bindings(&self, bindings: &mut KeyBindingsList) {
bindings.binding("space", "fold current message's subtree");
bindings.binding("s", "toggle current message's seen status");
bindings.binding("S", "mark all visible messages as seen");
bindings.binding("ctrl+s", "mark all older messages as seen");
}
async fn handle_action_input_event(
&mut self,
event: &InputEvent,
id: Option<&M::Id>,
) -> Result<bool, S::Error> {
match event {
key!(' ') => {
if let Some(id) = id {
if !self.folded.remove(id) {
self.folded.insert(id.clone());
}
return Ok(true);
}
}
key!('s') => {
if let Some(id) = id {
if let Some(msg) = self.store.tree(id).await?.msg(id) {
self.store.set_seen(id, !msg.seen()).await?;
}
return Ok(true);
}
}
key!('S') => {
for id in &self.last_visible_msgs {
self.store.set_seen(id, true).await?;
}
return Ok(true);
}
key!(Ctrl + 's') => {
if let Some(id) = id {
self.store.set_older_seen(id, true).await?;
} else {
self.store
.set_older_seen(&M::last_possible_id(), true)
.await?;
}
return Ok(true);
}
_ => {}
}
Ok(false)
}
pub fn list_edit_initiating_key_bindings(&self, bindings: &mut KeyBindingsList) {
bindings.binding("r", "reply to message (inline if possible, else directly)");
bindings.binding("R", "reply to message (opposite of R)");
bindings.binding("t", "start a new thread");
}
async fn handle_edit_initiating_input_event(
&mut self,
event: &InputEvent,
id: Option<M::Id>,
) -> Result<bool, S::Error> {
match event {
key!('r') => {
if let Some(parent) = self.parent_for_normal_reply().await? {
self.cursor = Cursor::editor(id, parent);
self.correction = Some(Correction::MakeCursorVisible);
}
}
key!('R') => {
if let Some(parent) = self.parent_for_alternate_reply().await? {
self.cursor = Cursor::editor(id, parent);
self.correction = Some(Correction::MakeCursorVisible);
}
}
key!('t') | key!('T') => {
self.cursor = Cursor::editor(id, None);
self.correction = Some(Correction::MakeCursorVisible);
}
_ => return Ok(false),
}
Ok(true)
}
pub fn list_normal_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) {
self.list_movement_key_bindings(bindings);
bindings.empty();
self.list_action_key_bindings(bindings);
if can_compose {
bindings.empty();
self.list_edit_initiating_key_bindings(bindings);
}
}
async fn handle_normal_input_event(
&mut self,
frame: &mut Frame,
event: &InputEvent,
can_compose: bool,
id: Option<M::Id>,
) -> Result<bool, S::Error> {
#[allow(clippy::if_same_then_else)]
Ok(if self.handle_movement_input_event(frame, event).await? {
true
} else if self.handle_action_input_event(event, id.as_ref()).await? {
true
} else if can_compose {
self.handle_edit_initiating_input_event(event, id).await?
} else {
false
})
}
fn list_editor_key_bindings(&self, bindings: &mut KeyBindingsList) {
bindings.binding("esc", "close editor");
bindings.binding("enter", "send message");
util::list_editor_key_bindings_allowing_external_editing(bindings, |_| true);
}
fn handle_editor_input_event(
&mut self,
terminal: &mut Terminal,
crossterm_lock: &Arc<FairMutex<()>>,
event: &InputEvent,
coming_from: Option<M::Id>,
parent: Option<M::Id>,
) -> Reaction<M> {
// TODO Tab-completion
match event {
key!(Esc) => {
self.cursor = coming_from.map(Cursor::Msg).unwrap_or(Cursor::Bottom);
self.correction = Some(Correction::MakeCursorVisible);
return Reaction::Handled;
}
key!(Enter) => {
let content = self.editor.text();
if !content.trim().is_empty() {
self.cursor = Cursor::Pseudo {
coming_from,
parent: parent.clone(),
};
return Reaction::Composed { parent, content };
}
}
_ => {
let handled = util::handle_editor_input_event_allowing_external_editing(
&self.editor,
terminal,
crossterm_lock,
event,
|_| true,
);
match handled {
Ok(true) => {}
Ok(false) => return Reaction::NotHandled,
Err(e) => return Reaction::ComposeError(e),
}
}
}
self.correction = Some(Correction::MakeCursorVisible);
Reaction::Handled
}
pub fn list_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) {
bindings.heading("Chat");
match &self.cursor {
Cursor::Bottom | Cursor::Msg(_) => {
self.list_normal_key_bindings(bindings, can_compose);
}
Cursor::Editor { .. } => self.list_editor_key_bindings(bindings),
Cursor::Pseudo { .. } => {
self.list_normal_key_bindings(bindings, false);
}
}
}
async fn handle_input_event(
&mut self,
terminal: &mut Terminal,
crossterm_lock: &Arc<FairMutex<()>>,
event: &InputEvent,
can_compose: bool,
) -> Result<Reaction<M>, S::Error> {
Ok(match &self.cursor {
Cursor::Bottom => {
if self
.handle_normal_input_event(terminal.frame(), event, can_compose, None)
.await?
{
Reaction::Handled
} else {
Reaction::NotHandled
}
}
Cursor::Msg(id) => {
let id = id.clone();
if self
.handle_normal_input_event(terminal.frame(), event, can_compose, Some(id))
.await?
{
Reaction::Handled
} else {
Reaction::NotHandled
}
}
Cursor::Editor {
coming_from,
parent,
} => self.handle_editor_input_event(
terminal,
crossterm_lock,
event,
coming_from.clone(),
parent.clone(),
),
Cursor::Pseudo { .. } => {
if self
.handle_movement_input_event(terminal.frame(), event)
.await?
{
Reaction::Handled
} else {
Reaction::NotHandled
}
}
})
}
fn cursor(&self) -> Option<M::Id> {
match &self.cursor {
Cursor::Msg(id) => Some(id.clone()),
Cursor::Bottom | Cursor::Editor { .. } | Cursor::Pseudo { .. } => None,
}
}
fn sent(&mut self, id: Option<M::Id>) {
if let Cursor::Pseudo { coming_from, .. } = &self.cursor {
if let Some(id) = id {
self.last_cursor = Cursor::Msg(id.clone());
self.cursor = Cursor::Msg(id);
self.editor.clear();
} else {
self.cursor = match coming_from {
Some(id) => Cursor::Msg(id.clone()),
None => Cursor::Bottom,
};
};
}
}
}
pub struct TreeViewState<M: Msg, S: MsgStore<M>>(Arc<Mutex<InnerTreeViewState<M, S>>>);
impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
pub fn new(store: S) -> Self {
Self(Arc::new(Mutex::new(InnerTreeViewState::new(store))))
}
pub fn widget(&self, nick: String, focused: bool) -> TreeView<M, S> {
TreeView {
inner: self.0.clone(),
nick,
focused,
}
}
pub async fn list_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) {
self.0.lock().await.list_key_bindings(bindings, can_compose);
}
pub async fn handle_input_event(
&mut self,
terminal: &mut Terminal,
crossterm_lock: &Arc<FairMutex<()>>,
event: &InputEvent,
can_compose: bool,
) -> Result<Reaction<M>, S::Error> {
self.0
.lock()
.await
.handle_input_event(terminal, crossterm_lock, event, can_compose)
.await
}
pub async fn cursor(&self) -> Option<M::Id> {
self.0.lock().await.cursor()
}
pub async fn sent(&mut self, id: Option<M::Id>) {
self.0.lock().await.sent(id)
}
}
////////////
// Widget //
////////////
pub struct TreeView<M: Msg, S: MsgStore<M>> {
inner: Arc<Mutex<InnerTreeViewState<M, S>>>,
nick: String,
focused: bool,
}
#[async_trait]
impl<M, S> Widget for TreeView<M, S>
where
M: Msg + ChatMsg + Send + Sync,
M::Id: Send + Sync,
S: MsgStore<M> + Send + Sync,
S::Error: fmt::Display,
{
async fn size(
&self,
_widthdb: &mut WidthDb,
_max_width: Option<u16>,
_max_height: Option<u16>,
) -> Size {
Size::ZERO
}
async fn render(self: Box<Self>, frame: &mut Frame) {
let mut guard = self.inner.lock().await;
let blocks = logging_unwrap!(guard.relayout(self.nick, self.focused, 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,498 +0,0 @@
//! Moving the cursor around.
use std::collections::HashSet;
use crate::store::{Msg, MsgStore, Tree};
use super::{Correction, InnerTreeViewState};
#[derive(Debug, Clone, Copy)]
pub enum Cursor<I> {
Bottom,
Msg(I),
Editor {
coming_from: Option<I>,
parent: Option<I>,
},
Pseudo {
coming_from: Option<I>,
parent: Option<I>,
},
}
impl<I> Cursor<I> {
pub fn editor(coming_from: Option<I>, parent: Option<I>) -> Self {
Self::Editor {
coming_from,
parent,
}
}
}
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 {
parent: Some(parent),
..
}
| Self::Pseudo {
parent: Some(parent),
..
} = self
{
parent == id
} else {
false
}
}
}
impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
fn find_parent(tree: &Tree<M>, id: &mut M::Id) -> bool {
if let Some(parent) = tree.parent(id) {
*id = parent;
true
} else {
false
}
}
fn find_first_child(folded: &HashSet<M::Id>, tree: &Tree<M>, id: &mut M::Id) -> bool {
if folded.contains(id) {
return false;
}
if let Some(child) = tree.children(id).and_then(|c| c.first()) {
*id = child.clone();
true
} else {
false
}
}
fn find_last_child(folded: &HashSet<M::Id>, tree: &Tree<M>, id: &mut M::Id) -> bool {
if folded.contains(id) {
return false;
}
if let Some(child) = tree.children(id).and_then(|c| c.last()) {
*id = child.clone();
true
} else {
false
}
}
/// Move to the previous sibling, or don't move if this is not possible.
///
/// Always stays at the same level of indentation.
async fn find_prev_sibling(
store: &S,
tree: &mut Tree<M>,
id: &mut M::Id,
) -> Result<bool, S::Error> {
let moved = if let Some(prev_sibling) = tree.prev_sibling(id) {
*id = prev_sibling;
true
} else if tree.parent(id).is_none() {
// We're at the root of our tree, so we need to move to the root of
// the previous tree.
if let Some(prev_root_id) = store.prev_root_id(tree.root()).await? {
*tree = store.tree(&prev_root_id).await?;
*id = prev_root_id;
true
} else {
false
}
} else {
false
};
Ok(moved)
}
/// Move to the next sibling, or don't move if this is not possible.
///
/// Always stays at the same level of indentation.
async fn find_next_sibling(
store: &S,
tree: &mut Tree<M>,
id: &mut M::Id,
) -> Result<bool, S::Error> {
let moved = if let Some(next_sibling) = tree.next_sibling(id) {
*id = next_sibling;
true
} else if tree.parent(id).is_none() {
// We're at the root of our tree, so we need to move to the root of
// the next tree.
if let Some(next_root_id) = store.next_root_id(tree.root()).await? {
*tree = store.tree(&next_root_id).await?;
*id = next_root_id;
true
} else {
false
}
} else {
false
};
Ok(moved)
}
/// Move to the previous message, or don't move if this is not possible.
async fn find_prev_msg(
store: &S,
folded: &HashSet<M::Id>,
tree: &mut Tree<M>,
id: &mut M::Id,
) -> Result<bool, S::Error> {
// Move to previous sibling, then to its last child
// If not possible, move to parent
let moved = if Self::find_prev_sibling(store, tree, id).await? {
while Self::find_last_child(folded, tree, id) {}
true
} else {
Self::find_parent(tree, id)
};
Ok(moved)
}
/// Move to the next message, or don't move if this is not possible.
async fn find_next_msg(
store: &S,
folded: &HashSet<M::Id>,
tree: &mut Tree<M>,
id: &mut M::Id,
) -> Result<bool, S::Error> {
if Self::find_first_child(folded, tree, id) {
return Ok(true);
}
if Self::find_next_sibling(store, tree, id).await? {
return Ok(true);
}
// Temporary id to avoid modifying the original one if no parent-sibling
// can be found.
let mut tmp_id = id.clone();
while Self::find_parent(tree, &mut tmp_id) {
if Self::find_next_sibling(store, tree, &mut tmp_id).await? {
*id = tmp_id;
return Ok(true);
}
}
Ok(false)
}
pub async fn move_cursor_up(&mut self) -> Result<(), S::Error> {
match &mut self.cursor {
Cursor::Bottom | Cursor::Pseudo { parent: None, .. } => {
if let Some(last_root_id) = self.store.last_root_id().await? {
let tree = self.store.tree(&last_root_id).await?;
let mut id = last_root_id;
while Self::find_last_child(&self.folded, &tree, &mut id) {}
self.cursor = Cursor::Msg(id);
}
}
Cursor::Msg(msg) => {
let path = self.store.path(msg).await?;
let mut tree = self.store.tree(path.first()).await?;
Self::find_prev_msg(&self.store, &self.folded, &mut tree, msg).await?;
}
Cursor::Editor { .. } => {}
Cursor::Pseudo {
parent: Some(parent),
..
} => {
let tree = self.store.tree(parent).await?;
let mut id = parent.clone();
while Self::find_last_child(&self.folded, &tree, &mut id) {}
self.cursor = Cursor::Msg(id);
}
}
self.correction = Some(Correction::MakeCursorVisible);
Ok(())
}
pub async fn move_cursor_down(&mut self) -> Result<(), S::Error> {
match &mut self.cursor {
Cursor::Msg(msg) => {
let path = self.store.path(msg).await?;
let mut tree = self.store.tree(path.first()).await?;
if !Self::find_next_msg(&self.store, &self.folded, &mut tree, msg).await? {
self.cursor = Cursor::Bottom;
}
}
Cursor::Pseudo { parent: None, .. } => {
self.cursor = Cursor::Bottom;
}
Cursor::Pseudo {
parent: Some(parent),
..
} => {
let mut tree = self.store.tree(parent).await?;
let mut id = parent.clone();
while Self::find_last_child(&self.folded, &tree, &mut id) {}
// Now we're at the previous message
if Self::find_next_msg(&self.store, &self.folded, &mut tree, &mut id).await? {
self.cursor = Cursor::Msg(id);
} else {
self.cursor = Cursor::Bottom;
}
}
_ => {}
}
self.correction = Some(Correction::MakeCursorVisible);
Ok(())
}
pub async fn move_cursor_up_sibling(&mut self) -> Result<(), S::Error> {
match &mut self.cursor {
Cursor::Bottom | Cursor::Pseudo { parent: None, .. } => {
if let Some(last_root_id) = self.store.last_root_id().await? {
self.cursor = Cursor::Msg(last_root_id);
}
}
Cursor::Msg(msg) => {
let path = self.store.path(msg).await?;
let mut tree = self.store.tree(path.first()).await?;
Self::find_prev_sibling(&self.store, &mut tree, msg).await?;
}
Cursor::Editor { .. } => {}
Cursor::Pseudo {
parent: Some(parent),
..
} => {
let path = self.store.path(parent).await?;
let tree = self.store.tree(path.first()).await?;
if let Some(children) = tree.children(parent) {
if let Some(last_child) = children.last() {
self.cursor = Cursor::Msg(last_child.clone());
}
}
}
}
self.correction = Some(Correction::MakeCursorVisible);
Ok(())
}
pub async fn move_cursor_down_sibling(&mut self) -> Result<(), S::Error> {
match &mut self.cursor {
Cursor::Msg(msg) => {
let path = self.store.path(msg).await?;
let mut tree = self.store.tree(path.first()).await?;
if !Self::find_next_sibling(&self.store, &mut tree, msg).await?
&& tree.parent(msg).is_none()
{
self.cursor = Cursor::Bottom;
}
}
Cursor::Pseudo { parent: None, .. } => {
self.cursor = Cursor::Bottom;
}
_ => {}
}
self.correction = Some(Correction::MakeCursorVisible);
Ok(())
}
pub async fn move_cursor_to_parent(&mut self) -> Result<(), S::Error> {
match &mut self.cursor {
Cursor::Pseudo {
parent: Some(parent),
..
} => self.cursor = Cursor::Msg(parent.clone()),
Cursor::Msg(id) => {
// Could also be done via retrieving the path, but it doesn't
// really matter here
let tree = self.store.tree(id).await?;
Self::find_parent(&tree, id);
}
_ => {}
}
self.correction = Some(Correction::MakeCursorVisible);
Ok(())
}
pub async fn move_cursor_to_root(&mut self) -> Result<(), S::Error> {
match &mut self.cursor {
Cursor::Pseudo {
parent: Some(parent),
..
} => {
let path = self.store.path(parent).await?;
self.cursor = Cursor::Msg(path.first().clone());
}
Cursor::Msg(msg) => {
let path = self.store.path(msg).await?;
*msg = path.first().clone();
}
_ => {}
}
self.correction = Some(Correction::MakeCursorVisible);
Ok(())
}
pub async fn move_cursor_older(&mut self) -> Result<(), S::Error> {
match &mut self.cursor {
Cursor::Msg(id) => {
if let Some(prev_id) = self.store.older_msg_id(id).await? {
*id = prev_id;
}
}
Cursor::Bottom | Cursor::Pseudo { .. } => {
if let Some(id) = self.store.newest_msg_id().await? {
self.cursor = Cursor::Msg(id);
}
}
_ => {}
}
self.correction = Some(Correction::MakeCursorVisible);
Ok(())
}
pub async fn move_cursor_newer(&mut self) -> Result<(), S::Error> {
match &mut self.cursor {
Cursor::Msg(id) => {
if let Some(prev_id) = self.store.newer_msg_id(id).await? {
*id = prev_id;
} else {
self.cursor = Cursor::Bottom;
}
}
Cursor::Pseudo { .. } => {
self.cursor = Cursor::Bottom;
}
_ => {}
}
self.correction = Some(Correction::MakeCursorVisible);
Ok(())
}
pub async fn move_cursor_older_unseen(&mut self) -> Result<(), S::Error> {
match &mut self.cursor {
Cursor::Msg(id) => {
if let Some(prev_id) = self.store.older_unseen_msg_id(id).await? {
*id = prev_id;
}
}
Cursor::Bottom | Cursor::Pseudo { .. } => {
if let Some(id) = self.store.newest_unseen_msg_id().await? {
self.cursor = Cursor::Msg(id);
}
}
_ => {}
}
self.correction = Some(Correction::MakeCursorVisible);
Ok(())
}
pub async fn move_cursor_newer_unseen(&mut self) -> Result<(), S::Error> {
match &mut self.cursor {
Cursor::Msg(id) => {
if let Some(prev_id) = self.store.newer_unseen_msg_id(id).await? {
*id = prev_id;
} else {
self.cursor = Cursor::Bottom;
}
}
Cursor::Pseudo { .. } => {
self.cursor = Cursor::Bottom;
}
_ => {}
}
self.correction = Some(Correction::MakeCursorVisible);
Ok(())
}
pub async fn move_cursor_to_top(&mut self) -> Result<(), S::Error> {
if let Some(first_root_id) = self.store.first_root_id().await? {
self.cursor = Cursor::Msg(first_root_id);
self.correction = Some(Correction::MakeCursorVisible);
}
Ok(())
}
pub async fn move_cursor_to_bottom(&mut self) {
self.cursor = Cursor::Bottom;
// Not really necessary; only here for consistency with other methods
self.correction = Some(Correction::MakeCursorVisible);
}
pub fn scroll_up(&mut self, amount: i32) {
self.scroll += amount;
self.correction = Some(Correction::MoveCursorToVisibleArea);
}
pub fn scroll_down(&mut self, amount: i32) {
self.scroll -= amount;
self.correction = Some(Correction::MoveCursorToVisibleArea);
}
pub fn center_cursor(&mut self) {
self.correction = Some(Correction::CenterCursor);
}
/// The outer `Option` shows whether a parent exists or not. The inner
/// `Option` shows if that parent has an id.
pub async fn parent_for_normal_reply(&self) -> Result<Option<Option<M::Id>>, S::Error> {
Ok(match &self.cursor {
Cursor::Bottom => Some(None),
Cursor::Msg(id) => {
let path = self.store.path(id).await?;
let tree = self.store.tree(path.first()).await?;
Some(Some(if tree.next_sibling(id).is_some() {
// A reply to a message that has further siblings should be a
// direct reply. An indirect reply might end up a lot further
// down in the current conversation.
id.clone()
} else if let Some(parent) = tree.parent(id) {
// A reply to a message without younger siblings should be
// an indirect reply so as not to create unnecessarily deep
// threads. In the case that our message has children, this
// might get a bit confusing. I'm not sure yet how well this
// "smart" reply actually works in practice.
parent
} else {
// When replying to a top-level message, it makes sense to avoid
// creating unnecessary new threads.
id.clone()
}))
}
_ => None,
})
}
/// The outer `Option` shows whether a parent exists or not. The inner
/// `Option` shows if that parent has an id.
pub async fn parent_for_alternate_reply(&self) -> Result<Option<Option<M::Id>>, S::Error> {
Ok(match &self.cursor {
Cursor::Bottom => Some(None),
Cursor::Msg(id) => {
let path = self.store.path(id).await?;
let tree = self.store.tree(path.first()).await?;
Some(Some(if tree.next_sibling(id).is_none() {
// The opposite of replying normally
id.clone()
} else if let Some(parent) = tree.parent(id) {
// The opposite of replying normally
parent
} else {
// The same as replying normally, still to avoid creating
// unnecessary new threads
id.clone()
}))
}
_ => None,
})
}
}

View file

@ -1,612 +0,0 @@
use async_recursion::async_recursion;
use toss::Frame;
use crate::store::{Msg, MsgStore, Path, Tree};
use crate::ui::chat::blocks::Block;
use crate::ui::widgets::empty::Empty;
use crate::ui::ChatMsg;
use super::tree_blocks::{BlockId, Root, TreeBlocks};
use super::{widgets, Correction, Cursor, InnerTreeViewState};
const SCROLLOFF: i32 = 2;
const MIN_CONTENT_HEIGHT: i32 = 10;
fn scrolloff(height: i32) -> i32 {
let scrolloff = (height - MIN_CONTENT_HEIGHT).max(0) / 2;
scrolloff.min(SCROLLOFF)
}
struct Context {
nick: String,
focused: bool,
}
impl<M, S> InnerTreeViewState<M, S>
where
M: Msg + ChatMsg + Send + Sync,
M::Id: Send + Sync,
S: MsgStore<M> + Send + Sync,
{
async fn cursor_path(&self, cursor: &Cursor<M::Id>) -> Result<Path<M::Id>, S::Error> {
Ok(match cursor {
Cursor::Msg(id) => self.store.path(id).await?,
Cursor::Bottom
| Cursor::Editor { parent: None, .. }
| Cursor::Pseudo { parent: None, .. } => Path::new(vec![M::last_possible_id()]),
Cursor::Editor {
parent: Some(parent),
..
}
| Cursor::Pseudo {
parent: Some(parent),
..
} => {
let mut path = self.store.path(parent).await?;
path.push(M::last_possible_id());
path
}
})
}
fn make_path_visible(&mut self, path: &Path<M::Id>) {
for segment in path.parent_segments() {
self.folded.remove(segment);
}
}
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 {
blocks
.blocks()
.find(&BlockId::from_cursor(&self.cursor))
.expect("no cursor found")
.top_line
}
}
fn contains_cursor(&self, blocks: &TreeBlocks<M::Id>) -> bool {
blocks
.blocks()
.find(&BlockId::from_cursor(&self.cursor))
.is_some()
}
async fn editor_block(
&self,
context: &Context,
frame: &mut Frame,
indent: usize,
) -> Block<BlockId<M::Id>> {
let (widget, cursor_row) =
widgets::editor::<M>(frame.widthdb(), indent, &context.nick, &self.editor);
let cursor_row = cursor_row as i32;
Block::new(frame, BlockId::Cursor, widget)
.await
.focus(cursor_row..cursor_row + 1)
}
async fn pseudo_block(
&self,
context: &Context,
frame: &mut Frame,
indent: usize,
) -> Block<BlockId<M::Id>> {
let widget = widgets::pseudo::<M>(indent, &context.nick, &self.editor);
Block::new(frame, BlockId::Cursor, widget).await
}
#[async_recursion]
async fn layout_subtree(
&self,
context: &Context,
frame: &mut Frame,
tree: &Tree<M>,
indent: usize,
id: &M::Id,
blocks: &mut TreeBlocks<M::Id>,
) {
// 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::new()).await;
blocks.blocks_mut().push_back(block);
}
// Last part of message body if message is folded
let folded = self.folded.contains(id);
let folded_info = if folded {
Some(tree.subtree_size(id)).filter(|s| *s > 0)
} else {
None
};
// Main message body
let highlighted = context.focused && self.cursor.refers_to(id);
let widget = if let Some(msg) = tree.msg(id) {
widgets::msg(highlighted, indent, msg, folded_info)
} else {
widgets::msg_placeholder(highlighted, indent, folded_info)
};
let block = Block::new(frame, BlockId::Msg(id.clone()), widget).await;
blocks.blocks_mut().push_back(block);
// Children, recursively
if !folded {
if let Some(children) = tree.children(id) {
for child in children {
self.layout_subtree(context, frame, tree, indent + 1, child, blocks)
.await;
}
}
}
// 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::new()).await;
blocks.blocks_mut().push_back(block);
}
// Trailing editor or pseudomessage
if self.cursor.refers_to_last_child_of(id) {
match self.cursor {
Cursor::Editor { .. } => blocks
.blocks_mut()
.push_back(self.editor_block(context, frame, indent + 1).await),
Cursor::Pseudo { .. } => blocks
.blocks_mut()
.push_back(self.pseudo_block(context, frame, indent + 1).await),
_ => {}
}
}
}
async fn layout_tree(
&self,
context: &Context,
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(context, frame, &tree, 0, tree.root(), &mut blocks)
.await;
blocks
}
async fn layout_bottom(&self, context: &Context, frame: &mut Frame) -> TreeBlocks<M::Id> {
let mut blocks = TreeBlocks::new(Root::Bottom, Root::Bottom);
// Ghost cursor, for positioning according to last cursor line
if let Cursor::Editor { parent: None, .. } | Cursor::Pseudo { parent: None, .. } =
self.last_cursor
{
let block = Block::new(frame, BlockId::LastCursor, Empty::new()).await;
blocks.blocks_mut().push_back(block);
}
match self.cursor {
Cursor::Bottom => {
let block = Block::new(frame, BlockId::Cursor, Empty::new()).await;
blocks.blocks_mut().push_back(block);
}
Cursor::Editor { parent: None, .. } => blocks
.blocks_mut()
.push_back(self.editor_block(context, frame, 0).await),
Cursor::Pseudo { parent: None, .. } => blocks
.blocks_mut()
.push_back(self.pseudo_block(context, frame, 0).await),
_ => {}
}
blocks
}
async fn expand_to_top(
&self,
context: &Context,
frame: &mut Frame,
blocks: &mut TreeBlocks<M::Id>,
) -> Result<(), S::Error> {
let top_line = 0;
while blocks.blocks().top_line > top_line {
let top_root = blocks.top_root();
let prev_root_id = match top_root {
Root::Bottom => self.store.last_root_id().await?,
Root::Tree(root_id) => self.store.prev_root_id(root_id).await?,
};
let prev_root_id = match prev_root_id {
Some(id) => id,
None => break,
};
let prev_tree = self.store.tree(&prev_root_id).await?;
blocks.prepend(self.layout_tree(context, frame, prev_tree).await);
}
Ok(())
}
async fn expand_to_bottom(
&self,
context: &Context,
frame: &mut Frame,
blocks: &mut TreeBlocks<M::Id>,
) -> Result<(), S::Error> {
let bottom_line = frame.size().height as i32 - 1;
while blocks.blocks().bottom_line < bottom_line {
let bottom_root = blocks.bottom_root();
let next_root_id = match bottom_root {
Root::Bottom => break,
Root::Tree(root_id) => self.store.next_root_id(root_id).await?,
};
if let Some(next_root_id) = next_root_id {
let next_tree = self.store.tree(&next_root_id).await?;
blocks.append(self.layout_tree(context, frame, next_tree).await);
} else {
blocks.append(self.layout_bottom(context, frame).await);
}
}
Ok(())
}
async fn fill_screen_and_clamp_scrolling(
&self,
context: &Context,
frame: &mut Frame,
blocks: &mut TreeBlocks<M::Id>,
) -> Result<(), S::Error> {
let top_line = 0;
let bottom_line = frame.size().height as i32 - 1;
self.expand_to_top(context, frame, blocks).await?;
if blocks.blocks().top_line > top_line {
blocks.blocks_mut().set_top_line(0);
}
self.expand_to_bottom(context, frame, blocks).await?;
if blocks.blocks().bottom_line < bottom_line {
blocks.blocks_mut().set_bottom_line(bottom_line);
}
self.expand_to_top(context, frame, blocks).await?;
Ok(())
}
async fn layout_last_cursor_seed(
&self,
context: &Context,
frame: &mut Frame,
last_cursor_path: &Path<M::Id>,
) -> Result<TreeBlocks<M::Id>, S::Error> {
Ok(match &self.last_cursor {
Cursor::Bottom => {
let mut blocks = self.layout_bottom(context, frame).await;
let bottom_line = frame.size().height as i32 - 1;
blocks.blocks_mut().set_bottom_line(bottom_line);
blocks
}
Cursor::Editor { parent: None, .. } | Cursor::Pseudo { parent: None, .. } => {
let mut blocks = self.layout_bottom(context, frame).await;
blocks
.blocks_mut()
.recalculate_offsets(&BlockId::LastCursor, self.last_cursor_line);
blocks
}
Cursor::Msg(_)
| Cursor::Editor {
parent: Some(_), ..
}
| Cursor::Pseudo {
parent: Some(_), ..
} => {
let root = last_cursor_path.first();
let tree = self.store.tree(root).await?;
let mut blocks = self.layout_tree(context, frame, tree).await;
blocks
.blocks_mut()
.recalculate_offsets(&BlockId::LastCursor, self.last_cursor_line);
blocks
}
})
}
async fn layout_cursor_seed(
&self,
context: &Context,
frame: &mut Frame,
last_cursor_path: &Path<M::Id>,
cursor_path: &Path<M::Id>,
) -> Result<TreeBlocks<M::Id>, S::Error> {
let bottom_line = frame.size().height as i32 - 1;
Ok(match &self.cursor {
Cursor::Bottom
| Cursor::Editor { parent: None, .. }
| Cursor::Pseudo { parent: None, .. } => {
let mut blocks = self.layout_bottom(context, frame).await;
blocks.blocks_mut().set_bottom_line(bottom_line);
blocks
}
Cursor::Msg(_)
| Cursor::Editor {
parent: Some(_), ..
}
| Cursor::Pseudo {
parent: Some(_), ..
} => {
let root = cursor_path.first();
let tree = self.store.tree(root).await?;
let mut blocks = self.layout_tree(context, frame, tree).await;
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,
context: &Context,
frame: &mut Frame,
last_cursor_path: &Path<M::Id>,
cursor_path: &Path<M::Id>,
) -> Result<TreeBlocks<M::Id>, S::Error> {
if let Cursor::Bottom = self.cursor {
self.layout_cursor_seed(context, frame, last_cursor_path, cursor_path)
.await
} else {
self.layout_last_cursor_seed(context, frame, last_cursor_path)
.await
}
}
fn scroll_so_cursor_is_visible(&self, frame: &Frame, blocks: &mut TreeBlocks<M::Id>) {
if matches!(self.cursor, Cursor::Bottom) {
return; // Cursor is locked to bottom
}
let block = blocks
.blocks()
.find(&BlockId::from_cursor(&self.cursor))
.expect("no cursor found");
let height = frame.size().height as i32;
let scrolloff = scrolloff(height);
let min_line = -block.focus.start + scrolloff;
let max_line = height - block.focus.end - scrolloff;
// If the message is higher than the available space, the top of the
// message should always be visible. I'm not using top_line.clamp(...)
// because the order of the min and max matters.
let top_line = block.top_line;
#[allow(clippy::manual_clamp)]
let new_top_line = top_line.min(max_line).max(min_line);
if new_top_line != top_line {
blocks.blocks_mut().offset(new_top_line - top_line);
}
}
fn scroll_so_cursor_is_centered(&self, frame: &Frame, blocks: &mut TreeBlocks<M::Id>) {
if matches!(self.cursor, Cursor::Bottom) {
return; // Cursor is locked to bottom
}
let block = blocks
.blocks()
.find(&BlockId::from_cursor(&self.cursor))
.expect("no cursor found");
let height = frame.size().height as i32;
let scrolloff = scrolloff(height);
let min_line = -block.focus.start + scrolloff;
let max_line = height - block.focus.end - scrolloff;
// If the message is higher than the available space, the top of the
// message should always be visible. I'm not using top_line.clamp(...)
// because the order of the min and max matters.
let top_line = block.top_line;
let new_top_line = (height - block.height) / 2;
#[allow(clippy::manual_clamp)]
let new_top_line = new_top_line.min(max_line).max(min_line);
if new_top_line != top_line {
blocks.blocks_mut().offset(new_top_line - top_line);
}
}
/// Try to obtain a [`Cursor::Msg`] pointing to the block.
fn msg_id(block: &Block<BlockId<M::Id>>) -> Option<M::Id> {
match &block.id {
BlockId::Msg(id) => Some(id.clone()),
_ => None,
}
}
fn visible(block: &Block<BlockId<M::Id>>, first_line: i32, last_line: i32) -> bool {
(first_line + 1 - block.height..=last_line).contains(&block.top_line)
}
fn move_cursor_so_it_is_visible(
&mut self,
frame: &Frame,
blocks: &TreeBlocks<M::Id>,
) -> Option<M::Id> {
if !matches!(self.cursor, Cursor::Bottom | Cursor::Msg(_)) {
// In all other cases, there is no need to make the cursor visible
// since scrolling behaves differently enough.
return None;
}
let height = frame.size().height as i32;
let scrolloff = scrolloff(height);
let first_line = scrolloff;
let last_line = height - 1 - scrolloff;
let new_cursor = if matches!(self.cursor, Cursor::Bottom) {
blocks
.blocks()
.iter()
.rev()
.filter(|b| Self::visible(b, first_line, last_line))
.find_map(Self::msg_id)
} else {
let block = blocks
.blocks()
.find(&BlockId::from_cursor(&self.cursor))
.expect("no cursor found");
if Self::visible(block, first_line, last_line) {
return None;
} else if block.top_line < first_line {
blocks
.blocks()
.iter()
.filter(|b| Self::visible(b, first_line, last_line))
.find_map(Self::msg_id)
} else {
blocks
.blocks()
.iter()
.rev()
.filter(|b| Self::visible(b, first_line, last_line))
.find_map(Self::msg_id)
}
};
if let Some(id) = new_cursor {
self.cursor = Cursor::Msg(id.clone());
Some(id)
} else {
None
}
}
fn visible_msgs(frame: &Frame, blocks: &TreeBlocks<M::Id>) -> Vec<M::Id> {
let height: i32 = frame.size().height.into();
let first_line = 0;
let last_line = first_line + height - 1;
let mut result = vec![];
for block in blocks.blocks().iter() {
if Self::visible(block, first_line, last_line) {
if let BlockId::Msg(id) = &block.id {
result.push(id.clone());
}
}
}
result
}
pub async fn relayout(
&mut self,
nick: String,
focused: bool,
frame: &mut Frame,
) -> Result<TreeBlocks<M::Id>, S::Error> {
// The basic idea is this:
//
// First, layout a full screen of blocks around self.last_cursor, using
// self.last_cursor_line for offset positioning. At this point, any
// outstanding scrolling is performed as well.
//
// 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 context = Context { nick, focused };
let last_cursor_path = self.cursor_path(&self.last_cursor).await?;
let cursor_path = self.cursor_path(&self.cursor).await?;
self.make_path_visible(&cursor_path);
let mut blocks = self
.layout_initial_seed(&context, frame, &last_cursor_path, &cursor_path)
.await?;
blocks.blocks_mut().offset(self.scroll);
self.fill_screen_and_clamp_scrolling(&context, frame, &mut blocks)
.await?;
if !self.contains_cursor(&blocks) {
blocks = self
.layout_cursor_seed(&context, frame, &last_cursor_path, &cursor_path)
.await?;
self.fill_screen_and_clamp_scrolling(&context, frame, &mut blocks)
.await?;
}
match self.correction {
Some(Correction::MakeCursorVisible) => {
self.scroll_so_cursor_is_visible(frame, &mut blocks);
self.fill_screen_and_clamp_scrolling(&context, frame, &mut blocks)
.await?;
}
Some(Correction::MoveCursorToVisibleArea) => {
let new_cursor_msg_id = self.move_cursor_so_it_is_visible(frame, &blocks);
if let Some(cursor_msg_id) = new_cursor_msg_id {
// Moving the cursor invalidates our current blocks, so we sadly
// have to either perform an expensive operation or redraw the
// entire thing. I'm choosing the latter for now.
self.last_cursor = self.cursor.clone();
self.last_cursor_line = self.cursor_line(&blocks);
self.last_visible_msgs = Self::visible_msgs(frame, &blocks);
self.scroll = 0;
self.correction = None;
let last_cursor_path = self.store.path(&cursor_msg_id).await?;
blocks = self
.layout_last_cursor_seed(&context, frame, &last_cursor_path)
.await?;
self.fill_screen_and_clamp_scrolling(&context, frame, &mut blocks)
.await?;
}
}
Some(Correction::CenterCursor) => {
self.scroll_so_cursor_is_centered(frame, &mut blocks);
self.fill_screen_and_clamp_scrolling(&context, frame, &mut blocks)
.await?;
}
None => {}
}
self.last_cursor = self.cursor.clone();
self.last_cursor_line = self.cursor_line(&blocks);
self.last_visible_msgs = Self::visible_msgs(frame, &blocks);
self.scroll = 0;
self.correction = None;
Ok(blocks)
}
}

View file

@ -1,71 +0,0 @@
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,
}
}
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 bottom_root(&self) -> &Root<I> {
&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

@ -1,165 +0,0 @@
mod indent;
mod seen;
mod time;
use crossterm::style::Stylize;
use toss::{Style, Styled, WidthDb};
use super::super::ChatMsg;
use crate::store::Msg;
use crate::ui::widgets::editor::EditorState;
use crate::ui::widgets::join::{HJoin, Segment};
use crate::ui::widgets::layer::Layer;
use crate::ui::widgets::padding::Padding;
use crate::ui::widgets::text::Text;
use crate::ui::widgets::BoxedWidget;
use self::indent::Indent;
pub const PLACEHOLDER: &str = "[...]";
pub fn style_placeholder() -> Style {
Style::new().dark_grey()
}
fn style_time(highlighted: bool) -> Style {
if highlighted {
Style::new().black().on_white()
} else {
Style::new().grey()
}
}
fn style_indent(highlighted: bool) -> Style {
if highlighted {
Style::new().black().on_white()
} else {
Style::new().dark_grey()
}
}
fn style_info() -> Style {
Style::new().italic().dark_grey()
}
fn style_editor_highlight() -> Style {
Style::new().black().on_cyan()
}
fn style_pseudo_highlight() -> Style {
Style::new().black().on_yellow()
}
pub fn msg<M: Msg + ChatMsg>(
highlighted: bool,
indent: usize,
msg: &M,
folded_info: Option<usize>,
) -> BoxedWidget {
let (nick, mut content) = msg.styled();
if let Some(amount) = folded_info {
content = content
.then_plain("\n")
.then(format!("[{amount} more]"), style_info());
}
HJoin::new(vec![
Segment::new(seen::widget(msg.seen())),
Segment::new(
Padding::new(time::widget(Some(msg.time()), style_time(highlighted)))
.stretch(true)
.right(1),
),
Segment::new(Indent::new(indent, style_indent(highlighted))),
Segment::new(Layer::new(vec![
Padding::new(Indent::new(1, style_indent(false)))
.top(1)
.into(),
Padding::new(Text::new(nick)).right(1).into(),
])),
// TODO Minimum content width
// TODO Minimizing and maximizing messages
Segment::new(Text::new(content).wrap(true)).priority(1),
])
.into()
}
pub fn msg_placeholder(
highlighted: bool,
indent: usize,
folded_info: Option<usize>,
) -> BoxedWidget {
let mut content = Styled::new(PLACEHOLDER, style_placeholder());
if let Some(amount) = folded_info {
content = content
.then_plain("\n")
.then(format!("[{amount} more]"), style_info());
}
HJoin::new(vec![
Segment::new(seen::widget(true)),
Segment::new(
Padding::new(time::widget(None, style_time(highlighted)))
.stretch(true)
.right(1),
),
Segment::new(Indent::new(indent, style_indent(highlighted))),
Segment::new(Text::new(content)),
])
.into()
}
pub fn editor<M: ChatMsg>(
widthdb: &mut WidthDb,
indent: usize,
nick: &str,
editor: &EditorState,
) -> (BoxedWidget, usize) {
let (nick, content) = M::edit(nick, &editor.text());
let editor = editor.widget().highlight(|_| content);
let cursor_row = editor.cursor_row(widthdb);
let widget = HJoin::new(vec![
Segment::new(seen::widget(true)),
Segment::new(
Padding::new(time::widget(None, style_editor_highlight()))
.stretch(true)
.right(1),
),
Segment::new(Indent::new(indent, style_editor_highlight())),
Segment::new(Layer::new(vec![
Padding::new(Indent::new(1, style_indent(false)))
.top(1)
.into(),
Padding::new(Text::new(nick)).right(1).into(),
])),
Segment::new(editor).priority(1).expanding(true),
])
.into();
(widget, cursor_row)
}
pub fn pseudo<M: ChatMsg>(indent: usize, nick: &str, editor: &EditorState) -> BoxedWidget {
let (nick, content) = M::edit(nick, &editor.text());
HJoin::new(vec![
Segment::new(seen::widget(true)),
Segment::new(
Padding::new(time::widget(None, style_pseudo_highlight()))
.stretch(true)
.right(1),
),
Segment::new(Indent::new(indent, style_pseudo_highlight())),
Segment::new(Layer::new(vec![
Padding::new(Indent::new(1, style_indent(false)))
.top(1)
.into(),
Padding::new(Text::new(nick)).right(1).into(),
])),
Segment::new(Text::new(content).wrap(true)).priority(1),
])
.into()
}

View file

@ -1,41 +0,0 @@
use async_trait::async_trait;
use toss::{Frame, Pos, Size, Style, WidthDb};
use crate::ui::widgets::Widget;
pub const INDENT: &str = "";
pub const INDENT_WIDTH: usize = 2;
pub struct Indent {
level: usize,
style: Style,
}
impl Indent {
pub fn new(level: usize, style: Style) -> Self {
Self { level, style }
}
}
#[async_trait]
impl Widget for Indent {
async fn size(
&self,
_widthdb: &mut WidthDb,
_max_width: Option<u16>,
_max_height: Option<u16>,
) -> Size {
Size::new((INDENT_WIDTH * self.level) as u16, 0)
}
async fn render(self: Box<Self>, frame: &mut Frame) {
let size = frame.size();
for y in 0..size.height {
frame.write(
Pos::new(0, y.into()),
(INDENT.repeat(self.level), self.style),
)
}
}
}

View file

@ -1,25 +0,0 @@
use crossterm::style::Stylize;
use toss::Style;
use crate::ui::widgets::background::Background;
use crate::ui::widgets::empty::Empty;
use crate::ui::widgets::text::Text;
use crate::ui::widgets::BoxedWidget;
const UNSEEN: &str = "*";
const WIDTH: u16 = 1;
fn seen_style() -> Style {
Style::new().black().on_green()
}
pub fn widget(seen: bool) -> BoxedWidget {
if seen {
Empty::new().width(WIDTH).into()
} else {
let style = seen_style();
Background::new(Text::new((UNSEEN, style)))
.style(style)
.into()
}
}

View file

@ -1,25 +0,0 @@
use time::format_description::FormatItem;
use time::macros::format_description;
use time::OffsetDateTime;
use toss::Style;
use crate::ui::widgets::background::Background;
use crate::ui::widgets::empty::Empty;
use crate::ui::widgets::text::Text;
use crate::ui::widgets::BoxedWidget;
const TIME_FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day] [hour]:[minute]");
const TIME_WIDTH: u16 = 16;
pub fn widget(time: Option<OffsetDateTime>, style: Style) -> BoxedWidget {
if let Some(time) = time {
let text = time.format(TIME_FORMAT).expect("could not format time");
Background::new(Text::new((text, style)))
.style(style)
.into()
} else {
Background::new(Empty::new().width(TIME_WIDTH))
.style(style)
.into()
}
}

View file

@ -1,166 +0,0 @@
use std::io;
use std::sync::Arc;
use parking_lot::FairMutex;
use toss::Terminal;
use super::input::{key, InputEvent, KeyBindingsList};
use super::widgets::editor::EditorState;
use super::widgets::list::ListState;
pub fn prompt(
terminal: &mut Terminal,
crossterm_lock: &Arc<FairMutex<()>>,
initial_text: &str,
) -> io::Result<String> {
let content = {
let _guard = crossterm_lock.lock();
terminal.suspend().expect("could not suspend");
let content = edit::edit(initial_text);
terminal.unsuspend().expect("could not unsuspend");
content
};
content
}
//////////
// List //
//////////
pub fn list_list_key_bindings(bindings: &mut KeyBindingsList) {
bindings.binding("j/k, ↓/↑", "move cursor up/down");
bindings.binding("g, home", "move cursor to top");
bindings.binding("G, end", "move cursor to bottom");
bindings.binding("ctrl+y/e", "scroll up/down");
}
pub fn handle_list_input_event<Id: Clone>(list: &mut ListState<Id>, event: &InputEvent) -> bool {
match event {
key!('k') | key!(Up) => list.move_cursor_up(),
key!('j') | key!(Down) => list.move_cursor_down(),
key!('g') | key!(Home) => list.move_cursor_to_top(),
key!('G') | key!(End) => list.move_cursor_to_bottom(),
key!(Ctrl + 'y') => list.scroll_up(1),
key!(Ctrl + 'e') => list.scroll_down(1),
_ => return false,
}
true
}
////////////
// Editor //
////////////
fn list_editor_editing_key_bindings(
bindings: &mut KeyBindingsList,
char_filter: impl Fn(char) -> bool,
) {
if char_filter('\n') {
bindings.binding("enter+<any modifier>", "insert newline");
}
bindings.binding("ctrl+h, backspace", "delete before cursor");
bindings.binding("ctrl+d, delete", "delete after cursor");
bindings.binding("ctrl+l", "clear editor contents");
}
fn list_editor_cursor_movement_key_bindings(bindings: &mut KeyBindingsList) {
bindings.binding("ctrl+b, ←", "move cursor left");
bindings.binding("ctrl+f, →", "move cursor right");
bindings.binding("alt+b, ctrl+←", "move cursor left a word");
bindings.binding("alt+f, ctrl+→", "move cursor right a word");
bindings.binding("ctrl+a, home", "move cursor to start of line");
bindings.binding("ctrl+e, end", "move cursor to end of line");
bindings.binding("↑/↓", "move cursor up/down");
}
pub fn list_editor_key_bindings(
bindings: &mut KeyBindingsList,
char_filter: impl Fn(char) -> bool,
) {
list_editor_editing_key_bindings(bindings, char_filter);
bindings.empty();
list_editor_cursor_movement_key_bindings(bindings);
}
pub fn handle_editor_input_event(
editor: &EditorState,
terminal: &mut Terminal,
event: &InputEvent,
char_filter: impl Fn(char) -> bool,
) -> bool {
match event {
// Enter with *any* modifier pressed - if ctrl and shift don't
// work, maybe alt does
key!(Enter) => return false,
InputEvent::Key(crate::ui::input::KeyEvent {
code: crossterm::event::KeyCode::Enter,
..
}) if char_filter('\n') => editor.insert_char(terminal.widthdb(), '\n'),
// Editing
key!(Char ch) if char_filter(*ch) => editor.insert_char(terminal.widthdb(), *ch),
key!(Paste str) => {
// It seems that when pasting, '\n' are converted into '\r' for some
// reason. I don't really know why, or at what point this happens.
// Vim converts any '\r' pasted via the terminal into '\n', so I
// decided to mirror that behaviour.
let str = str.replace('\r', "\n");
if str.chars().all(char_filter) {
editor.insert_str(terminal.widthdb(), &str);
} else {
return false;
}
}
key!(Ctrl + 'h') | key!(Backspace) => editor.backspace(terminal.widthdb()),
key!(Ctrl + 'd') | key!(Delete) => editor.delete(),
key!(Ctrl + 'l') => editor.clear(),
// TODO Key bindings to delete words
// Cursor movement
key!(Ctrl + 'b') | key!(Left) => editor.move_cursor_left(terminal.widthdb()),
key!(Ctrl + 'f') | key!(Right) => editor.move_cursor_right(terminal.widthdb()),
key!(Alt + 'b') | key!(Ctrl + Left) => editor.move_cursor_left_a_word(terminal.widthdb()),
key!(Alt + 'f') | key!(Ctrl + Right) => editor.move_cursor_right_a_word(terminal.widthdb()),
key!(Ctrl + 'a') | key!(Home) => editor.move_cursor_to_start_of_line(terminal.widthdb()),
key!(Ctrl + 'e') | key!(End) => editor.move_cursor_to_end_of_line(terminal.widthdb()),
key!(Up) => editor.move_cursor_up(terminal.widthdb()),
key!(Down) => editor.move_cursor_down(terminal.widthdb()),
_ => return false,
}
true
}
pub fn list_editor_key_bindings_allowing_external_editing(
bindings: &mut KeyBindingsList,
char_filter: impl Fn(char) -> bool,
) {
list_editor_editing_key_bindings(bindings, char_filter);
bindings.binding("ctrl+x", "edit in external editor");
bindings.empty();
list_editor_cursor_movement_key_bindings(bindings);
}
pub fn handle_editor_input_event_allowing_external_editing(
editor: &EditorState,
terminal: &mut Terminal,
crossterm_lock: &Arc<FairMutex<()>>,
event: &InputEvent,
char_filter: impl Fn(char) -> bool,
) -> io::Result<bool> {
if let key!(Ctrl + 'x') = event {
editor.edit_externally(terminal, crossterm_lock)?;
Ok(true)
} else {
Ok(handle_editor_input_event(
editor,
terminal,
event,
char_filter,
))
}
}

View file

@ -1,109 +0,0 @@
// Since the widget module is effectively a library and will probably be moved
// to toss later, warnings about unused functions are mostly inaccurate.
// TODO Restrict this a bit more?
#![allow(dead_code)]
pub mod background;
pub mod border;
pub mod cursor;
pub mod editor;
pub mod empty;
pub mod float;
pub mod join;
pub mod layer;
pub mod list;
pub mod padding;
pub mod popup;
pub mod resize;
pub mod rules;
pub mod text;
use async_trait::async_trait;
use toss::{AsyncWidget, Frame, Size, WidthDb};
use super::UiError;
// TODO Add Error type and return Result-s (at least in Widget::render)
#[async_trait]
pub trait Widget {
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Size;
async fn render(self: Box<Self>, frame: &mut Frame);
}
pub type BoxedWidget = Box<dyn Widget + Send + Sync>;
impl<W: 'static + Widget + Send + Sync> From<W> for BoxedWidget {
fn from(widget: W) -> Self {
Box::new(widget)
}
}
/// Wrapper that implements [`Widget`] for an [`AsyncWidget`].
pub struct AsyncWidgetWrapper<I> {
inner: I,
}
impl<I> AsyncWidgetWrapper<I> {
pub fn new(inner: I) -> Self {
Self { inner }
}
}
#[async_trait]
impl<I> Widget for AsyncWidgetWrapper<I>
where
I: AsyncWidget<UiError> + Send + Sync,
{
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Size {
self.inner
.size(widthdb, max_width, max_height)
.await
.unwrap()
}
async fn render(self: Box<Self>, frame: &mut Frame) {
self.inner.draw(frame).await.unwrap();
}
}
/// Wrapper that implements [`AsyncWidget`] for a [`Widget`].
pub struct WidgetWrapper {
inner: BoxedWidget,
}
impl WidgetWrapper {
pub fn new<W: Into<BoxedWidget>>(inner: W) -> Self {
Self {
inner: inner.into(),
}
}
}
#[async_trait]
impl<E> AsyncWidget<E> for WidgetWrapper {
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Result<Size, E> {
Ok(self.inner.size(widthdb, max_width, max_height).await)
}
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
self.inner.render(frame).await;
Ok(())
}
}

View file

@ -1,46 +0,0 @@
use async_trait::async_trait;
use toss::{Frame, Pos, Size, Style, WidthDb};
use super::{BoxedWidget, Widget};
pub struct Background {
inner: BoxedWidget,
style: Style,
}
impl Background {
pub fn new<W: Into<BoxedWidget>>(inner: W) -> Self {
Self {
inner: inner.into(),
style: Style::new().opaque(),
}
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
}
#[async_trait]
impl Widget for Background {
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Size {
self.inner.size(widthdb, max_width, max_height).await
}
async fn render(self: Box<Self>, frame: &mut Frame) {
let size = frame.size();
for dy in 0..size.height {
for dx in 0..size.width {
frame.write(Pos::new(dx.into(), dy.into()), (" ", self.style));
}
}
self.inner.render(frame).await;
}
}

View file

@ -1,65 +0,0 @@
use async_trait::async_trait;
use toss::{Frame, Pos, Size, Style, WidthDb};
use super::{BoxedWidget, Widget};
pub struct Border {
inner: BoxedWidget,
style: Style,
}
impl Border {
pub fn new<W: Into<BoxedWidget>>(inner: W) -> Self {
Self {
inner: inner.into(),
style: Style::new(),
}
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
}
#[async_trait]
impl Widget for Border {
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Size {
let max_width = max_width.map(|w| w.saturating_sub(2));
let max_height = max_height.map(|h| h.saturating_sub(2));
let size = self.inner.size(widthdb, max_width, max_height).await;
size + Size::new(2, 2)
}
async fn render(self: Box<Self>, frame: &mut Frame) {
let mut size = frame.size();
size.width = size.width.max(2);
size.height = size.height.max(2);
let right = size.width as i32 - 1;
let bottom = size.height as i32 - 1;
frame.write(Pos::new(0, 0), ("", self.style));
frame.write(Pos::new(right, 0), ("", self.style));
frame.write(Pos::new(0, bottom), ("", self.style));
frame.write(Pos::new(right, bottom), ("", self.style));
for y in 1..bottom {
frame.write(Pos::new(0, y), ("", self.style));
frame.write(Pos::new(right, y), ("", self.style));
}
for x in 1..right {
frame.write(Pos::new(x, 0), ("", self.style));
frame.write(Pos::new(x, bottom), ("", self.style));
}
frame.push(Pos::new(1, 1), size - Size::new(2, 2));
self.inner.render(frame).await;
frame.pop();
}
}

View file

@ -1,44 +0,0 @@
use async_trait::async_trait;
use toss::{Frame, Pos, Size, WidthDb};
use super::{BoxedWidget, Widget};
pub struct Cursor {
inner: BoxedWidget,
pos: Pos,
}
impl Cursor {
pub fn new<W: Into<BoxedWidget>>(inner: W) -> Self {
Self {
inner: inner.into(),
pos: Pos::ZERO,
}
}
pub fn at(mut self, pos: Pos) -> Self {
self.pos = pos;
self
}
pub fn at_xy(self, x: i32, y: i32) -> Self {
self.at(Pos::new(x, y))
}
}
#[async_trait]
impl Widget for Cursor {
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Size {
self.inner.size(widthdb, max_width, max_height).await
}
async fn render(self: Box<Self>, frame: &mut Frame) {
self.inner.render(frame).await;
frame.set_cursor(Some(self.pos));
}
}

View file

@ -1,566 +0,0 @@
use std::sync::Arc;
use std::{io, iter};
use async_trait::async_trait;
use crossterm::style::Stylize;
use parking_lot::{FairMutex, Mutex};
use toss::{Frame, Pos, Size, Style, Styled, Terminal, WidthDb};
use unicode_segmentation::UnicodeSegmentation;
use crate::ui::util;
use super::text::Text;
use super::Widget;
/// Like [`WidthDb::wrap`] but includes a final break index if the text ends
/// with a newline.
fn wrap(widthdb: &mut WidthDb, text: &str, width: usize) -> Vec<usize> {
let mut breaks = widthdb.wrap(text, width);
if text.ends_with('\n') {
breaks.push(text.len())
}
breaks
}
///////////
// State //
///////////
struct InnerEditorState {
text: String,
/// Index of the cursor in the text.
///
/// Must point to a valid grapheme boundary.
idx: usize,
/// Column of the cursor on the screen just after it was last moved
/// horizontally.
col: usize,
/// Width of the text when the editor was last rendered.
///
/// Does not include additional column for cursor.
last_width: u16,
}
impl InnerEditorState {
fn new(text: String) -> Self {
Self {
idx: text.len(),
col: 0,
last_width: u16::MAX,
text,
}
}
///////////////////////////////
// Grapheme helper functions //
///////////////////////////////
fn grapheme_boundaries(&self) -> Vec<usize> {
self.text
.grapheme_indices(true)
.map(|(i, _)| i)
.chain(iter::once(self.text.len()))
.collect()
}
/// Ensure the cursor index lies on a grapheme boundary. If it doesn't, it
/// is moved to the next grapheme boundary.
///
/// Can handle arbitrary cursor index.
fn move_cursor_to_grapheme_boundary(&mut self) {
for i in self.grapheme_boundaries() {
#[allow(clippy::comparison_chain)]
if i == self.idx {
// We're at a valid grapheme boundary already
return;
} else if i > self.idx {
// There was no valid grapheme boundary at our cursor index, so
// we'll take the next one we can get.
self.idx = i;
return;
}
}
// The cursor was out of bounds, so move it to the last valid index.
self.idx = self.text.len();
}
///////////////////////////////
// Line/col helper functions //
///////////////////////////////
/// Like [`Self::grapheme_boundaries`] but for lines.
///
/// Note that the last line can have a length of 0 if the text ends with a
/// newline.
fn line_boundaries(&self) -> Vec<usize> {
let newlines = self
.text
.char_indices()
.filter(|(_, c)| *c == '\n')
.map(|(i, _)| i + 1); // utf-8 encodes '\n' as a single byte
iter::once(0)
.chain(newlines)
.chain(iter::once(self.text.len()))
.collect()
}
/// Find the cursor's current line.
///
/// Returns `(line_nr, start_idx, end_idx)`.
fn cursor_line(&self, boundaries: &[usize]) -> (usize, usize, usize) {
let mut result = (0, 0, 0);
for (i, (start, end)) in boundaries.iter().zip(boundaries.iter().skip(1)).enumerate() {
if self.idx >= *start {
result = (i, *start, *end);
} else {
break;
}
}
result
}
fn cursor_col(&self, widthdb: &mut WidthDb, line_start: usize) -> usize {
widthdb.width(&self.text[line_start..self.idx])
}
fn line(&self, line: usize) -> (usize, usize) {
let boundaries = self.line_boundaries();
boundaries
.iter()
.copied()
.zip(boundaries.iter().copied().skip(1))
.nth(line)
.expect("line exists")
}
fn move_cursor_to_line_col(&mut self, widthdb: &mut WidthDb, line: usize, col: usize) {
let (start, end) = self.line(line);
let line = &self.text[start..end];
let mut width = 0;
for (gi, g) in line.grapheme_indices(true) {
self.idx = start + gi;
if col > width {
width += widthdb.grapheme_width(g, width) as usize;
} else {
return;
}
}
if !line.ends_with('\n') {
self.idx = end;
}
}
fn record_cursor_col(&mut self, widthdb: &mut WidthDb) {
let boundaries = self.line_boundaries();
let (_, start, _) = self.cursor_line(&boundaries);
self.col = self.cursor_col(widthdb, start);
}
/////////////
// Editing //
/////////////
fn clear(&mut self) {
self.text = String::new();
self.idx = 0;
self.col = 0;
}
fn set_text(&mut self, widthdb: &mut WidthDb, text: String) {
self.text = text;
self.move_cursor_to_grapheme_boundary();
self.record_cursor_col(widthdb);
}
/// Insert a character at the current cursor position and move the cursor
/// accordingly.
fn insert_char(&mut self, widthdb: &mut WidthDb, ch: char) {
self.text.insert(self.idx, ch);
self.idx += ch.len_utf8();
self.record_cursor_col(widthdb);
}
/// Insert a string at the current cursor position and move the cursor
/// accordingly.
fn insert_str(&mut self, widthdb: &mut WidthDb, str: &str) {
self.text.insert_str(self.idx, str);
self.idx += str.len();
self.record_cursor_col(widthdb);
}
/// Delete the grapheme before the cursor position.
fn backspace(&mut self, widthdb: &mut WidthDb) {
let boundaries = self.grapheme_boundaries();
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
if *end == self.idx {
self.text.replace_range(start..end, "");
self.idx = *start;
self.record_cursor_col(widthdb);
break;
}
}
}
/// Delete the grapheme after the cursor position.
fn delete(&mut self) {
let boundaries = self.grapheme_boundaries();
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
if *start == self.idx {
self.text.replace_range(start..end, "");
break;
}
}
}
/////////////////////
// Cursor movement //
/////////////////////
fn move_cursor_left(&mut self, widthdb: &mut WidthDb) {
let boundaries = self.grapheme_boundaries();
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
if *end == self.idx {
self.idx = *start;
self.record_cursor_col(widthdb);
break;
}
}
}
fn move_cursor_right(&mut self, widthdb: &mut WidthDb) {
let boundaries = self.grapheme_boundaries();
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
if *start == self.idx {
self.idx = *end;
self.record_cursor_col(widthdb);
break;
}
}
}
fn move_cursor_left_a_word(&mut self, widthdb: &mut WidthDb) {
let boundaries = self.grapheme_boundaries();
let mut encountered_word = false;
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)).rev() {
if *end == self.idx {
let g = &self.text[*start..*end];
let whitespace = g.chars().all(|c| c.is_whitespace());
if encountered_word && whitespace {
break;
} else if !whitespace {
encountered_word = true;
}
self.idx = *start;
}
}
self.record_cursor_col(widthdb);
}
fn move_cursor_right_a_word(&mut self, widthdb: &mut WidthDb) {
let boundaries = self.grapheme_boundaries();
let mut encountered_word = false;
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
if *start == self.idx {
let g = &self.text[*start..*end];
let whitespace = g.chars().all(|c| c.is_whitespace());
if encountered_word && whitespace {
break;
} else if !whitespace {
encountered_word = true;
}
self.idx = *end;
}
}
self.record_cursor_col(widthdb);
}
fn move_cursor_to_start_of_line(&mut self, widthdb: &mut WidthDb) {
let boundaries = self.line_boundaries();
let (line, _, _) = self.cursor_line(&boundaries);
self.move_cursor_to_line_col(widthdb, line, 0);
self.record_cursor_col(widthdb);
}
fn move_cursor_to_end_of_line(&mut self, widthdb: &mut WidthDb) {
let boundaries = self.line_boundaries();
let (line, _, _) = self.cursor_line(&boundaries);
self.move_cursor_to_line_col(widthdb, line, usize::MAX);
self.record_cursor_col(widthdb);
}
fn move_cursor_up(&mut self, widthdb: &mut WidthDb) {
let boundaries = self.line_boundaries();
let (line, _, _) = self.cursor_line(&boundaries);
if line > 0 {
self.move_cursor_to_line_col(widthdb, line - 1, self.col);
}
}
fn move_cursor_down(&mut self, widthdb: &mut WidthDb) {
let boundaries = self.line_boundaries();
// There's always at least one line, and always at least two line
// boundaries at 0 and self.text.len().
let amount_of_lines = boundaries.len() - 1;
let (line, _, _) = self.cursor_line(&boundaries);
if line + 1 < amount_of_lines {
self.move_cursor_to_line_col(widthdb, line + 1, self.col);
}
}
}
pub struct EditorState(Arc<Mutex<InnerEditorState>>);
impl EditorState {
pub fn new() -> Self {
Self(Arc::new(Mutex::new(InnerEditorState::new(String::new()))))
}
pub fn with_initial_text(text: String) -> Self {
Self(Arc::new(Mutex::new(InnerEditorState::new(text))))
}
pub fn widget(&self) -> Editor {
let guard = self.0.lock();
let text = Styled::new_plain(guard.text.clone());
let idx = guard.idx;
Editor {
state: self.0.clone(),
text,
idx,
focus: true,
hidden: None,
}
}
pub fn text(&self) -> String {
self.0.lock().text.clone()
}
pub fn clear(&self) {
self.0.lock().clear();
}
pub fn set_text(&self, widthdb: &mut WidthDb, text: String) {
self.0.lock().set_text(widthdb, text);
}
pub fn insert_char(&self, widthdb: &mut WidthDb, ch: char) {
self.0.lock().insert_char(widthdb, ch);
}
pub fn insert_str(&self, widthdb: &mut WidthDb, str: &str) {
self.0.lock().insert_str(widthdb, str);
}
/// Delete the grapheme before the cursor position.
pub fn backspace(&self, widthdb: &mut WidthDb) {
self.0.lock().backspace(widthdb);
}
/// Delete the grapheme after the cursor position.
pub fn delete(&self) {
self.0.lock().delete();
}
pub fn move_cursor_left(&self, widthdb: &mut WidthDb) {
self.0.lock().move_cursor_left(widthdb);
}
pub fn move_cursor_right(&self, widthdb: &mut WidthDb) {
self.0.lock().move_cursor_right(widthdb);
}
pub fn move_cursor_left_a_word(&self, widthdb: &mut WidthDb) {
self.0.lock().move_cursor_left_a_word(widthdb);
}
pub fn move_cursor_right_a_word(&self, widthdb: &mut WidthDb) {
self.0.lock().move_cursor_right_a_word(widthdb);
}
pub fn move_cursor_to_start_of_line(&self, widthdb: &mut WidthDb) {
self.0.lock().move_cursor_to_start_of_line(widthdb);
}
pub fn move_cursor_to_end_of_line(&self, widthdb: &mut WidthDb) {
self.0.lock().move_cursor_to_end_of_line(widthdb);
}
pub fn move_cursor_up(&self, widthdb: &mut WidthDb) {
self.0.lock().move_cursor_up(widthdb);
}
pub fn move_cursor_down(&self, widthdb: &mut WidthDb) {
self.0.lock().move_cursor_down(widthdb);
}
pub fn edit_externally(
&self,
terminal: &mut Terminal,
crossterm_lock: &Arc<FairMutex<()>>,
) -> io::Result<()> {
let mut guard = self.0.lock();
let text = util::prompt(terminal, crossterm_lock, &guard.text)?;
if text.trim().is_empty() {
// The user likely wanted to abort the edit and has deleted the
// entire text (bar whitespace left over by some editors).
return Ok(());
}
if let Some(text) = text.strip_suffix('\n') {
// Some editors like vim add a trailing newline that would look out
// of place in cove's editor. To intentionally add a trailing
// newline, simply add two in-editor.
guard.set_text(terminal.widthdb(), text.to_string());
} else {
guard.set_text(terminal.widthdb(), text);
}
Ok(())
}
}
////////////
// Widget //
////////////
pub struct Editor {
state: Arc<Mutex<InnerEditorState>>,
text: Styled,
idx: usize,
focus: bool,
hidden: Option<Box<Text>>,
}
impl Editor {
pub fn highlight<F>(mut self, f: F) -> Self
where
F: FnOnce(&str) -> Styled,
{
let new_text = f(self.text.text());
assert_eq!(self.text.text(), new_text.text());
self.text = new_text;
self
}
pub fn focus(mut self, active: bool) -> Self {
self.focus = active;
self
}
pub fn hidden(self) -> Self {
self.hidden_with_placeholder(("<hidden>", Style::new().grey().italic()))
}
pub fn hidden_with_placeholder<S: Into<Styled>>(mut self, placeholder: S) -> Self {
self.hidden = Some(Box::new(Text::new(placeholder)));
self
}
fn wrapped_cursor(cursor_idx: usize, break_indices: &[usize]) -> (usize, usize) {
let mut row = 0;
let mut line_idx = cursor_idx;
for break_idx in break_indices {
if cursor_idx < *break_idx {
break;
} else {
row += 1;
line_idx = cursor_idx - break_idx;
}
}
(row, line_idx)
}
pub fn cursor_row(&self, widthdb: &mut WidthDb) -> usize {
let width = self.state.lock().last_width;
let text_width = (width - 1) as usize;
let indices = wrap(widthdb, self.text.text(), text_width);
let (row, _) = Self::wrapped_cursor(self.idx, &indices);
row
}
}
#[async_trait]
impl Widget for Editor {
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Size {
if let Some(placeholder) = &self.hidden {
let mut size = placeholder.size(widthdb, max_width, max_height).await;
// Cursor needs to fit regardless of focus
size.width = size.width.max(1);
size.height = size.height.max(1);
return size;
}
let max_width = max_width.map(|w| w as usize).unwrap_or(usize::MAX).max(1);
let max_text_width = max_width - 1;
let indices = wrap(widthdb, self.text.text(), max_text_width);
let lines = self.text.clone().split_at_indices(&indices);
let min_width = lines
.iter()
.map(|l| widthdb.width(l.text().trim_end()))
.max()
.unwrap_or(0)
+ 1;
let min_height = lines.len();
Size::new(min_width as u16, min_height as u16)
}
async fn render(self: Box<Self>, frame: &mut Frame) {
if let Some(placeholder) = self.hidden {
if !self.text.text().is_empty() {
placeholder.render(frame).await;
}
if self.focus {
frame.set_cursor(Some(Pos::ZERO));
}
return;
}
let size = frame.size();
let widthdb = frame.widthdb();
let width = size.width.max(1);
let text_width = (width - 1) as usize;
let indices = wrap(widthdb, self.text.text(), text_width);
let lines = self.text.split_at_indices(&indices);
// Determine cursor position now while we still have the lines.
let cursor_pos = if self.focus {
let (cursor_row, cursor_line_idx) = Self::wrapped_cursor(self.idx, &indices);
let cursor_col = widthdb.width(lines[cursor_row].text().split_at(cursor_line_idx).0);
let cursor_col = cursor_col.min(text_width);
Some(Pos::new(cursor_col as i32, cursor_row as i32))
} else {
None
};
for (i, line) in lines.into_iter().enumerate() {
frame.write(Pos::new(0, i as i32), line);
}
if let Some(pos) = cursor_pos {
frame.set_cursor(Some(pos));
}
self.state.lock().last_width = width;
}
}

View file

@ -1,44 +0,0 @@
use async_trait::async_trait;
use toss::{Frame, Size, WidthDb};
use super::Widget;
#[derive(Debug, Default, Clone, Copy)]
pub struct Empty {
size: Size,
}
impl Empty {
pub fn new() -> Self {
Self { size: Size::ZERO }
}
pub fn width(mut self, width: u16) -> Self {
self.size.width = width;
self
}
pub fn height(mut self, height: u16) -> Self {
self.size.height = height;
self
}
pub fn size(mut self, size: Size) -> Self {
self.size = size;
self
}
}
#[async_trait]
impl Widget for Empty {
async fn size(
&self,
_widthdb: &mut WidthDb,
_max_width: Option<u16>,
_max_height: Option<u16>,
) -> Size {
self.size
}
async fn render(self: Box<Self>, _frame: &mut Frame) {}
}

View file

@ -1,73 +0,0 @@
use async_trait::async_trait;
use toss::{Frame, Pos, Size, WidthDb};
use super::{BoxedWidget, Widget};
pub struct Float {
inner: BoxedWidget,
horizontal: Option<f32>,
vertical: Option<f32>,
}
impl Float {
pub fn new<W: Into<BoxedWidget>>(inner: W) -> Self {
Self {
inner: inner.into(),
horizontal: None,
vertical: None,
}
}
pub fn horizontal(mut self, position: f32) -> Self {
self.horizontal = Some(position);
self
}
pub fn vertical(mut self, position: f32) -> Self {
self.vertical = Some(position);
self
}
}
#[async_trait]
impl Widget for Float {
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Size {
self.inner.size(widthdb, max_width, max_height).await
}
async fn render(self: Box<Self>, frame: &mut Frame) {
let size = frame.size();
let mut inner_size = self
.inner
.size(frame.widthdb(), Some(size.width), Some(size.height))
.await;
inner_size.width = inner_size.width.min(size.width);
inner_size.height = inner_size.height.min(size.height);
let mut inner_pos = Pos::ZERO;
if let Some(horizontal) = self.horizontal {
let available = (size.width - inner_size.width) as f32;
// Biased towards the left if horizontal lands exactly on the
// boundary between two cells
inner_pos.x = (horizontal * available).floor().min(available) as i32;
}
if let Some(vertical) = self.vertical {
let available = (size.height - inner_size.height) as f32;
// Biased towards the top if vertical lands exactly on the boundary
// between two cells
inner_pos.y = (vertical * available).floor().min(available) as i32;
}
frame.push(inner_pos, inner_size);
self.inner.render(frame).await;
frame.pop();
}
}

View file

@ -1,265 +0,0 @@
use async_trait::async_trait;
use toss::{Frame, Pos, Size, WidthDb};
use super::{BoxedWidget, Widget};
pub struct Segment {
widget: BoxedWidget,
expanding: bool,
priority: Option<u8>,
}
impl Segment {
pub fn new<W: Into<BoxedWidget>>(widget: W) -> Self {
Self {
widget: widget.into(),
expanding: false,
priority: None,
}
}
/// Expand this segment into the remaining space after all segment minimum
/// sizes have been determined. The remaining space is split up evenly.
pub fn expanding(mut self, active: bool) -> Self {
self.expanding = active;
self
}
/// The size of segments with a priority is calculated in order of
/// increasing priority, using the remaining available space as maximum
/// space for the widget during size calculations.
///
/// Widgets without priority are processed first without size restrictions.
pub fn priority(mut self, priority: u8) -> Self {
self.priority = Some(priority);
self
}
}
struct SizedSegment {
idx: usize,
size: Size,
expanding: bool,
priority: Option<u8>,
}
impl SizedSegment {
pub fn new(idx: usize, segment: &Segment) -> Self {
Self {
idx,
size: Size::ZERO,
expanding: segment.expanding,
priority: segment.priority,
}
}
}
async fn sizes_horiz(
segments: &[Segment],
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Vec<SizedSegment> {
let mut sized = segments
.iter()
.enumerate()
.map(|(i, s)| SizedSegment::new(i, s))
.collect::<Vec<_>>();
sized.sort_by_key(|s| s.priority);
let mut total_width = 0;
for s in &mut sized {
let available_width = max_width
.filter(|_| s.priority.is_some())
.map(|w| w.saturating_sub(total_width));
s.size = segments[s.idx]
.widget
.size(widthdb, available_width, max_height)
.await;
if let Some(available_width) = available_width {
s.size.width = s.size.width.min(available_width);
}
total_width += s.size.width;
}
sized
}
async fn sizes_vert(
segments: &[Segment],
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Vec<SizedSegment> {
let mut sized = segments
.iter()
.enumerate()
.map(|(i, s)| SizedSegment::new(i, s))
.collect::<Vec<_>>();
sized.sort_by_key(|s| s.priority);
let mut total_height = 0;
for s in &mut sized {
let available_height = max_height
.filter(|_| s.priority.is_some())
.map(|w| w.saturating_sub(total_height));
s.size = segments[s.idx]
.widget
.size(widthdb, max_width, available_height)
.await;
if let Some(available_height) = available_height {
s.size.height = s.size.height.min(available_height);
}
total_height += s.size.height;
}
sized
}
fn expand_horiz(segments: &mut [SizedSegment], available_width: u16) {
if !segments.iter().any(|s| s.expanding) {
return;
}
// Interestingly, rustc needs this type annotation while rust-analyzer
// manages to derive the correct type in an inlay hint.
let current_width = segments.iter().map(|s| s.size.width).sum::<u16>();
if current_width < available_width {
let mut remaining_width = available_width - current_width;
while remaining_width > 0 {
for segment in segments.iter_mut() {
if segment.expanding {
if remaining_width > 0 {
segment.size.width += 1;
remaining_width -= 1;
} else {
break;
}
}
}
}
}
}
fn expand_vert(segments: &mut [SizedSegment], available_height: u16) {
if !segments.iter().any(|s| s.expanding) {
return;
}
// Interestingly, rustc needs this type annotation while rust-analyzer
// manages to derive the correct type in an inlay hint.
let current_height = segments.iter().map(|s| s.size.height).sum::<u16>();
if current_height < available_height {
let mut remaining_height = available_height - current_height;
while remaining_height > 0 {
for segment in segments.iter_mut() {
if segment.expanding {
if remaining_height > 0 {
segment.size.height += 1;
remaining_height -= 1;
} else {
break;
}
}
}
}
}
}
/// Place multiple widgets next to each other horizontally.
pub struct HJoin {
segments: Vec<Segment>,
}
impl HJoin {
pub fn new(segments: Vec<Segment>) -> Self {
Self { segments }
}
}
#[async_trait]
impl Widget for HJoin {
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Size {
let sizes = sizes_horiz(&self.segments, widthdb, max_width, max_height).await;
let width = sizes.iter().map(|s| s.size.width).sum::<u16>();
let height = sizes.iter().map(|s| s.size.height).max().unwrap_or(0);
Size::new(width, height)
}
async fn render(self: Box<Self>, frame: &mut Frame) {
let size = frame.size();
let mut sizes = sizes_horiz(
&self.segments,
frame.widthdb(),
Some(size.width),
Some(size.height),
)
.await;
expand_horiz(&mut sizes, size.width);
sizes.sort_by_key(|s| s.idx);
let mut x = 0;
for (segment, sized) in self.segments.into_iter().zip(sizes.into_iter()) {
frame.push(Pos::new(x, 0), Size::new(sized.size.width, size.height));
segment.widget.render(frame).await;
frame.pop();
x += sized.size.width as i32;
}
}
}
/// Place multiple widgets next to each other vertically.
pub struct VJoin {
segments: Vec<Segment>,
}
impl VJoin {
pub fn new(segments: Vec<Segment>) -> Self {
Self { segments }
}
}
#[async_trait]
impl Widget for VJoin {
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Size {
let sizes = sizes_vert(&self.segments, widthdb, max_width, max_height).await;
let width = sizes.iter().map(|s| s.size.width).max().unwrap_or(0);
let height = sizes.iter().map(|s| s.size.height).sum::<u16>();
Size::new(width, height)
}
async fn render(self: Box<Self>, frame: &mut Frame) {
let size = frame.size();
let mut sizes = sizes_vert(
&self.segments,
frame.widthdb(),
Some(size.width),
Some(size.height),
)
.await;
expand_vert(&mut sizes, size.height);
sizes.sort_by_key(|s| s.idx);
let mut y = 0;
for (segment, sized) in self.segments.into_iter().zip(sizes.into_iter()) {
frame.push(Pos::new(0, y), Size::new(size.width, sized.size.height));
segment.widget.render(frame).await;
frame.pop();
y += sized.size.height as i32;
}
}
}

View file

@ -1,38 +0,0 @@
use async_trait::async_trait;
use toss::{Frame, Size, WidthDb};
use super::{BoxedWidget, Widget};
pub struct Layer {
layers: Vec<BoxedWidget>,
}
impl Layer {
pub fn new(layers: Vec<BoxedWidget>) -> Self {
Self { layers }
}
}
#[async_trait]
impl Widget for Layer {
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Size {
let mut max_size = Size::ZERO;
for layer in &self.layers {
let size = layer.size(widthdb, max_width, max_height).await;
max_size.width = max_size.width.max(size.width);
max_size.height = max_size.height.max(size.height);
}
max_size
}
async fn render(self: Box<Self>, frame: &mut Frame) {
for layer in self.layers {
layer.render(frame).await;
}
}
}

View file

@ -1,395 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use parking_lot::Mutex;
use toss::{Frame, Pos, Size, WidthDb};
use super::{BoxedWidget, Widget};
///////////
// State //
///////////
#[derive(Debug, Clone)]
struct Cursor<Id> {
/// Id of the element the cursor is pointing to.
///
/// If the rows change (e.g. reorder) but there is still a row with this id,
/// the cursor is moved to this row.
id: Id,
/// Index of the row the cursor is pointing to.
///
/// If the rows change and there is no longer a row with the cursor's id,
/// the cursor is moved up or down to the next selectable row. This way, it
/// stays close to its previous position.
idx: usize,
}
impl<Id> Cursor<Id> {
pub fn new(id: Id, idx: usize) -> Self {
Self { id, idx }
}
}
#[derive(Debug)]
struct InnerListState<Id> {
rows: Vec<Option<Id>>,
/// Offset of the first line visible on the screen.
offset: usize,
cursor: Option<Cursor<Id>>,
make_cursor_visible: bool,
}
impl<Id> InnerListState<Id> {
fn new() -> Self {
Self {
rows: vec![],
offset: 0,
cursor: None,
make_cursor_visible: true,
}
}
}
impl<Id: Clone> InnerListState<Id> {
fn first_selectable(&self) -> Option<Cursor<Id>> {
self.rows
.iter()
.enumerate()
.find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i)))
}
fn last_selectable(&self) -> Option<Cursor<Id>> {
self.rows
.iter()
.enumerate()
.rev()
.find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i)))
}
fn selectable_at_or_before_index(&self, i: usize) -> Option<Cursor<Id>> {
self.rows
.iter()
.enumerate()
.take(i + 1)
.rev()
.find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i)))
}
fn selectable_at_or_after_index(&self, i: usize) -> Option<Cursor<Id>> {
self.rows
.iter()
.enumerate()
.skip(i)
.find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i)))
}
fn selectable_before_index(&self, i: usize) -> Option<Cursor<Id>> {
self.rows
.iter()
.enumerate()
.take(i)
.rev()
.find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i)))
}
fn selectable_after_index(&self, i: usize) -> Option<Cursor<Id>> {
self.rows
.iter()
.enumerate()
.skip(i + 1)
.find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i)))
}
fn scroll_so_cursor_is_visible(&mut self, height: usize) {
if height == 0 {
// Cursor can't be visible because nothing is visible
return;
}
if let Some(cursor) = &self.cursor {
// As long as height > 0, min <= max is true
let min = (cursor.idx + 1).saturating_sub(height);
let max = cursor.idx;
self.offset = self.offset.clamp(min, max);
}
}
fn move_cursor_to_make_it_visible(&mut self, height: usize) {
if let Some(cursor) = &self.cursor {
let min_idx = self.offset;
let max_idx = self.offset.saturating_add(height).saturating_sub(1);
let new_cursor = if cursor.idx < min_idx {
self.selectable_at_or_after_index(min_idx)
} else if cursor.idx > max_idx {
self.selectable_at_or_before_index(max_idx)
} else {
return;
};
if let Some(new_cursor) = new_cursor {
self.cursor = Some(new_cursor);
}
}
}
fn clamp_scrolling(&mut self, height: usize) {
let min = 0;
let max = self.rows.len().saturating_sub(height);
self.offset = self.offset.clamp(min, max);
}
}
impl<Id: Clone + Eq> InnerListState<Id> {
fn selectable_of_id(&self, id: &Id) -> Option<Cursor<Id>> {
self.rows.iter().enumerate().find_map(|(i, r)| match r {
Some(rid) if rid == id => Some(Cursor::new(id.clone(), i)),
_ => None,
})
}
fn fix_cursor(&mut self) {
self.cursor = if let Some(cursor) = &self.cursor {
self.selectable_of_id(&cursor.id)
.or_else(|| self.selectable_at_or_before_index(cursor.idx))
.or_else(|| self.selectable_at_or_after_index(cursor.idx))
} else {
self.first_selectable()
}
}
/// Bring the list into a state consistent with the current rows and height.
fn stabilize(&mut self, rows: &[Row<Id>], height: usize) {
self.rows = rows.iter().map(|r| r.id().cloned()).collect();
self.fix_cursor();
if self.make_cursor_visible {
self.scroll_so_cursor_is_visible(height);
self.clamp_scrolling(height);
} else {
self.clamp_scrolling(height);
self.move_cursor_to_make_it_visible(height);
}
self.make_cursor_visible = true;
}
}
pub struct ListState<Id>(Arc<Mutex<InnerListState<Id>>>);
impl<Id> ListState<Id> {
pub fn new() -> Self {
Self(Arc::new(Mutex::new(InnerListState::new())))
}
pub fn widget(&self) -> List<Id> {
List::new(self.0.clone())
}
pub fn scroll_up(&mut self, amount: usize) {
let mut guard = self.0.lock();
guard.offset = guard.offset.saturating_sub(amount);
guard.make_cursor_visible = false;
}
pub fn scroll_down(&mut self, amount: usize) {
let mut guard = self.0.lock();
guard.offset = guard.offset.saturating_add(amount);
guard.make_cursor_visible = false;
}
}
impl<Id: Clone> ListState<Id> {
pub fn cursor(&self) -> Option<Id> {
self.0.lock().cursor.as_ref().map(|c| c.id.clone())
}
pub fn move_cursor_up(&mut self) {
let mut guard = self.0.lock();
if let Some(cursor) = &guard.cursor {
if let Some(new_cursor) = guard.selectable_before_index(cursor.idx) {
guard.cursor = Some(new_cursor);
}
}
guard.make_cursor_visible = true;
}
pub fn move_cursor_down(&mut self) {
let mut guard = self.0.lock();
if let Some(cursor) = &guard.cursor {
if let Some(new_cursor) = guard.selectable_after_index(cursor.idx) {
guard.cursor = Some(new_cursor);
}
}
guard.make_cursor_visible = true;
}
pub fn move_cursor_to_top(&mut self) {
let mut guard = self.0.lock();
if let Some(new_cursor) = guard.first_selectable() {
guard.cursor = Some(new_cursor);
}
guard.make_cursor_visible = true;
}
pub fn move_cursor_to_bottom(&mut self) {
let mut guard = self.0.lock();
if let Some(new_cursor) = guard.last_selectable() {
guard.cursor = Some(new_cursor);
}
guard.make_cursor_visible = true;
}
}
////////////
// Widget //
////////////
enum Row<Id> {
Unselectable {
normal: BoxedWidget,
},
Selectable {
id: Id,
normal: BoxedWidget,
selected: BoxedWidget,
},
}
impl<Id> Row<Id> {
fn id(&self) -> Option<&Id> {
match self {
Self::Unselectable { .. } => None,
Self::Selectable { id, .. } => Some(id),
}
}
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Size {
match self {
Self::Unselectable { normal } => normal.size(widthdb, max_width, max_height).await,
Self::Selectable {
normal, selected, ..
} => {
let normal_size = normal.size(widthdb, max_width, max_height).await;
let selected_size = selected.size(widthdb, max_width, max_height).await;
Size::new(
normal_size.width.max(selected_size.width),
normal_size.height.max(selected_size.height),
)
}
}
}
}
pub struct List<Id> {
state: Arc<Mutex<InnerListState<Id>>>,
rows: Vec<Row<Id>>,
focus: bool,
}
impl<Id> List<Id> {
fn new(state: Arc<Mutex<InnerListState<Id>>>) -> Self {
Self {
state,
rows: vec![],
focus: false,
}
}
pub fn focus(mut self, focus: bool) -> Self {
self.focus = focus;
self
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn add_unsel<W: Into<BoxedWidget>>(&mut self, normal: W) {
self.rows.push(Row::Unselectable {
normal: normal.into(),
});
}
pub fn add_sel<W1, W2>(&mut self, id: Id, normal: W1, selected: W2)
where
W1: Into<BoxedWidget>,
W2: Into<BoxedWidget>,
{
self.rows.push(Row::Selectable {
id,
normal: normal.into(),
selected: selected.into(),
});
}
}
#[async_trait]
impl<Id: Clone + Eq + Send + Sync> Widget for List<Id> {
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
_max_height: Option<u16>,
) -> Size {
let mut width = 0;
for row in &self.rows {
let size = row.size(widthdb, max_width, Some(1)).await;
width = width.max(size.width);
}
let height = self.rows.len();
Size::new(width, height as u16)
}
async fn render(self: Box<Self>, frame: &mut Frame) {
let size = frame.size();
// Guard acquisition and dropping must be inside its own block or the
// compiler complains that "future created by async block is not
// `Send`", pointing to the function body.
//
// I assume this is because I'm using the parking lot mutex whose guard
// is not Send, and even though I was explicitly dropping it with
// drop(), rustc couldn't figure this out without some help.
let (offset, cursor) = {
let mut guard = self.state.lock();
guard.stabilize(&self.rows, size.height.into());
(guard.offset as i32, guard.cursor.clone())
};
let row_size = Size::new(size.width, 1);
for (i, row) in self.rows.into_iter().enumerate() {
let dy = i as i32 - offset;
if dy < 0 || dy >= size.height as i32 {
continue;
}
frame.push(Pos::new(0, dy), row_size);
match row {
Row::Unselectable { normal } => normal.render(frame).await,
Row::Selectable {
id,
normal,
selected,
} => {
let focusing = self.focus
&& if let Some(cursor) = &cursor {
cursor.id == id
} else {
false
};
let widget = if focusing { selected } else { normal };
widget.render(frame).await;
}
}
frame.pop();
}
}
}

View file

@ -1,103 +0,0 @@
use async_trait::async_trait;
use toss::{Frame, Pos, Size, WidthDb};
use super::{BoxedWidget, Widget};
pub struct Padding {
inner: BoxedWidget,
stretch: bool,
left: u16,
right: u16,
top: u16,
bottom: u16,
}
impl Padding {
pub fn new<W: Into<BoxedWidget>>(inner: W) -> Self {
Self {
inner: inner.into(),
stretch: false,
left: 0,
right: 0,
top: 0,
bottom: 0,
}
}
/// Whether the inner widget should be stretched to fill the additional
/// space.
pub fn stretch(mut self, active: bool) -> Self {
self.stretch = active;
self
}
pub fn left(mut self, amount: u16) -> Self {
self.left = amount;
self
}
pub fn right(mut self, amount: u16) -> Self {
self.right = amount;
self
}
pub fn horizontal(self, amount: u16) -> Self {
self.left(amount).right(amount)
}
pub fn top(mut self, amount: u16) -> Self {
self.top = amount;
self
}
pub fn bottom(mut self, amount: u16) -> Self {
self.bottom = amount;
self
}
pub fn vertical(self, amount: u16) -> Self {
self.top(amount).bottom(amount)
}
pub fn all(self, amount: u16) -> Self {
self.horizontal(amount).vertical(amount)
}
}
#[async_trait]
impl Widget for Padding {
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Size {
let horizontal = self.left + self.right;
let vertical = self.top + self.bottom;
let max_width = max_width.map(|w| w.saturating_sub(horizontal));
let max_height = max_height.map(|h| h.saturating_sub(vertical));
let size = self.inner.size(widthdb, max_width, max_height).await;
size + Size::new(horizontal, vertical)
}
async fn render(self: Box<Self>, frame: &mut Frame) {
let size = frame.size();
let inner_pos = Pos::new(self.left.into(), self.top.into());
let inner_size = if self.stretch {
size
} else {
Size::new(
size.width.saturating_sub(self.left + self.right),
size.height.saturating_sub(self.top + self.bottom),
)
};
frame.push(inner_pos, inner_size);
self.inner.render(frame).await;
frame.pop();
}
}

View file

@ -1,74 +0,0 @@
use toss::{Style, Styled};
use super::background::Background;
use super::border::Border;
use super::float::Float;
use super::layer::Layer;
use super::padding::Padding;
use super::text::Text;
use super::BoxedWidget;
pub struct Popup {
inner: BoxedWidget,
inner_padding: bool,
title: Option<Styled>,
border_style: Style,
bg_style: Style,
}
impl Popup {
pub fn new<W: Into<BoxedWidget>>(inner: W) -> Self {
Self {
inner: inner.into(),
inner_padding: true,
title: None,
border_style: Style::new(),
bg_style: Style::new().opaque(),
}
}
pub fn inner_padding(mut self, active: bool) -> Self {
self.inner_padding = active;
self
}
pub fn title<S: Into<Styled>>(mut self, title: S) -> Self {
self.title = Some(title.into());
self
}
pub fn border(mut self, style: Style) -> Self {
self.border_style = style;
self
}
pub fn background(mut self, style: Style) -> Self {
self.bg_style = style;
self
}
pub fn build(self) -> BoxedWidget {
let inner = if self.inner_padding {
Padding::new(self.inner).horizontal(1).into()
} else {
self.inner
};
let window =
Border::new(Background::new(inner).style(self.bg_style)).style(self.border_style);
let widget: BoxedWidget = if let Some(title) = self.title {
let title = Float::new(
Padding::new(
Background::new(Padding::new(Text::new(title)).horizontal(1))
.style(self.border_style),
)
.horizontal(2),
);
Layer::new(vec![window.into(), title.into()]).into()
} else {
window.into()
};
Float::new(widget).vertical(0.5).horizontal(0.5).into()
}
}

View file

@ -1,86 +0,0 @@
use async_trait::async_trait;
use toss::{Frame, Size, WidthDb};
use super::{BoxedWidget, Widget};
pub struct Resize {
inner: BoxedWidget,
min_width: Option<u16>,
min_height: Option<u16>,
max_width: Option<u16>,
max_height: Option<u16>,
}
impl Resize {
pub fn new<W: Into<BoxedWidget>>(inner: W) -> Self {
Self {
inner: inner.into(),
min_width: None,
min_height: None,
max_width: None,
max_height: None,
}
}
pub fn min_width(mut self, amount: u16) -> Self {
self.min_width = Some(amount);
self
}
pub fn max_width(mut self, amount: u16) -> Self {
self.max_width = Some(amount);
self
}
pub fn min_height(mut self, amount: u16) -> Self {
self.min_height = Some(amount);
self
}
pub fn max_height(mut self, amount: u16) -> Self {
self.max_height = Some(amount);
self
}
}
#[async_trait]
impl Widget for Resize {
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Size {
let max_width = match (max_width, self.max_width) {
(None, None) => None,
(Some(w), None) => Some(w),
(None, Some(sw)) => Some(sw),
(Some(w), Some(sw)) => Some(w.min(sw)),
};
let max_height = match (max_height, self.max_height) {
(None, None) => None,
(Some(h), None) => Some(h),
(None, Some(sh)) => Some(sh),
(Some(h), Some(sh)) => Some(h.min(sh)),
};
let size = self.inner.size(widthdb, max_width, max_height).await;
let width = match self.min_width {
Some(min_width) => size.width.max(min_width),
None => size.width,
};
let height = match self.min_height {
Some(min_height) => size.height.max(min_height),
None => size.height,
};
Size::new(width, height)
}
async fn render(self: Box<Self>, frame: &mut Frame) {
self.inner.render(frame).await;
}
}

View file

@ -1,46 +0,0 @@
use async_trait::async_trait;
use toss::{Frame, Pos, Size, WidthDb};
use super::Widget;
pub struct HRule;
#[async_trait]
impl Widget for HRule {
async fn size(
&self,
_widthdb: &mut WidthDb,
_max_width: Option<u16>,
_max_height: Option<u16>,
) -> Size {
Size::new(0, 1)
}
async fn render(self: Box<Self>, frame: &mut Frame) {
let size = frame.size();
for x in 0..size.width as i32 {
frame.write(Pos::new(x, 0), "");
}
}
}
pub struct VRule;
#[async_trait]
impl Widget for VRule {
async fn size(
&self,
_widthdb: &mut WidthDb,
_max_width: Option<u16>,
_max_height: Option<u16>,
) -> Size {
Size::new(1, 0)
}
async fn render(self: Box<Self>, frame: &mut Frame) {
let size = frame.size();
for y in 0..size.height as i32 {
frame.write(Pos::new(0, y), "");
}
}
}

View file

@ -1,65 +0,0 @@
use async_trait::async_trait;
use toss::{Frame, Pos, Size, Styled, WidthDb};
use super::Widget;
pub struct Text {
styled: Styled,
wrap: bool,
}
impl Text {
pub fn new<S: Into<Styled>>(styled: S) -> Self {
Self {
styled: styled.into(),
wrap: false,
}
}
pub fn wrap(mut self, active: bool) -> Self {
// TODO Re-think and check what behaviour this setting should entail
self.wrap = active;
self
}
fn wrapped(&self, widthdb: &mut WidthDb, max_width: Option<u16>) -> Vec<Styled> {
let max_width = if self.wrap {
max_width.map(|w| w as usize).unwrap_or(usize::MAX)
} else {
usize::MAX
};
let indices = widthdb.wrap(self.styled.text(), max_width);
self.styled.clone().split_at_indices(&indices)
}
}
#[async_trait]
impl Widget for Text {
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
_max_height: Option<u16>,
) -> Size {
let lines = self.wrapped(widthdb, max_width);
let min_width = lines
.iter()
.map(|l| widthdb.width(l.text().trim_end()))
.max()
.unwrap_or(0);
let min_height = lines.len();
Size::new(min_width as u16, min_height as u16)
}
async fn render(self: Box<Self>, frame: &mut Frame) {
let size = frame.size();
for (i, line) in self
.wrapped(frame.widthdb(), Some(size.width))
.into_iter()
.enumerate()
{
frame.write(Pos::new(0, i as i32), line);
}
}
}