mirror of
https://github.com/FyroxEngine/Fyrox.git
synced 2026-10-12 02:02:20 +00:00
Improving FormattedText
This commit is contained in:
@@ -230,9 +230,22 @@ impl Control for PanelPreview {
|
||||
}
|
||||
|
||||
ctx.transform_stack.pop();
|
||||
let position = bounds.right_bottom_corner() - self.handle_text_size;
|
||||
let rect = Rect {
|
||||
position,
|
||||
size: self.handle_text_size,
|
||||
};
|
||||
ctx.push_rect_filled(&rect, None);
|
||||
ctx.commit(
|
||||
self.clip_bounds(),
|
||||
Brush::Solid(Color::from_rgba(0, 0, 0, 200)),
|
||||
CommandTexture::None,
|
||||
&self.material,
|
||||
None,
|
||||
);
|
||||
ctx.draw_text(
|
||||
self.clip_bounds(),
|
||||
bounds.right_bottom_corner() - self.handle_text_size,
|
||||
position,
|
||||
&self.material,
|
||||
&self.handle_text,
|
||||
);
|
||||
@@ -270,6 +283,7 @@ impl PanelPreviewBuilder {
|
||||
tile_size: Vector2::repeat(32.0),
|
||||
transform: Matrix3::identity(),
|
||||
handle_text: FormattedTextBuilder::new(ctx.inner().default_font.clone())
|
||||
.with_constraint(Vector2::new(f32::INFINITY, f32::INFINITY))
|
||||
.with_brush(Brush::Solid(Color::WHITE))
|
||||
.build(),
|
||||
handle_text_size: Vector2::default(),
|
||||
|
||||
+173
-64
@@ -29,7 +29,7 @@ use crate::{
|
||||
HorizontalAlignment, VerticalAlignment,
|
||||
};
|
||||
pub use run::*;
|
||||
use std::ops::Range;
|
||||
use std::ops::{Range, RangeBounds};
|
||||
use strum_macros::{AsRefStr, EnumString, VariantNames};
|
||||
use textwrapper::*;
|
||||
|
||||
@@ -215,7 +215,8 @@ fn build_glyph(
|
||||
|
||||
struct WrapSink<'a> {
|
||||
lines: &'a mut Vec<TextLine>,
|
||||
max_width: f32,
|
||||
normal_width: f32,
|
||||
first_width: f32,
|
||||
}
|
||||
|
||||
impl LineSink for WrapSink<'_> {
|
||||
@@ -228,7 +229,11 @@ impl LineSink for WrapSink<'_> {
|
||||
}
|
||||
|
||||
fn max_width(&self) -> f32 {
|
||||
self.max_width
|
||||
if self.lines.is_empty() {
|
||||
self.first_width
|
||||
} else {
|
||||
self.normal_width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,6 +270,13 @@ pub struct FormattedText {
|
||||
pub shadow_offset: InheritableVariable<Vector2<f32>>,
|
||||
#[visit(optional)]
|
||||
pub runs: RunSet,
|
||||
/// The indent amount of the first line of the text.
|
||||
/// A negative indent will cause every line except the first to indent.
|
||||
#[visit(optional)]
|
||||
pub line_indent: InheritableVariable<f32>,
|
||||
/// The space between lines.
|
||||
#[visit(optional)]
|
||||
pub line_space: InheritableVariable<f32>,
|
||||
}
|
||||
|
||||
impl FormattedText {
|
||||
@@ -436,44 +448,20 @@ impl FormattedText {
|
||||
}
|
||||
|
||||
pub fn position_to_local(&self, position: Position) -> Vector2<f32> {
|
||||
let mut state = self.font.state();
|
||||
let Some(font) = state.data() else {
|
||||
if self.font.state().data().is_none() {
|
||||
return Default::default();
|
||||
};
|
||||
let mut metrics = GlyphMetrics {
|
||||
font,
|
||||
size: **self.font_size,
|
||||
};
|
||||
let mut caret_pos = Vector2::default();
|
||||
let position = self.nearest_valid_position(position);
|
||||
|
||||
let line = self.lines[position.line];
|
||||
let raw_text = self.get_raw_text();
|
||||
caret_pos += Vector2::new(line.x_offset, line.y_offset);
|
||||
for (offset, char_index) in (line.begin..line.end).enumerate() {
|
||||
if offset >= position.offset {
|
||||
break;
|
||||
}
|
||||
if let Some(advance) = raw_text.get(char_index).map(|c| metrics.advance(*c)) {
|
||||
caret_pos.x += advance;
|
||||
} else {
|
||||
caret_pos.x += metrics.size;
|
||||
}
|
||||
}
|
||||
caret_pos
|
||||
let position = self.nearest_valid_position(position);
|
||||
let line = &self.lines[position.line];
|
||||
let caret_pos = Vector2::new(line.x_offset, line.y_offset);
|
||||
let range = line.begin..line.begin + position.offset;
|
||||
caret_pos + Vector2::new(self.get_range_width(range), 0.0)
|
||||
}
|
||||
|
||||
pub fn local_to_position(&self, point: Vector2<f32>) -> Position {
|
||||
let font_size = **self.font_size();
|
||||
let font = self.get_font();
|
||||
let mut state = font.state();
|
||||
let Some(font) = state.data() else {
|
||||
if self.get_font().state().data().is_none() {
|
||||
return Position::default();
|
||||
};
|
||||
let mut metrics = GlyphMetrics {
|
||||
font,
|
||||
size: font_size,
|
||||
};
|
||||
}
|
||||
let y = point.y;
|
||||
|
||||
let Some(line_index) = self
|
||||
@@ -491,13 +479,8 @@ impl FormattedText {
|
||||
let mut glyph_x: f32 = 0.0;
|
||||
let mut min_dist: f32 = x.abs();
|
||||
let mut min_index: usize = 0;
|
||||
let raw_text = self.get_raw_text();
|
||||
for (offset, char_index) in (line.begin..line.end).enumerate() {
|
||||
if let Some(advance) = raw_text.get(char_index).map(|c| metrics.advance(*c)) {
|
||||
glyph_x += advance;
|
||||
} else {
|
||||
glyph_x += font_size;
|
||||
}
|
||||
glyph_x += self.get_char_width(char_index).unwrap_or_default();
|
||||
let dist = (x - glyph_x).abs();
|
||||
if dist < min_dist {
|
||||
min_dist = dist;
|
||||
@@ -619,29 +602,69 @@ impl FormattedText {
|
||||
self.text[range].iter().collect()
|
||||
}
|
||||
|
||||
/// The width of the character at the given index.
|
||||
pub fn get_char_width(&self, index: usize) -> Option<f32> {
|
||||
let glyph = self.text.get(index)?;
|
||||
Some(
|
||||
GlyphMetrics {
|
||||
font: &mut self.font_at(index).data_ref(),
|
||||
size: self.font_size_at(index),
|
||||
}
|
||||
.advance(*glyph),
|
||||
)
|
||||
}
|
||||
|
||||
/// The width of the characters at the indices in the given iterator.
|
||||
/// This is equivalent to calling [`get_char_width`](Self::get_char_width) repeatedly and summing the results.
|
||||
pub fn get_range_width<T: IntoIterator<Item = usize>>(&self, range: T) -> f32 {
|
||||
let mut width = 0.0;
|
||||
if let Some(font) = self.font.state().data() {
|
||||
let mut metrics = GlyphMetrics {
|
||||
font,
|
||||
size: **self.font_size(),
|
||||
};
|
||||
for index in range {
|
||||
// We can't trust the range values, check to prevent panic.
|
||||
if let Some(glyph) = self.text.get(index) {
|
||||
width += metrics.advance(*glyph);
|
||||
}
|
||||
}
|
||||
for index in range {
|
||||
width += self.get_char_width(index).unwrap_or_default();
|
||||
}
|
||||
width
|
||||
}
|
||||
|
||||
/// A rectangle relative to the top-left corner of the text that contains the given
|
||||
/// range of characters on the given line. None is returned if the `line` is out of
|
||||
/// bounds. The `range` is relative to the start of the line, so 0 is the first character
|
||||
/// of the line, not the first character of the text.
|
||||
///
|
||||
/// This rect is appropriate for drawing a selection or highlight for the text,
|
||||
/// and the lower edge of the rectangle can be used to draw an underline.
|
||||
pub fn text_rect<R: RangeBounds<usize>>(&self, line: usize, range: R) -> Option<Rect<f32>> {
|
||||
let line = self.lines.get(line)?;
|
||||
let x = line.x_offset;
|
||||
let y = line.y_offset;
|
||||
let h = line.height;
|
||||
use std::ops::Bound;
|
||||
let start = match range.start_bound() {
|
||||
Bound::Included(&n) => n,
|
||||
Bound::Excluded(&n) => n + 1,
|
||||
Bound::Unbounded => 0,
|
||||
};
|
||||
let end = match range.end_bound() {
|
||||
Bound::Included(&n) => n + 1,
|
||||
Bound::Excluded(&n) => n,
|
||||
Bound::Unbounded => line.len(),
|
||||
};
|
||||
let start = line.begin + start;
|
||||
let end = line.begin + end;
|
||||
let offset = self.get_range_width(line.begin..start);
|
||||
let w = self.get_range_width(start..end);
|
||||
Some(Rect::new(offset + x, y, w, h))
|
||||
}
|
||||
|
||||
pub fn set_text<P: AsRef<str>>(&mut self, text: P) -> &mut Self {
|
||||
self.text
|
||||
.set_value_and_mark_modified(text.as_ref().chars().collect());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_chars(&mut self, text: Vec<char>) -> &mut Self {
|
||||
self.text.set_value_and_mark_modified(text);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_wrap(&mut self, wrap: WrapMode) -> &mut Self {
|
||||
self.wrap.set_value_and_mark_modified(wrap);
|
||||
self
|
||||
@@ -672,6 +695,58 @@ impl FormattedText {
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs can optionally modify various style settings for portions of the text.
|
||||
/// Later runs override earlier runs if their ranges overlap and the later run
|
||||
/// sets a property that conflicts with an earlier run.
|
||||
pub fn runs(&self) -> &RunSet {
|
||||
&self.runs
|
||||
}
|
||||
|
||||
/// Modify runs of the text to set the style for portions of the text.
|
||||
/// Later runs potentially override earlier runs if the ranges of the runs overlap and the later run
|
||||
/// sets a property that conflicts with an earlier run.
|
||||
pub fn runs_mut(&mut self) -> &mut RunSet {
|
||||
&mut self.runs
|
||||
}
|
||||
|
||||
/// Replace runs of the text to set the style for portions of the text.
|
||||
/// Later runs potentially override earlier runs if the ranges of the runs overlap and the later run
|
||||
/// sets a property that conflicts with an earlier run.
|
||||
pub fn set_runs(&mut self, runs: RunSet) -> &mut Self {
|
||||
self.runs = runs;
|
||||
self
|
||||
}
|
||||
|
||||
/// The amount of indent of the first line, horizontally separating it
|
||||
/// from the start of the remaining lines.
|
||||
/// If the indent is negative, then the first line will not be indented
|
||||
/// while all the other lines will be indented. By default this is 0.0.
|
||||
pub fn set_line_indent(&mut self, indent: f32) -> &mut Self {
|
||||
self.line_indent.set_value_and_mark_modified(indent);
|
||||
self
|
||||
}
|
||||
|
||||
/// The amount of indent of the first line, horizontally separating it
|
||||
/// from the start of the remaining lines.
|
||||
/// If the indent is negative, then the first line will not be indented
|
||||
/// while all the other lines will be indented. By default this is 0.0.
|
||||
pub fn line_indent(&mut self) -> f32 {
|
||||
*self.line_indent
|
||||
}
|
||||
|
||||
/// The space separating each line from the line above and below.
|
||||
/// By default this is 0.0.
|
||||
pub fn set_line_space(&mut self, space: f32) -> &mut Self {
|
||||
self.line_space.set_value_and_mark_modified(space);
|
||||
self
|
||||
}
|
||||
|
||||
/// The space separating each line from the line above and below.
|
||||
/// By default this is 0.0.
|
||||
pub fn line_space(&self) -> f32 {
|
||||
*self.line_space
|
||||
}
|
||||
|
||||
pub fn wrap_mode(&self) -> WrapMode {
|
||||
*self.wrap
|
||||
}
|
||||
@@ -702,9 +777,12 @@ impl FormattedText {
|
||||
pub fn build(&mut self) -> Vector2<f32> {
|
||||
let mut lines = std::mem::take(&mut self.lines);
|
||||
lines.clear();
|
||||
let first_indent = self.line_indent.max(0.0);
|
||||
let normal_indent = -self.line_indent.min(0.0);
|
||||
let sink = WrapSink {
|
||||
lines: &mut lines,
|
||||
max_width: self.constraint.x,
|
||||
normal_width: self.constraint.x - normal_indent,
|
||||
first_width: self.constraint.x - first_indent,
|
||||
};
|
||||
if let Some(mask) = *self.mask_char {
|
||||
let advance = GlyphMetrics {
|
||||
@@ -740,24 +818,25 @@ impl FormattedText {
|
||||
|
||||
let mut total_height = 0.0;
|
||||
// Align lines according to desired alignment.
|
||||
for line in lines.iter_mut() {
|
||||
for (i, line) in lines.iter_mut().enumerate() {
|
||||
let indent = if i == 0 { first_indent } else { normal_indent };
|
||||
match *self.horizontal_alignment {
|
||||
HorizontalAlignment::Left => line.x_offset = 0.0,
|
||||
HorizontalAlignment::Left => line.x_offset = indent,
|
||||
HorizontalAlignment::Center => {
|
||||
if self.constraint.x.is_infinite() {
|
||||
line.x_offset = 0.0;
|
||||
line.x_offset = indent;
|
||||
} else {
|
||||
line.x_offset = 0.5 * (self.constraint.x - line.width).max(0.0);
|
||||
}
|
||||
}
|
||||
HorizontalAlignment::Right => {
|
||||
if self.constraint.x.is_infinite() {
|
||||
line.x_offset = 0.0;
|
||||
line.x_offset = indent;
|
||||
} else {
|
||||
line.x_offset = (self.constraint.x - line.width).max(0.0)
|
||||
line.x_offset = (self.constraint.x - line.width - indent).max(0.0)
|
||||
}
|
||||
}
|
||||
HorizontalAlignment::Stretch => line.x_offset = 0.0,
|
||||
HorizontalAlignment::Stretch => line.x_offset = indent,
|
||||
}
|
||||
}
|
||||
// Calculate line height
|
||||
@@ -778,8 +857,9 @@ impl FormattedText {
|
||||
line.height = line.height.max(h);
|
||||
}
|
||||
}
|
||||
total_height += line.height;
|
||||
total_height += line.height + self.line_space();
|
||||
}
|
||||
total_height -= self.line_space();
|
||||
|
||||
// Generate glyphs for each text line.
|
||||
self.glyphs.clear();
|
||||
@@ -844,7 +924,7 @@ impl FormattedText {
|
||||
}
|
||||
}
|
||||
line.y_offset = y;
|
||||
y += line.height;
|
||||
y += line.height + self.line_space();
|
||||
}
|
||||
|
||||
let size_x = if self.constraint.x.is_finite() {
|
||||
@@ -909,7 +989,7 @@ pub struct FormattedTextBuilder {
|
||||
font: FontResource,
|
||||
brush: Brush,
|
||||
constraint: Vector2<f32>,
|
||||
text: String,
|
||||
text: Vec<char>,
|
||||
vertical_alignment: VerticalAlignment,
|
||||
horizontal_alignment: HorizontalAlignment,
|
||||
wrap: WrapMode,
|
||||
@@ -921,6 +1001,10 @@ pub struct FormattedTextBuilder {
|
||||
font_size: StyledProperty<f32>,
|
||||
super_sampling_scaling: f32,
|
||||
runs: Vec<Run>,
|
||||
/// The amount of indentation on the first line of the text.
|
||||
line_indent: f32,
|
||||
/// The space between lines.
|
||||
line_space: f32,
|
||||
}
|
||||
|
||||
impl FormattedTextBuilder {
|
||||
@@ -928,7 +1012,7 @@ impl FormattedTextBuilder {
|
||||
pub fn new(font: FontResource) -> FormattedTextBuilder {
|
||||
FormattedTextBuilder {
|
||||
font,
|
||||
text: "".to_owned(),
|
||||
text: Vec::default(),
|
||||
horizontal_alignment: HorizontalAlignment::Left,
|
||||
vertical_alignment: VerticalAlignment::Top,
|
||||
brush: Brush::Solid(Color::WHITE),
|
||||
@@ -942,6 +1026,8 @@ impl FormattedTextBuilder {
|
||||
font_size: 14.0f32.into(),
|
||||
super_sampling_scaling: 1.0,
|
||||
runs: Vec::default(),
|
||||
line_indent: 0.0,
|
||||
line_space: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -961,6 +1047,11 @@ impl FormattedTextBuilder {
|
||||
}
|
||||
|
||||
pub fn with_text(mut self, text: String) -> Self {
|
||||
self.text = text.chars().collect();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_chars(mut self, text: Vec<char>) -> Self {
|
||||
self.text = text;
|
||||
self
|
||||
}
|
||||
@@ -1032,9 +1123,25 @@ impl FormattedTextBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// The amount of indent of the first line, horizontally separating it
|
||||
/// from the start of the remaining lines.
|
||||
/// If the indent is negative, then the first line will not be indented
|
||||
/// while all the other lines will be indented. By default this is 0.0.
|
||||
pub fn with_line_indent(mut self, indent: f32) -> Self {
|
||||
self.line_indent = indent;
|
||||
self
|
||||
}
|
||||
|
||||
/// The space separating each line from the line above and below.
|
||||
/// By default this is 0.0.
|
||||
pub fn with_line_space(mut self, space: f32) -> Self {
|
||||
self.line_space = space;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> FormattedText {
|
||||
FormattedText {
|
||||
text: self.text.chars().collect::<Vec<char>>().into(),
|
||||
text: self.text.into(),
|
||||
lines: Vec::new(),
|
||||
glyphs: Vec::new(),
|
||||
vertical_alignment: self.vertical_alignment.into(),
|
||||
@@ -1051,6 +1158,8 @@ impl FormattedTextBuilder {
|
||||
shadow_dilation: self.shadow_dilation.into(),
|
||||
shadow_offset: self.shadow_offset.into(),
|
||||
runs: self.runs.into(),
|
||||
line_indent: self.line_indent.into(),
|
||||
line_space: self.line_space.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ use crate::font::FontHeight;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[deprecated]
|
||||
pub type RunBuilder = Run;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Default, Reflect)]
|
||||
pub struct RunSet(Vec<Run>);
|
||||
|
||||
@@ -133,6 +136,18 @@ pub struct Run {
|
||||
}
|
||||
|
||||
impl Run {
|
||||
pub fn new(range: Range<u32>) -> Self {
|
||||
Self {
|
||||
range,
|
||||
font: None,
|
||||
brush: None,
|
||||
font_size: None,
|
||||
shadow: None,
|
||||
shadow_brush: None,
|
||||
shadow_dilation: None,
|
||||
shadow_offset: None,
|
||||
}
|
||||
}
|
||||
/// The font of the characters in this run, or None if the font is unmodified.
|
||||
pub fn font(&self) -> Option<&FontResource> {
|
||||
self.font.as_ref()
|
||||
@@ -162,58 +177,9 @@ impl Run {
|
||||
pub fn shadow_offset(&self) -> Option<Vector2<f32>> {
|
||||
self.shadow_offset
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
pub enum DrawValueLayer {
|
||||
Main,
|
||||
Shadow,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct GlyphDrawValues {
|
||||
pub atlas_page_index: usize,
|
||||
pub font: FontResource,
|
||||
pub brush: Brush,
|
||||
/// Font size scaled by super sampling scaling to pick correct atlas page.
|
||||
pub height: FontHeight,
|
||||
}
|
||||
|
||||
pub struct RunBuilder {
|
||||
range: Range<u32>,
|
||||
font: Option<FontResource>,
|
||||
brush: Option<Brush>,
|
||||
font_size: Option<f32>,
|
||||
shadow: Option<bool>,
|
||||
shadow_brush: Option<Brush>,
|
||||
shadow_dilation: Option<f32>,
|
||||
shadow_offset: Option<Vector2<f32>>,
|
||||
}
|
||||
|
||||
impl RunBuilder {
|
||||
pub fn new(range: Range<u32>) -> Self {
|
||||
Self {
|
||||
range,
|
||||
font: None,
|
||||
brush: None,
|
||||
font_size: None,
|
||||
shadow: None,
|
||||
shadow_brush: None,
|
||||
shadow_dilation: None,
|
||||
shadow_offset: None,
|
||||
}
|
||||
}
|
||||
pub fn build(self) -> Run {
|
||||
Run {
|
||||
range: self.range,
|
||||
font: self.font,
|
||||
brush: self.brush,
|
||||
font_size: self.font_size,
|
||||
shadow: self.shadow,
|
||||
shadow_brush: self.shadow_brush,
|
||||
shadow_dilation: self.shadow_dilation,
|
||||
shadow_offset: self.shadow_offset,
|
||||
}
|
||||
#[deprecated]
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
/// Set this run to modify the font of the text within the range.
|
||||
pub fn with_font(mut self, font: FontResource) -> Self {
|
||||
@@ -251,3 +217,18 @@ impl RunBuilder {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
pub enum DrawValueLayer {
|
||||
Main,
|
||||
Shadow,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct GlyphDrawValues {
|
||||
pub atlas_page_index: usize,
|
||||
pub font: FontResource,
|
||||
pub brush: Brush,
|
||||
/// Font size scaled by super sampling scaling to pick correct atlas page.
|
||||
pub height: FontHeight,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user