From 470267ccfee046b2e940d94418822fbb32097799 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Sun, 24 Dec 2023 08:25:48 -0800 Subject: [PATCH] Input selection constraining Closes #113 --- CHANGELOG.md | 7 ++- src/widgets/input.rs | 112 ++++++++++++++++++++----------------------- 2 files changed, 58 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0526c3e..b173577 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,14 +27,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Using a `Resize` widget in the root hierarchy allows setting minimum width and heights for the content. -[92]: https://github.com/khonsulabs/gooey/issues/92 - ### Fixed - A memory leak has been fixed that prevented the underlying widget tree of each 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 and having the window hold the strong reference to the tree. +- [#113][113]: `Input` now constraints its internal selection to the value's + length automatically. This fixes an issue where the backspace key no longer + would work after clearing the text field by setting the `Dynamic`. ### Added @@ -59,6 +60,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Window::on_close` sets a callback to be invoked when the window has closed. [91]: https://github.com/khonsulabs/gooey/issues/91 +[92]: https://github.com/khonsulabs/gooey/issues/92 +[113]: https://github.com/khonsulabs/gooey/issues/113 ## v0.1.3 (2023-12-19) diff --git a/src/widgets/input.rs b/src/widgets/input.rs index 7443a27..58af4dc 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -50,35 +50,23 @@ pub struct Input { window_focused: bool, } -struct CachedLayout { - bytes: usize, - color: Color, +#[derive(Eq, PartialEq, Clone, Copy)] +struct CacheKey { generation: Generation, mask_generation: Option, placeholder_generation: Option, - mask_bytes: usize, width: Option, - measured: MeasuredText, - placeholder: MeasuredText, + color: Color, + mask_bytes: usize, + cursor: Cursor, + selection: Option, } -impl CachedLayout { - pub fn is_current( - &self, - generation: Generation, - mask_generation: Option, - placeholder_generation: Option, - width: Option, - color: Color, - mask_bytes: usize, - ) -> bool { - self.generation == generation - && self.mask_generation == mask_generation - && self.placeholder_generation == placeholder_generation - && self.width == width - && self.color == color - && self.mask_bytes == mask_bytes - } +struct CachedLayout { + bytes: usize, + measured: MeasuredText, + placeholder: MeasuredText, + key: CacheKey, } /// The current selection of an [`Input`]. @@ -223,7 +211,8 @@ where } else if cursor.offset > 0 { let mut value = self.value.lock(); let length = value.as_str().len(); - if length == 0 || cursor.offset == 0 || cursor.offset > length { + + if length == 0 || cursor.offset == 0 { return; } @@ -379,7 +368,16 @@ where self.selection.cursor = self.cursor_from_point(position, context); } + fn constrain_selection(&mut self) { + let length = self.value.map_ref(|s| s.as_str().len()); + self.selection.cursor.offset = self.selection.cursor.offset.min(length); + if let Some(start) = &mut self.selection.start { + start.offset = start.offset.min(length); + } + } + fn selected_range(&mut self) -> (Cursor, Option) { + self.constrain_selection(); match self.selection.start { Some(start) => match start.offset.cmp(&self.selection.cursor.offset) { Ordering::Less => (start, Some(self.selection.cursor)), @@ -556,25 +554,25 @@ where width: Option, context: &mut GraphicsContext<'_, '_, '_, '_, '_>, ) -> CacheInfo<'_> { - let (mut cursor, mut selection) = self.selected_range(); - let generation = self.value.generation(); - let mask_generation = self.mask_symbol.generation(); - let placeholder_generation = self.placeholder.generation(); - let mut mask_bytes = self - .mask_symbol - .map(|sym| sym.graphemes(true).next().map_or(0, str::len)); - let color = context.get(&TextColor); context.invalidate_when_changed(&self.value); + + let mut key = { + let (cursor, selection) = self.selected_range(); + CacheKey { + generation: self.value.generation(), + mask_generation: self.mask_symbol.generation(), + placeholder_generation: self.placeholder.generation(), + width, + color: context.get(&TextColor), + mask_bytes: self + .mask_symbol + .map(|sym| sym.graphemes(true).next().map_or(0, str::len)), + cursor, + selection, + } + }; match &mut self.cache { - Some(cache) - if cache.is_current( - generation, - mask_generation, - placeholder_generation, - width, - color, - mask_bytes, - ) => {} + Some(cache) if cache.key == key => {} _ => { let (bytes, measured, placeholder, ) = self.value.map_ref(|storage| { let mut text = storage.as_str(); @@ -591,9 +589,9 @@ where // Technically something more optimal than asking the // layout system to lay out a repeated string should be // doable, but it seems like a lot of code. - mask_bytes = first_grapheme.len(); + key.mask_bytes = first_grapheme.len(); let char_count = text.graphemes(true).count(); - bytes = mask_bytes * char_count; + bytes = key.mask_bytes * char_count; self.mask.truncate(bytes); while self.mask.len() < bytes { @@ -601,12 +599,12 @@ where } text = &self.mask; } else { - mask_bytes = 0; + key.mask_bytes = 0; } }); context.apply_current_font_settings(); - let mut text = Text::new(text, color); + let mut text = Text::new(text, key.color); if let Some(width) = width { text = text.wrap_at(width); } @@ -617,29 +615,25 @@ where }); self.cache = Some(CachedLayout { bytes, - color, - generation, - mask_generation, - placeholder_generation, - mask_bytes, - width, measured, placeholder, + key, }); } } // Adjust the selection cursors to accommodate the difference in unicode // widths of characters in the source string and the mask_char. - if mask_bytes > 0 { + if key.mask_bytes > 0 { self.value.map_ref(|value| { let value = value.as_str(); - assert!(cursor.offset <= value.len()); - cursor.offset = value[..cursor.offset].graphemes(true).count() * mask_bytes; - if let Some(selection) = &mut selection { + assert!(key.cursor.offset <= value.len()); + key.cursor.offset = + value[..key.cursor.offset].graphemes(true).count() * key.mask_bytes; + if let Some(selection) = &mut key.selection { assert!(selection.offset <= value.len()); selection.offset = - value[..selection.offset].graphemes(true).count() * mask_bytes; + value[..selection.offset].graphemes(true).count() * key.mask_bytes; } }); } @@ -649,9 +643,9 @@ where measured: &cache.measured, placeholder: &cache.placeholder, bytes: cache.bytes, - masked: mask_bytes > 0, - cursor, - selection, + masked: key.mask_bytes > 0, + cursor: key.cursor, + selection: key.selection, } }