fix: use highlight and cursor from cosmic-text editor

This fixes RTL text in the text_editor widget.
This commit is contained in:
Hojjat 2026-03-26 23:37:30 -06:00 committed by Ashley Wulber
parent 1fdd24ab99
commit e1fc659e64
5 changed files with 57 additions and 188 deletions

2
Cargo.lock generated
View file

@ -1316,7 +1316,7 @@ dependencies = [
[[package]]
name = "cosmic-text"
version = "0.18.2"
source = "git+https://github.com/pop-os/cosmic-text.git#ddad5fb7410e374612925415a13843ed38f14245"
source = "git+https://github.com/pop-os/cosmic-text.git#5651c2d96727749f8b19df4fda332bd8a2e6d823"
dependencies = [
"bitflags 2.11.0",
"fontdb",

View file

@ -256,15 +256,11 @@ pub fn align(
}
}
// TODO: Avoid relayout with some changes to `cosmic-text` (?)
if needs_relayout {
log::trace!("Relayouting paragraph...");
buffer.set_size(
font_system,
Some(min_bounds.width),
Some(min_bounds.height),
);
buffer.set_size(Some(min_bounds.width), Some(min_bounds.height));
buffer.shape_until_scroll(font_system, false);
}
min_bounds

View file

@ -47,18 +47,17 @@ impl Cache {
let mut buffer = cosmic_text::Buffer::new(font_system, metrics);
buffer.set_size(
font_system,
Some(key.bounds.width),
Some(key.bounds.height.max(key.line_height)),
);
buffer.set_text(
font_system,
key.content,
&text::to_attributes(key.font),
text::to_shaping(key.shaping, key.content),
None,
);
buffer.shape_until_scroll(font_system, false);
let bounds = text::align(&mut buffer, font_system, key.align_x);
let _ = entry.insert(Entry {

View file

@ -96,13 +96,13 @@ impl editor::Editor for Editor {
text::font_system().write().expect("Write font system");
buffer.set_text(
font_system.raw(),
text,
&cosmic_text::Attrs::new(),
cosmic_text::Shaping::Advanced,
None,
);
buffer.shape_until_scroll(font_system.raw(), false);
Editor(Some(Arc::new(Internal {
editor: cosmic_text::Editor::new(buffer),
version: font_system.version(),
@ -151,120 +151,54 @@ impl editor::Editor for Editor {
let cursor = match internal.editor.selection_bounds() {
Some((start, end)) => {
let line_height = buffer.metrics().line_height;
let selected_lines = end.line - start.line + 1;
let visual_lines_offset =
visual_lines_offset(start.line, buffer);
let scroll_y = buffer.scroll().vertical;
let regions = buffer
.lines
.iter()
.skip(start.line)
.take(selected_lines)
.enumerate()
.flat_map(|(i, line)| {
highlight_line(
line,
if i == 0 { start.index } else { 0 },
if i == selected_lines - 1 {
end.index
} else {
line.text().len()
},
)
.layout_runs()
.filter(|run| {
run.line_i >= start.line && run.line_i <= end.line
})
.enumerate()
.filter_map(|(visual_line, (x, width))| {
if width > 0.0 {
Some(Rectangle {
.flat_map(|run| {
let line_top = run.line_top;
run.highlight(start, end)
.filter(|(_, width)| *width > 0.0)
.map(move |(x, width)| Rectangle {
x,
width,
y: (visual_line as i32 + visual_lines_offset)
as f32
* line_height
- buffer.scroll().vertical,
y: line_top - scroll_y,
height: line_height,
})
} else {
None
}
.collect::<Vec<_>>()
})
.collect();
Selection::Range(regions)
}
_ => {
let line_height = buffer.metrics().line_height;
let scroll_y = buffer.scroll().vertical;
let visual_lines_offset =
visual_lines_offset(cursor.line, buffer);
let line = buffer
.lines
.get(cursor.line)
.expect("Cursor line should be present");
let layout =
line.layout_opt().expect("Line layout should be cached");
let mut lines = layout.iter().enumerate();
let (visual_line, offset) = lines
.find_map(|(i, line)| {
let start = line
.glyphs
.first()
.map(|glyph| glyph.start)
.unwrap_or(0);
let end = line
.glyphs
.last()
.map(|glyph| glyph.end)
.unwrap_or(0);
let is_cursor_before_start = start > cursor.index;
let is_cursor_before_end = match cursor.affinity {
cosmic_text::Affinity::Before => {
cursor.index <= end
}
cosmic_text::Affinity::After => cursor.index < end,
};
if is_cursor_before_start {
// Sometimes, the glyph we are looking for is right
// between lines. This can happen when a line wraps
// on a space.
// In that case, we can assume the cursor is at the
// end of the previous line.
// i is guaranteed to be > 0 because `start` is always
// 0 for the first line, so there is no way for the
// cursor to be before it.
Some((i - 1, layout[i - 1].w))
} else if is_cursor_before_end {
let offset = line
.glyphs
.iter()
.take_while(|glyph| cursor.index > glyph.start)
.map(|glyph| glyph.w)
.sum();
Some((i, offset))
} else {
None
}
let point = buffer
.layout_runs()
.filter(|run| run.line_i == cursor.line)
.find_map(|run| {
run.cursor_position(&cursor).map(|x| {
let buffer_w = buffer.size().0.unwrap_or(x + 1.0);
let x = x.min((buffer_w - 1.0).max(0.0));
Point::new(x, run.line_top - scroll_y)
})
})
.unwrap_or((
layout.len().saturating_sub(1),
layout.last().map(|line| line.w).unwrap_or(0.0),
));
.unwrap_or_else(|| {
// Fallback: cursor not found in any run (e.g. empty buffer).
let line_height = buffer.metrics().line_height;
let visual_lines_offset =
visual_lines_offset(cursor.line, buffer);
Point::new(
0.0,
visual_lines_offset as f32 * line_height - scroll_y,
)
});
Selection::Caret(Point::new(
offset,
(visual_lines_offset + visual_line as i32) as f32
* line_height
- buffer.scroll().vertical,
))
Selection::Caret(point)
}
};
@ -590,10 +524,10 @@ impl editor::Editor for Editor {
{
log::trace!("Updating `Metrics` of `Editor`...");
buffer.set_metrics(
font_system.raw(),
cosmic_text::Metrics::new(new_size.0, new_line_height.0),
);
buffer.set_metrics(cosmic_text::Metrics::new(
new_size.0,
new_line_height.0,
));
}
let new_wrap = text::to_wrap(new_wrapping);
@ -601,17 +535,14 @@ impl editor::Editor for Editor {
if new_wrap != buffer.wrap() {
log::trace!("Updating `Wrap` strategy of `Editor`...");
buffer.set_wrap(font_system.raw(), new_wrap);
buffer.set_wrap(new_wrap);
}
if new_bounds != internal.bounds {
log::trace!("Updating size of `Editor`...");
buffer.set_size(
font_system.raw(),
Some(new_bounds.width),
Some(new_bounds.height),
);
buffer
.set_size(Some(new_bounds.width), Some(new_bounds.height));
internal.bounds = new_bounds;
}
@ -778,53 +709,6 @@ impl PartialEq for Weak {
}
}
fn highlight_line(
line: &cosmic_text::BufferLine,
from: usize,
to: usize,
) -> impl Iterator<Item = (f32, f32)> + '_ {
let layout = line.layout_opt().map(Vec::as_slice).unwrap_or_default();
layout.iter().map(move |visual_line| {
let start = visual_line
.glyphs
.first()
.map(|glyph| glyph.start)
.unwrap_or(0);
let end = visual_line
.glyphs
.last()
.map(|glyph| glyph.end)
.unwrap_or(0);
let range = start.max(from)..end.min(to);
if range.is_empty() {
(0.0, 0.0)
} else if range.start == start && range.end == end {
(0.0, visual_line.w)
} else {
let first_glyph = visual_line
.glyphs
.iter()
.position(|glyph| range.start <= glyph.start)
.unwrap_or(0);
let mut glyphs = visual_line.glyphs.iter();
let x =
glyphs.by_ref().take(first_glyph).map(|glyph| glyph.w).sum();
let width: f32 = glyphs
.take_while(|glyph| range.end > glyph.start)
.map(|glyph| glyph.w)
.sum();
(x, width)
}
})
}
fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 {
let scroll = buffer.scroll();

View file

@ -79,26 +79,19 @@ impl core::text::Paragraph for Paragraph {
),
);
buffer.set_size(
font_system.raw(),
Some(text.bounds.width),
Some(text.bounds.height),
);
buffer.set_size(Some(text.bounds.width), Some(text.bounds.height));
buffer.set_wrap(font_system.raw(), text::to_wrap(text.wrapping));
buffer.set_ellipsize(
font_system.raw(),
text::to_ellipsize(text.ellipsize),
);
buffer.set_wrap(text::to_wrap(text.wrapping));
buffer.set_ellipsize(text::to_ellipsize(text.ellipsize));
buffer.set_text(
font_system.raw(),
text.content,
&text::to_attributes(text.font),
text::to_shaping(text.shaping, text.content),
None,
);
buffer.shape_until_scroll(font_system.raw(), false);
let min_bounds =
text::align(&mut buffer, font_system.raw(), text.align_x);
@ -130,16 +123,11 @@ impl core::text::Paragraph for Paragraph {
),
);
buffer.set_size(
font_system.raw(),
Some(text.bounds.width),
Some(text.bounds.height),
);
buffer.set_size(Some(text.bounds.width), Some(text.bounds.height));
buffer.set_wrap(font_system.raw(), text::to_wrap(text.wrapping));
buffer.set_wrap(text::to_wrap(text.wrapping));
buffer.set_rich_text(
font_system.raw(),
text.content.iter().enumerate().map(|(i, span)| {
let attrs = text::to_attributes(span.font.unwrap_or(text.font));
@ -176,6 +164,7 @@ impl core::text::Paragraph for Paragraph {
},
);
buffer.shape_until_scroll(font_system.raw(), false);
let min_bounds =
text::align(&mut buffer, font_system.raw(), text.align_x);
@ -199,11 +188,12 @@ impl core::text::Paragraph for Paragraph {
let mut font_system =
text::font_system().write().expect("Write font system");
paragraph.buffer.set_size(
font_system.raw(),
Some(new_bounds.width),
Some(new_bounds.height),
);
paragraph
.buffer
.set_size(Some(new_bounds.width), Some(new_bounds.height));
paragraph
.buffer
.shape_until_scroll(font_system.raw(), false);
let min_bounds = text::align(
&mut paragraph.buffer,