Skip to main content

oxitext_layout/
linebreak.rs

1//! UAX #14 Unicode Line Breaking Algorithm.
2//!
3//! Wraps the `unicode-linebreak` crate to expose a typed [`LineBreaker`]
4//! iterator over [`LineBreak`] opportunities in a string.
5
6use unicode_linebreak::{linebreaks, BreakOpportunity};
7
8/// A line-break opportunity classified by urgency.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum LineBreak {
11    /// A mandatory line break (e.g. `U+000A` NEWLINE, `U+0085` NEL).
12    Mandatory,
13    /// An optional line break that a layout engine may take when wrapping.
14    Allowed,
15}
16
17/// Pre-collected line-break opportunities for a string.
18///
19/// Because `unicode_linebreak::linebreaks()` returns an anonymous iterator
20/// type, all break opportunities are collected eagerly into a `Vec` and
21/// exposed through [`IntoIterator`] and an index-based slice accessor.
22///
23/// Each item is `(byte_offset, LineBreak)` where `byte_offset` is the
24/// position *after* the last character of the breakable unit (exclusive end
25/// of the pre-break segment).
26pub struct LineBreaker {
27    breaks: Vec<(usize, LineBreak)>,
28}
29
30impl LineBreaker {
31    /// Analyse `text` and collect all line-break opportunities.
32    pub fn new(text: &str) -> Self {
33        let breaks = linebreaks(text)
34            .map(|(pos, opp)| {
35                let lb = match opp {
36                    BreakOpportunity::Mandatory => LineBreak::Mandatory,
37                    BreakOpportunity::Allowed => LineBreak::Allowed,
38                };
39                (pos, lb)
40            })
41            .collect();
42        Self { breaks }
43    }
44
45    /// Returns all collected break opportunities as a slice.
46    pub fn breaks(&self) -> &[(usize, LineBreak)] {
47        &self.breaks
48    }
49
50    /// Returns an iterator over `(byte_offset, LineBreak)` pairs.
51    pub fn iter(&self) -> impl Iterator<Item = &(usize, LineBreak)> {
52        self.breaks.iter()
53    }
54}
55
56impl IntoIterator for LineBreaker {
57    type Item = (usize, LineBreak);
58    type IntoIter = std::vec::IntoIter<(usize, LineBreak)>;
59
60    fn into_iter(self) -> Self::IntoIter {
61        self.breaks.into_iter()
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn space_allows_break() {
71        let lb = LineBreaker::new("hello world");
72        let breaks: Vec<_> = lb.iter().cloned().collect();
73        assert!(
74            !breaks.is_empty(),
75            "should have at least one break opportunity"
76        );
77    }
78
79    #[test]
80    fn newline_is_mandatory() {
81        let lb = LineBreaker::new("hello\nworld");
82        let mandatory = lb.iter().any(|(_, kind)| *kind == LineBreak::Mandatory);
83        assert!(mandatory, "newline should produce a mandatory break");
84    }
85}