experimental ability to insert inline elements in text

This commit is contained in:
Dmitry Stepanov
2026-09-24 22:47:04 +03:00
parent b6089617c6
commit dc354e0d98
4 changed files with 157 additions and 65 deletions
+91 -28
View File
@@ -26,8 +26,11 @@ use crate::{
},
font::{Font, FontGlyph, FontHeight, FontResource, BUILT_IN_FONT},
style::{resource::StyleResource, StyledProperty},
HorizontalAlignment, Thickness, VerticalAlignment,
HorizontalAlignment, Thickness, UiNode, UserInterface, VerticalAlignment,
};
use fxhash::FxHashMap;
use fyrox_core::pool::Handle;
use fyrox_graph::SceneGraph;
use fyrox_resource::state::{LoadError, ResourceState};
pub use run::*;
use std::{
@@ -308,6 +311,11 @@ pub struct FormattedText {
pub trim_text: InheritableVariable<bool>,
}
pub struct InlineContext<'a> {
pub children: &'a [Handle<UiNode>],
pub ui: &'a UserInterface,
}
impl FormattedText {
pub fn font_at(&self, index: usize) -> FontResource {
self.runs.font_at(index).unwrap_or_else(|| self.get_font())
@@ -884,12 +892,51 @@ impl FormattedText {
}
pub fn measure_and_arrange(&mut self) -> Vector2<f32> {
let size = self.measure();
self.arrange(self.constraint);
let size = self.measure(None);
self.arrange(self.constraint, None);
size
}
pub fn measure(&mut self) -> Vector2<f32> {
fn wrap(
&self,
mut wrapper: impl TextWrapper,
inline_bounds: &FxHashMap<usize, Vector2<f32>>,
text: &[char],
) {
for (i, c) in text.iter().enumerate() {
// Try wrap an inline first.
if let Some(inline_bounds) = inline_bounds.get(&i) {
wrapper.push(None, inline_bounds.x);
}
// Then wrap the character.
let advance = GlyphMetrics {
font: &mut self.font_at(i).data_ref(),
size: self.font_size_at(i),
}
.advance(*c);
wrapper.push(Some(*c), advance);
}
wrapper.finish();
}
pub fn measure(&mut self, inline_ctx: Option<InlineContext>) -> Vector2<f32> {
let mut inline_bounds = FxHashMap::default();
if let Some(ref inline_ctx) = inline_ctx {
for &inline in inline_ctx.children {
let inline_ref = inline_ctx.ui.node(inline);
inline_ctx
.ui
.measure_node(inline, Vector2::new(f32::INFINITY, f32::INFINITY));
let desired_size = inline_ref.desired_size();
let measured_inline = inline_bounds
.entry(*inline_ref.column)
.or_insert_with(|| desired_size);
*measured_inline = measured_inline.sup(&desired_size);
}
}
let mut lines = std::mem::take(&mut self.lines);
lines.clear();
// Fail early if any font is not available.
@@ -929,18 +976,10 @@ impl FormattedText {
WrapMode::Word => wrap_mask(WordWrap::new(sink), self.text.len(), mask, advance),
}
} else {
let source = self.text.iter().enumerate().map(|(i, c)| {
let a = GlyphMetrics {
font: &mut self.font_at(i).data_ref(),
size: self.font_size_at(i),
}
.advance(*c);
(*c, a)
});
match *self.wrap {
WrapMode::NoWrap => wrap(NoWrap::new(sink), source),
WrapMode::Letter => wrap(LetterWrap::new(sink), source),
WrapMode::Word => wrap(WordWrap::new(sink), source),
WrapMode::NoWrap => self.wrap(NoWrap::new(sink), &inline_bounds, &self.text),
WrapMode::Letter => self.wrap(LetterWrap::new(sink), &inline_bounds, &self.text),
WrapMode::Word => self.wrap(WordWrap::new(sink), &inline_bounds, &self.text),
}
}
@@ -962,6 +1001,9 @@ impl FormattedText {
}
.ascender();
line.height = line.height.max(h);
if let Some(inline) = inline_bounds.get(&i) {
line.height = line.height.max(inline.y);
}
}
}
self.total_height += line.height + self.line_space();
@@ -1010,7 +1052,18 @@ impl FormattedText {
)
}
pub fn arrange(&mut self, constraint: Vector2<f32>) {
pub fn arrange(&mut self, constraint: Vector2<f32>, inline_ctx: Option<InlineContext>) {
let mut inlines = FxHashMap::default();
if let Some(ref inline_ctx) = inline_ctx {
for &inline in inline_ctx.children {
let inline_ref = inline_ctx.ui.node(inline);
inlines
.entry(*inline_ref.column)
.or_insert_with(Vec::default)
.push(inline);
}
}
self.constraint = constraint;
let constraint = Vector2::new(
(self.constraint.x - (self.padding.left + self.padding.right)).max(0.0),
@@ -1094,6 +1147,27 @@ impl FormattedText {
x += metrics.newline_advance();
}
_ => {
// Put inlines first (if any).
if let Some(ref inline_ctx) = inline_ctx {
if let Some(inlines) = inlines.get(&i) {
let mut bounds = Vector2::zeros();
for &inline in inlines {
let inline_ref = inline_ctx.ui.node(inline);
let desired_size = inline_ref.desired_size();
inline_ctx.ui.arrange_node(
inline,
&Rect {
position: Vector2::new(x, y),
size: desired_size,
},
);
bounds = bounds.sup(&desired_size);
}
x += bounds.x;
}
}
// Then the glyph.
let y1 = y + line.height - metrics.ascender();
let scale = self.super_sampling_scale;
let (glyph, advance) =
@@ -1126,20 +1200,9 @@ impl FormattedText {
}
}
fn wrap<W, I>(mut wrapper: W, source: I)
where
W: TextWrapper,
I: Iterator<Item = (char, f32)>,
{
for (character, advance) in source {
wrapper.push(character, advance);
}
wrapper.finish();
}
fn wrap_mask<W: TextWrapper>(mut wrapper: W, length: usize, mask_char: char, advance: f32) {
for _ in 0..length {
wrapper.push(mask_char, advance);
wrapper.push(Some(mask_char), advance);
}
wrapper.finish();
}
+37 -31
View File
@@ -21,7 +21,7 @@
use std::ops::Range;
pub trait TextWrapper {
fn push(&mut self, c: char, advance: f32);
fn push(&mut self, c: Option<char>, advance: f32);
fn finish(&mut self);
}
@@ -53,16 +53,18 @@ impl<S> NoWrap<S> {
}
impl<S: LineSink> TextWrapper for NoWrap<S> {
fn push(&mut self, c: char, advance: f32) {
if is_newline(c) {
self.sink.push_line(self.start..self.position, self.width);
self.start = self.position + 1; // Next like starts after the newline, so skip ahead by one.
self.position += 1;
self.width = 0.0;
} else {
self.position += 1;
self.width += advance;
fn push(&mut self, c: Option<char>, advance: f32) {
if let Some(c) = c {
if is_newline(c) {
self.sink.push_line(self.start..self.position, self.width);
self.start = self.position + 1; // Next like starts after the newline, so skip ahead by one.
self.position += 1;
self.width = 0.0;
return;
}
}
self.position += 1;
self.width += advance;
}
fn finish(&mut self) {
self.sink.push_line(self.start..self.position, self.width);
@@ -88,7 +90,7 @@ impl<S> LetterWrap<S> {
}
impl<S: LineSink> TextWrapper for LetterWrap<S> {
fn push(&mut self, c: char, advance: f32) {
fn push(&mut self, c: Option<char>, advance: f32) {
if self.position != self.start && self.width + advance > self.sink.max_width() {
self.sink.push_line(self.start..self.position, self.width);
self.start = self.position;
@@ -96,10 +98,12 @@ impl<S: LineSink> TextWrapper for LetterWrap<S> {
}
self.position += 1;
self.width += advance;
if is_newline(c) {
self.sink.push_line(self.start..self.position, self.width);
self.start = self.position; // Next line starts after the newline
self.width = 0.0;
if let Some(c) = c {
if is_newline(c) {
self.sink.push_line(self.start..self.position, self.width);
self.start = self.position; // Next line starts after the newline
self.width = 0.0;
}
}
}
@@ -138,7 +142,7 @@ impl<S> WordWrap<S> {
}
impl<S: LineSink> TextWrapper for WordWrap<S> {
fn push(&mut self, c: char, advance: f32) {
fn push(&mut self, c: Option<char>, advance: f32) {
if self.position != self.start && self.width + advance > self.sink.max_width() {
if self.start < self.word_start {
self.sink
@@ -156,20 +160,22 @@ impl<S: LineSink> TextWrapper for WordWrap<S> {
}
self.position += 1;
self.width += advance;
if is_newline(c) {
self.sink.push_line(self.start..self.position, self.width);
self.start = self.position;
self.width = 0.0;
// newline is not part of a word, so move word_start ahead.
self.word_start = self.position;
self.word_width = 0.0;
} else if c.is_whitespace() {
// We are not in a word, so move word_start ahead.
self.word_start = self.position;
self.word_width = 0.0;
} else {
// We are in a word, so leave word_start alone and increase word_width.
self.word_width += advance;
if let Some(c) = c {
if is_newline(c) {
self.sink.push_line(self.start..self.position, self.width);
self.start = self.position;
self.width = 0.0;
// newline is not part of a word, so move word_start ahead.
self.word_start = self.position;
self.word_width = 0.0;
} else if c.is_whitespace() {
// We are not in a word, so move word_start ahead.
self.word_start = self.position;
self.word_width = 0.0;
} else {
// We are in a word, so leave word_start alone and increase word_width.
self.word_width += advance;
}
}
}
@@ -205,7 +211,7 @@ mod tests {
fn wrap<W: TextWrapper>(wrap: &mut W, text: &str) {
for c in text.chars() {
wrap.push(c, 1.0);
wrap.push(Some(c), 1.0);
}
wrap.finish();
}
+13 -3
View File
@@ -23,6 +23,7 @@
#![warn(missing_docs)]
use crate::formatted_text::InlineContext;
use crate::{
brush::Brush,
core::{
@@ -359,16 +360,25 @@ impl ConstructorProvider<UiNode, UserInterface> for Text {
crate::define_widget_deref!(Text);
impl Control for Text {
fn measure_override(&self, _: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
self.formatted_text
.borrow_mut()
.set_super_sampling_scale(self.visual_max_scaling())
.set_constraint(available_size)
.measure()
.measure(Some(InlineContext {
children: &self.children,
ui,
}))
}
fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
self.formatted_text.borrow_mut().arrange(final_size);
self.formatted_text.borrow_mut().arrange(
final_size,
Some(InlineContext {
children: &self.children,
ui,
}),
);
self.widget.arrange_override(ui, final_size)
}
+16 -3
View File
@@ -23,6 +23,7 @@
#![warn(missing_docs)]
use crate::formatted_text::InlineContext;
use crate::{
brush::Brush,
core::{
@@ -694,7 +695,10 @@ impl TextBox {
.unwrap_or_default();
let mut text = self.formatted_text.borrow_mut();
text.insert_str(&str, position);
text.measure();
text.measure(Some(InlineContext {
children: &self.children,
ui,
}));
drop(text);
self.set_caret_position(
self.char_index_to_position(position + str.chars().count())
@@ -954,13 +958,22 @@ impl Control for TextBox {
.borrow_mut()
.set_super_sampling_scale(self.visual_max_scaling())
.set_constraint(available_size)
.measure();
.measure(Some(InlineContext {
children: &self.children,
ui,
}));
let children_size = self.widget.measure_override(ui, available_size);
text_size.sup(&children_size)
}
fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
self.formatted_text.borrow_mut().arrange(final_size);
self.formatted_text.borrow_mut().arrange(
final_size,
Some(InlineContext {
children: &self.children,
ui,
}),
);
self.widget.arrange_override(ui, final_size)
}