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}, font::{Font, FontGlyph, FontHeight, FontResource, BUILT_IN_FONT},
style::{resource::StyleResource, StyledProperty}, 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}; use fyrox_resource::state::{LoadError, ResourceState};
pub use run::*; pub use run::*;
use std::{ use std::{
@@ -308,6 +311,11 @@ pub struct FormattedText {
pub trim_text: InheritableVariable<bool>, pub trim_text: InheritableVariable<bool>,
} }
pub struct InlineContext<'a> {
pub children: &'a [Handle<UiNode>],
pub ui: &'a UserInterface,
}
impl FormattedText { impl FormattedText {
pub fn font_at(&self, index: usize) -> FontResource { pub fn font_at(&self, index: usize) -> FontResource {
self.runs.font_at(index).unwrap_or_else(|| self.get_font()) 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> { pub fn measure_and_arrange(&mut self) -> Vector2<f32> {
let size = self.measure(); let size = self.measure(None);
self.arrange(self.constraint); self.arrange(self.constraint, None);
size 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); let mut lines = std::mem::take(&mut self.lines);
lines.clear(); lines.clear();
// Fail early if any font is not available. // 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), WrapMode::Word => wrap_mask(WordWrap::new(sink), self.text.len(), mask, advance),
} }
} else { } 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 { match *self.wrap {
WrapMode::NoWrap => wrap(NoWrap::new(sink), source), WrapMode::NoWrap => self.wrap(NoWrap::new(sink), &inline_bounds, &self.text),
WrapMode::Letter => wrap(LetterWrap::new(sink), source), WrapMode::Letter => self.wrap(LetterWrap::new(sink), &inline_bounds, &self.text),
WrapMode::Word => wrap(WordWrap::new(sink), source), WrapMode::Word => self.wrap(WordWrap::new(sink), &inline_bounds, &self.text),
} }
} }
@@ -962,6 +1001,9 @@ impl FormattedText {
} }
.ascender(); .ascender();
line.height = line.height.max(h); 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(); 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; self.constraint = constraint;
let constraint = Vector2::new( let constraint = Vector2::new(
(self.constraint.x - (self.padding.left + self.padding.right)).max(0.0), (self.constraint.x - (self.padding.left + self.padding.right)).max(0.0),
@@ -1094,6 +1147,27 @@ impl FormattedText {
x += metrics.newline_advance(); 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 y1 = y + line.height - metrics.ascender();
let scale = self.super_sampling_scale; let scale = self.super_sampling_scale;
let (glyph, advance) = 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) { fn wrap_mask<W: TextWrapper>(mut wrapper: W, length: usize, mask_char: char, advance: f32) {
for _ in 0..length { for _ in 0..length {
wrapper.push(mask_char, advance); wrapper.push(Some(mask_char), advance);
} }
wrapper.finish(); wrapper.finish();
} }
+37 -31
View File
@@ -21,7 +21,7 @@
use std::ops::Range; use std::ops::Range;
pub trait TextWrapper { pub trait TextWrapper {
fn push(&mut self, c: char, advance: f32); fn push(&mut self, c: Option<char>, advance: f32);
fn finish(&mut self); fn finish(&mut self);
} }
@@ -53,16 +53,18 @@ impl<S> NoWrap<S> {
} }
impl<S: LineSink> TextWrapper for NoWrap<S> { impl<S: LineSink> TextWrapper for NoWrap<S> {
fn push(&mut self, c: char, advance: f32) { fn push(&mut self, c: Option<char>, advance: f32) {
if is_newline(c) { if let Some(c) = c {
self.sink.push_line(self.start..self.position, self.width); if is_newline(c) {
self.start = self.position + 1; // Next like starts after the newline, so skip ahead by one. self.sink.push_line(self.start..self.position, self.width);
self.position += 1; self.start = self.position + 1; // Next like starts after the newline, so skip ahead by one.
self.width = 0.0; self.position += 1;
} else { self.width = 0.0;
self.position += 1; return;
self.width += advance; }
} }
self.position += 1;
self.width += advance;
} }
fn finish(&mut self) { fn finish(&mut self) {
self.sink.push_line(self.start..self.position, self.width); 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> { 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() { if self.position != self.start && self.width + advance > self.sink.max_width() {
self.sink.push_line(self.start..self.position, self.width); self.sink.push_line(self.start..self.position, self.width);
self.start = self.position; self.start = self.position;
@@ -96,10 +98,12 @@ impl<S: LineSink> TextWrapper for LetterWrap<S> {
} }
self.position += 1; self.position += 1;
self.width += advance; self.width += advance;
if is_newline(c) { if let Some(c) = c {
self.sink.push_line(self.start..self.position, self.width); if is_newline(c) {
self.start = self.position; // Next line starts after the newline self.sink.push_line(self.start..self.position, self.width);
self.width = 0.0; 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> { 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.position != self.start && self.width + advance > self.sink.max_width() {
if self.start < self.word_start { if self.start < self.word_start {
self.sink self.sink
@@ -156,20 +160,22 @@ impl<S: LineSink> TextWrapper for WordWrap<S> {
} }
self.position += 1; self.position += 1;
self.width += advance; self.width += advance;
if is_newline(c) { if let Some(c) = c {
self.sink.push_line(self.start..self.position, self.width); if is_newline(c) {
self.start = self.position; self.sink.push_line(self.start..self.position, self.width);
self.width = 0.0; self.start = self.position;
// newline is not part of a word, so move word_start ahead. self.width = 0.0;
self.word_start = self.position; // newline is not part of a word, so move word_start ahead.
self.word_width = 0.0; self.word_start = self.position;
} else if c.is_whitespace() { self.word_width = 0.0;
// We are not in a word, so move word_start ahead. } else if c.is_whitespace() {
self.word_start = self.position; // We are not in a word, so move word_start ahead.
self.word_width = 0.0; self.word_start = self.position;
} else { self.word_width = 0.0;
// We are in a word, so leave word_start alone and increase word_width. } else {
self.word_width += advance; // 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) { fn wrap<W: TextWrapper>(wrap: &mut W, text: &str) {
for c in text.chars() { for c in text.chars() {
wrap.push(c, 1.0); wrap.push(Some(c), 1.0);
} }
wrap.finish(); wrap.finish();
} }
+13 -3
View File
@@ -23,6 +23,7 @@
#![warn(missing_docs)] #![warn(missing_docs)]
use crate::formatted_text::InlineContext;
use crate::{ use crate::{
brush::Brush, brush::Brush,
core::{ core::{
@@ -359,16 +360,25 @@ impl ConstructorProvider<UiNode, UserInterface> for Text {
crate::define_widget_deref!(Text); crate::define_widget_deref!(Text);
impl Control for 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 self.formatted_text
.borrow_mut() .borrow_mut()
.set_super_sampling_scale(self.visual_max_scaling()) .set_super_sampling_scale(self.visual_max_scaling())
.set_constraint(available_size) .set_constraint(available_size)
.measure() .measure(Some(InlineContext {
children: &self.children,
ui,
}))
} }
fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> { 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) self.widget.arrange_override(ui, final_size)
} }
+16 -3
View File
@@ -23,6 +23,7 @@
#![warn(missing_docs)] #![warn(missing_docs)]
use crate::formatted_text::InlineContext;
use crate::{ use crate::{
brush::Brush, brush::Brush,
core::{ core::{
@@ -694,7 +695,10 @@ impl TextBox {
.unwrap_or_default(); .unwrap_or_default();
let mut text = self.formatted_text.borrow_mut(); let mut text = self.formatted_text.borrow_mut();
text.insert_str(&str, position); text.insert_str(&str, position);
text.measure(); text.measure(Some(InlineContext {
children: &self.children,
ui,
}));
drop(text); drop(text);
self.set_caret_position( self.set_caret_position(
self.char_index_to_position(position + str.chars().count()) self.char_index_to_position(position + str.chars().count())
@@ -954,13 +958,22 @@ impl Control for TextBox {
.borrow_mut() .borrow_mut()
.set_super_sampling_scale(self.visual_max_scaling()) .set_super_sampling_scale(self.visual_max_scaling())
.set_constraint(available_size) .set_constraint(available_size)
.measure(); .measure(Some(InlineContext {
children: &self.children,
ui,
}));
let children_size = self.widget.measure_override(ui, available_size); let children_size = self.widget.measure_override(ui, available_size);
text_size.sup(&children_size) text_size.sup(&children_size)
} }
fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> { 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) self.widget.arrange_override(ui, final_size)
} }