floem/views/editor/
layout.rs1use std::ops::Range;
2
3use crate::{
4 peniko::Color,
5 text::{Affinity, TextLayout, paragraph_ranges},
6};
7use floem_editor_core::buffer::rope_text::RopeText;
8
9use super::{phantom_text::PhantomTextLine, visual_line::TextLayoutProvider};
10
11#[derive(Clone, Debug)]
12pub struct LineExtraStyle {
13 pub x: f64,
14 pub y: f64,
15 pub width: Option<f64>,
16 pub height: f64,
17 pub bg_color: Option<Color>,
18 pub under_line: Option<Color>,
19 pub wave_line: Option<Color>,
20}
21
22#[derive(Clone)]
23pub struct TextLayoutLine {
24 pub extra_style: Vec<LineExtraStyle>,
27 pub text: TextLayout,
28 pub whitespaces: Option<Vec<(char, (f64, f64))>>,
29 pub indent: f64,
30 pub phantom_text: PhantomTextLine,
31}
32
33fn has_visible_content(full_text: &str, range: &Range<usize>) -> bool {
36 if range.end > full_text.len() || range.start >= range.end {
37 return false;
38 }
39 full_text.as_bytes()[range.start..range.end]
40 .iter()
41 .any(|&b| !b.is_ascii_whitespace())
42}
43
44impl TextLayoutLine {
45 pub fn line_count(&self) -> usize {
48 self.relevant_layout_count().max(1)
49 }
50
51 pub fn relevant_layout_count(&self) -> usize {
55 let count = self.text.visual_line_count();
56 let full_text = self.text.text();
57 (0..count)
58 .rev()
59 .find(|&i| {
60 self.text
61 .visual_line_text_range(i)
62 .is_some_and(|r| has_visible_content(full_text, &r))
63 })
64 .map_or(0, |last| last + 1)
65 }
66
67 pub fn layout_cols<'a>(
69 &'a self,
70 text_prov: impl TextLayoutProvider + 'a,
71 line: usize,
72 ) -> impl Iterator<Item = (usize, usize)> + 'a {
73 let visual_line_count = self.text.visual_line_count();
74 let full_text = self.text.text();
75 let line_v = line;
76
77 let visual_ranges: Vec<_> = (0..visual_line_count)
79 .filter_map(|i| {
80 let range = self.text.visual_line_text_range(i)?;
81 has_visible_content(full_text, &range).then_some(range)
82 })
83 .collect();
84
85 let prefix = if visual_ranges.is_empty()
86 && paragraph_ranges(full_text).count() == 1
87 && visual_line_count > 0
88 {
89 let s = paragraph_ranges(full_text)
90 .next()
91 .map_or(0, |range| range.start);
92 Some((s, s))
93 } else {
94 None
95 };
96
97 let iter = visual_ranges.into_iter().map(move |text_range| {
98 let start_idx = text_range.start;
99 let mut end_idx = text_range.end.min(full_text.len());
100
101 while end_idx > start_idx {
103 let ch = full_text.as_bytes().get(end_idx - 1).copied().unwrap_or(0);
104 if ch == b' ' || ch == b'\t' || ch == b'\n' || ch == b'\r' {
105 end_idx -= 1;
106 } else {
107 break;
108 }
109 }
110
111 let start = start_idx;
112 let end = end_idx;
113
114 let text = text_prov.rope_text();
115 let pre_end = text_prov.before_phantom_col(line_v, end);
116 let line_offset = text.offset_of_line(line);
117 let line_end = text.line_end_col(line, true);
118
119 let end = if pre_end <= line_end {
120 let after = text.slice_to_cow(line_offset + pre_end..line_offset + line_end);
121 if after.starts_with(' ') && !after.starts_with(" ") {
122 end + 1
123 } else {
124 end
125 }
126 } else {
127 end
128 };
129
130 (start, end)
131 });
132
133 prefix.into_iter().chain(iter)
134 }
135
136 pub fn start_layout_cols(&self) -> impl Iterator<Item = usize> + '_ {
140 let visual_line_count = self.text.visual_line_count();
141 let full_text = self.text.text();
142
143 let mut starts: Vec<usize> = (0..visual_line_count)
144 .filter_map(|i| {
145 let range = self.text.visual_line_text_range(i)?;
146 has_visible_content(full_text, &range).then_some(range.start)
147 })
148 .collect();
149
150 if starts.is_empty() && paragraph_ranges(full_text).count() == 1 && visual_line_count > 0 {
152 starts.push(
153 paragraph_ranges(full_text)
154 .next()
155 .map_or(0, |range| range.start),
156 );
157 }
158
159 starts.into_iter()
160 }
161
162 pub fn get_layout_y(&self, nth: usize) -> Option<f32> {
164 self.text.visual_line_y(nth)
165 }
166
167 pub fn get_layout_x(&self, nth: usize) -> Option<(f32, f32)> {
169 let text_range = self.text.visual_line_text_range(nth)?;
170 let full_text = self.text.text();
171
172 if text_range.is_empty() || text_range.end > full_text.len() {
173 return Some((0.0, 0.0));
174 }
175
176 let start_x = self
177 .text
178 .cursor_point(text_range.start, Affinity::Upstream)
179 .x;
180 let mut end_byte = text_range.end;
182 while end_byte > text_range.start {
183 let ch = full_text.as_bytes().get(end_byte - 1).copied().unwrap_or(0);
184 if ch == b' ' || ch == b'\t' || ch == b'\n' || ch == b'\r' {
185 end_byte -= 1;
186 } else {
187 break;
188 }
189 }
190 let end_x = self.text.cursor_point(end_byte, Affinity::Upstream).x;
191
192 Some((start_x as f32, end_x as f32))
193 }
194}