kas-text 0.6.0

Text layout and font management
Documentation
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License in the LICENSE-APACHE file or at:
//     https://www.apache.org/licenses/LICENSE-2.0

//! Text preparation: line breaking and BIDI

#![allow(clippy::unnecessary_unwrap)]

use super::TextDisplay;
use crate::conv::{to_u32, to_usize};
use crate::fonts::{fonts, FontId, InvalidFontId};
use crate::format::FormattableText;
use crate::{shaper, Action, Direction, Range};
use unicode_bidi::{BidiClass, BidiInfo, Level, LTR_LEVEL, RTL_LEVEL};
use xi_unicode::LineBreakIterator;

#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum RunSpecial {
    None,
    /// Run ends with a hard break
    HardBreak,
    /// Run does not end with a break
    NoBreak,
    /// Run is a horizontal tab (run is a single char only)
    HTab,
}

impl TextDisplay {
    /// Update font size
    ///
    /// This updates the result of [`TextDisplay::prepare_runs`] due to change
    /// in font size.
    ///
    /// Prerequisites: prepared runs: requires action is no greater than `Action::Wrap`.
    /// Post-requirements: prepare lines (requires action `Action::Wrap`).  
    /// Parameters: see [`crate::Environment`] documentation.
    pub(crate) fn resize_runs<F: FormattableText + ?Sized>(&mut self, text: &F, dpem: f32) {
        assert!(self.action <= Action::Resize);
        self.action = Action::Wrap;

        let mut font_tokens = text.font_tokens(dpem);
        let mut next_fmt = font_tokens.next();

        let mut input = shaper::Input {
            text: text.as_str(),
            dpem,
            face_id: crate::fonts::FaceId(0),
            level: LTR_LEVEL,
        };

        for run in &mut self.runs {
            while let Some(fmt) = next_fmt.as_ref() {
                if fmt.start > run.range.start {
                    break;
                }
                input.dpem = fmt.dpem;
                next_fmt = font_tokens.next();
            }

            input.face_id = run.face_id;
            input.level = run.level;
            let mut breaks = Default::default();
            std::mem::swap(&mut breaks, &mut run.breaks);
            if run.level.is_rtl() {
                breaks.reverse();
            }
            *run = shaper::shape(input, run.range, breaks, run.special);
        }
    }

