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]] [[package]]
name = "cosmic-text" name = "cosmic-text"
version = "0.18.2" 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 = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.11.0",
"fontdb", "fontdb",

View file

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

View file

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

View file

@ -96,13 +96,13 @@ impl editor::Editor for Editor {
text::font_system().write().expect("Write font system"); text::font_system().write().expect("Write font system");
buffer.set_text( buffer.set_text(
font_system.raw(),
text, text,
&cosmic_text::Attrs::new(), &cosmic_text::Attrs::new(),
cosmic_text::Shaping::Advanced, cosmic_text::Shaping::Advanced,
None, None,
); );
buffer.shape_until_scroll(font_system.raw(), false);
Editor(Some(Arc::new(Internal { Editor(Some(Arc::new(Internal {
editor: cosmic_text::Editor::new(buffer), editor: cosmic_text::Editor::new(buffer),
version: font_system.version(), version: font_system.version(),
@ -151,120 +151,54 @@ impl editor::Editor for Editor {
let cursor = match internal.editor.selection_bounds() { let cursor = match internal.editor.selection_bounds() {
Some((start, end)) => { Some((start, end)) => {
let line_height = buffer.metrics().line_height; let line_height = buffer.metrics().line_height;
let selected_lines = end.line - start.line + 1; let scroll_y = buffer.scroll().vertical;
let visual_lines_offset =
visual_lines_offset(start.line, buffer);
let regions = buffer let regions = buffer
.lines .layout_runs()
.iter() .filter(|run| {
.skip(start.line) run.line_i >= start.line && run.line_i <= end.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()
},
)
}) })
.enumerate() .flat_map(|run| {
.filter_map(|(visual_line, (x, width))| { let line_top = run.line_top;
if width > 0.0 { run.highlight(start, end)
Some(Rectangle { .filter(|(_, width)| *width > 0.0)
.map(move |(x, width)| Rectangle {
x, x,
width, width,
y: (visual_line as i32 + visual_lines_offset) y: line_top - scroll_y,
as f32
* line_height
- buffer.scroll().vertical,
height: line_height, height: line_height,
}) })
} else { .collect::<Vec<_>>()
None
}
}) })
.collect(); .collect();
Selection::Range(regions) Selection::Range(regions)
} }
_ => { _ => {
let line_height = buffer.metrics().line_height; let scroll_y = buffer.scroll().vertical;
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_else(|| {
// Fallback: cursor not found in any run (e.g. empty buffer).
let line_height = buffer.metrics().line_height;
let visual_lines_offset = let visual_lines_offset =
visual_lines_offset(cursor.line, buffer); visual_lines_offset(cursor.line, buffer);
Point::new(
0.0,
visual_lines_offset as f32 * line_height - scroll_y,
)
});
let line = buffer Selection::Caret(point)
.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
}
})
.unwrap_or((
layout.len().saturating_sub(1),
layout.last().map(|line| line.w).unwrap_or(0.0),
));
Selection::Caret(Point::new(
offset,
(visual_lines_offset + visual_line as i32) as f32
* line_height
- buffer.scroll().vertical,
))
} }
}; };
@ -590,10 +524,10 @@ impl editor::Editor for Editor {
{ {
log::trace!("Updating `Metrics` of `Editor`..."); log::trace!("Updating `Metrics` of `Editor`...");
buffer.set_metrics( buffer.set_metrics(cosmic_text::Metrics::new(
font_system.raw(), new_size.0,
cosmic_text::Metrics::new(new_size.0, new_line_height.0), new_line_height.0,
); ));
} }
let new_wrap = text::to_wrap(new_wrapping); let new_wrap = text::to_wrap(new_wrapping);
@ -601,17 +535,14 @@ impl editor::Editor for Editor {
if new_wrap != buffer.wrap() { if new_wrap != buffer.wrap() {
log::trace!("Updating `Wrap` strategy of `Editor`..."); 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 { if new_bounds != internal.bounds {
log::trace!("Updating size of `Editor`..."); log::trace!("Updating size of `Editor`...");
buffer.set_size( buffer
font_system.raw(), .set_size(Some(new_bounds.width), Some(new_bounds.height));
Some(new_bounds.width),
Some(new_bounds.height),
);
internal.bounds = new_bounds; 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 { fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 {
let scroll = buffer.scroll(); let scroll = buffer.scroll();

View file

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