mudrs-milk 0.0.1

WIP Mud Client
Documentation
use std::{
    sync::Arc,
    time::Instant,
};

use crossterm::event::{
    Event,
    KeyCode,
    KeyEvent,
    KeyModifiers,
    MouseEvent,
    MouseEventKind,
};
use futures::{
    channel::mpsc,
    lock::Mutex,
    prelude::*,
};
use textwrap::core::Fragment;
use tracing::{
    debug,
    instrument,
};
use tui::{
    buffer::Buffer,
    layout::Rect,
    text::StyledGrapheme,
    widgets::Widget,
};

use crate::text::{
    Line,
    Text,
};

#[derive(Debug, Default)]
pub struct Scroll {
    pub y: Option<usize>,
    pub x: Option<usize>,
}

pub struct ScrollbackBuffer {
    pub lines: Vec<Line>,
    pub current: Text,
    pub scroll: Scroll,
    pub wrap: bool,
    pub last_height: usize,
    pub render_tx: mpsc::Sender<Instant>,
}

impl ScrollbackBuffer {
    pub fn new(render_tx: mpsc::Sender<Instant>) -> Self {
        ScrollbackBuffer {
            lines: vec![],
            current: Text::new(),
            scroll: Default::default(),
            last_height: 20,
            wrap: true,
            render_tx,
        }
    }

    pub fn append_lines(this: Arc<Mutex<Self>>, mut lines: mpsc::Receiver<(Vec<Line>, Text)>) {
        tokio::spawn(async move {
            while let Some(line) = lines.next().await {
                let mut this = this.lock().await;
                this.extend(line);
                let _ = this.render_tx.send(Instant::now()).await;
            }
        });
    }

    #[instrument(level = "trace", skip(self))]
    pub async fn handle_user_event(&mut self, event: &Event) -> bool {
        match event {
            Event::Key(KeyEvent {
                code,
                modifiers: KeyModifiers::NONE,
            }) => match code {
                KeyCode::PageDown => self.page_down(),
                KeyCode::PageUp => self.page_up(),
                _ => return false,
            },
            Event::Mouse(MouseEvent { kind, .. }) => match kind {
                MouseEventKind::ScrollDown => self.scroll_down(1),
                MouseEventKind::ScrollUp => self.scroll_up(1),
                _ => return false,
            },
            _ => return false,
        }
        let _ = self.render_tx.send(Instant::now()).await;
        true
    }

    pub fn scroll_up(&mut self, by: usize) {
        // Start the scrolling at lines.len()+1 to account for an incomplete line
        let scroll_y = self.scroll.y.get_or_insert(self.lines.len() + 1);
        *scroll_y = scroll_y.saturating_sub(by);
    }

    pub fn scroll_down(&mut self, by: usize) {
        if let Some(mut y) = self.scroll.y.take() {
            y += by;
            if y <= self.lines.len() {
                self.scroll.y = Some(y);
            }
        }
    }

    pub fn page_up(&mut self) {
        self.scroll_up(self.last_height);
    }

    pub fn page_down(&mut self) {
        self.scroll_down(self.last_height);
    }

    pub fn extend(&mut self, (lines, mut incomplete): (impl IntoIterator<Item = Line>, Text)) {
        for line in lines {
            let full_line = self.current.swap_complete(line);
            self.lines.push(full_line);
        }

        self.current.extend(&mut incomplete)
    }

    pub fn widget(&self) -> ScrollbackBufferWidget {
        let scroll = self.scroll.y;
        ScrollbackBufferWidget {
            lines: &self.lines[..scroll.unwrap_or(self.lines.len())],
            last: Some(&self.current).filter(|_| scroll.is_none()),
            x: if !self.wrap {
                self.scroll.x.unwrap_or_default()
            } else {
                0
            },
            wrap: self.wrap,
        }
    }
}

