diff options
| author | Polesznyák Márk <contact@pml68.dev> | 2025-10-26 20:22:20 +0100 |
|---|---|---|
| committer | Polesznyák Márk <contact@pml68.dev> | 2025-10-26 20:22:20 +0100 |
| commit | 0dcf63d06e17aa64caa64d26392d3187f9b730ea (patch) | |
| tree | 1800e235cb33167fedc270742c16eef2ffb0c926 | |
| parent | fix(wip): multi line text selection box drawing (works for non-wrapped) (diff) | |
| download | iced_selection-0dcf63d06e17aa64caa64d26392d3187f9b730ea.tar.gz | |
feat: clean up debugging code, add multi-line fix to `Rich`
| -rw-r--r-- | README.md | 11 | ||||
| -rw-r--r-- | TODO.md | 4 | ||||
| -rw-r--r-- | examples/name/src/main.rs | 9 | ||||
| -rw-r--r-- | src/text.rs | 92 | ||||
| -rw-r--r-- | src/text/rich.rs | 146 |
5 files changed, 169 insertions, 93 deletions
@@ -16,6 +16,9 @@ Roughly: - `markdown.rs`: A custom [`Viewer`](https://docs.iced.rs/iced/widget/markdown/trait.Viewer.html) and its corresponding custom methods. - `lib.rs`: Helper methods, macros and re-exports. +## Wrapped text support +Wrapped text currently isn't supported. Although single-click mouse selection and most keyboard shortcuts will select text correctly, the selection box(es) drawn will be incorrect for wrapped segments and by-line selection (`Shift + Up Arrow` / `Shift + Down Arrow` & triple-click mouse selection) will treat all wrapped segments as part of the same line. + ## Installation Simply add it to under your `Cargo.toml`'s `dependencies` section. ```toml @@ -31,6 +34,14 @@ iced_selection = { git = "https://git.sr.ht/~pml68/iced_selection" } - `default`: - `markdown`: Provides support for rendering markdown through a custom viewer. +## TODO + +- [ ] allow out-of-bounds selection dragging +- [X] custom markdown `Viewer` +- [ ] double-click + drag for by-word selection +- [X] triple-click + drag for by-line selection +- [ ] support wrapped lines + ## Special thanks - [`iced`](https://iced.rs), for making this possible in the first place, and for the modified source code of `Text`, `Rich` and `Selection` (based on [`text_input/cursor.rs`](https://github.com/iced-rs/iced/blob/master/widget/src/text_input/cursor.rs)). diff --git a/TODO.md b/TODO.md deleted file mode 100644 index fe881aa..0000000 --- a/TODO.md +++ /dev/null @@ -1,4 +0,0 @@ -- [ ] allow out-of-bounds selection dragging -- [X] custom markdown `Viewer` -- [ ] double-click + drag for by-word selection -- [X] triple-click + drag for by-line selection diff --git a/examples/name/src/main.rs b/examples/name/src/main.rs index 2a733d5..bfb60be 100644 --- a/examples/name/src/main.rs +++ b/examples/name/src/main.rs @@ -1,9 +1,10 @@ +use iced::widget::operation::focus_next; use iced::widget::{center, column, text_input}; -use iced::{Center, Element}; +use iced::{Center, Element, Task}; use iced_selection::text; fn main() -> iced::Result { - iced::run(State::update, State::view) + iced::application(State::new, State::update, State::view).run() } #[derive(Default)] @@ -17,6 +18,10 @@ enum Message { } impl State { + fn new() -> (Self, Task<Message>) { + (Self::default(), focus_next()) + } + fn update(&mut self, message: Message) { match message { Message::UpdateText(name) => self.name = name, diff --git a/src/text.rs b/src/text.rs index 82e9c2c..cd066c3 100644 --- a/src/text.rs +++ b/src/text.rs @@ -199,11 +199,11 @@ impl State { .grapheme_position(end.line, end.index) .unwrap_or_default(); - dbg!(( + ( end_row.saturating_sub(start_row) + 1, start_position, end_position, - )) + ) } fn grapheme_position( @@ -241,15 +241,8 @@ impl State { }) }); - let (run_idx, glyph, _) = glyphs + let (_, glyph, _) = glyphs .find(|(run_idx, glyph, text)| { - let new_run_index = if Some(*run_idx) != last_run_index { - last_run_graphemes = 0; - Some(*run_idx) - } else { - last_run_index - }; - if Some(glyph.start) != last_start { last_grapheme_count = text[glyph.start..glyph.end].graphemes(false).count(); @@ -258,17 +251,20 @@ impl State { last_run_graphemes += last_grapheme_count; real_index += last_grapheme_count; - if new_run_index != last_run_index && graphemes_seen < index + if Some(*run_idx) != last_run_index + && graphemes_seen < index { real_index = last_grapheme_count; + last_run_graphemes = last_grapheme_count; } - } else if new_run_index != last_run_index + } else if Some(*run_idx) != last_run_index && graphemes_seen < index { real_index = 0; + last_run_graphemes = 0; } - last_run_index = new_run_index; + last_run_index = Some(*run_idx); graphemes_seen >= index }) @@ -276,38 +272,28 @@ impl State { real_index -= graphemes_seen.saturating_sub(index); real_index = - dbg!(real_index.saturating_sub(last_run_index? - first_run_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? - 1); - - let advance = if real_index == 0 { - 0.0 + .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 + -(glyph.w * (1.0 - - last_run_graphemes.saturating_sub(real_index) as f32 - / last_grapheme_count.max(1) as f32) + + last_run_graphemes.saturating_sub(real_index) as f32 + / last_grapheme_count.max(1) as f32)) }; - // println!( - // "text: total: {} index: {}\nreal: total: {} index: {}\nlast_seen: {}\n\n", - // graphemes_seen, - // index, - // last_run_graphemes, - // real_index, - // last_grapheme_count - // ); - - // println!( - // "{}, {}: {}, {} (index {})", - // line, - // index, - // glyph.x + glyph.x_offset * glyph.font_size + advance, - // glyph.y - glyph.y_offset * glyph.font_size, - // run_idx - // ); - Some(( last_run_index?, Point::new( @@ -407,12 +393,12 @@ where state.last_click, ); + let (line, index) = state + .grapheme_line_and_index(position) + .unwrap_or((0, 0)); + match click.kind() { click::Kind::Single => { - let (line, index) = state - .grapheme_line_and_index(position) - .unwrap_or((0, 0)); - let new_end = SelectionEnd { line, index }; if state.keyboard_modifiers.shift() { @@ -424,23 +410,15 @@ where state.dragging = Some(Dragging::Grapheme); } click::Kind::Double => { - let (line, index) = state - .grapheme_line_and_index(position) - .unwrap_or((0, 0)); - state.selection.select_word( line, index, &state.paragraph, ); - state.dragging = None; + state.dragging = Some(Dragging::Word); } click::Kind::Triple => { - let (line, _) = state - .grapheme_line_and_index(position) - .unwrap_or((0, 0)); - state.selection.select_line(line, &state.paragraph); state.dragging = Some(Dragging::Line); } @@ -652,16 +630,6 @@ where .floor() * line_height; - // The correct code, uncomment when glyphs report a correct `y` value. - // - //let height = dbg!(end.y) - baseline_y - 0.5; - //let rows = - // usize::max(dbg!((height / line_height).ceil() as usize), 1); - // - // Temporary solution - //let rows = - // state.selection.end.line - state.selection.start.line + 1; - for row in 0..rows { let (x, width) = if row == 0 { ( diff --git a/src/text/rich.rs b/src/text/rich.rs index 93cefa2..3268160 100644 --- a/src/text/rich.rs +++ b/src/text/rich.rs @@ -204,20 +204,119 @@ impl<Link> State<Link> { )) } - fn selection_end_points(&self) -> [Point; 2] { + fn selection_end_points(&self) -> (usize, Point, Point) { let Selection { start, end, .. } = self.selection; - let start_position = self - .paragraph + let (start_row, start_position) = self .grapheme_position(start.line, start.index) - .unwrap_or(Point::ORIGIN); + .unwrap_or_default(); - let end_position = self - .paragraph + let (end_row, end_position) = self .grapheme_position(end.line, end.index) - .unwrap_or(Point::ORIGIN); + .unwrap_or_default(); - [start_position, end_position] + ( + end_row.saturating_sub(start_row) + 1, + start_position, + end_position, + ) + } + + 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() + .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; + } + + 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, + ), + )) } } @@ -390,9 +489,9 @@ where if !state.selection.is_empty() { let bounds = layout.bounds(); - let [start, end] = state - .selection_end_points() - .map(|pos| pos + Vector::new(bounds.x, bounds.y)); + let (rows, mut start, mut end) = state.selection_end_points(); + start = start + core::Vector::new(bounds.x, bounds.y); + end = end + core::Vector::new(bounds.x, bounds.y); let line_height = self .line_height @@ -402,16 +501,9 @@ where .0; let baseline_y = bounds.y - + ((start.y - bounds.y) / line_height).floor() * line_height; - - // The correct code, uncomment when glyphs report a correct `y` value. - // - // let height = end.y - baseline_y - 0.5; - // let rows = (height / line_height).ceil() as usize; - // - // Temporary solution - let rows = - state.selection.end.line - state.selection.start.line + 1; + + (((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 { @@ -524,11 +616,11 @@ where if state.keyboard_modifiers.shift() { state.selection.change_selection(new_end); - state.dragging = Some(Dragging::Grapheme); - } else if state.span_pressed.is_none() { + } else { state.selection.select_range(new_end, new_end); - state.dragging = Some(Dragging::Grapheme); } + + state.dragging = Some(Dragging::Grapheme); } mouse::click::Kind::Double => { state.selection.select_word( @@ -536,13 +628,17 @@ where index, &state.paragraph, ); - state.dragging = None; + state.dragging = Some(Dragging::Word); } mouse::click::Kind::Triple => { state.selection.select_line(line, &state.paragraph); state.dragging = Some(Dragging::Line); } } + + state.last_click = Some(click); + + shell.capture_event(); } else { state.selection = Selection::default(); } |
