From fe631ff9ea999209b8109482e10356dbf357c709 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Sun, 6 Apr 2025 12:45:20 -0400 Subject: [PATCH] Use `TextBuffer` for `layouter` in `TextEdit` instead of `&str` (#5712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change allows `layouter` to use the `TextBuffer` instead of `&str` in the closure. It is necessary when layout decisions depend on more than just the raw string content, such as metadata stored in the concrete type implementing `TextBuffer`. In [our use case](https://github.com/damus-io/notedeck/pull/723), we needed this to support mention highlighting when a user selects a mention. Since mentions can contain spaces, determining mention boundaries from the `&str` alone is impossible. Instead, we use the `TextBuffer` implementation to retrieve the correct bounds. See the video below for a demonstration: https://github.com/user-attachments/assets/3cba2906-5546-4b52-b728-1da9c56a83e1 # Breaking change This PR introduces a breaking change to the `layouter` function in `TextEdit`. Previous API: ```rust pub fn layouter(mut self, layouter: &'t mut dyn FnMut(&Ui, &str, f32) -> Arc) -> Self ``` New API: ```rust pub fn layouter(mut self, layouter: &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc) -> Self ``` ## Impact on Existing Code • Any existing usage of `layouter` will **no longer compile**. • Callers must update their closures to use `&dyn TextBuffer` instead of `&str`. ## Migration Guide Before: ```rust let mut layouter = |ui: &Ui, text: &str, wrap_width: f32| {     let layout_job = my_highlighter(text);     layout_job.wrap.max_width = wrap_width;     ui.fonts(|f| f.layout_job(layout_job)) }; ``` After: ```rust let mut layouter = |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| { let layout_job = my_highlighter(text.as_str()); layout_job.wrap.max_width = wrap_width; ui.fonts(|f| f.layout_job(layout_job)) }; ``` --- * There is not an issue for this change. * [x] I have followed the instructions in the PR template Signed-off-by: kernelkind --- crates/egui/src/widgets/text_edit/builder.rs | 23 ++++++---- .../egui/src/widgets/text_edit/text_buffer.rs | 45 +++++++++++++++++++ crates/egui_demo_lib/src/demo/code_editor.rs | 4 +- .../src/easy_mark/easy_mark_editor.rs | 4 +- 4 files changed, 63 insertions(+), 13 deletions(-) diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index d73ecd3f..6a424449 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -19,6 +19,8 @@ use crate::{ use super::{TextEditOutput, TextEditState}; +type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc; + /// A text region that the user can edit the contents of. /// /// See also [`Ui::text_edit_singleline`] and [`Ui::text_edit_multiline`]. @@ -71,7 +73,7 @@ pub struct TextEdit<'t> { id_salt: Option, font_selection: FontSelection, text_color: Option, - layouter: Option<&'t mut dyn FnMut(&Ui, &str, f32) -> Arc>, + layouter: Option>, password: bool, frame: bool, margin: Margin, @@ -261,8 +263,8 @@ impl<'t> TextEdit<'t> { /// # egui::__run_test_ui(|ui| { /// # let mut my_code = String::new(); /// # fn my_memoized_highlighter(s: &str) -> egui::text::LayoutJob { Default::default() } - /// let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| { - /// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(string); + /// let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| { + /// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(buf.as_str()); /// layout_job.wrap.max_width = wrap_width; /// ui.fonts(|f| f.layout_job(layout_job)) /// }; @@ -270,7 +272,10 @@ impl<'t> TextEdit<'t> { /// # }); /// ``` #[inline] - pub fn layouter(mut self, layouter: &'t mut dyn FnMut(&Ui, &str, f32) -> Arc) -> Self { + pub fn layouter( + mut self, + layouter: &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc, + ) -> Self { self.layouter = Some(layouter); self @@ -510,8 +515,8 @@ impl TextEdit<'_> { }; let font_id_clone = font_id.clone(); - let mut default_layouter = move |ui: &Ui, text: &str, wrap_width: f32| { - let text = mask_if_password(password, text); + let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| { + let text = mask_if_password(password, text.as_str()); let layout_job = if multiline { LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width) } else { @@ -522,7 +527,7 @@ impl TextEdit<'_> { let layouter = layouter.unwrap_or(&mut default_layouter); - let mut galley = layouter(ui, text.as_str(), wrap_width); + let mut galley = layouter(ui, text, wrap_width); let desired_inner_width = if clip_text { wrap_width // visual clipping with scroll in singleline input. @@ -879,7 +884,7 @@ fn events( state: &mut TextEditState, text: &mut dyn TextBuffer, galley: &mut Arc, - layouter: &mut dyn FnMut(&Ui, &str, f32) -> Arc, + layouter: &mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc, id: Id, wrap_width: f32, multiline: bool, @@ -1094,7 +1099,7 @@ fn events( any_change = true; // Layout again to avoid frame delay, and to keep `text` and `galley` in sync. - *galley = layouter(ui, text.as_str(), wrap_width); + *galley = layouter(ui, text, wrap_width); // Set cursor_range using new galley: cursor_range = new_ccursor_range; diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index ccf3a095..6cf7da15 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -172,6 +172,39 @@ pub trait TextBuffer { self.delete_selected(&CCursorRange::two(min, max)) } } + + /// Returns a unique identifier for the implementing type. + /// + /// This is useful for downcasting from this trait to the implementing type. + /// Here is an example usage: + /// ``` + /// use egui::TextBuffer; + /// use std::any::TypeId; + /// + /// struct ExampleBuffer {} + /// + /// impl TextBuffer for ExampleBuffer { + /// fn is_mutable(&self) -> bool { unimplemented!() } + /// fn as_str(&self) -> &str { unimplemented!() } + /// fn insert_text(&mut self, text: &str, char_index: usize) -> usize { unimplemented!() } + /// fn delete_char_range(&mut self, char_range: std::ops::Range) { unimplemented!() } + /// + /// // Implement it like the following: + /// fn type_id(&self) -> TypeId { + /// TypeId::of::() + /// } + /// } + /// + /// // Example downcast: + /// pub fn downcast_example(buffer: &dyn TextBuffer) -> Option<&ExampleBuffer> { + /// if buffer.type_id() == TypeId::of::() { + /// unsafe { Some(&*(buffer as *const dyn TextBuffer as *const ExampleBuffer)) } + /// } else { + /// None + /// } + /// } + /// ``` + fn type_id(&self) -> std::any::TypeId; } impl TextBuffer for String { @@ -218,6 +251,10 @@ impl TextBuffer for String { fn take(&mut self) -> String { std::mem::take(self) } + + fn type_id(&self) -> std::any::TypeId { + std::any::TypeId::of::() + } } impl TextBuffer for Cow<'_, str> { @@ -248,6 +285,10 @@ impl TextBuffer for Cow<'_, str> { fn take(&mut self) -> String { std::mem::take(self).into_owned() } + + fn type_id(&self) -> std::any::TypeId { + std::any::TypeId::of::>() + } } /// Immutable view of a `&str`! @@ -265,4 +306,8 @@ impl TextBuffer for &str { } fn delete_char_range(&mut self, _ch_range: Range) {} + + fn type_id(&self) -> std::any::TypeId { + std::any::TypeId::of::<&str>() + } } diff --git a/crates/egui_demo_lib/src/demo/code_editor.rs b/crates/egui_demo_lib/src/demo/code_editor.rs index fe39e1be..2d67f7d4 100644 --- a/crates/egui_demo_lib/src/demo/code_editor.rs +++ b/crates/egui_demo_lib/src/demo/code_editor.rs @@ -76,12 +76,12 @@ impl crate::View for CodeEditor { }); }); - let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| { + let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| { let mut layout_job = egui_extras::syntax_highlighting::highlight( ui.ctx(), ui.style(), &theme, - string, + buf.as_str(), language, ); layout_job.wrap.max_width = wrap_width; diff --git a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs index 9e730fac..a0fcdbbf 100644 --- a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs +++ b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs @@ -80,8 +80,8 @@ impl EasyMarkEditor { } = self; let response = if self.highlight_editor { - let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| { - let mut layout_job = highlighter.highlight(ui.style(), easymark); + let mut layouter = |ui: &egui::Ui, easymark: &dyn TextBuffer, wrap_width: f32| { + let mut layout_job = highlighter.highlight(ui.style(), easymark.as_str()); layout_job.wrap.max_width = wrap_width; ui.fonts(|f| f.layout_job(layout_job)) };