deft 0.15.0

Cross platform ui framework
use crate::some_or_return;
use crate::text::textbox::TextCoord;

#[derive(Clone, Debug)]
pub struct EditDetail {
    pub content: String,
    pub end: TextCoord,
}

#[derive(Clone, Debug)]
pub struct EditOp {
    pub caret: TextCoord,
    pub delete: Option<EditDetail>,
    pub insert: Option<EditDetail>,
}

pub struct EditHistory {
    max_history: usize,
    history: Vec<EditOp>,
    history_ptr: usize,
}

impl EditHistory {
    pub fn new(max_history: usize) -> Self {
        EditHistory {
            history: Vec::new(),
            history_ptr: 0,
            max_history,
        }
    }

    pub fn set_max_history(&mut self, max_history: usize) {
        self.max_history = max_history;
    }
    
    pub fn get_max_history(&self) -> usize {
        self.max_history
    }

    pub fn record_input(
        &mut self,
        caret: TextCoord,
        delete_detail: Option<EditDetail>,
        insert_detail: Option<EditDetail>,
    ) {
        if self.max_history == 0 || (delete_detail.is_none() && insert_detail.is_none()) {
            return;
        }
        if !self.merge_input(caret, &delete_detail, &insert_detail) {
            self.push_op(EditOp {
                caret,
                delete: delete_detail,
                insert: insert_detail,
            });
        }
    }

    pub fn undo(&mut self) -> Option<EditOp> {
        if self.history_ptr == 0 {
            return None;
        }
        let prev_op = unsafe { self.history.get_unchecked(self.history_ptr - 1) };
        self.history_ptr -= 1;
        Some(prev_op.clone())
    }

    pub fn redo(&mut self) -> Option<EditOp> {
        if self.history.is_empty() || self.history_ptr >= self.history.len() {
            return None;
        }
        let next_op = unsafe { self.history.get_unchecked(self.history_ptr) };
        self.history_ptr += 1;
        Some(next_op.clone())
    }

    fn merge_input(
        &mut self,
        caret: TextCoord,
        delete_detail: &Option<EditDetail>,
        insert_detail: &Option<EditDetail>,
    ) -> bool {
        if self.history_ptr == 0 || self.history_ptr != self.history.len() {
            return false;
        }
        let last_op = unsafe { self.history.get_unchecked_mut(self.history_ptr - 1) };
        match (
            delete_detail,
            insert_detail,
            &mut last_op.delete,
            &mut last_op.insert,
        ) {
            (None, Some(insert_detail), None, Some(last_insert)) => {
                if last_insert.end == caret
                    && !Self::starts_with_whitespace(&insert_detail.content)
                {
                    last_insert.content.push_str(&insert_detail.content);
                    last_insert.end = insert_detail.end;
                    true
                } else {
                    false
                }
            }
            (Some(delete_detail), None, Some(last_delete), None) => {
                if delete_detail.end == last_op.caret {
                    last_delete.content = format!("{}{}", delete_detail.content, last_delete.content);
                    last_op.caret = caret;
                    true
                } else {
                    false
                }
            }
            _ => false,
        }
    }

    fn push_op(&mut self, op: EditOp) {
        let expected_len = self.history_ptr;
        while self.history.len() > expected_len {
            self.history.pop().unwrap();
        }
        if self.history.len() >= self.max_history {
            self.history.remove(0);
            self.history_ptr -= 1;
        }
        self.history.push(op);
        self.history_ptr += 1;
    }

    fn starts_with_whitespace(content: &str) -> bool {
        let first = some_or_return!(content.chars().next(), false);
        ['\t', '\r', '\n', ' '].contains(&first)
    }

}