Skip to main content

bevy_ui/widget/
text_input_layout.rs

1use core::hash::BuildHasher;
2use core::time::Duration;
3
4use crate::{ComputedNode, ComputedUiRenderTargetInfo, ContentSize, NodeMeasure};
5use bevy_asset::Assets;
6
7use bevy_ecs::{
8    change_detection::{DetectChanges, DetectChangesMut},
9    component::Component,
10    entity::Entity,
11    system::{Local, Query, Res, ResMut},
12    world::Ref,
13};
14use bevy_image::prelude::*;
15use bevy_input_focus::InputFocus;
16use bevy_math::{Rect, Vec2};
17use bevy_platform::hash::FixedHasher;
18
19use bevy_text::{
20    add_glyph_to_atlas, cursor_reveal_rect, get_glyph_atlas_info, scrollable_text_layout_width,
21    EditableText, EditableTextGeneration, Font, FontAtlasKey, FontAtlasSet, FontCx, FontHinting,
22    FontSize, GlyphCacheKey, LayoutCx, LineBreak, LineHeight, PositionedGlyph, RemSize,
23    RunGeometry, ScaleCx, TextBrush, TextFont, TextLayout, TextLayoutInfo, TextLineYBounds,
24};
25use bevy_time::{Real, Time};
26use parley::{BoundingBox, PositionedLayoutItem, StyleProperty};
27use smallvec::SmallVec;
28use swash::FontRef;
29use taffy::MaybeMath;
30
31struct TextInputMeasure {
32    width: Option<f32>,
33    height: Option<f32>,
34}
35
36/// Cached inputs that determine an editable text node's intrinsic size.
37#[derive(impl bevy_ecs::component::Component for EditableTextContentSizeState where
    Self: ::core::marker::Send + ::core::marker::Sync + 'static {
    const STORAGE_TYPE: bevy_ecs::component::StorageType =
        bevy_ecs::component::StorageType::Table;
    type Mutability = bevy_ecs::component::Mutable;
    fn register_required_components(_requiree:
            bevy_ecs::component::ComponentId,
        required_components:
            &mut bevy_ecs::component::RequiredComponentsRegistrator) {}
    fn clone_behavior() -> bevy_ecs::component::ComponentCloneBehavior {
        use bevy_ecs::component::{
            DefaultCloneBehaviorBase, DefaultCloneBehaviorViaClone,
        };
        (&&&bevy_ecs::component::DefaultCloneBehaviorSpecialization::<Self>::default()).default_clone_behavior()
    }
    fn relationship_accessor()
        ->
            ::core::option::Option<bevy_ecs::relationship::ComponentRelationshipAccessor<Self>> {
        ::core::option::Option::None
    }
}Component, #[automatically_derived]impl ::core::default::Default for
    EditableTextContentSizeState {
    #[inline]
    fn default() -> Self {
        EditableTextContentSizeState {
            visible_width: ::core::default::Default::default(),
            visible_lines: ::core::default::Default::default(),
        }
    }
}Default)]
38pub struct EditableTextContentSizeState {
39    visible_width: Option<f32>,
40    visible_lines: Option<f32>,
41}
42
43impl crate::Measure for TextInputMeasure {
44    fn measure(&mut self, measure_args: crate::MeasureArgs<'_>) -> Vec2 {
45        let width = measure_args.resolve_width();
46        let height = measure_args.resolve_height();
47
48        let x = width
49            .effective
50            .unwrap_or(self.width.unwrap_or(match measure_args.available_width {
51                crate::AvailableSpace::Definite(x) => x,
52                crate::AvailableSpace::MinContent | crate::AvailableSpace::MaxContent => 0.0,
53            }))
54            .maybe_clamp(width.min, width.max);
55        let y = height
56            .effective
57            .unwrap_or(self.height.unwrap_or(match measure_args.available_height {
58                crate::AvailableSpace::Definite(y) => y,
59                crate::AvailableSpace::MinContent | crate::AvailableSpace::MaxContent => 0.0,
60            }))
61            .maybe_clamp(height.min, height.max);
62
63        Vec2::new(x, y).ceil()
64    }
65}
66
67fn query_family<'a, 'b>(
68    family: &'a parley::FontFamilyName<'b>,
69) -> parley::fontique::QueryFamily<'a> {
70    match family {
71        parley::FontFamilyName::Named(name) => parley::fontique::QueryFamily::Named(name.as_ref()),
72        parley::FontFamilyName::Generic(generic) => {
73            parley::fontique::QueryFamily::Generic(*generic)
74        }
75    }
76}
77
78/// If `visible_lines` or `visible_width` are `Some`, sets a `ContentSize` that determines:
79/// - node height as `line_height * visible_lines`, using the resolved font line height.
80/// - node width as `advance('0') * visible_width`, where `advance('0')` is looked up from font metrics.
81pub fn update_editable_text_content_size(
82    mut text_input_query: Query<(
83        &EditableText,
84        Ref<TextFont>,
85        Ref<LineHeight>,
86        Ref<ComputedUiRenderTargetInfo>,
87        &mut ContentSize,
88        &mut EditableTextContentSizeState,
89    )>,
90    fonts: Res<Assets<Font>>,
91    mut font_cx: ResMut<FontCx>,
92    rem_size: Res<RemSize>,
93) {
94    for (editable_text, text_font, line_height, target, mut content_size, mut size_state) in
95        &mut text_input_query
96    {
97        let sizing_changed = size_state.visible_width != editable_text.visible_width
98            || size_state.visible_lines != editable_text.visible_lines;
99        if !(sizing_changed
100            || text_font.is_changed()
101            || line_height.is_changed()
102            || target.is_changed()
103            || rem_size.is_changed())
104        {
105            continue;
106        }
107
108        size_state.visible_width = editable_text.visible_width;
109        size_state.visible_lines = editable_text.visible_lines;
110
111        let font_size = text_font.font_size.eval(target.logical_size(), *rem_size);
112
113        let width = editable_text.visible_width.and_then(|visible_width| {
114            let font_context = &mut font_cx.context;
115            let mut query = font_context
116                .collection
117                .query(&mut font_context.source_cache);
118
119            match text_font.font.resolve_font_family(fonts.as_ref()).ok()? {
120                parley::FontFamily::Source(source) => {
121                    query.set_families(
122                        parley::FontFamilyName::parse_css_list(&source)
123                            .map_while(Result::ok)
124                            .collect::<SmallVec<[_; 4]>>()
125                            .iter()
126                            .map(query_family),
127                    );
128                }
129                parley::FontFamily::Single(family) => {
130                    query.set_families([query_family(&family)]);
131                }
132                parley::FontFamily::List(families) => {
133                    query.set_families(families.iter().map(query_family));
134                }
135            }
136            query.set_attributes(parley::fontique::Attributes::new(
137                text_font.width.into(),
138                text_font.style.into(),
139                text_font.weight.into(),
140            ));
141
142            let mut width = None;
143            query.matches_with(|query_font| {
144                let Some((glyph_id, font_ref)) = query_font
145                    .charmap()
146                    .and_then(|char_map| char_map.map('0'))
147                    .and_then(|glyph_id| u16::try_from(glyph_id).ok())
148                    .zip(FontRef::from_index(
149                        query_font.blob.as_ref(),
150                        query_font.index as usize,
151                    ))
152                else {
153                    return parley::fontique::QueryStatus::Continue;
154                };
155
156                let advance = font_ref
157                    .glyph_metrics(&[])
158                    .scale(font_size)
159                    .advance_width(glyph_id);
160                if advance.is_finite() {
161                    width = Some(advance.max(0.0));
162                    parley::fontique::QueryStatus::Stop
163                } else {
164                    parley::fontique::QueryStatus::Continue
165                }
166            });
167
168            width.map(|width| width * visible_width * target.scale_factor())
169        });
170
171        let height = editable_text.visible_lines.map(|visible_lines| {
172            let logical_line_height = match *line_height {
173                LineHeight::Px(px) => px,
174                LineHeight::RelativeToFont(scale) => scale * font_size,
175            };
176            visible_lines * logical_line_height * target.scale_factor()
177        });
178
179        if width.is_some() || height.is_some() {
180            content_size.set(NodeMeasure::Custom(Box::new(TextInputMeasure {
181                width,
182                height,
183            })));
184        } else {
185            content_size.clear();
186        }
187    }
188}
189
190/// Syncs each [`EditableText`] entity's [`PlainEditor`](parley::PlainEditor)
191/// style properties to match its [`TextFont`], [`LineHeight`], and [`TextLayout`] components.
192pub fn update_editable_text_styles(
193    fonts: Res<Assets<Font>>,
194    mut editable_text_query: Query<(
195        &mut EditableText,
196        Ref<TextFont>,
197        Ref<LineHeight>,
198        Ref<ComputedUiRenderTargetInfo>,
199        Ref<TextLayout>,
200    )>,
201    rem_size: Res<RemSize>,
202) {
203    for (mut editable_text, text_font, line_height, target, text_layout) in
204        editable_text_query.iter_mut()
205    {
206        if f32::EPSILON < (target.scale_factor() - editable_text.editor.get_scale()).abs() {
207            editable_text.editor.set_scale(target.scale_factor());
208        }
209
210        if text_font.is_changed()
211            || #[allow(non_exhaustive_omitted_patterns)] match text_font.font_size {
    FontSize::Rem(_) => true,
    _ => false,
}matches!(text_font.font_size, FontSize::Rem(_)) && rem_size.is_changed()
212            || #[allow(non_exhaustive_omitted_patterns)] match text_font.font_size {
    FontSize::Vw(_) | FontSize::Vh(_) | FontSize::VMin(_) | FontSize::VMax(_)
        => true,
    _ => false,
}matches!(
213                text_font.font_size,
214                FontSize::Vw(_) | FontSize::Vh(_) | FontSize::VMin(_) | FontSize::VMax(_)
215            ) && target.is_changed()
216        {
217            editable_text
218                .editor
219                .edit_styles()
220                .insert(StyleProperty::FontSize(
221                    text_font.font_size.eval(target.logical_size(), *rem_size),
222                ));
223        }
224
225        if text_font.is_changed()
226            && let Ok(resolved_family) = text_font.font.resolve_font_family(fonts.as_ref())
227        {
228            let family = resolved_family.into_owned();
229            let style_set = editable_text.editor.edit_styles();
230            style_set.insert(StyleProperty::FontFamily(family));
231        }
232
233        if text_font.is_changed() {
234            let style_set = editable_text.editor.edit_styles();
235            style_set.insert(StyleProperty::FontWeight(text_font.weight.into()));
236            style_set.insert(StyleProperty::FontWidth(text_font.width.into()));
237            style_set.insert(StyleProperty::FontStyle(text_font.style.into()));
238            style_set.insert(StyleProperty::FontFeatures(
239                (&text_font.font_features).into(),
240            ));
241            style_set.insert(StyleProperty::FontVariations(
242                (&text_font.font_variations).into(),
243            ));
244            style_set.insert(StyleProperty::Brush(TextBrush::new(
245                0,
246                text_font.font_smoothing,
247            )));
248        }
249
250        if line_height.is_changed() {
251            let style_set = editable_text.editor.edit_styles();
252            style_set.insert(StyleProperty::LineHeight(line_height.eval()));
253        }
254
255        if text_layout.is_changed() {
256            let style_set = editable_text.editor.edit_styles();
257            match text_layout.linebreak {
258                LineBreak::AnyCharacter => {
259                    style_set.insert(StyleProperty::WordBreak(parley::WordBreak::BreakAll));
260                    style_set.insert(StyleProperty::OverflowWrap(parley::OverflowWrap::Normal));
261                    style_set.insert(StyleProperty::TextWrapMode(parley::TextWrapMode::Wrap));
262                }
263                LineBreak::WordOrCharacter => {
264                    style_set.insert(StyleProperty::WordBreak(parley::WordBreak::Normal));
265                    style_set.insert(StyleProperty::OverflowWrap(parley::OverflowWrap::Anywhere));
266                    style_set.insert(StyleProperty::TextWrapMode(parley::TextWrapMode::Wrap));
267                }
268                LineBreak::NoWrap => {
269                    style_set.insert(StyleProperty::WordBreak(parley::WordBreak::Normal));
270                    style_set.insert(StyleProperty::OverflowWrap(parley::OverflowWrap::Normal));
271                    style_set.insert(StyleProperty::TextWrapMode(parley::TextWrapMode::NoWrap));
272                }
273                LineBreak::WordBoundary => {
274                    style_set.insert(StyleProperty::WordBreak(parley::WordBreak::Normal));
275                    style_set.insert(StyleProperty::OverflowWrap(parley::OverflowWrap::Normal));
276                    style_set.insert(StyleProperty::TextWrapMode(parley::TextWrapMode::Wrap));
277                }
278            }
279
280            editable_text
281                .editor
282                .set_alignment(text_layout.justify.into());
283        }
284    }
285}
286
287/// Syncs each [`EditableText`]'s viewport size with their `ComputedNode`'s content size before text edits are applied.
288pub fn sync_editable_text_viewports(mut query: Query<(&mut EditableText, &ComputedNode)>) {
289    for (mut editable_text, computed_node) in &mut query {
290        let size = computed_node.content_box().size();
291        if editable_text.viewport.size != size {
292            editable_text.viewport.size = size;
293            editable_text.editor.set_width(Some(size.x));
294        }
295    }
296}
297
298/// Refreshes the [`EditableText`]'s layout if stale and then writes it
299/// it to [`TextLayoutInfo`] for rendering and picking.
300/// Adds required glyphs to the texture atlas
301pub fn update_editable_text_layout(
302    mut font_cx: ResMut<FontCx>,
303    mut layout_cx: ResMut<LayoutCx>,
304    mut scale_cx: ResMut<ScaleCx>,
305    mut font_atlas_set: ResMut<FontAtlasSet>,
306    mut textures: ResMut<Assets<Image>>,
307    mut input_field_query: Query<(
308        Entity,
309        &TextFont,
310        &TextLayout,
311        Ref<FontHinting>,
312        Ref<ComputedUiRenderTargetInfo>,
313        &mut EditableText,
314        &mut TextLayoutInfo,
315        &mut EditableTextGeneration,
316    )>,
317    rem_size: Res<RemSize>,
318    input_focus: Option<Res<InputFocus>>,
319    mut cursor_timer: Local<Duration>,
320    mut previous_focus: Local<Option<Entity>>,
321    time: Res<Time<Real>>,
322) {
323    *cursor_timer += time.delta();
324    let current_focus = input_focus
325        .as_ref()
326        .and_then(|input_focus| input_focus.get());
327    let focus_changed = *previous_focus != current_focus;
328
329    for (
330        entity,
331        text_font,
332        text_layout,
333        hinting,
334        target,
335        mut editable_text,
336        mut info,
337        mut generation,
338    ) in input_field_query.iter_mut()
339    {
340        let cursor_width = editable_text.cursor_width;
341        let cursor_blink_period = editable_text.cursor_blink_period;
342        let cursor_margin = editable_text.cursor_margin;
343        let viewport_before = editable_text.viewport;
344
345        // Bypass change detection, we will mark editable_text as changed
346        // at the bottom if genuinely changed, however editor.driver requires
347        // a mut ref.
348        let inner = editable_text.bypass_change_detection();
349        let (editor, viewport) = (&mut inner.editor, &mut inner.viewport);
350
351        let mut driver = editor.driver(font_cx.as_mut(), layout_cx.as_mut());
352
353        driver.refresh_layout();
354
355        let compose_range = driver.editor.raw_compose().clone();
356
357        let layout_changed = driver.editor.generation() != **generation;
358        if layout_changed {
359            **generation = driver.editor.generation();
360        }
361
362        if layout_changed || hinting.is_changed() {
363            let layout = driver.layout();
364
365            info.scale_factor = layout.scale();
366            info.size = (layout.full_width(), layout.height()).into();
367
368            info.preedit_underline_rects.clear();
369            info.glyphs.clear();
370            info.run_geometry.clear();
371
372            for (line_index, line) in layout.lines().enumerate() {
373                for item in line.items() {
374                    match item {
375                        PositionedLayoutItem::GlyphRun(glyph_run) => {
376                            let brush = glyph_run.style().brush;
377
378                            let run = glyph_run.run();
379
380                            let font_data = run.font();
381                            let font_size = run.font_size();
382                            let coords = run.normalized_coords();
383
384                            let font_atlas_key = FontAtlasKey {
385                                id: font_data.data.id() as u32,
386                                index: font_data.index,
387                                font_size_bits: font_size.to_bits(),
388                                variations_hash: FixedHasher.hash_one(coords),
389                                hinting: *hinting,
390                                font_smoothing: brush.font_smoothing,
391                            };
392
393                            for glyph in glyph_run.positioned_glyphs() {
394                                let font_atlases =
395                                    font_atlas_set.entry(font_atlas_key).or_default();
396                                let Ok(atlas_info) = get_glyph_atlas_info(
397                                    font_atlases,
398                                    GlyphCacheKey {
399                                        glyph_id: glyph.id as u16,
400                                    },
401                                )
402                                .map(Ok)
403                                .unwrap_or_else(|| {
404                                    let font_ref = FontRef::from_index(
405                                        font_data.data.as_ref(),
406                                        font_data.index as usize,
407                                    )
408                                    .unwrap();
409                                    let font_id = [font_data.data.id(), font_data.index.into()];
410
411                                    let mut scaler = scale_cx
412                                        .builder_with_id(font_ref, font_id)
413                                        .size(font_size)
414                                        .hint(#[allow(non_exhaustive_omitted_patterns)] match *hinting {
    FontHinting::Enabled => true,
    _ => false,
}matches!(*hinting, FontHinting::Enabled))
415                                        .normalized_coords(coords)
416                                        .build();
417                                    add_glyph_to_atlas(
418                                        font_atlases,
419                                        textures.as_mut(),
420                                        &mut scaler,
421                                        text_font.font_smoothing,
422                                        glyph.id as u16,
423                                    )
424                                }) else {
425                                    continue;
426                                };
427
428                                info.glyphs.push(PositionedGlyph {
429                                    position: Vec2::new(glyph.x, glyph.y)
430                                        + atlas_info.rect.size() / 2.
431                                        + atlas_info.offset,
432                                    atlas_info,
433                                    section_index: brush.section_index,
434                                    line_index: line_index as u32,
435                                });
436                            }
437
438                            let metrics = run.metrics();
439                            let underline_y = glyph_run.baseline() - metrics.underline_offset;
440                            let underline_thickness = metrics.underline_size;
441
442                            let run_text_range = run.text_range();
443                            if let Some(cr) = &compose_range
444                                && run_text_range.start < cr.end
445                                && run_text_range.end > cr.start
446                            {
447                                let mut x = glyph_run.offset();
448                                let mut underline_start_x = None;
449                                let mut underline_end_x = x;
450
451                                for cluster in run.visual_clusters() {
452                                    let ct = cluster.text_range();
453                                    if ct.start < cr.end && ct.end > cr.start {
454                                        underline_start_x.get_or_insert(x);
455                                        underline_end_x = x + cluster.advance();
456                                    }
457                                    x += cluster.advance();
458                                }
459
460                                if let Some(start_x) = underline_start_x {
461                                    info.preedit_underline_rects.push(Rect {
462                                        min: Vec2::new(start_x, underline_y),
463                                        max: Vec2::new(
464                                            underline_end_x,
465                                            underline_y + underline_thickness,
466                                        ),
467                                    });
468                                }
469                            }
470
471                            info.run_geometry.push(RunGeometry {
472                                section_index: brush.section_index,
473                                bounds: Rect {
474                                    min: Vec2::new(
475                                        glyph_run.offset(),
476                                        line.metrics().block_min_coord,
477                                    ),
478                                    max: Vec2::new(
479                                        glyph_run.offset() + glyph_run.advance(),
480                                        line.metrics().block_max_coord,
481                                    ),
482                                },
483                                strikethrough_y: glyph_run.baseline()
484                                    - metrics.strikethrough_offset,
485                                strikethrough_thickness: metrics.strikethrough_size,
486                                underline_y,
487                                underline_thickness,
488                            });
489                        }
490                        PositionedLayoutItem::InlineBox(_inline) => {
491                            // TODO: handle inline boxes
492                        }
493                    }
494                }
495            }
496
497            info.selection_rects = driver
498                .editor
499                .selection_geometry()
500                .iter()
501                .map(|&b| bounding_box_to_rect(b.0))
502                .collect();
503
504            for i in 0..info.selection_rects.len().saturating_sub(1) {
505                let [a, b] = &mut info.selection_rects[i..i + 2] else {
506                    ::core::panicking::unreachable_default_msg();unreachable!();
507                };
508                if a.max.y < b.min.y {
509                    a.max.y = b.min.y;
510                }
511            }
512        }
513
514        let full_layout_size = {
515            let layout = driver.layout();
516            Vec2::new(layout.full_width(), layout.height())
517        };
518        let is_focused = Some(entity) == current_focus;
519        let cursor_reveal = is_focused
520            .then(|| cursor_reveal_rect(&mut driver))
521            .flatten();
522
523        if is_focused {
524            if focus_changed || layout_changed || *cursor_timer >= cursor_blink_period {
525                *cursor_timer = Duration::ZERO;
526            }
527
528            info.cursor = driver
529                .editor
530                .cursor_geometry(
531                    cursor_width * text_font.font_size.eval(target.logical_size(), *rem_size),
532                )
533                .map(bounding_box_to_rect)
534                .map(|rect| (*cursor_timer < cursor_blink_period / 2, rect));
535        } else {
536            info.cursor = driver
537                .editor
538                .cursor_geometry(0.)
539                .map(bounding_box_to_rect)
540                .map(|rect| (false, rect));
541        }
542
543        if focus_changed && let Some(cursor) = cursor_reveal {
544            let line_bounds = driver
545                .layout()
546                .lines()
547                .map(|line| TextLineYBounds::from_line(&line));
548            viewport.reveal_caret(cursor, full_layout_size, cursor_margin, line_bounds);
549        }
550        let viewport_width = viewport.size.x;
551        viewport.clamp_inside(Vec2::new(
552            scrollable_text_layout_width(
553                text_layout.linebreak,
554                full_layout_size.x,
555                viewport_width,
556                cursor_reveal,
557            ),
558            full_layout_size.y,
559        ));
560
561        // Mark editable_text as changed if it has been, since we bypassed
562        // change detection when taking a mut ref at the top
563        if layout_changed || *viewport != viewport_before {
564            editable_text.set_changed();
565        }
566    }
567
568    *previous_focus = current_focus;
569}
570
571fn bounding_box_to_rect(geom: BoundingBox) -> Rect {
572    Rect {
573        min: Vec2 {
574            x: geom.x0 as f32,
575            y: geom.y0 as f32,
576        },
577        max: Vec2 {
578            x: geom.x1 as f32,
579            y: geom.y1 as f32,
580        },
581    }
582}