172 lines · 6.7 KB · Rust
RawDownload
use std::ops::{Add, Sub}; use itertools::Itertools;use num_traits::SaturatingSub;use string_offset::CharOffset; use super::FrameOffset; #[cfg(test)]#[path = "offset_map_tests.rs"]mod tests; /// Mapping between visible character offsets and interactive content character offsets. Due to/// placeholder text, not all characters painted on the screen correspond to interactive characters/// in the content model.////// The mapping model assumes that, within the overall set of characters in a text frame, only/// certain character runs are interactive. Within those runs, there's a 1:1 mapping between/// `char`s in the content model and `char`s in the text frame. It translates in two directions:/// * From a [`CharOffset`] relative to the start of the content model block (a [`super::Paragraph`])///   to the character index in the [`warpui_core::text_layout::TextFrame`]./// * From a [`FrameOffset`] in the `TextFrame` to the closest `CharOffset` in the content model///   block (for example, clicking within a placeholder should snap the cursor to a regular content///   character).////// An interactive character is one that the user can select and edit.#[derive(Debug, Clone)]pub struct OffsetMap {    /// Sorted list of interactive content runs.    runs: Vec<SelectableTextRun>,} /// A run of selectable/interactive content in the [`OffsetMap`]. This includes all user-written/// characters, but not placeholder text.#[derive(Debug, Clone, PartialEq, Eq)]pub struct SelectableTextRun {    /// The offset of the first content-character in this run.    pub content_start: CharOffset,    /// The offset of the first [`TextFrame`] visible character in this run.    pub frame_start: FrameOffset,    /// The total number of characters in this run. The run length is the same for both content    /// characters and text frame characters. That is, this run represents:    /// * Characters `content_start..content_start+length` within the containing paragraph    /// * Characters `frame_start..frame_start+length` within the containing `TextFrame`    pub length: usize,} impl OffsetMap {    /// Creates a new [`OffsetMap`] that translates 1:1 between visible character offsets and    /// interactive content character offsets.    pub fn direct(length: usize) -> Self {        Self::new(vec![SelectableTextRun {            content_start: CharOffset::zero(),            frame_start: FrameOffset::zero(),            length,        }])    }     pub fn new(mut runs: Vec<SelectableTextRun>) -> Self {        runs.sort_unstable_by_key(|r| r.frame_start);        if cfg!(debug_assertions) {            // Content- and frame-offsets should both increase from run to run.            for (a, b) in runs.iter().tuple_windows() {                assert!(a.content_start < b.content_start, "Runs must be ascending");            }        }        Self { runs }    }     /// Translate a visible character to the closest content character.    pub fn to_content(&self, offset: FrameOffset) -> CharOffset {        self.translate(offset)    }     /// Translate a content character to the corresponding visible character.    pub fn to_frame(&self, offset: CharOffset) -> FrameOffset {        // When going from content to frame offsets, the character is almost always going to be        // inside an interactive run. The exception is the placeholder marker character, and so        // it's simpler to reuse the translation algorithm from the other direction rather than        // detect and handle this special case.        self.translate(offset)    }     /// Translate from one kind of offset to the other. If the source offset is within an interactive    /// run, this returns the exact location of the character within that run. Otherwise, it rounds    /// up or down to the nearest interactive run.    fn translate<T: ParagraphOffset, U: ParagraphOffset>(&self, offset: T) -> U {        // Find the runs before and after `offset`, and calculate for each:        // - The absolute distance from it to `offset`        // - The translated offset according to it        let after_idx = self.runs.partition_point(|run| T::start(run) < offset);         let after = self.runs.get(after_idx).map(|run| {            let distance = T::start(run) - offset;            // If the run after `offset` is closer, then snap to the start of that run (e.g. the            // first regular character after a placeholder).            (distance, U::start(run))        });         let before = after_idx            .checked_sub(1)            .and_then(|idx| self.runs.get(idx))            .map(|run| {                // For the before run, we measure the distance from its end to offset. However, the                // offset could be within the run, so we clamp down to 0.                let run_end = T::start(run) + run.length;                let distance = offset.saturating_sub(&run_end);                // If we pick the before run, we essentially rebase it onto the destination start, but then                // clamp to stay within the run.                let translation =                    U::start(run) + (offset - T::start(run)).as_usize().min(run.length);                (distance, translation)            });         match (before, after) {            (None, None) => U::zero(), // This should only happen if the frame is empty.            (None, Some((_, translation))) => translation,            (Some((_, translation)), None) => translation,            (                Some((before_distance, before_translation)),                Some((after_distance, after_translation)),            ) => {                if before_distance < after_distance {                    before_translation                } else {                    after_translation                }            }        }    }} /// Abstracts over [`FrameOffset`] and [`CharOffset`] so that we can use the same conversion/// algorithm for both.trait ParagraphOffset:    Add<usize, Output = Self> + Ord + Sub<Output = Self> + SaturatingSub + Copy + Sized{    /// Gets the start offset of a [`SelectableTextRun`] for this offset/coordinate system.    fn start(run: &SelectableTextRun) -> Self;     fn zero() -> Self;     fn as_usize(self) -> usize;} impl ParagraphOffset for CharOffset {    fn start(run: &SelectableTextRun) -> Self {        run.content_start    }     fn zero() -> Self {        CharOffset::zero()    }     fn as_usize(self) -> usize {        CharOffset::as_usize(self)    }} impl ParagraphOffset for FrameOffset {    fn start(run: &SelectableTextRun) -> Self {        run.frame_start    }     fn zero() -> Self {        FrameOffset::zero()    }     fn as_usize(self) -> usize {        FrameOffset::as_usize(self)    }}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t