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 chat2;
|
||||||
mod euph;
|
mod euph;
|
||||||
mod input;
|
mod input;
|
||||||
mod rooms;
|
mod rooms;
|
||||||
mod util;
|
|
||||||
mod util2;
|
mod util2;
|
||||||
mod widgets;
|
|
||||||
mod widgets2;
|
mod widgets2;
|
||||||
|
|
||||||
use std::convert::Infallible;
|
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