sequoia-buffer 0.1.1

Sequoia GUI framework — framework-free text buffer engine (piece table, line index, cursors, undo)
Documentation
use sequoia_core::Color;

#[derive(Clone, Debug, PartialEq)]
pub struct SpanAttr {
    pub bold: bool,
    pub italic: bool,
    pub underline: bool,
    pub strike: bool,
    pub color: Option<Color>,
    pub bg: Option<Color>,
    pub size_delta: f32,
    pub link: Option<String>,
    pub code: bool,
    pub mention: Option<u64>,
}

impl Default for SpanAttr {
    fn default() -> Self {
        Self::plain()
    }
}

impl SpanAttr {
    pub fn plain() -> Self {
        Self {
            bold: false,
            italic: false,
            underline: false,
            strike: false,
            color: None,
            bg: None,
            size_delta: 0.0,
            link: None,
            code: false,
            mention: None,
        }
    }

    pub fn is_plain(&self) -> bool {
        !self.bold
            && !self.italic
            && !self.underline
            && !self.strike
            && self.color.is_none()
            && self.bg.is_none()
            && self.size_delta == 0.0
            && self.link.is_none()
            && !self.code
            && self.mention.is_none()
    }

    pub fn bold() -> Self {
        Self {
            bold: true,
            ..Self::plain()
        }
    }

