aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoralex-ds13 <145657253+alex-ds13@users.noreply.github.com>2025-11-25 14:42:06 +0000
committerPolesznyák Márk <contact@pml68.dev>2025-12-29 23:23:41 +0100
commitb46c556f863917a753c55359fb05543db0ccef7c (patch)
tree5e7cd96ef880fd66cfd8ad23bc685d1ffc5cac33
parentchore: add missing hint_factor fields (iced update) (diff)
downloadiced_selection-b46c556f863917a753c55359fb05543db0ccef7c.tar.gz
feat: correct selection on wrapped lines and allow mouse drag out of bounds
-rw-r--r--examples/name/src/main.rs3
-rw-r--r--src/text.rs287
-rw-r--r--src/text/rich.rs287
3 files changed, 263 insertions, 314 deletions
diff --git a/examples/name/src/main.rs b/examples/name/src/main.rs
index 9bf4b72..2e2582c 100644
--- a/examples/name/src/main.rs
+++ b/examples/name/src/main.rs
@@ -1,4 +1,3 @@
-use iced::advanced::text::Wrapping;
use iced::widget::operation::focus_next;
use iced::widget::{center, column, text_editor};
use iced::{Center, Element, Task};
@@ -41,7 +40,7 @@ impl State {
fn view(&self) -> Element<'_, Message> {
center(
column![
- text!("Hello {}", &self.name).wrapping(Wrapping::None),
+ text!("Hello {}", &self.name),
text_editor(&self.content).on_action(Message::UpdateText)
]
.spacing(10)
diff --git a/src/text.rs b/src/text.rs
index 7f67b86..5e1356f 100644
--- a/src/text.rs
+++ b/src/text.rs
@@ -19,6 +19,7 @@
mod rich;
use iced_widget::graphics::text::Paragraph;
+use iced_widget::graphics::text::cosmic_text;
pub use rich::Rich;
use text::{Alignment, LineHeight, Shaping, Wrapping};
pub use text::{Fragment, Highlighter, IntoFragment, Span};
@@ -189,9 +190,26 @@ pub enum Dragging {
}
impl State {
- fn grapheme_line_and_index(&self, point: Point) -> Option<(usize, usize)> {
- let cursor = self.paragraph.buffer().hit(point.x, point.y)?;
- let value = self.paragraph.buffer().lines[cursor.line].text();
+ fn grapheme_line_and_index(
+ &self,
+ point: Point,
+ bounds: core::Rectangle,
+ ) -> Option<(usize, usize)> {
+ let bounded_x = if point.y < bounds.y {
+ bounds.x
+ } else if point.y > bounds.y + bounds.height {
+ bounds.x + bounds.width
+ } else {
+ point.x.max(bounds.x).min(bounds.x + bounds.width)
+ };
+ let bounded_y = point.y.max(bounds.y).min(bounds.y + bounds.height);
+ let bounded_point = Point::new(bounded_x, bounded_y);
+ let relative_point =
+ bounded_point - core::Vector::new(bounds.x, bounds.y);
+
+ let buffer = self.paragraph.buffer();
+ let cursor = buffer.hit(relative_point.x, relative_point.y)?;
+ let value = buffer.lines[cursor.line].text();
Some((
cursor.line,
@@ -203,119 +221,48 @@ impl State {
))
}
- fn selection_end_points(&self) -> (usize, Point, Point) {
+ fn selection(&self) -> Vec<core::Rectangle> {
let Selection { start, end, .. } = self.selection;
- let (start_row, start_position) = self
- .grapheme_position(start.line, start.index)
- .unwrap_or_default();
-
- let (end_row, end_position) = self
- .grapheme_position(end.line, end.index)
- .unwrap_or_default();
+ let buffer = self.paragraph.buffer();
+ let line_height = self.paragraph.buffer().metrics().line_height;
+ let selected_lines = end.line - start.line + 1;
- (
- end_row.saturating_sub(start_row) + 1,
- start_position,
- end_position,
- )
- }
+ let visual_lines_offset = visual_lines_offset(start.line, buffer);
- fn grapheme_position(
- &self,
- line: usize,
- index: usize,
- ) -> Option<(usize, Point)> {
- use unicode_segmentation::UnicodeSegmentation;
-
- let mut first_run_index = None;
- let mut last_run_index = None;
- let mut last_start = None;
- let mut last_grapheme_count = 0;
- let mut last_run_graphemes = 0;
- let mut real_index = 0;
- let mut graphemes_seen = 0;
-
- let mut glyphs = self
- .paragraph
- .buffer()
- .layout_runs()
+ buffer
+ .lines
+ .iter()
+ .skip(start.line)
+ .take(selected_lines)
.enumerate()
- .filter(|(_, run)| run.line_i == line)
- .flat_map(|(run_idx, run)| {
- let line_top = run.line_top;
-
- if first_run_index.is_none() {
- first_run_index = Some(run_idx);
- }
-
- run.glyphs.iter().map(move |glyph| {
- let mut glyph = glyph.clone();
- glyph.y += line_top;
- (run_idx, glyph, run.text)
- })
- });
-
- let (_, glyph, _) = glyphs
- .find(|(run_idx, glyph, text)| {
- if Some(glyph.start) != last_start {
- last_grapheme_count =
- text[glyph.start..glyph.end].graphemes(false).count();
- last_start = Some(glyph.start);
- graphemes_seen += last_grapheme_count;
- last_run_graphemes += last_grapheme_count;
- real_index += last_grapheme_count;
-
- if Some(*run_idx) != last_run_index
- && graphemes_seen < index
- {
- real_index = last_grapheme_count;
- last_run_graphemes = last_grapheme_count;
- }
- } else if Some(*run_idx) != last_run_index
- && graphemes_seen < index
- {
- real_index = 0;
- last_run_graphemes = 0;
+ .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()
+ .filter_map(|(visual_line, (x, width))| {
+ if width > 0.0 {
+ Some(core::Rectangle {
+ x,
+ width,
+ y: (visual_line as i32 + visual_lines_offset) as f32
+ * line_height
+ - buffer.scroll().vertical,
+ height: line_height,
+ })
+ } else {
+ None
}
-
- last_run_index = Some(*run_idx);
-
- graphemes_seen >= index
})
- .or_else(|| glyphs.last())?;
-
- real_index -= graphemes_seen.saturating_sub(index);
- real_index =
- real_index.saturating_sub(last_run_index? - first_run_index?);
-
- last_run_graphemes = last_run_graphemes
- .saturating_sub(last_run_index? - first_run_index?);
-
- let advance = if last_run_index? - first_run_index? <= 1 {
- if real_index == 0 {
- 0.0
- } else {
- glyph.w
- * (1.0
- - last_run_graphemes.saturating_sub(real_index) as f32
- / last_grapheme_count.max(1) as f32)
- - glyph.w * (last_run_index? - first_run_index?) as f32
- }
- } else {
- -(glyph.w
- * (1.0
- + last_run_graphemes.saturating_sub(real_index) as f32
- / last_grapheme_count.max(1) as f32))
- };
-
- Some((
- last_run_index?,
- Point::new(
- glyph.x + glyph.x_offset * glyph.font_size + advance,
- glyph.y - glyph.y_offset * glyph.font_size,
- ),
- ))
+ .collect()
}
fn update(&mut self, text: text::Text<&str, Font>) {
@@ -401,7 +348,7 @@ where
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
- if let Some(position) = click_position {
+ if let Some(position) = cursor.position_over(bounds) {
let click = mouse::Click::new(
position,
mouse::Button::Left,
@@ -409,7 +356,7 @@ where
);
let (line, index) = state
- .grapheme_line_and_index(position)
+ .grapheme_line_and_index(position, bounds)
.unwrap_or((0, 0));
match click.kind() {
@@ -453,11 +400,11 @@ where
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
- if let Some(position) = click_position
+ if let Some(position) = cursor.position()
&& let Some(dragging) = state.dragging
{
let (line, index) = state
- .grapheme_line_and_index(position)
+ .grapheme_line_and_index(position, bounds)
.unwrap_or((0, 0));
match dragging {
@@ -634,51 +581,17 @@ where
if !state.selection.is_empty() {
let bounds = layout.bounds();
+ let translation = bounds.position() - Point::ORIGIN;
+ let ranges = state.selection();
- let (rows, mut start, mut end) = state.selection_end_points();
- start += core::Vector::new(bounds.x, bounds.y);
- end += core::Vector::new(bounds.x, bounds.y);
-
- let line_height = self
- .format
- .line_height
- .to_absolute(
- self.format.size.unwrap_or_else(|| renderer.default_size()),
- )
- .0;
-
- let baseline_y = bounds.y
- + (((start.y - bounds.y) * 10.0).ceil() / 10.0 / line_height)
- .floor()
- * line_height;
-
- for row in 0..rows {
- let (x, width) = if row == 0 {
- (
- start.x,
- if rows == 1 {
- end.x.min(bounds.x + bounds.width) - start.x
- } else {
- bounds.x + bounds.width - start.x
- },
- )
- } else if row == rows - 1 {
- (bounds.x, end.x - bounds.x)
- } else {
- (bounds.x, bounds.width)
- };
- let y = baseline_y + row as f32 * line_height;
-
+ for range in ranges
+ .into_iter()
+ .filter_map(|range| bounds.intersection(&(range + translation)))
+ {
renderer.fill_quad(
renderer::Quad {
- bounds: core::Rectangle {
- x,
- y,
- width,
- height: line_height,
- },
- snap: true,
- ..Default::default()
+ bounds: range,
+ ..renderer::Quad::default()
},
style.selection,
);
@@ -715,7 +628,7 @@ where
) -> mouse::Interaction {
let state = tree.state.downcast_ref::<State>();
- if state.is_hovered {
+ if state.is_hovered || state.dragging.is_some() {
mouse::Interaction::Text
} else {
mouse::Interaction::default()
@@ -854,3 +767,65 @@ pub fn default(theme: &Theme) -> Style {
selection: theme.extended_palette().primary.weak.color,
}
}
+
+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: f32 =
+ 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();
+
+ let start = scroll.line.min(line);
+ let end = scroll.line.max(line);
+
+ let visual_lines_offset: usize = buffer.lines[start..]
+ .iter()
+ .take(end - start)
+ .map(|line| line.layout_opt().map(Vec::len).unwrap_or_default())
+ .sum();
+
+ visual_lines_offset as i32 * if scroll.line < line { 1 } else { -1 }
+}
diff --git a/src/text/rich.rs b/src/text/rich.rs
index 7d162a7..29c43f5 100644
--- a/src/text/rich.rs
+++ b/src/text/rich.rs
@@ -1,4 +1,5 @@
use iced_widget::graphics::text::Paragraph;
+use iced_widget::graphics::text::cosmic_text;
use crate::core::alignment;
use crate::core::clipboard;
@@ -227,9 +228,26 @@ struct State<Link> {
}
impl<Link> State<Link> {
- fn grapheme_line_and_index(&self, point: Point) -> Option<(usize, usize)> {
- let cursor = self.paragraph.buffer().hit(point.x, point.y)?;
- let value = self.paragraph.buffer().lines[cursor.line].text();
+ fn grapheme_line_and_index(
+ &self,
+ point: Point,
+ bounds: core::Rectangle,
+ ) -> Option<(usize, usize)> {
+ let bounded_x = if point.y < bounds.y {
+ bounds.x
+ } else if point.y > bounds.y + bounds.height {
+ bounds.x + bounds.width
+ } else {
+ point.x.max(bounds.x).min(bounds.x + bounds.width)
+ };
+ let bounded_y = point.y.max(bounds.y).min(bounds.y + bounds.height);
+ let bounded_point = Point::new(bounded_x, bounded_y);
+ let relative_point =
+ bounded_point - core::Vector::new(bounds.x, bounds.y);
+
+ let buffer = self.paragraph.buffer();
+ let cursor = buffer.hit(relative_point.x, relative_point.y)?;
+ let value = buffer.lines[cursor.line].text();
Some((
cursor.line,
@@ -241,119 +259,48 @@ impl<Link> State<Link> {
))
}
- fn selection_end_points(&self) -> (usize, Point, Point) {
+ fn selection(&self) -> Vec<core::Rectangle> {
let Selection { start, end, .. } = self.selection;
- let (start_row, start_position) = self
- .grapheme_position(start.line, start.index)
- .unwrap_or_default();
+ let buffer = self.paragraph.buffer();
+ let line_height = self.paragraph.buffer().metrics().line_height;
+ let selected_lines = end.line - start.line + 1;
- let (end_row, end_position) = self
- .grapheme_position(end.line, end.index)
- .unwrap_or_default();
-
- (
- end_row.saturating_sub(start_row) + 1,
- start_position,
- end_position,
- )
- }
+ let visual_lines_offset = visual_lines_offset(start.line, buffer);
- fn grapheme_position(
- &self,
- line: usize,
- index: usize,
- ) -> Option<(usize, Point)> {
- use unicode_segmentation::UnicodeSegmentation;
-
- let mut first_run_index = None;
- let mut last_run_index = None;
- let mut last_start = None;
- let mut last_grapheme_count = 0;
- let mut last_run_graphemes = 0;
- let mut real_index = 0;
- let mut graphemes_seen = 0;
-
- let mut glyphs = self
- .paragraph
- .buffer()
- .layout_runs()
+ buffer
+ .lines
+ .iter()
+ .skip(start.line)
+ .take(selected_lines)
.enumerate()
- .filter(|(_, run)| run.line_i == line)
- .flat_map(|(run_idx, run)| {
- let line_top = run.line_top;
-
- if first_run_index.is_none() {
- first_run_index = Some(run_idx);
- }
-
- run.glyphs.iter().map(move |glyph| {
- let mut glyph = glyph.clone();
- glyph.y += line_top;
- (run_idx, glyph, run.text)
- })
- });
-
- let (_, glyph, _) = glyphs
- .find(|(run_idx, glyph, text)| {
- if Some(glyph.start) != last_start {
- last_grapheme_count =
- text[glyph.start..glyph.end].graphemes(false).count();
- last_start = Some(glyph.start);
- graphemes_seen += last_grapheme_count;
- last_run_graphemes += last_grapheme_count;
- real_index += last_grapheme_count;
-
- if Some(*run_idx) != last_run_index
- && graphemes_seen < index
- {
- real_index = last_grapheme_count;
- last_run_graphemes = last_grapheme_count;
- }
- } else if Some(*run_idx) != last_run_index
- && graphemes_seen < index
- {
- real_index = 0;
- last_run_graphemes = 0;
+ .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()
+ .filter_map(|(visual_line, (x, width))| {
+ if width > 0.0 {
+ Some(core::Rectangle {
+ x,
+ width,
+ y: (visual_line as i32 + visual_lines_offset) as f32
+ * line_height
+ - buffer.scroll().vertical,
+ height: line_height,
+ })
+ } else {
+ None
}
-
- last_run_index = Some(*run_idx);
-
- graphemes_seen >= index
})
- .or_else(|| glyphs.last())?;
-
- real_index -= graphemes_seen.saturating_sub(index);
- real_index =
- real_index.saturating_sub(last_run_index? - first_run_index?);
-
- last_run_graphemes = last_run_graphemes
- .saturating_sub(last_run_index? - first_run_index?);
-
- let advance = if last_run_index? - first_run_index? <= 1 {
- if real_index == 0 {
- 0.0
- } else {
- glyph.w
- * (1.0
- - last_run_graphemes.saturating_sub(real_index) as f32
- / last_grapheme_count.max(1) as f32)
- - glyph.w * (last_run_index? - first_run_index?) as f32
- }
- } else {
- -(glyph.w
- * (1.0
- + last_run_graphemes.saturating_sub(real_index) as f32
- / last_grapheme_count.max(1) as f32))
- };
-
- Some((
- last_run_index?,
- Point::new(
- glyph.x + glyph.x_offset * glyph.font_size + advance,
- glyph.y - glyph.y_offset * glyph.font_size,
- ),
- ))
+ .collect()
}
}
@@ -523,51 +470,17 @@ where
if !state.selection.is_empty() {
let bounds = layout.bounds();
+ let translation = bounds.position() - Point::ORIGIN;
+ let ranges = state.selection();
- let (rows, mut start, mut end) = state.selection_end_points();
- start += core::Vector::new(bounds.x, bounds.y);
- end += core::Vector::new(bounds.x, bounds.y);
-
- let line_height = self
- .line_height
- .to_absolute(
- self.size.unwrap_or_else(|| renderer.default_size()),
- )
- .0;
-
- let baseline_y = bounds.y
- + (((start.y - bounds.y) * 10.0).ceil() / 10.0 / line_height)
- .floor()
- * line_height;
-
- for row in 0..rows {
- let (x, width) = if row == 0 {
- (
- start.x,
- if rows == 1 {
- end.x.min(bounds.x + bounds.width) - start.x
- } else {
- bounds.x + bounds.width - start.x
- },
- )
- } else if row == rows - 1 {
- (bounds.x, end.x - bounds.x)
- } else {
- (bounds.x, bounds.width)
- };
-
- let y = baseline_y + row as f32 * line_height;
-
+ for range in ranges
+ .into_iter()
+ .filter_map(|range| bounds.intersection(&(range + translation)))
+ {
renderer.fill_quad(
renderer::Quad {
- bounds: Rectangle {
- x,
- y,
- width,
- height: line_height,
- },
- snap: true,
- ..Default::default()
+ bounds: range,
+ ..renderer::Quad::default()
},
style.selection,
);
@@ -634,7 +547,7 @@ where
shell.capture_event();
}
- if let Some(position) = click_position {
+ if let Some(position) = cursor.position_over(bounds) {
let click = mouse::Click::new(
position,
mouse::Button::Left,
@@ -642,7 +555,7 @@ where
);
let (line, index) = state
- .grapheme_line_and_index(position)
+ .grapheme_line_and_index(position, bounds)
.unwrap_or((0, 0));
match click.kind() {
@@ -708,11 +621,11 @@ where
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
- if let Some(position) = click_position
+ if let Some(position) = cursor.position()
&& let Some(dragging) = state.dragging
{
let (line, index) = state
- .grapheme_line_and_index(position)
+ .grapheme_line_and_index(position, bounds)
.unwrap_or((0, 0));
match dragging {
@@ -898,7 +811,7 @@ where
if state.hovered_link.is_some() {
mouse::Interaction::Pointer
- } else if cursor.is_over(layout.bounds()) {
+ } else if cursor.is_over(layout.bounds()) || state.dragging.is_some() {
mouse::Interaction::Text
} else {
mouse::Interaction::None
@@ -1007,3 +920,65 @@ where
Element::new(text)
}
}
+
+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: f32 =
+ 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();
+
+ let start = scroll.line.min(line);
+ let end = scroll.line.max(line);
+
+ let visual_lines_offset: usize = buffer.lines[start..]
+ .iter()
+ .take(end - start)
+ .map(|line| line.layout_opt().map(Vec::len).unwrap_or_default())
+ .sum();
+
+ visual_lines_offset as i32 * if scroll.line < line { 1 } else { -1 }
+}