floem/views/editor/
layout.rs

1use crate::{
2    peniko::Color,
3    text::{LayoutLine, TextLayout},
4};
5use floem_editor_core::buffer::rope_text::RopeText;
6
7use super::{phantom_text::PhantomTextLine, visual_line::TextLayoutProvider};
8
9#[derive(Clone, Debug)]
10pub struct LineExtraStyle {
11    pub x: f64,
12    pub y: f64,
13    pub width: Option<f64>,
14    pub height: f64,
15    pub bg_color: Option<Color>,
16    pub under_line: Option<Color>,
17    pub wave_line: Option<Color>,
18}
19
20#[derive(Clone)]
21pub struct TextLayoutLine {
22    /// Extra styling that should be applied to the text
23    /// (x0, x1 or line display end, style)
24    pub extra_style: Vec<LineExtraStyle>,
25    pub text: TextLayout,
26    pub whitespaces: Option<Vec<(char, (f64, f64))>>,
27    pub indent: f64,
28    pub phantom_text: PhantomTextLine,
29}
30
31impl TextLayoutLine {
32    /// The number of line breaks in the text layout. Always at least `1`.
33    pub fn line_count(&self) -> usize {
34        self.relevant_layouts().count().max(1)
35    }
36
37    /// Iterate over all the layouts that are nonempty.
38    /// Note that this may be empty if the line is completely empty, like the last line
39    pub fn relevant_layouts(&self) -> impl Iterator<Item = &'_ LayoutLine> + '_ {
40        // Even though we only have one hard line (and thus only one `lines` entry) typically, for
41        // normal buffer lines, we can have more than one due to multiline phantom text. So we have
42        // to sum over all of the entries line counts.
43        self.text
44            .lines()
45            .iter()
46            .flat_map(|l| l.layout_opt())
47            .flat_map(|ls| ls.iter())
48            .filter(|l| !l.glyphs.is_empty())
49    }
50
51    /// Iterator over the (start, end) columns of the relevant layouts.
52    pub fn layout_cols<'a>(
53        &'a self,
54        text_prov: impl TextLayoutProvider + 'a,
55        line: usize,
56    ) -> impl Iterator<Item = (usize, usize)> + 'a {
57        let mut prefix = None;
58        // Include an entry if there is nothing
59        if self.text.lines().len() == 1 {
60            let line_start = self.text.lines_range()[0].start;
61            if let Some(layouts) = self.text.lines()[0].layout_opt() {
62                // Do we need to require !layouts.is_empty()?
63                if !layouts.is_empty() && layouts.iter().all(|l| l.glyphs.is_empty()) {
64                    // We assume the implicit glyph start is zero
65                    prefix = Some((line_start, line_start));
66                }
67            }
68        }
69
70        let line_v = line;
71        let iter = self
72            .text
73            .lines()
74            .iter()
75            .zip(self.text.lines_range().iter())
76            .filter_map(|(line, line_range)| line.layout_opt().map(|ls| (line, line_range, ls)))
77            .flat_map(|(line, line_range, ls)| ls.iter().map(move |l| (line, line_range, l)))
78            .filter(|(_, _, l)| !l.glyphs.is_empty())
79            .map(move |(tl_line, line_range, l)| {
80                let line_start = line_range.start;
81                tl_line.align();
82
83                let start = line_start + l.glyphs[0].start;
84                let end = line_start + l.glyphs.last().unwrap().end;
85
86                let text = text_prov.rope_text();
87                // We can't just use the original end, because the *true* last glyph on the line
88                // may be a space, but it isn't included in the layout! Though this only happens
89                // for single spaces, for some reason.
90                let pre_end = text_prov.before_phantom_col(line_v, end);
91                let line_offset = text.offset_of_line(line);
92
93                // TODO(minor): We don't really need the entire line, just the two characters after
94                let line_end = text.line_end_col(line, true);
95
96                let end = if pre_end <= line_end {
97                    let after = text.slice_to_cow(line_offset + pre_end..line_offset + line_end);
98                    if after.starts_with(' ') && !after.starts_with("  ") {
99                        end + 1
100                    } else {
101                        end
102                    }
103                } else {
104                    end
105                };
106
107                (start, end)
108            });
109
110        prefix.into_iter().chain(iter)
111    }
112
113    /// Iterator over the start columns of the relevant layouts
114    pub fn start_layout_cols<'a>(
115        &'a self,
116        text_prov: impl TextLayoutProvider + 'a,
117        line: usize,
118    ) -> impl Iterator<Item = usize> + 'a {
119        self.layout_cols(text_prov, line).map(|(start, _)| start)
120    }
121
122    /// Get the top y position of the given line index
123    pub fn get_layout_y(&self, nth: usize) -> Option<f32> {
124        self.text.layout_runs().nth(nth).map(|run| run.line_y)
125    }
126
127    /// Get the (start x, end x) positions of the given line index
128    pub fn get_layout_x(&self, nth: usize) -> Option<(f32, f32)> {
129        self.text.layout_runs().nth(nth).map(|run| {
130            (
131                run.glyphs.first().map(|g| g.x).unwrap_or(0.0),
132                run.glyphs.last().map(|g| g.x + g.w).unwrap_or(0.0),
133            )
134        })
135    }
136}