    pub fn link(url: impl Into<String>) -> Self {
        Self {
            link: Some(url.into()),
            ..Self::plain()
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct StyledSpan {
    pub start: usize,
    pub end: usize,
    pub attr: SpanAttr,
}

impl StyledSpan {
    pub fn new(start: usize, end: usize, attr: SpanAttr) -> Self {
        Self { start, end, attr }
    }

    pub fn len(&self) -> usize {
        self.end.saturating_sub(self.start)
    }

    pub fn is_empty(&self) -> bool {
        self.start >= self.end
    }

    pub fn contains_middle(&self, pos: usize) -> bool {
        self.start < pos && pos < self.end
    }

    pub fn overlaps(&self, range: std::ops::Range<usize>) -> bool {
        self.start < range.end && range.start < self.end
    }

    pub fn clipped(&self, range: std::ops::Range<usize>) -> Option<Self> {
        let s = self.start.max(range.start);
        let e = self.end.min(range.end);
        if s < e {
            Some(Self {
                start: s,
                end: e,
                attr: self.attr.clone(),
            })
        } else {
            None
        }
    }
}

pub fn normalize_spans(spans: &mut Vec<StyledSpan>) {
    spans.retain(|s| s.start < s.end);
    spans.sort_by_key(|s| (s.start, s.end));
    let mut out: Vec<StyledSpan> = Vec::with_capacity(spans.len());
    for mut cur in spans.drain(..) {
        if cur.start >= cur.end {
            continue;
        }
        if let Some(prev) = out.last_mut() {
            if cur.start < prev.end {
                if prev.attr == cur.attr {
                    prev.end = prev.end.max(cur.end);
                    continue;
                }
                cur.start = prev.end;
                if cur.start >= cur.end {
                    continue;
                }
            }
            if prev.end == cur.start && prev.attr == cur.attr {
                prev.end = cur.end;
                continue;
            }
        }
        out.push(cur);
    }
    *spans = out;
}

pub fn hash_span_attr<H: std::hash::Hasher>(a: &SpanAttr, h: &mut H) {
    use std::hash::Hash;
    a.bold.hash(h);
    a.italic.hash(h);
    a.underline.hash(h);
    a.strike.hash(h);
    a.color
        .map(|c| (c.r.to_bits(), c.g.to_bits(), c.b.to_bits(), c.a.to_bits()))
        .hash(h);
    a.bg.map(|c| (c.r.to_bits(), c.g.to_bits(), c.b.to_bits(), c.a.to_bits()))
        .hash(h);
    let q = if a.size_delta.is_finite() {
        (a.size_delta * 4.0).round() as i32
    } else {
        0
    };
    q.hash(h);
    a.link.hash(h);
    a.code.hash(h);
    a.mention.hash(h);
}

pub fn hash_spans_rebased<H: std::hash::Hasher>(spans: &[StyledSpan], base: usize, h: &mut H) {
    use std::hash::Hash;
    spans.len().hash(h);
    for s in spans {
        if s.is_empty() {
            continue;
        }
        s.start.saturating_sub(base).hash(h);
        s.end.saturating_sub(base).hash(h);
        hash_span_attr(&s.attr, h);
    }
}

pub fn spans_hash(spans: &[StyledSpan]) -> u64 {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::Hasher;
    let mut h = DefaultHasher::new();
    hash_spans_rebased(spans, 0, &mut h);
    h.finish()
}

pub fn line_hash(text: &str, spans: &[StyledSpan], width: f32) -> u64 {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};
    let mut h = DefaultHasher::new();
    text.hash(&mut h);
    hash_spans_rebased(spans, 0, &mut h);
    ((width.max(0.0) * 4.0).round() as u32).hash(&mut h);
    h.finish()
}

pub fn rebase_spans(spans: &[StyledSpan], base: usize) -> Vec<StyledSpan> {
    spans
        .iter()
        .map(|s| {
            StyledSpan::new(
                s.start.saturating_sub(base),
                s.end.saturating_sub(base),
                s.attr.clone(),
            )
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn plain_roundtrip() {
        let p = SpanAttr::plain();
        assert!(p.is_plain());
        assert_eq!(p, SpanAttr::default());
        let b = SpanAttr::bold();
        assert!(!b.is_plain());
        assert!(b.bold);
    }

    #[test]
    fn neighbours_merge() {
        let mut v = vec![
            StyledSpan::new(0, 2, SpanAttr::bold()),
            StyledSpan::new(2, 5, SpanAttr::bold()),
            StyledSpan::new(5, 6, SpanAttr::plain()),
        ];

        normalize_spans(&mut v);
        assert_eq!(v.len(), 2);
        assert_eq!((v[0].start, v[0].end), (0, 5));
    }

    #[test]
    fn overlap_truncates_left_biased() {
        let mut v = vec![
            StyledSpan::new(0, 5, SpanAttr::bold()),
            StyledSpan::new(3, 8, SpanAttr::link("x")),
        ];
        normalize_spans(&mut v);
        assert_eq!(v.len(), 2);
        assert_eq!((v[0].start, v[0].end), (0, 5));
        assert_eq!((v[1].start, v[1].end), (5, 8));
    }

    #[test]
    fn empties_dropped_and_sorted() {
        let mut v = vec![
            StyledSpan::new(8, 9, SpanAttr::bold()),
            StyledSpan::new(4, 4, SpanAttr::bold()),
            StyledSpan::new(0, 2, SpanAttr::bold()),
        ];
        normalize_spans(&mut v);
        assert_eq!(v.len(), 2);
        assert_eq!((v[0].start, v[0].end), (0, 2));
        assert_eq!((v[1].start, v[1].end), (8, 9));
    }

    #[test]
    fn line_hash_contract() {
        let a = vec![StyledSpan::new(0, 2, SpanAttr::bold())];
        let h1 = line_hash("abcdef", &a, 100.0);

        assert_eq!(h1, line_hash("abcdef", &a, 100.0));

        assert_ne!(h1, line_hash("abcdeg", &a, 100.0));
        assert_ne!(
            h1,
            line_hash(
                "abcdef",
                &[StyledSpan::new(0, 2, SpanAttr::link("x"))],
                100.0
            )
        );
        assert_ne!(
            h1,
            line_hash("abcdef", &[StyledSpan::new(1, 3, SpanAttr::bold())], 100.0)
        );
        assert_ne!(h1, line_hash("abcdef", &[], 100.0));
        assert_ne!(h1, line_hash("abcdef", &a, 200.0));

        let abs_a = vec![StyledSpan::new(100, 102, SpanAttr::bold())];
        let abs_b = vec![StyledSpan::new(200, 202, SpanAttr::bold())];
        assert_eq!(
            line_hash("abcdef", &rebase_spans(&abs_a, 100), 0.0),
            line_hash("abcdef", &rebase_spans(&abs_b, 200), 0.0)
        );
    }
}