Remove old chat, widgets, util modules
This commit is contained in:
parent
e2b75d2f52
commit
bc8c5968d6
27 changed files with 0 additions and 4417 deletions
|
|
@ -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;
|
||||
|
|
|
|||
163
src/ui/chat.rs
163
src/ui/chat.rs
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
166
src/ui/util.rs
166
src/ui/util.rs
|
|
@ -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,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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), "│");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue