mirror of
https://github.com/FyroxEngine/Fyrox.git
synced 2026-10-12 02:02:20 +00:00
experimental ability to insert inline elements in text
This commit is contained in:
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user