Fix: Click selection across merged glpyhs

Closes #112
This commit is contained in:
Jonathan Johnson 2023-12-24 16:54:01 -08:00
parent 470267ccfe
commit 443c5b40d1
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
2 changed files with 90 additions and 54 deletions

View file

@ -33,6 +33,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
window from being dropped. This was caused by a reference counting cycle, and window from being dropped. This was caused by a reference counting cycle, and
has been fixed by switching `MountedWidget` to use a weak reference internally has been fixed by switching `MountedWidget` to use a weak reference internally
and having the window hold the strong reference to the tree. and having the window hold the strong reference to the tree.
- [#112][112]: Click-selection is handled correctly across graphemes now.
Previously, code that was handling selecting between "ff" where cosmic_text
had merged the two ASCII characters into a single glpyh was not honoring
graphemes, allowing dragging selections inbetween multi-character glyphs.
- [#113][113]: `Input` now constraints its internal selection to the value's - [#113][113]: `Input` now constraints its internal selection to the value's
length automatically. This fixes an issue where the backspace key no longer length automatically. This fixes an issue where the backspace key no longer
would work after clearing the text field by setting the `Dynamic`. would work after clearing the text field by setting the `Dynamic`.
@ -61,6 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[91]: https://github.com/khonsulabs/gooey/issues/91 [91]: https://github.com/khonsulabs/gooey/issues/91
[92]: https://github.com/khonsulabs/gooey/issues/92 [92]: https://github.com/khonsulabs/gooey/issues/92
[112]: https://github.com/khonsulabs/gooey/issues/112
[113]: https://github.com/khonsulabs/gooey/issues/113 [113]: https://github.com/khonsulabs/gooey/issues/113
## v0.1.3 (2023-12-19) ## v0.1.3 (2023-12-19)

View file

@ -324,8 +324,7 @@ where
return; return;
}; };
let (mut position, _) = let (mut position, _) = self.point_from_cursor(cache, self.selection.cursor, cache.bytes);
Self::point_from_cursor(&cache.measured, self.selection.cursor, cache.bytes);
position.y += context position.y += context
.get(&IntrinsicPadding) .get(&IntrinsicPadding)
.into_px(context.kludgine.scale()) .into_px(context.kludgine.scale())
@ -345,8 +344,7 @@ where
return; return;
}; };
let (mut position, _) = let (mut position, _) = self.point_from_cursor(&cache, self.selection.cursor, cache.bytes);
Self::point_from_cursor(&cache.measured, self.selection.cursor, cache.bytes);
position += Point::squared( position += Point::squared(
context context
.get(&IntrinsicPadding) .get(&IntrinsicPadding)
@ -553,7 +551,7 @@ where
&mut self, &mut self,
width: Option<Px>, width: Option<Px>,
context: &mut GraphicsContext<'_, '_, '_, '_, '_>, context: &mut GraphicsContext<'_, '_, '_, '_, '_>,
) -> CacheInfo<'_> { ) {
context.invalidate_when_changed(&self.value); context.invalidate_when_changed(&self.value);
let mut key = { let mut key = {
@ -621,41 +619,51 @@ where
}); });
} }
} }
}
fn cache_info(&self) -> CacheInfo<'_> {
let cache = self
.cache
.as_ref()
.expect("always called after layout_text");
// Adjust the selection cursors to accommodate the difference in unicode // Adjust the selection cursors to accommodate the difference in unicode
// widths of characters in the source string and the mask_char. // widths of characters in the source string and the mask_char.
if key.mask_bytes > 0 {
let masked = cache.key.mask_bytes > 0;
let mut cursor = cache.key.cursor;
let mut selection = cache.key.selection;
if masked {
self.value.map_ref(|value| { self.value.map_ref(|value| {
let value = value.as_str(); let value = value.as_str();
assert!(key.cursor.offset <= value.len()); assert!(cache.key.cursor.offset <= value.len());
key.cursor.offset = cursor.offset =
value[..key.cursor.offset].graphemes(true).count() * key.mask_bytes; value[..cache.key.cursor.offset].graphemes(true).count() * cache.key.mask_bytes;
if let Some(selection) = &mut key.selection { if let Some(selection) = &mut selection {
assert!(selection.offset <= value.len()); assert!(selection.offset <= value.len());
selection.offset = selection.offset =
value[..selection.offset].graphemes(true).count() * key.mask_bytes; value[..selection.offset].graphemes(true).count() * cache.key.mask_bytes;
} }
}); });
} }
let cache = self.cache.as_ref().expect("always initialized");
CacheInfo { CacheInfo {
measured: &cache.measured, cache,
placeholder: &cache.placeholder, masked,
bytes: cache.bytes, cursor,
masked: key.mask_bytes > 0, selection,
cursor: key.cursor,
selection: key.selection,
} }
} }
#[allow(clippy::too_many_lines)] // it's text layout, c'mon #[allow(clippy::too_many_lines)] // it's text layout, c'mon
fn point_from_cursor( fn point_from_cursor(
measured: &MeasuredText<Px>, &self,
cache: &CachedLayout,
cursor: Cursor, cursor: Cursor,
total_bytes: usize, total_bytes: usize,
) -> (Point<Px>, Px) { ) -> (Point<Px>, Px) {
if measured.glyphs.is_empty() || (cursor.offset == 0 && cursor.affinity == Affinity::Before) if cache.measured.glyphs.is_empty()
|| (cursor.offset == 0 && cursor.affinity == Affinity::Before)
{ {
return (Point::default(), Px::ZERO); return (Point::default(), Px::ZERO);
} }
@ -669,7 +677,7 @@ where
let mut bottom_right_line = 0; let mut bottom_right_line = 0;
let mut bottom_right_rect = Rect::default(); let mut bottom_right_rect = Rect::default();
let mut unrendered_offset = 0; let mut unrendered_offset = 0;
for (index, glyph) in measured.glyphs.iter().enumerate() { for (index, glyph) in cache.measured.glyphs.iter().enumerate() {
unrendered_offset = unrendered_offset.max(glyph.info.end); unrendered_offset = unrendered_offset.max(glyph.info.end);
let rect = glyph.rect(); let rect = glyph.rect();
if bottom_right_rect.size.width == 0 if bottom_right_rect.size.width == 0
@ -692,17 +700,30 @@ where
if glyph.info.start < cursor.offset { if glyph.info.start < cursor.offset {
let clustered_bytes = glyph.info.end - glyph.info.start; let clustered_bytes = glyph.info.end - glyph.info.start;
if clustered_bytes > 1 { if clustered_bytes > 1 {
let cursor_offset = cursor.offset - glyph.info.start; let clustered_graphemes = if cache.key.mask_bytes > 0 {
self.mask[glyph.info.start..glyph.info.end]
.graphemes(true)
.count()
} else {
self.value.map_ref(|value| {
value.as_str()[glyph.info.start..glyph.info.end]
.graphemes(true)
.count()
})
};
if clustered_graphemes > 1 {
let cursor_offset = cursor.offset - glyph.info.start;
grapheme_offset = rect.size.width * cursor_offset.cast::<f32>() grapheme_offset = rect.size.width * cursor_offset.cast::<f32>()
/ clustered_bytes.cast::<f32>(); / clustered_graphemes.cast::<f32>();
}
} }
} }
return ( return (
Point::new( Point::new(
rect.origin.x + grapheme_offset, rect.origin.x + grapheme_offset,
measured.line_height.saturating_mul(Px::new( cache.measured.line_height.saturating_mul(Px::new(
i32::try_from(glyph.info.line).unwrap_or(i32::MAX), i32::try_from(glyph.info.line).unwrap_or(i32::MAX),
)), )),
), ),
@ -720,8 +741,8 @@ where
} }
if closest_after_index == usize::MAX { if closest_after_index == usize::MAX {
let bottom_right = &measured.glyphs[bottom_right_index]; let bottom_right = &cache.measured.glyphs[bottom_right_index];
let bottom_y = measured.line_height.saturating_mul(Px::new( let bottom_y = cache.measured.line_height.saturating_mul(Px::new(
i32::try_from(bottom_right.info.line).unwrap_or(i32::MAX), i32::try_from(bottom_right.info.line).unwrap_or(i32::MAX),
)); ));
// No glyph could be found that started/contained the cursors offset. // No glyph could be found that started/contained the cursors offset.
@ -746,11 +767,12 @@ where
// The cursor should be placed after the bottom_right glyph // The cursor should be placed after the bottom_right glyph
(bottom_right_cursor, Px::ZERO) (bottom_right_cursor, Px::ZERO)
} else { } else {
let before = &measured.glyphs[closest_before_index]; let before = &cache.measured.glyphs[closest_before_index];
let after = &measured.glyphs[closest_after_index]; let after = &cache.measured.glyphs[closest_after_index];
let before_rect = before.rect(); let before_rect = before.rect();
let after_rect = after.rect(); let after_rect = after.rect();
let before_y = measured let before_y = cache
.measured
.line_height .line_height
.saturating_mul(Px::new(i32::try_from(before.info.line).unwrap_or(i32::MAX))); .saturating_mul(Px::new(i32::try_from(before.info.line).unwrap_or(i32::MAX)));
@ -775,7 +797,7 @@ where
(origin, before_y) (origin, before_y)
} }
Affinity::After => ( Affinity::After => (
Point::new(Px::ZERO, before_y + measured.line_height), Point::new(Px::ZERO, before_y + cache.measured.line_height),
Px::ZERO, Px::ZERO,
), ),
} }
@ -916,9 +938,7 @@ where
} }
struct CacheInfo<'a> { struct CacheInfo<'a> {
measured: &'a MeasuredText<Px>, cache: &'a CachedLayout,
placeholder: &'a MeasuredText<Px>,
bytes: usize,
masked: bool, masked: bool,
cursor: Cursor, cursor: Cursor,
selection: Option<Cursor>, selection: Option<Cursor>,
@ -1031,7 +1051,8 @@ where
.round(); .round();
let padding = Point::squared(padding); let padding = Point::squared(padding);
let cache = self.layout_text(Some(size.width.into_signed()), context); self.layout_text(Some(size.width.into_signed()), context);
let info = self.cache_info();
let highlight = if context.focused(false) && window_focused { let highlight = if context.focused(false) && window_focused {
context.draw_focus_ring(); context.draw_focus_ring();
@ -1044,30 +1065,33 @@ where
if context.focused(false) { if context.focused(false) {
context.set_ime_allowed(true); context.set_ime_allowed(true);
context.set_ime_purpose(if cache.masked { context.set_ime_purpose(if info.masked {
ImePurpose::Password ImePurpose::Password
} else { } else {
ImePurpose::Normal ImePurpose::Normal
}); });
if let Some(selection) = cache.selection { if let Some(selection) = info.selection {
let (start, end) = if selection < cache.cursor { let (start, end) = if selection < info.cursor {
(selection, cache.cursor) (selection, info.cursor)
} else { } else {
(cache.cursor, selection) (info.cursor, selection)
}; };
let (start_position, _) = let (start_position, _) =
Self::point_from_cursor(cache.measured, start, cache.bytes); self.point_from_cursor(info.cache, start, info.cache.bytes);
let (end_position, end_width) = let (end_position, end_width) =
Self::point_from_cursor(cache.measured, end, cache.bytes); self.point_from_cursor(info.cache, end, info.cache.bytes);
if start_position.y == end_position.y { if start_position.y == end_position.y {
// Single line selection // Single line selection
let width = end_position.x - start_position.x; let width = end_position.x - start_position.x;
context.gfx.draw_shape( context.gfx.draw_shape(
Shape::filled_rect( Shape::filled_rect(
Rect::new(start_position, Size::new(width, cache.measured.line_height)), Rect::new(
start_position,
Size::new(width, info.cache.measured.line_height),
),
highlight, highlight,
) )
.translate_by(padding), .translate_by(padding),
@ -1077,13 +1101,16 @@ where
let width = size.width.into_signed() - start_position.x; let width = size.width.into_signed() - start_position.x;
context.gfx.draw_shape( context.gfx.draw_shape(
Shape::filled_rect( Shape::filled_rect(
Rect::new(start_position, Size::new(width, cache.measured.line_height)), Rect::new(
start_position,
Size::new(width, info.cache.measured.line_height),
),
highlight, highlight,
) )
.translate_by(padding), .translate_by(padding),
); );
// Fill region between // Fill region between
let bottom_of_first_line = start_position.y + cache.measured.line_height; let bottom_of_first_line = start_position.y + info.cache.measured.line_height;
let distance_between = end_position.y - bottom_of_first_line; let distance_between = end_position.y - bottom_of_first_line;
if distance_between > 0 { if distance_between > 0 {
context.gfx.draw_shape( context.gfx.draw_shape(
@ -1102,7 +1129,10 @@ where
Shape::filled_rect( Shape::filled_rect(
Rect::new( Rect::new(
Point::new(Px::ZERO, end_position.y), Point::new(Px::ZERO, end_position.y),
Size::new(end_position.x + end_width, cache.measured.line_height), Size::new(
end_position.x + end_width,
info.cache.measured.line_height,
),
), ),
highlight, highlight,
) )
@ -1111,14 +1141,14 @@ where
} }
} else if window_focused && context.enabled() { } else if window_focused && context.enabled() {
let (location, _) = let (location, _) =
Self::point_from_cursor(cache.measured, cache.cursor, cache.bytes); self.point_from_cursor(info.cache, info.cursor, info.cache.bytes);
if cursor_state.visible { if cursor_state.visible {
let cursor_width = Lp::points(2).into_px(context.gfx.scale()); let cursor_width = Lp::points(2).into_px(context.gfx.scale());
context.gfx.draw_shape( context.gfx.draw_shape(
Shape::filled_rect( Shape::filled_rect(
Rect::new( Rect::new(
Point::new(location.x - cursor_width / 2, location.y), Point::new(location.x - cursor_width / 2, location.y),
Size::new(cursor_width, cache.measured.line_height), Size::new(cursor_width, info.cache.measured.line_height),
), ),
highlight, highlight,
) )
@ -1131,10 +1161,10 @@ where
} }
} }
let text = if cache.bytes > 0 { let text = if info.cache.bytes > 0 {
cache.measured &info.cache.measured
} else { } else {
cache.placeholder &info.cache.placeholder
}; };
context context
.gfx .gfx
@ -1153,12 +1183,13 @@ where
let width = available_space.width.max().saturating_sub(padding * 2); let width = available_space.width.max().saturating_sub(padding * 2);
let cache = self.layout_text(Some(width.into_signed()), &mut context.graphics); self.layout_text(Some(width.into_signed()), &mut context.graphics);
let info = self.cache_info();
cache info.cache
.measured .measured
.size .size
.max(cache.placeholder.size) .max(info.cache.placeholder.size)
.into_unsigned() .into_unsigned()
+ Size::squared(padding * 2) + Size::squared(padding * 2)
} }