Minimal unstyled Text Input with Parley#23282
Minimal unstyled Text Input with Parley#23282Zeophlite wants to merge 36 commits intobevyengine:mainfrom
Conversation
|
The generated |
9a88110 to
8e06897
Compare
8567f0a to
0a4c802
Compare
alice-i-cecile
left a comment
There was a problem hiding this comment.
Getting close! I've spotted a few issues during review, but I'm really pleased with the progress here.
IMO one more round of fixes and we should be good to merge.
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
| @@ -0,0 +1,118 @@ | |||
| use bevy_asset::AssetId; | |||
There was a problem hiding this comment.
Feels excessive to add a module just for cursor rendering. If we do this, it would make more sense to rename it to text and move all text extraction into it.
| let mut scaler = scale_cx | ||
| .builder(font_ref) | ||
| .size(font_size) | ||
| .hint(true) |
There was a problem hiding this comment.
| .hint(true) | |
| .hint(matches!(hinting, FontHinting::Enabled)) |
And made a quick PR to change should_hint to pub: #23397
| let logical_viewport_size = target.logical_size(); | ||
| let font_size = text_font.font_size.eval(logical_viewport_size, rem_size.0); | ||
| style_set.insert(parley::StyleProperty::FontSize(font_size)); | ||
|
|
There was a problem hiding this comment.
| style_set.insert(parley::StyleProperty::Brush(TextBrush::new( | |
| 0, | |
| text_font.font_smoothing, | |
| ))); | |
| /// Text edit actions that have been requested but not yet applied. | ||
| /// | ||
| /// These edits are processed in first-in, first-out order. | ||
| pub pending_edits: VecDeque<TextEdit>, |
There was a problem hiding this comment.
I think the edits buffer should probably be managed by the widget implementation, otherwise there might be synchronisation issues with non-editing actions like submit and insert/overwrite mode switching.
| /// Text edit actions that have been requested but not yet applied. | ||
| /// | ||
| /// These edits are processed in first-in, first-out order. | ||
| pub pending_edits: VecDeque<TextEdit>, |
There was a problem hiding this comment.
Is there a reason for using a VecDeque? If all the edits are always applied, it can just be drained.
| // Take the `pending_edits` out of the struct so we can apply them without mutable aliasing issues. | ||
| // We do not need to put the `pending_edits` back into the struct, | ||
| // as all edits should be consumed by this method, leaving `pending_edits` empty at the end. | ||
| let mut pending_edits = core::mem::take(&mut self.pending_edits); | ||
| let mut driver = self.editor_mut().driver(font_context, layout_context); | ||
|
|
||
| while let Some(edit) = pending_edits.pop_front() { | ||
| driver = apply_edit(edit, driver); | ||
| } |
There was a problem hiding this comment.
| // Take the `pending_edits` out of the struct so we can apply them without mutable aliasing issues. | |
| // We do not need to put the `pending_edits` back into the struct, | |
| // as all edits should be consumed by this method, leaving `pending_edits` empty at the end. | |
| let mut pending_edits = core::mem::take(&mut self.pending_edits); | |
| let mut driver = self.editor_mut().driver(font_context, layout_context); | |
| while let Some(edit) = pending_edits.pop_front() { | |
| driver = apply_edit(edit, driver); | |
| } | |
| let Self { | |
| editor, | |
| pending_edits, | |
| .. | |
| } = self; | |
| let mut driver = editor.driver(font_context, layout_context); | |
| for edit in pending_edits.drain(..) { | |
| driver = apply_edit(edit, driver); | |
| } |
| /// Cursor width, relative to font size | ||
| pub cursor_width: f32, |
There was a problem hiding this comment.
Cursor width options should be in a separate CursorStyle/Shape/Size component I think.
| font_size_bits: font_size.to_bits(), | ||
| variations_hash: FixedHasher.hash_one(coords), | ||
| hinting: *hinting, | ||
| font_smoothing: brush.font_smoothing, |
There was a problem hiding this comment.
Brush needs to be set, otherwise this always uses the default smoothing setting.
| pub const TEXT: f32 = 0.06; | ||
| pub const TEXT_CURSOR: f32 = 0.065; | ||
| pub const TEXT_STRIKETHROUGH: f32 = 0.07; | ||
| pub const TEXT_SELECTION: f32 = 0.08; |
There was a problem hiding this comment.
Text selection rects should be drawn behind the text and cursor.
| pub const TEXT_SELECTION: f32 = 0.08; | |
| pub const TEXT_SELECTION: f32 = 0.055; |
There was a problem hiding this comment.
Oh I see you set it transparent atm, it should be behind the cursor at least though.
| .add(RadioGroupPlugin) | ||
| .add(ScrollbarPlugin) | ||
| .add(SliderPlugin) | ||
| .add(EditableTextInputPlugin) |
There was a problem hiding this comment.
Adding UiWidgetsPlugin will panic if you don't add InputDispatchPlugin as process_text_inputs uses Res<InputFocus>.
| } | ||
| } | ||
|
|
||
| pub fn extract_text_editable( |
There was a problem hiding this comment.
The EditableText entity has all the necessary components so the text is already rendered by extract_text_system, this system just queues the glyphs to be drawn a second time.
| ) in text_node_query.iter() | ||
| { | ||
| // Skip if not visible | ||
| // Note that `uinode.is_empty()` may be empty when there is no text, but we still want to display the cursor |
There was a problem hiding this comment.
There should be a uinode.is_empty() check here. In general the size of the node shouldn't depend on the size of the text layout.
| content_size.set(NodeMeasure::Fixed(FixedMeasure { | ||
| size: info.size * target.scale_factor(), | ||
| })); |
There was a problem hiding this comment.
Shouldn't add a measure func, the EditableText node should just be set to fill its parent. That is is unless it's using a fixed height based on the number of lines and/or a fixed width based on a max number of glyphs.
| content_size.set(NodeMeasure::Fixed(FixedMeasure { | |
| size: info.size * target.scale_factor(), | |
| })); |
| info.glyphs.clear(); | ||
| info.run_geometry.clear(); | ||
|
|
||
| for line in layout.lines() { |
There was a problem hiding this comment.
Should just call TextPipeline::update_text_layout_info here.
| width: px(200), | ||
| height: px(100), |
There was a problem hiding this comment.
Set the size on the EditableText node itself, then don't have to bother with the measure func.
| width: px(200), | |
| height: px(100), |
| .id(); | ||
|
|
||
| let edit = commands | ||
| .spawn(( |
There was a problem hiding this comment.
| .spawn(( | |
| .spawn(( | |
| Node { | |
| width: px(200), | |
| height: px(100), | |
| ..Default::default() | |
| }, |
| /// and provides methods for applying text edits and cursor movements correctly | ||
| /// according to Unicode rules. | ||
| #[derive(Component)] | ||
| #[require(TextLayout, TextFont, TextColor, LineHeight, FontHinting)] |
There was a problem hiding this comment.
I feel like EditableText should just be newtype around PlainEditor and not have any requirements. Maybe no helper functions even.
IMO the basic design should instead use a primary TextInput (or whatever) component that newtypes a string and require's everything. Then the widget's update system keeps the TextInput component synchronised with the PlainEditor's text buffer, for seamless change detection. So users can modify the TextInput's String to update the PlainEditors text buffer and they can use change detection on TextInput to watch for any changes due to edits on the PlainEditor. We can just clone the strings, it doesn't matter as only one input will be active at a time and we aren't building a text editor for giant text files.
| /// | ||
| /// Note that this does not immediately apply the edits; they are queued up in [`EditableText::pending_edits`], | ||
| /// and then applied later by the [`apply_text_edits`](`bevy_text::apply_text_edits`) system. | ||
| pub fn process_text_inputs( |
There was a problem hiding this comment.
This should be an observer watching for On<FocusedInput<KeyboardInput> I guess. It's more idiomatic and an observer can propagate unhandled keystrokes if needed.
There was a problem hiding this comment.
Agreed: any keystrokes that are not handled by the text input widget should be allowed to propagate. This lets things like "Ctrl-W to close window" work.
| app.register_required_components::<EditableText, Node>() | ||
| .register_required_components::<EditableText, TextNodeFlags>() | ||
| .register_required_components::<EditableText, ContentSize>(); |
There was a problem hiding this comment.
The primary component should be in this module, then there's no need to bother with this.
| .ambiguous_with(bevy_sprite::update_text2d_layout) | ||
| .ambiguous_with(bevy_sprite::calculate_bounds_text2d), | ||
| widget::editable_text_system | ||
| .in_set(UiSystems::PostLayout) |
There was a problem hiding this comment.
This is incorrect atm, measure funcs should be updated before UiSystems::Layout. Will need to be split up into two systems as it is for UI text, one in UiSystems::Content and one in UiSystems::PostLayout.
| content_size.set(NodeMeasure::Fixed(FixedMeasure { | ||
| size: info.size * target.scale_factor(), | ||
| })); |
There was a problem hiding this comment.
Another problem here is that most edits are going to change the size of the text layout, so the measure func will be updated which will cause an unnecessary relayout of the node.
Objective
Solution
EditableTextcomponent wraps aparley::PlainEditorTextEdititemsPlainEditorfor layout, and render from thereRelevant schedules are:
PreUpdate,bevy_ui_widgets::process_text_inputsPostUpdate,bevy_text::apply_text_editsPostUpdate,bevy_ui::editable_text_systemAnd in
render_app:ExtractSchedule,bevy_ui_render::extract_text_editableExtractSchedule,bevy_ui_render::extract_text_cursorTesting
cargo run --example editable_textShowcase