aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPolesznyák Márk <contact@pml68.dev>2025-10-26 20:22:20 +0100
committerPolesznyák Márk <contact@pml68.dev>2025-10-26 20:22:20 +0100
commit0dcf63d06e17aa64caa64d26392d3187f9b730ea (patch)
tree1800e235cb33167fedc270742c16eef2ffb0c926
parentfix(wip): multi line text selection box drawing (works for non-wrapped) (diff)
downloadiced_selection-0dcf63d06e17aa64caa64d26392d3187f9b730ea.tar.gz
feat: clean up debugging code, add multi-line fix to `Rich`
-rw-r--r--README.md11
-rw-r--r--TODO.md4
-rw-r--r--examples/name/src/main.rs9
-rw-r--r--src/text.rs92
-rw-r--r--src/text/rich.rs146
5 files changed, 169 insertions, 93 deletions
diff --git a/README.md b/README.md
index a828598..0321630 100644
--- a/README.md
+++ b/README.md
@@ -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();
}