    /// Prepare text runs
    ///
    /// This is the first step of preparation: breaking text into runs according
    /// to font properties, bidi-levels and line-wrap points.
    ///
    /// This method only updates self as required; use [`Self::require_action`] if necessary.
    /// On [`Action::All`], this prepares runs from scratch; on [`Action::Resize`] existing runs
    /// are resized; afterwards, action is no greater than [`Action::Wrap`].
    ///
    /// Parameters: see [`crate::Environment`] documentation.
    pub fn prepare_runs<F: FormattableText + ?Sized>(
        &mut self,
        text: &F,
        direction: Direction,
        mut font_id: FontId,
        mut dpem: f32,
    ) -> Result<(), InvalidFontId> {
        match self.action {
            Action::None | Action::VAlign | Action::Wrap => return Ok(()),
            Action::Resize => return Ok(self.resize_runs(text, dpem)),
            Action::All => (),
        }

        // This method constructs a list of "hard lines" (the initial line and any
        // caused by a hard break), each composed of a list of "level runs" (the
        // result of splitting and reversing according to Unicode TR9 aka
        // Bidirectional algorithm), plus a list of "soft break" positions
        // (where wrapping may introduce new lines depending on available space).

        self.runs.clear();

        let mut font_tokens = text.font_tokens(dpem);
        let mut next_fmt = font_tokens.next();
        if let Some(fmt) = next_fmt.as_ref() {
            if fmt.start == 0 {
                font_id = fmt.font_id;
                dpem = fmt.dpem;
                next_fmt = font_tokens.next();
            }
        }

        let fonts = fonts();
        let text = text.as_str();

        let (bidi, default_para_level) = match direction {
            Direction::Bidi => (true, None),
            Direction::BidiRtl => (true, Some(RTL_LEVEL)),
            Direction::Single => (false, None),
            Direction::Ltr => (false, Some(LTR_LEVEL)),
            Direction::Rtl => (false, Some(RTL_LEVEL)),
        };
        let level: Level;
        let levels;
        let classes;
        if bidi || default_para_level.is_none() {
            let info = BidiInfo::new(text, default_para_level);
            levels = info.levels;
            assert_eq!(text.len(), levels.len());
            level = levels.get(0).cloned().unwrap_or(LTR_LEVEL);
            classes = info.original_classes;
        } else {
            level = default_para_level.unwrap();
            levels = vec![];
            classes = vec![];
        }

        let mut input = shaper::Input {
            text,
            dpem,
            face_id: fonts.first_face_for(font_id)?,
            level,
        };

        let mut start = 0;
        let mut breaks = Default::default();

        // Iterates over `(pos, hard)` tuples:
        let mut breaks_iter = LineBreakIterator::new(text);
        let mut next_break = breaks_iter.next().unwrap_or((0, false));

        let mut last_is_control = false;
        let mut last_is_htab = false;
        let mut non_control_end = 0;

        for (pos, c) in text.char_indices() {
            // Handling for control chars
            if !last_is_control {
                non_control_end = pos;
            }
            let is_control = c.is_control();
            let is_htab = c == '\t';
            let control_break = is_htab || (last_is_control && !is_control);

            // Is wrapping allowed at this position?
            let is_break = next_break.0 == pos;
            // Forcibly end the line?
            let hard_break = is_break && next_break.1;
            if is_break {
                next_break = breaks_iter.next().unwrap_or((0, false));
            }

            // Force end of current run?
            let bidi_break = bidi && levels[pos] != input.level;

            let mut fmt_break = false;
            if let Some(fmt) = next_fmt.as_ref() {
                if to_usize(fmt.start) == pos {
                    fmt_break = true;
                    font_id = fmt.font_id;
                    dpem = fmt.dpem;
                    next_fmt = font_tokens.next();
                }
            }

            let opt_last_face = if matches!(
                classes[pos],
                BidiClass::L | BidiClass::R | BidiClass::AL | BidiClass::EN | BidiClass::AN
            ) {
                None
            } else {
                Some(input.face_id)
            };
            let face_id = fonts.face_for_char_or_first(font_id, opt_last_face, c)?;
            let font_break = pos > 0 && face_id != input.face_id;

            if hard_break || control_break || bidi_break || fmt_break || font_break {
                // TODO: sometimes this results in empty runs immediately
                // following another run. Ideally we would either merge these
                // into the previous run or not simply break in this case.
                // Note: the prior run may end with NoBreak while the latter
                // (and the merge result) do not.
                let range = (start..non_control_end).into();
                let special = match () {
                    _ if hard_break => RunSpecial::HardBreak,
                    _ if last_is_htab => RunSpecial::HTab,
                    _ if last_is_control || is_break => RunSpecial::None,
                    _ => RunSpecial::NoBreak,
                };
                self.runs.push(shaper::shape(input, range, breaks, special));

                start = pos;
                non_control_end = pos;
                if bidi {
                    input.level = levels[pos];
                }
                breaks = Default::default();
            } else if is_break && !is_control {
                // We do break runs when hitting control chars, but only when
                // encountering the next non-control character.
                breaks.push(shaper::GlyphBreak::new(to_u32(pos)));
            }

            last_is_control = is_control;
            last_is_htab = is_htab;
            input.face_id = face_id;
            input.dpem = dpem;
        }

        // The LineBreakIterator finishes with a break (unless the string is empty).
        // This is a hard break when the string finishes with an explicit line-break.
        debug_assert_eq!(next_break.0, text.len());
        let hard_break = next_break.1;

        // Conclude: add last run. This may be empty, but we want it anyway.
        if !last_is_control {
            non_control_end = text.len();
        }
        let range = (start..non_control_end).into();
        let special = match () {
            _ if hard_break => RunSpecial::HardBreak,
            _ if last_is_htab => RunSpecial::HTab,
            _ => RunSpecial::None,
        };
        self.runs.push(shaper::shape(input, range, breaks, special));

        // Following a hard break we have an implied empty line.
        if hard_break {
            let range = Range::from(text.len()..text.len());
            input.level = default_para_level.unwrap_or(LTR_LEVEL);
            breaks = Default::default();
            self.runs
                .push(shaper::shape(input, range, breaks, RunSpecial::None));
        }

        /*
        println!("text: {}", text);
        for run in &self.runs {
            let slice = &text[run.range];
            print!(
                "\t{:?}, text[{}..{}]: '{}', ",
                run.level, run.range.start, run.range.end, slice
            );
            match run.special {
                RunSpecial::None => (),
                RunSpecial::HardBreak => println!("HardBreak, "),
                RunSpecial::NoBreak => print!("NoBreak, "),
                RunSpecial::HTab => print!("HTab, "),
            }
            print!("breaks=[");
            let mut iter = run.breaks.iter();
            if let Some(b) = iter.next() {
                print!("{}", b.index);
            }
            for b in iter {
                print!(", {}", b.index);
            }
            println!("]");
        }
        */
        self.action = Action::Wrap;
        Ok(())
    }
}