pub struct ScrollbackBufferWidget<'a> {
    pub lines: &'a [Line],
    pub last: Option<&'a Text>,
    pub x: usize,
    pub wrap: bool,
}

impl<'a> Widget for ScrollbackBufferWidget<'a> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let max_buffer = &self.lines[self.lines.len().saturating_sub(area.height as usize)..];
        let mut out_lines = vec![];
        let lines = max_buffer
            .iter()
            .map(|l| l.spans())
            .chain(self.last.iter().map(|t| t.spans()))
            .map(|l| {
                l.iter()
                    .flat_map(|s| s.styled_graphemes(Default::default()))
            });

        if self.wrap {
            // TODO: Cache this and only recalculate when the screen width changes.
            for line in lines {
                let words = WordsIter::new(line).collect::<Vec<_>>();
                let lines =
                    textwrap::wrap_algorithms::wrap_first_fit(&words, &[area.width as usize; 512]);
                out_lines.extend(lines.into_iter().map(|words| {
                    words
                        .iter()
                        .flat_map(|word| word.graphemes.iter().cloned())
                        .collect::<Vec<StyledGrapheme>>()
                }))
            }
        } else {
            out_lines.extend(lines.map(Vec::from_iter));
        }
        let out_lines = &out_lines[out_lines.len().saturating_sub(area.height as usize)..];
        let y_offset = area.height.saturating_sub(out_lines.len() as _);
        for (mut y, line) in out_lines.iter().enumerate() {
            y += y_offset as usize;
            if y > area.height as usize {
                break;
            }
            for (x, g) in line.iter().enumerate() {
                if x >= area.width as usize {
                    break;
                }
                buf.get_mut(area.x + x as u16, area.y + y as u16)
                    .set_style(g.style.clone())
                    .set_symbol(g.symbol);
            }
        }
    }
}

#[derive(Default, Debug, Clone)]
struct StyledWord<'a> {
    graphemes: Vec<StyledGrapheme<'a>>,
    word_len: usize,
    trailing_ws_len: usize,
}

impl<'a> StyledWord<'a> {
    fn is_complete_word(&self) -> bool {
        self.graphemes.iter().any(|g| !is_whitespace(g))
            && self.graphemes.last().map(is_whitespace).unwrap_or(false)
    }

    fn push(&mut self, g: StyledGrapheme<'a>) {
        let ws = is_whitespace(&g);
        let existing_word = self.word_len > 0;
        if !ws {
            self.word_len += 1;
        } else if existing_word {
            self.trailing_ws_len += 1;
        }
        self.graphemes.push(g);
    }
}

struct WordsIter<'a, I> {
    graphemes: I,
    current: Option<StyledWord<'a>>,
}

impl<'a, I> WordsIter<'a, I> {
    fn new(graphemes: I) -> Self {
        Self {
            graphemes,
            current: None,
        }
    }
}

impl<'a, I> Iterator for WordsIter<'a, I>
where
    I: Iterator<Item = StyledGrapheme<'a>>,
{
    type Item = StyledWord<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            let g = if let Some(g) = self.graphemes.next() {
                g
            } else {
                return self.current.take();
            };

            let whitespace = is_whitespace(&g);

            let current = self.current.get_or_insert_with(Default::default);

            if current.is_complete_word() {
                if !whitespace {
                    let word = self.current.take();
                    let mut new = StyledWord::default();
                    new.push(g);
                    self.current = Some(new);
                    return word;
                }
            }

            current.push(g);
        }
    }
}

fn is_whitespace(g: &StyledGrapheme) -> bool {
    g.symbol
        .chars()
        .nth(0)
        .map(|c| c.is_whitespace())
        .unwrap_or(true)
}

impl<'a> Fragment for StyledWord<'a> {
    fn whitespace_width(&self) -> usize {
        self.trailing_ws_len
    }
    fn penalty_width(&self) -> usize {
        0
    }
    fn width(&self) -> usize {
        self.graphemes.len() - self.trailing_ws_len
    }
}