This commit is contained in:
b-guild
2025-10-28 16:59:41 -07:00
parent 3950e65b23
commit 9d0547e1bc
19 changed files with 1212 additions and 63 deletions
+23 -15
View File
@@ -99,7 +99,6 @@ use crate::{
},
dropdown_list::DropdownListBuilder,
file_browser::{FileBrowserMode, FileSelectorBuilder, Filter},
font::Font,
formatted_text::WrapMode,
grid::{Column, GridBuilder, Row},
key::HotKey,
@@ -169,7 +168,7 @@ use crate::{
settings::{general::EditorStyle, Settings},
stats::{StatisticsWindow, StatisticsWindowAction},
ui_scene::{
commands::graph::PasteWidgetCommand, menu::WidgetContextMenu,
bbcode::BBCodePanel, commands::graph::PasteWidgetCommand, menu::WidgetContextMenu,
utils::UiSceneWorldViewerDataProvider, UiScene,
},
utils::doc::DocWindow,
@@ -634,6 +633,7 @@ pub struct Editor {
pub mode: Mode,
pub build_window: Option<BuildWindow>,
pub scene_settings: SceneSettingsWindow,
pub bbcode_panel: BBCodePanel,
pub particle_system_control_panel: ParticleSystemPreviewControlPanel,
pub camera_control_panel: CameraPreviewControlPanel,
pub mesh_control_panel: MeshControlPanel,
@@ -807,19 +807,6 @@ impl Editor {
let (message_sender, message_receiver) = mpsc::channel();
let message_sender = MessageSender(message_sender);
{
let mut font_state = engine.user_interfaces.first_mut().default_font.state();
let font_state_data = font_state.data().unwrap();
*font_state_data = Font::from_memory(
include_bytes!("../resources/Roboto-Regular.ttf").as_slice(),
1024,
None,
None,
Vec::default(),
)
.unwrap();
}
let ui = engine.user_interfaces.first_mut();
if let Some(style) = styles.get(&settings.general.style) {
ui.set_style(style.clone());
@@ -857,6 +844,7 @@ impl Editor {
);
let inspector_plugin =
InspectorPlugin::new(ctx, message_sender.clone(), engine.resource_manager.clone());
let bbcode_panel = BBCodePanel::new(inspector_plugin.head, ctx);
let particle_system_control_panel =
ParticleSystemPreviewControlPanel::new(inspector_plugin.head, ctx);
let camera_control_panel = CameraPreviewControlPanel::new(scene_viewer.frame(), ctx);
@@ -1070,6 +1058,7 @@ impl Editor {
},
build_window: None,
scene_settings,
bbcode_panel,
particle_system_control_panel,
camera_control_panel,
mesh_control_panel,
@@ -1568,6 +1557,13 @@ impl Editor {
.handle_ui_message(message, game_scene, engine);
} else if let Some(ui_scene) = current_scene_entry.controller.downcast_mut::<UiScene>()
{
self.bbcode_panel.handle_ui_message(
message,
&current_scene_entry.selection,
ui_scene,
engine,
&self.message_sender,
);
self.world_viewer.handle_ui_message(
message,
&mut UiSceneWorldViewerDataProvider {
@@ -1842,6 +1838,11 @@ impl Editor {
);
} else if let Some(ui_scene) = current_scene_entry.controller.downcast_mut::<UiScene>()
{
self.bbcode_panel.sync_to_model(
&current_scene_entry.selection,
ui_scene,
engine.user_interfaces.first_mut(),
);
self.world_viewer.sync_to_model(
&UiSceneWorldViewerDataProvider {
ui: &mut ui_scene.ui,
@@ -2571,6 +2572,13 @@ impl Editor {
game_scene,
&mut self.engine,
);
} else if let Some(ui_scene) = entry.controller.downcast_mut::<UiScene>() {
self.bbcode_panel.handle_message(
&message,
&entry.selection,
ui_scene,
&mut self.engine,
);
}
needs_sync |=
entry
@@ -293,6 +293,7 @@ pub fn make_property_editors_container(
container.insert(TexturePropertyEditorDefinition { untyped: true });
container.insert(FontPropertyEditorDefinition { resource_manager });
container.insert(InheritablePropertyEditorDefinition::<FontResource>::new());
container.insert(InheritablePropertyEditorDefinition::<Option<FontResource>>::new());
container.insert(InheritablePropertyEditorDefinition::<Option<TextureResource>>::new());
container.insert(InheritablePropertyEditorDefinition::<Option<UntypedResource>>::new());
container.register_inheritable_vec_collection::<Option<FontResource>>();
+184
View File
@@ -0,0 +1,184 @@
use fyrox::{
core::{pool::Handle, variable::InheritableVariable},
engine::Engine,
gui::{
formatted_text::RunSet,
message::{MessageDirection, UiMessage},
stack_panel::StackPanelBuilder,
text::{Text, TextBuilder, TextMessage},
text_box::{TextBoxBuilder, TextCommitMode},
widget::{WidgetBuilder, WidgetMessage},
BBCode, BuildContext, UiNode, UserInterface,
},
};
use crate::{
command::{CommandContext, CommandTrait},
message::MessageSender,
scene::Selection,
send_sync_message,
ui_scene::{commands::UiSceneContext, UiScene},
Message,
};
pub struct BBCodePanel {
pub root_widget: Handle<UiNode>,
pub text_box: Handle<UiNode>,
}
impl BBCodePanel {
pub fn new(inspector_head: Handle<UiNode>, ctx: &mut BuildContext) -> Self {
let text_box = TextBoxBuilder::new(WidgetBuilder::new())
.with_text_commit_mode(TextCommitMode::Changed)
.build(ctx);
let root_widget = StackPanelBuilder::new(
WidgetBuilder::new()
.with_visibility(false)
.with_child(
TextBuilder::new(WidgetBuilder::new())
.with_text("BBCode")
.build(ctx),
)
.with_child(text_box),
)
.build(ctx);
ctx.send_message(WidgetMessage::link(
root_widget,
MessageDirection::ToWidget,
inspector_head,
));
Self {
root_widget,
text_box,
}
}
pub fn sync_to_model(
&mut self,
selection: &Selection,
ui_scene: &mut UiScene,
ui: &UserInterface,
) {
let Some(selection) = selection.as_ui() else {
return;
};
let mut bbcode = String::new();
for handle in &selection.widgets {
if let Some(text) = ui_scene
.ui
.try_get_node_mut(*handle)
.and_then(|n| n.cast::<Text>())
{
bbcode = text.bbcode.clone_inner();
break;
}
}
send_sync_message(
ui,
TextMessage::text(self.text_box, MessageDirection::ToWidget, bbcode),
);
}
pub fn handle_message(
&mut self,
message: &Message,
editor_selection: &Selection,
ui_scene: &mut UiScene,
engine: &mut Engine,
) {
if let Message::SelectionChanged { .. } = message {
let text_selected = editor_selection.as_ui().is_some_and(|s| {
s.widgets.iter().any(|n| {
ui_scene
.ui
.try_get_node_mut(*n)
.map(|n| n.cast::<Text>().is_some())
.unwrap_or_default()
})
});
engine
.user_interfaces
.first_mut()
.send_message(WidgetMessage::visibility(
self.root_widget,
MessageDirection::ToWidget,
text_selected,
));
}
}
pub fn handle_ui_message(
&mut self,
message: &UiMessage,
editor_selection: &Selection,
ui_scene: &mut UiScene,
_engine: &mut Engine,
_sender: &MessageSender,
) {
if message.destination() != self.text_box {
return;
}
if message.direction() != MessageDirection::FromWidget {
return;
}
let Some(TextMessage::Text(bbcode)) = message.data::<TextMessage>() else {
return;
};
let Some(selection) = editor_selection.as_ui() else {
return;
};
for handle in selection.widgets.iter().copied() {
if let Some(text) = ui_scene
.ui
.try_get_node_mut(handle)
.and_then(|n| n.cast::<Text>())
{
let font = text.font();
let parsed_code: BBCode = bbcode.parse().unwrap();
let text = parsed_code.text.chars().collect::<Vec<char>>();
ui_scene.message_sender.do_command(BBCodeCommand {
text_handle: handle,
code: InheritableVariable::new_modified(bbcode.clone()),
text: InheritableVariable::new_modified(text),
runs: parsed_code.build_runs(&font),
});
}
}
}
}
#[derive(Debug)]
struct BBCodeCommand {
text_handle: Handle<UiNode>,
code: InheritableVariable<String>,
text: InheritableVariable<Vec<char>>,
runs: RunSet,
}
impl BBCodeCommand {
fn swap(&mut self, context: &mut dyn CommandContext) {
let ctx = context.get_mut::<UiSceneContext>();
let text = ctx
.ui
.try_get_node_mut(self.text_handle)
.and_then(|n| n.cast_mut::<Text>())
.expect("must be Text");
std::mem::swap(&mut text.bbcode, &mut self.code);
let formatted_text = text.formatted_text.get_mut();
std::mem::swap(&mut formatted_text.text, &mut self.text);
std::mem::swap(&mut formatted_text.runs, &mut self.runs);
text.invalidate_layout();
}
}
impl CommandTrait for BBCodeCommand {
fn name(&mut self, _context: &dyn CommandContext) -> String {
"Modify BBCode".into()
}
fn execute(&mut self, context: &mut dyn CommandContext) {
self.swap(context);
}
fn revert(&mut self, context: &mut dyn CommandContext) {
self.swap(context);
}
}
+1
View File
@@ -18,6 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
pub mod bbcode;
pub mod clipboard;
pub mod commands;
pub mod interaction;
+279 -1
View File
@@ -26,7 +26,11 @@ use crate::{
};
use bytemuck::{Pod, Zeroable};
use num_traits::Zero;
use std::ops::{Add, AddAssign, Sub, SubAssign};
use std::{
fmt::Display,
ops::{Add, AddAssign, Range, Sub, SubAssign},
str::FromStr,
};
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Eq, Visit, Reflect, Pod, Zeroable)]
#[repr(C)]
@@ -310,6 +314,59 @@ impl From<Color> for Hsl {
}
}
/// An error indicating that a string could not be parsed into a color.
#[derive(Debug, Clone)]
pub struct ColorParseError {
/// The string that was given to be parsed, but could not be parsed.
pub input: String,
}
impl ColorParseError {
pub fn new(input: String) -> Self {
Self { input }
}
}
impl std::error::Error for ColorParseError {}
impl Display for ColorParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Could not parse color: {:?}", self.input)
}
}
impl FromStr for Color {
type Err = ColorParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err(ColorParseError::new(s.to_string()));
}
if s.starts_with('#') {
Color::from_hex_str(s)
} else {
let mut bytes = s.as_bytes().to_vec();
for c in &mut bytes {
c.make_ascii_uppercase();
if *c == b' ' {
*c = b'_';
}
}
Color::from_name(std::str::from_utf8(&bytes).unwrap())
.ok_or_else(|| ColorParseError::new(s.to_string()))
}
}
}
macro_rules! name_to_color {
($source:expr =>[$($name:ident,)*])=> {
match $source {
$(stringify!($name) => Some(Color::$name),)*
_ => None
}
}
}
impl Color {
pub const WHITE: Self = Self::repeat(255);
pub const BLACK: Self = Self::opaque(0, 0, 0);
@@ -595,6 +652,177 @@ impl Color {
Self::WHITE_SMOKE,
];
pub fn from_name(name: &str) -> Option<Self> {
name_to_color! {
name => [
TRANSPARENT,
WHITE,
BLACK,
RED,
GREEN,
BLUE,
MAROON,
DARK_RED,
BROWN,
FIREBRICK,
CRIMSON,
TOMATO,
CORAL,
INDIAN_RED,
LIGHT_CORAL,
DARK_SALMON,
SALMON,
LIGHT_SALMON,
ORANGE_RED,
DARK_ORANGE,
ORANGE,
GOLD,
DARK_GOLDEN_ROD,
GOLDEN_ROD,
PALE_GOLDEN_ROD,
DARK_KHAKI,
KHAKI,
OLIVE,
YELLOW,
YELLOW_GREEN,
DARK_OLIVE_GREEN,
OLIVE_DRAB,
LAWN_GREEN,
CHARTREUSE,
GREEN_YELLOW,
DARK_GREEN,
FOREST_GREEN,
LIME,
LIME_GREEN,
LIGHT_GREEN,
PALE_GREEN,
DARK_SEA_GREEN,
MEDIUM_SPRING_GREEN,
SPRING_GREEN,
SEA_GREEN,
MEDIUM_AQUA_MARINE,
MEDIUM_SEA_GREEN,
LIGHT_SEA_GREEN,
DARK_SLATE_GRAY,
TEAL,
DARK_CYAN,
AQUA,
CYAN,
LIGHT_CYAN,
DARK_TURQUOISE,
TURQUOISE,
MEDIUM_TURQUOISE,
PALE_TURQUOISE,
AQUA_MARINE,
POWDER_BLUE,
CADET_BLUE,
STEEL_BLUE,
CORN_FLOWER_BLUE,
DEEP_SKY_BLUE,
DODGER_BLUE,
LIGHT_BLUE,
SKY_BLUE,
LIGHT_SKY_BLUE,
MIDNIGHT_BLUE,
NAVY,
DARK_BLUE,
MEDIUM_BLUE,
ROYAL_BLUE,
BLUE_VIOLET,
INDIGO,
DARK_SLATE_BLUE,
SLATE_BLUE,
MEDIUM_SLATE_BLUE,
MEDIUM_PURPLE,
DARK_MAGENTA,
DARK_VIOLET,
DARK_ORCHID,
MEDIUM_ORCHID,
PURPLE,
THISTLE,
PLUM,
VIOLET,
MAGENTA,
ORCHID,
MEDIUM_VIOLET_RED,
PALE_VIOLET_RED,
DEEP_PINK,
HOT_PINK,
LIGHT_PINK,
PINK,
ANTIQUE_WHITE,
BEIGE,
BISQUE,
BLANCHED_ALMOND,
WHEAT,
CORN_SILK,
LEMON_CHIFFON,
LIGHT_GOLDEN_ROD_YELLOW,
LIGHT_YELLOW,
SADDLE_BROWN,
SIENNA,
CHOCOLATE,
PERU,
SANDY_BROWN,
BURLY_WOOD,
TAN,
ROSY_BROWN,
MOCCASIN,
NAVAJO_WHITE,
PEACH_PUFF,
MISTY_ROSE,
LAVENDER_BLUSH,
LINEN,
OLD_LACE,
PAPAYA_WHIP,
SEA_SHELL,
MINT_CREAM,
SLATE_GRAY,
LIGHT_SLATE_GRAY,
LIGHT_STEEL_BLUE,
LAVENDER,
FLORAL_WHITE,
ALICE_BLUE,
GHOST_WHITE,
HONEYDEW,
IVORY,
AZURE,
SNOW,
DIM_GRAY,
GRAY,
DARK_GRAY,
SILVER,
LIGHT_GRAY,
GAINSBORO,
WHITE_SMOKE,
]
}
}
/// Constructs a color from a hex string of the form "#FF237A", where
/// the first two digits are the amount of red (FF), then green (23), then the final two digits are blue (7A).
/// An alpha value can be included using an optional fourth pair of digits, as in:
/// "#FF237A50", where the alpha is 50. If alpha is not included, then it is assumed to be FF.
pub fn from_hex_str(source: &str) -> Result<Self, ColorParseError> {
fn hex_to_u8(source: &str, range: Range<usize>) -> Result<u8, ColorParseError> {
source
.get(range)
.and_then(|s| u8::from_str_radix(s, 16).ok())
.ok_or_else(|| ColorParseError::new(source.into()))
}
let r = hex_to_u8(source, 1..3)?;
let g = hex_to_u8(source, 3..5)?;
let b = hex_to_u8(source, 5..7)?;
match source.len() {
7 => Ok(Self::opaque(r, g, b)),
9 => {
let a = hex_to_u8(source, 7..9)?;
Ok(Self::from_rgba(r, g, b, a))
}
_ => Err(ColorParseError::new(source.into())),
}
}
#[inline]
pub const fn opaque(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b, a: 255 }
@@ -755,6 +983,56 @@ mod test {
use crate::algebra::{Vector3, Vector4};
use crate::color::{Color, Hsl, Hsv};
#[test]
fn test_parse() {
assert_eq!("red".parse::<Color>().unwrap(), Color::RED);
assert_eq!("green".parse::<Color>().unwrap(), Color::GREEN);
assert_eq!("blue".parse::<Color>().unwrap(), Color::BLUE);
assert_eq!("olive drab".parse::<Color>().unwrap(), Color::OLIVE_DRAB);
assert_eq!(
"#FF0000".parse::<Color>().unwrap(),
Color::opaque(255, 0, 0)
);
assert_eq!(
"#00FF00".parse::<Color>().unwrap(),
Color::opaque(0, 255, 0)
);
assert_eq!(
"#0000FF".parse::<Color>().unwrap(),
Color::opaque(0, 0, 255)
);
assert_eq!(
"#251607".parse::<Color>().unwrap(),
Color::opaque(37, 22, 7)
);
assert_eq!(
"#25160700".parse::<Color>().unwrap(),
Color::from_rgba(37, 22, 7, 0)
);
assert_eq!(
"#251607FF".parse::<Color>().unwrap(),
Color::from_rgba(37, 22, 7, 255)
);
}
#[test]
fn test_parse_fail() {
// Confirm that we return an error instead of panicking when given an empty string.
assert_eq!("".parse::<Color>().err().unwrap().input, "");
assert_eq!("#".parse::<Color>().err().unwrap().input, "#");
// Not the real name of a color
assert_eq!("blah".parse::<Color>().err().unwrap().input, "blah");
// Invalid hex digits
assert_eq!("#XYZF00".parse::<Color>().err().unwrap().input, "#XYZF00");
// Too short
assert_eq!("#FF109".parse::<Color>().err().unwrap().input, "#FF109");
// Too long
assert_eq!(
"#FF10FF001".parse::<Color>().err().unwrap().input,
"#FF10FF001"
);
}
#[test]
fn test_hsl() {
// Hsl -> Rgb
+4
View File
@@ -117,6 +117,7 @@ use fyrox_sound::{
buffer::{loader::SoundBufferLoader, SoundBuffer},
renderer::hrtf::{HrirSphereLoader, HrirSphereResourceData},
};
use fyrox_ui::font::{BOLD_ITALIC, BUILT_IN_BOLD, BUILT_IN_ITALIC};
use fyrox_ui::RenderMode;
use std::{
any::TypeId,
@@ -1312,6 +1313,9 @@ pub(crate) fn initialize_resource_manager_loaders(
}
state.built_in_resources.add(BUILT_IN_FONT.clone());
state.built_in_resources.add(BUILT_IN_BOLD.clone());
state.built_in_resources.add(BUILT_IN_ITALIC.clone());
state.built_in_resources.add(BOLD_ITALIC.clone());
state.built_in_resources.add(texture::PLACEHOLDER.clone());
state.built_in_resources.add(texture::PURE_COLOR.clone());
+432
View File
@@ -0,0 +1,432 @@
use std::{
convert::Infallible,
fmt::{Debug, Display, Write},
ops::Range,
str::FromStr,
};
use crate::{
brush::Brush,
font::FontResource,
formatted_text::{FormattedTextBuilder, Run, RunSet},
};
/// A BBCode parser that is specially designed for the formatting options
/// available to [`FormattedText`](crate::formatted_text::FormattedText).
/// The available tags are:
/// * `[b]` **bold text** `[/b]`
/// * `[i]` *italic text* `[/i]`
/// * `[color=red]` red text `[/color]` (can be shortened to `[c=red]`... `[/c]`, and can use hex color as in `[color=#FF0000]`)
/// * `[size=24]` large text `[/size]` (can be shortened to `[s=24]` ... `[/s]`)
/// * `[shadow]` shadowed text `[/shadow]` (can be shortened to `[sh]` ... `[/sh]` and can change shadow color with `[shadow=blue]`)
/// * `[br]` for a line break.
#[derive(Debug, Clone)]
pub struct BBCode {
/// The plain text without tags.
pub text: String,
/// The tags that were removed from the text.
pub tags: Box<[BBTag]>,
}
#[derive(Clone, Eq, PartialEq)]
pub struct BBTag {
/// The position of the tag in the plain text, with 0 being the beginning of the text.
/// The position is relative to the text *without* tags, so for example, if the code text were:
/// `"Here is [b]bold text[/b]."` then the plain text would be:
/// `"Here is bold text."` and the tags would be at positions 8 and 17.
pub position: usize,
/// The content of the tag.
pub data: BBTagData,
}
impl std::ops::Deref for BBTag {
type Target = BBTagData;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl Debug for BBTag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self, f)
}
}
impl Display for BBTag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}@{}", self.data, self.position)
}
}
/// The content of a BBCode tag.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct BBTagData {
/// The text at the beginning of the tag, not including an initial /, upto and not including an = if present.
/// For example, if the tag were `[size=24]` then the label would be "size".
/// If the tag were `[/size]` then the label would be "size".
pub label: String,
/// The text that follows the = in the tag.
pub argument: Option<String>,
/// True if the tag starts with a / to indicate that it is the end of some span.
pub is_close: bool,
}
impl BBTagData {
pub fn open(label: String, argument: Option<String>) -> Self {
Self {
is_close: false,
label,
argument,
}
}
pub fn close(label: String, argument: Option<String>) -> Self {
Self {
is_close: true,
label,
argument,
}
}
}
impl Display for BBTagData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_char('[')?;
if self.is_close {
f.write_char('/')?;
}
f.write_str(&self.label)?;
if let Some(arg) = &self.argument {
f.write_char('=')?;
f.write_str(arg)?;
}
f.write_char(']')
}
}
impl FromStr for BBTagData {
type Err = Infallible;
fn from_str(source: &str) -> Result<Self, Infallible> {
let mut source = source.as_bytes();
let mut is_close = false;
if let Some((b'/', rest)) = source.split_first() {
is_close = true;
source = rest;
}
if let Some(equals_pos) = source.iter().position(|c| *c == b'=') {
let (label, argument) = source.split_at(equals_pos);
let label = label.trim_ascii();
let argument = argument[1..].trim_ascii();
Ok(Self {
is_close,
label: std::str::from_utf8(label).unwrap().to_string(),
argument: Some(std::str::from_utf8(argument).unwrap().to_string()),
})
} else {
Ok(Self {
is_close,
label: std::str::from_utf8(source.trim_ascii())
.unwrap()
.to_string(),
argument: None,
})
}
}
}
impl FromStr for BBCode {
type Err = Infallible;
fn from_str(source: &str) -> Result<Self, Infallible> {
let mut source = source.as_bytes();
let mut text = Vec::new();
let mut tags = Vec::new();
while !source.is_empty() {
match source[0] {
b'[' => {
source = &source[1..];
if let Some(end_pos) = source.iter().position(|c| *c == b']') {
let content = std::str::from_utf8(&source[0..end_pos]).unwrap();
source = &source[end_pos + 1..];
let data: BBTagData = content.parse()?;
if !data.is_close && data.argument.is_none() && data.label == "br" {
text.push(b'\n');
} else {
tags.push(BBTag {
position: text.len(),
data,
});
}
} else {
source = &[];
}
}
c => {
text.push(c);
source = &source[1..];
}
}
}
Ok(Self {
text: std::str::from_utf8(&text).unwrap().to_string(),
tags: tags.into_boxed_slice(),
})
}
}
fn find_close<'a, I: Iterator<Item = &'a BBTag>>(label: &str, iter: I) -> Option<&'a BBTag> {
let mut nesting_level = 0;
for tag in iter {
if tag.is_close {
if nesting_level == 0 {
return (tag.label == label).then_some(tag);
} else {
nesting_level -= 1;
}
} else {
nesting_level += 1;
}
}
None
}
fn find_font_run(runs: &mut [Run], pos: u32) -> Option<&mut Run> {
runs.iter_mut()
.rev()
.find(|r| r.range.contains(&pos) && r.font().is_some())
}
fn update_font(
runs: &mut RunSet,
range: Range<u32>,
new_font: Option<&FontResource>,
other_font: Option<&FontResource>,
bold_italic: Option<&FontResource>,
) {
if let Some(run) = find_font_run(runs, range.start) {
if other_font == run.font() {
if let Some(bold_italic) = bold_italic {
if range == run.range {
*run = Run::new(range).with_font(bold_italic.clone());
} else {
runs.push(Run::new(range).with_font(bold_italic.clone()));
}
}
}
} else if let Some(new_font) = new_font {
runs.push(Run::new(range).with_font(new_font.clone()));
}
}
fn apply_tag(
runs: &mut RunSet,
label: &str,
argument: Option<&str>,
range: Range<u32>,
font: &FontResource,
) {
match (label, argument) {
("i", None) => {
if font.is_ok() {
let font = font.data_ref();
update_font(
runs,
range,
font.italic.as_ref(),
font.bold.as_ref(),
font.bold_italic.as_ref(),
);
}
}
("b", None) => {
if font.is_ok() {
let font = font.data_ref();
update_font(
runs,
range,
font.bold.as_ref(),
font.italic.as_ref(),
font.bold_italic.as_ref(),
);
}
}
("size" | "s", Some(size)) => {
if let Ok(size) = size.parse() {
runs.push(Run::new(range).with_size(size));
}
}
("color" | "c", Some(color)) => {
if let Ok(color) = color.parse() {
runs.push(Run::new(range).with_brush(Brush::Solid(color)));
}
}
("shadow" | "sh", color) => {
let mut run = Run::new(range).with_shadow(true);
if let Some(color) = color.and_then(|c| c.parse().ok()) {
run = run.with_shadow_brush(Brush::Solid(color));
}
runs.push(run);
}
_ => (),
}
}
impl BBCode {
pub fn build_formatted_text(self, font: FontResource) -> FormattedTextBuilder {
let runs = self.build_runs(&font);
FormattedTextBuilder::new(font)
.with_text(self.text)
.with_runs(runs)
}
pub fn build_runs(&self, font: &FontResource) -> RunSet {
let mut runs = RunSet::default();
let mut iter = self.tags.iter();
while let Some(tag) = iter.next() {
if tag.is_close {
continue;
}
if let Some(close) = find_close(&tag.label, iter.clone()) {
let start_pos = self.text[0..tag.position].chars().count() as u32;
let end_pos = self.text[0..close.position].chars().count() as u32;
apply_tag(
&mut runs,
&tag.label,
tag.argument.as_deref(),
start_pos..end_pos,
font,
);
}
}
runs
}
}
#[cfg(test)]
mod test {
use fyrox_core::color::Color;
use fyrox_resource::untyped::ResourceKind;
use uuid::Uuid;
use crate::font::{Font, BUILT_IN_FONT};
use super::*;
#[test]
fn test_built_in_font() {
let font = BUILT_IN_FONT.resource();
assert!(font.data_ref().bold.is_some());
assert!(font.data_ref().italic.is_some());
assert!(font.data_ref().bold_italic.is_some());
}
#[test]
fn test_example() {
let code: BBCode = "Here is [b]bold text[/b].".parse().unwrap();
assert_eq!(&code.text, "Here is bold text.");
assert_eq!(
*code.tags,
*&[
BBTag {
position: 8,
data: BBTagData::open("b".into(), None)
},
BBTag {
position: 17,
data: BBTagData::close("b".into(), None)
}
]
);
}
#[test]
fn test_example2() {
let code: BBCode = "Here is [size = 24]big text[/ size= x ].".parse().unwrap();
assert_eq!(&code.text, "Here is big text.");
assert_eq!(
*code.tags,
*&[
BBTag {
position: 8,
data: BBTagData::open("size".into(), Some("24".into()))
},
BBTag {
position: 16,
data: BBTagData::close("size".into(), Some("x".into()))
}
]
);
}
#[test]
fn test_formatted() {
let bold = FontResource::new_ok(Uuid::new_v4(), ResourceKind::Embedded, Font::default());
let italic = FontResource::new_ok(Uuid::new_v4(), ResourceKind::Embedded, Font::default());
let bold_italic =
FontResource::new_ok(Uuid::new_v4(), ResourceKind::Embedded, Font::default());
let font = FontResource::new_ok(
Uuid::new_v4(),
ResourceKind::Embedded,
Font {
bold: Some(bold.clone()),
italic: Some(italic.clone()),
bold_italic: Some(bold_italic.clone()),
..Font::default()
},
);
let code: BBCode = "Here is [size=24]big text[/size].".parse().unwrap();
let text = code.build_formatted_text(font.clone()).build();
assert_eq!(**text.runs(), *&[Run::new(8..16).with_size(24.0)]);
let code: BBCode = "Here is [shadow]big text[/shadow].".parse().unwrap();
let text = code.build_formatted_text(font.clone()).build();
assert_eq!(**text.runs(), *&[Run::new(8..16).with_shadow(true)]);
let code: BBCode = "Here is [sh][s=24]big text[/s][/sh].".parse().unwrap();
let text = code.build_formatted_text(font.clone()).build();
assert_eq!(
**text.runs(),
*&[Run::new(8..16).with_shadow(true).with_size(24.0)]
);
let code: BBCode = "Here is [color=green]big text[/color].".parse().unwrap();
let text = code.build_formatted_text(font.clone()).build();
assert_eq!(
**text.runs(),
*&[Run::new(8..16).with_brush(Brush::Solid(Color::GREEN))]
);
let code: BBCode = "Here is [c=#010203]big text[/c].".parse().unwrap();
let text = code.build_formatted_text(font.clone()).build();
assert_eq!(
**text.runs(),
*&[Run::new(8..16).with_brush(Brush::Solid(Color::opaque(1, 2, 3)))]
);
let code: BBCode = "Here is [i]big text[/i].".parse().unwrap();
let text = code.build_formatted_text(font.clone()).build();
assert_eq!(**text.runs(), *&[Run::new(8..16).with_font(italic.clone())]);
let code: BBCode = "Here is [b]big text[/b].".parse().unwrap();
let text = code.build_formatted_text(font.clone()).build();
assert_eq!(**text.runs(), *&[Run::new(8..16).with_font(bold.clone())]);
let code: BBCode = "Here is [b][i]big text[/i][/b].".parse().unwrap();
let text = code.build_formatted_text(font.clone()).build();
assert_eq!(
**text.runs(),
*&[Run::new(8..16).with_font(bold_italic.clone())]
);
let code: BBCode = "Here is [i]big [b]text[/b]!!![/i]".parse().unwrap();
let text = code.build_formatted_text(font.clone()).build();
assert_eq!(
**text.runs(),
*&[
Run::new(8..19).with_font(italic.clone()),
Run::new(12..16).with_font(bold_italic.clone())
]
);
}
#[test]
fn test_nesting() {
let font = FontResource::new_ok(Uuid::new_v4(), ResourceKind::Embedded, Font::default());
let code: BBCode = "Here is [s=24]big [s=3]small[/s] text[/s]."
.parse()
.unwrap();
let text = code.build_formatted_text(font.clone()).build();
assert_eq!(
**text.runs(),
*&[
Run::new(8..22).with_size(24.0),
Run::new(12..17).with_size(3.0)
]
);
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
View File
@@ -53,6 +53,8 @@ pub struct FontImportOptions {
pub bold: Option<FontResource>,
/// The italic version of this font.
pub italic: Option<FontResource>,
/// The bold italic version of this font.
pub bold_italic: Option<FontResource>,
/// Fallback fonts are used for rendering special characters that do not have glyphs in this
/// font.
pub fallbacks: Vec<Option<FontResource>>,
@@ -64,6 +66,7 @@ impl Default for FontImportOptions {
page_size: default_page_size(),
bold: None,
italic: None,
bold_italic: None,
fallbacks: Vec::default(),
}
}
+89 -8
View File
@@ -352,6 +352,9 @@ pub struct Font {
/// A font representing the italic version of this font.
#[visit(skip)]
pub italic: Option<FontResource>,
/// A font representing the bold italic version of this font.
#[visit(skip)]
pub bold_italic: Option<FontResource>,
/// Fallback fonts are used for rendering special characters that do not have glyphs in this
/// font.
#[visit(skip)]
@@ -410,15 +413,60 @@ impl Hash for FontHeight {
pub type FontResource = Resource<Font>;
lazy_static! {
/// Fyrox's default build-in font for rendering bold italic text when no other font is specified.
pub static ref BOLD_ITALIC: BuiltInResource<Font> = BuiltInResource::new(
"__BOLD_ITALIC__",
embedded_data_source!("./bold_italic.ttf"),
|data| {
FontResource::new_ok(
uuid!("f5b02124-9601-452a-9368-3fa2a9703ecd"),
ResourceKind::External,
Font::from_memory(data.to_vec(), 1024, FontStyles::default(), Vec::default()).unwrap(),
)
}
);
/// Fyrox's default build-in font for rendering italic text when no other font is specified.
pub static ref BUILT_IN_ITALIC: BuiltInResource<Font> = BuiltInResource::new(
"__BUILT_IN_ITALIC__",
embedded_data_source!("./built_in_italic.ttf"),
|data| {
let bold = Some(BOLD_ITALIC.resource());
let styles = FontStyles{bold, ..FontStyles::default()};
FontResource::new_ok(
uuid!("1cd79487-6c76-4370-91c2-e6e1e728950a"),
ResourceKind::External,
Font::from_memory(data.to_vec(), 1024, styles, Vec::default()).unwrap(),
)
}
);
/// Fyrox's default build-in font for rendering bold text when no other font is specified.
pub static ref BUILT_IN_BOLD: BuiltInResource<Font> = BuiltInResource::new(
"__BUILT_IN_BOLD__",
embedded_data_source!("./built_in_bold.ttf"),
|data| {
let italic = Some(BOLD_ITALIC.resource());
let styles = FontStyles{italic, ..FontStyles::default()};
FontResource::new_ok(
uuid!("8a471243-2466-4241-a4cb-c341ce8e844a"),
ResourceKind::External,
Font::from_memory(data.to_vec(), 1024, styles, Vec::default()).unwrap(),
)
}
);
/// Fyrox's default build-in font for rendering text when no other font is specified.
pub static ref BUILT_IN_FONT: BuiltInResource<Font> = BuiltInResource::new(
"__BUILT_IN_FONT__",
embedded_data_source!("./built_in_font.ttf"),
|data| {
let styles = FontStyles{
bold: Some(BUILT_IN_BOLD.resource()),
italic: Some(BUILT_IN_ITALIC.resource()),
bold_italic: Some(BOLD_ITALIC.resource()),
};
FontResource::new_ok(
uuid!("77260e8e-f6fa-429c-8009-13dda2673925"),
ResourceKind::External,
Font::from_memory(data.to_vec(), 1024, None, None, Vec::default()).unwrap(),
Font::from_memory(data.to_vec(), 1024, styles, Vec::default()).unwrap(),
)
}
);
@@ -441,6 +489,11 @@ pub async fn wait_for_subfonts(font: FontResource) -> Result<FontResource, LoadE
wait_for_fallbacks(italic, &mut stack).await?;
stack.clear();
}
let bold_italic = font.data_ref().bold_italic.clone();
if let Some(bold_italic) = bold_italic {
wait_for_fallbacks(bold_italic, &mut stack).await?;
stack.clear();
}
wait_for_fallbacks(font, &mut stack).await
}
@@ -491,6 +544,13 @@ async fn wait_for_fallbacks(
Ok(font)
}
#[derive(Default, Debug, Clone)]
pub struct FontStyles {
pub bold: Option<FontResource>,
pub italic: Option<FontResource>,
pub bold_italic: Option<FontResource>,
}
impl Font {
/// The name of the font, if available.
pub fn name(&self) -> Option<&str> {
@@ -501,8 +561,7 @@ impl Font {
pub fn from_memory(
data: impl Deref<Target = [u8]>,
page_size: usize,
bold: Option<FontResource>,
italic: Option<FontResource>,
styles: FontStyles,
fallbacks: Vec<Option<FontResource>>,
) -> Result<Self, &'static str> {
let fontdue_font = fontdue::Font::from_bytes(data, fontdue::FontSettings::default())?;
@@ -510,8 +569,9 @@ impl Font {
inner: Some(fontdue_font),
atlases: Default::default(),
page_size,
bold,
italic,
bold: styles.bold,
italic: styles.italic,
bold_italic: styles.bold_italic,
fallbacks,
})
}
@@ -527,18 +587,26 @@ impl Font {
let page_size = options.page_size;
let mut bold = options.bold;
let mut italic = options.italic;
let mut bold_italic = options.bold_italic;
if let Some(bold) = &mut bold {
resource_manager.request_resource(bold);
}
if let Some(italic) = &mut italic {
resource_manager.request_resource(italic);
}
if let Some(bold_italic) = &mut bold_italic {
resource_manager.request_resource(bold_italic);
}
let mut fallbacks = options.fallbacks;
for font in fallbacks.iter_mut().flatten() {
resource_manager.request_resource(font);
}
Self::from_memory(file_content, page_size, bold, italic, fallbacks)
.map_err(LoadError::new)
let styles = FontStyles {
bold,
italic,
bold_italic,
};
Self::from_memory(file_content, page_size, styles, fallbacks).map_err(LoadError::new)
} else {
Err(LoadError::new("Unable to read file"))
}
@@ -625,6 +693,7 @@ pub struct FontBuilder {
page_size: usize,
bold: Option<FontResource>,
italic: Option<FontResource>,
bold_italic: Option<FontResource>,
fallbacks: Vec<Option<FontResource>>,
}
@@ -635,6 +704,7 @@ impl FontBuilder {
page_size: 1024,
bold: None,
italic: None,
bold_italic: None,
fallbacks: Vec::default(),
}
}
@@ -657,6 +727,11 @@ impl FontBuilder {
self
}
pub fn with_bold_italic(mut self, font: FontResource) -> Self {
self.bold_italic = Some(font);
self
}
/// A fallback font to supply glyphs for special characters that are not represented in
/// this font.
pub fn with_fallback(mut self, font: FontResource) -> Self {
@@ -677,6 +752,7 @@ impl FontBuilder {
page_size: self.page_size,
bold: self.bold,
italic: self.italic,
bold_italic: self.bold_italic,
fallbacks: self.fallbacks,
}
}
@@ -700,6 +776,11 @@ impl FontBuilder {
for font in self.fallbacks.iter_mut().flatten() {
resource_manager.request_resource(font);
}
Font::from_memory(data, self.page_size, self.bold, self.italic, self.fallbacks)
let styles = FontStyles {
bold: self.bold,
italic: self.italic,
bold_italic: self.bold_italic,
};
Font::from_memory(data, self.page_size, styles, self.fallbacks)
}
}
+25 -20
View File
@@ -24,7 +24,7 @@ use crate::{
algebra::Vector2, color::Color, math::Rect, reflect::prelude::*, uuid_provider,
variable::InheritableVariable, visitor::prelude::*,
},
font::{Font, FontGlyph, FontHeight, FontResource},
font::{Font, FontGlyph, FontHeight, FontResource, BUILT_IN_FONT},
style::StyledProperty,
HorizontalAlignment, VerticalAlignment,
};
@@ -244,8 +244,8 @@ impl LineSink for WrapSink<'_> {
#[derive(Default, Clone, Debug, Visit, Reflect)]
pub struct FormattedText {
font: InheritableVariable<FontResource>,
text: InheritableVariable<Vec<char>>,
font: InheritableVariable<Option<FontResource>>,
pub text: InheritableVariable<Vec<char>>,
// Temporary buffer used to split text on lines. We need it to reduce memory allocations
// when we changing text too frequently, here we sacrifice some memory in order to get
// more performance.
@@ -454,7 +454,7 @@ impl FormattedText {
}
pub fn position_to_local(&self, position: Position) -> Vector2<f32> {
if self.font.state().data().is_none() {
if self.get_font().state().data().is_none() {
return Default::default();
}
let position = self.nearest_valid_position(position);
@@ -529,11 +529,11 @@ impl FormattedText {
}
pub fn get_font(&self) -> FontResource {
(*self.font).clone()
(*self.font).clone().unwrap_or(BUILT_IN_FONT.resource())
}
pub fn set_font(&mut self, font: FontResource) -> &mut Self {
self.font.set_value_and_mark_modified(font);
self.font.set_value_and_mark_modified(Some(font));
self
}
@@ -782,7 +782,9 @@ impl FormattedText {
/// Returns once all fonts used by this FormattedText are finished loading.
pub async fn wait_for_fonts(&mut self) -> Result<(), LoadError> {
(*self.font).clone().await?;
if let Some(font) = self.font.clone_inner() {
font.await?;
}
for run in self.runs.iter() {
if let Some(font) = run.font() {
font.clone().await?;
@@ -794,7 +796,7 @@ impl FormattedText {
/// Returns true if all fonts used by this resource are Ok.
/// This `FormattedText` will not build successfully unless this returns true.
pub fn are_fonts_loaded(&self) -> bool {
if !self.font.is_ok() {
if !self.get_font().is_ok() {
return false;
}
for run in self.runs.iter() {
@@ -808,7 +810,7 @@ impl FormattedText {
}
pub fn are_fonts_loading(&self) -> bool {
if !self.font.is_loading() {
if !self.get_font().is_loading() {
return true;
}
for run in self.runs.iter() {
@@ -823,7 +825,7 @@ impl FormattedText {
pub fn font_load_error_list(&self) -> Vec<(PathBuf, LoadError)> {
let mut list = vec![];
if let ResourceState::LoadError { path, error } = &self.font.header().state {
if let ResourceState::LoadError { path, error } = &self.get_font().header().state {
list.push((path.clone(), error.clone()));
}
for run in self.runs.iter() {
@@ -839,7 +841,7 @@ impl FormattedText {
pub fn font_loading_summary(&self) -> String {
use std::fmt::Write;
let mut result = String::default();
write!(result, "Primary font: {}", self.font.header().state).unwrap();
write!(result, "Primary font: {}", self.get_font().header().state).unwrap();
for run in self.runs.iter() {
if let Some(font) = run.font() {
write!(result, "\nRun {:?}: {}", run.range, font.header().state).unwrap();
@@ -869,7 +871,7 @@ impl FormattedText {
};
if let Some(mask) = *self.mask_char {
let advance = GlyphMetrics {
font: &mut self.font.data_ref(),
font: &mut self.get_font().data_ref(),
size: **self.font_size,
}
.advance(mask);
@@ -926,7 +928,7 @@ impl FormattedText {
for line in lines.iter_mut() {
if self.mask_char.is_some() || self.runs.is_empty() {
line.height = GlyphMetrics {
font: &mut self.font.data_ref(),
font: &mut self.get_font().data_ref(),
size: **self.font_size,
}
.ascender();
@@ -970,8 +972,9 @@ impl FormattedText {
let mut x = line.x_offset.floor();
if let Some(mask) = *self.mask_char {
let mut prev = None;
let font = self.get_font();
let mut metrics = GlyphMetrics {
font: &mut self.font.data_ref(),
font: &mut font.data_ref(),
size: **self.font_size,
};
for c in std::iter::repeat_n(mask, line.len()) {
@@ -1024,7 +1027,7 @@ impl FormattedText {
} else {
let descender = if self.mask_char.is_some() || self.runs.is_empty() {
GlyphMetrics {
font: &mut self.font.data_ref(),
font: &mut self.get_font().data_ref(),
size: **self.font_size,
}
.descender()
@@ -1083,7 +1086,7 @@ pub struct FormattedTextBuilder {
shadow_offset: Vector2<f32>,
font_size: StyledProperty<f32>,
super_sampling_scaling: f32,
runs: Vec<Run>,
runs: RunSet,
/// The amount of indentation on the first line of the text.
line_indent: f32,
/// The space between lines.
@@ -1108,7 +1111,7 @@ impl FormattedTextBuilder {
shadow_offset: Vector2::new(1.0, 1.0),
font_size: 14.0f32.into(),
super_sampling_scaling: 1.0,
runs: Vec::default(),
runs: RunSet::default(),
line_indent: 0.0,
line_space: 0.0,
}
@@ -1202,7 +1205,9 @@ impl FormattedTextBuilder {
/// Later runs potentially overriding earlier runs if the ranges of the runs overlap and the later run
/// sets a property that conflicts with an earlier run.
pub fn with_runs<I: IntoIterator<Item = Run>>(mut self, runs: I) -> Self {
self.runs.extend(runs);
for run in runs {
self.runs.push(run);
}
self
}
@@ -1237,10 +1242,10 @@ impl FormattedTextBuilder {
font_size: self.font_size.into(),
shadow: self.shadow.into(),
shadow_brush: self.shadow_brush.into(),
font: self.font.into(),
font: Some(self.font).into(),
shadow_dilation: self.shadow_dilation.into(),
shadow_offset: self.shadow_offset.into(),
runs: self.runs.into(),
runs: self.runs,
line_indent: self.line_indent.into(),
line_space: self.line_space.into(),
}
+44
View File
@@ -30,6 +30,14 @@ pub type RunBuilder = Run;
#[derive(Clone, PartialEq, Debug, Default, Reflect)]
pub struct RunSet(Vec<Run>);
impl IntoIterator for RunSet {
type Item = Run;
type IntoIter = std::vec::IntoIter<Run>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl From<&[Run]> for RunSet {
fn from(value: &[Run]) -> Self {
Self(value.to_vec())
@@ -63,6 +71,21 @@ impl DerefMut for RunSet {
}
impl RunSet {
/// Updates the run set with the given run, overriding any previous runs
/// that may have set some of the same formatting attributes in the same range.
/// Runs within this set may be merged if appropriate.
pub fn push(&mut self, run: Run) {
if let Some(last) = self.0.last_mut() {
if last.range == run.range {
*last = last.clone().with_values_from(run);
} else {
self.0.push(run);
}
} else {
self.0.push(run);
}
}
/// Find the font at the given position.
pub fn font_at(&self, index: usize) -> Option<FontResource> {
for run in self.0.iter().rev() {
if run.range.contains(&(index as u32)) && run.font().is_some() {
@@ -71,6 +94,7 @@ impl RunSet {
}
None
}
/// Find the size at the given position.
pub fn font_size_at(&self, index: usize) -> Option<f32> {
for run in self.0.iter().rev() {
if run.range.contains(&(index as u32)) && run.font_size().is_some() {
@@ -79,6 +103,7 @@ impl RunSet {
}
None
}
/// Find the brush at the given position.
pub fn brush_at(&self, index: usize) -> Option<Brush> {
for run in self.0.iter().rev() {
if run.range.contains(&(index as u32)) && run.brush().is_some() {
@@ -87,6 +112,7 @@ impl RunSet {
}
None
}
/// Find whether the text shadow is enabled at the given position.
pub fn shadow_at(&self, index: usize) -> Option<bool> {
for run in self.0.iter().rev() {
if run.range.contains(&(index as u32)) && run.shadow().is_some() {
@@ -95,6 +121,7 @@ impl RunSet {
}
None
}
/// Find the shadow brush at the given position.
pub fn shadow_brush_at(&self, index: usize) -> Option<Brush> {
for run in self.0.iter().rev() {
if run.range.contains(&(index as u32)) && run.shadow_brush().is_some() {
@@ -103,6 +130,7 @@ impl RunSet {
}
None
}
/// Find the shadow dilation at the given position.
pub fn shadow_dilation_at(&self, index: usize) -> Option<f32> {
for run in self.0.iter().rev() {
if run.range.contains(&(index as u32)) && run.shadow_dilation().is_some() {
@@ -111,6 +139,7 @@ impl RunSet {
}
None
}
/// Find the shadow offsat at the given position.
pub fn shadow_offset_at(&self, index: usize) -> Option<Vector2<f32>> {
for run in self.0.iter().rev() {
if run.range.contains(&(index as u32)) && run.shadow_offset().is_some() {
@@ -137,6 +166,7 @@ pub struct Run {
}
impl Run {
/// Create a run that sets no formatting values across the given range.
pub fn new(range: Range<u32>) -> Self {
Self {
range,
@@ -182,6 +212,20 @@ impl Run {
pub fn build(self) -> Self {
self
}
/// Set this run to match the values set in the given run, overwriting the values
/// in this run only if the corresponding value is set in the given run.
pub fn with_values_from(self, run: Run) -> Self {
Self {
range: self.range,
font: run.font.or(self.font),
brush: run.brush.or(self.brush),
font_size: run.font_size.or(self.font_size),
shadow: run.shadow.or(self.shadow),
shadow_brush: run.shadow_brush.or(self.shadow_brush),
shadow_dilation: run.shadow_dilation.or(self.shadow_dilation),
shadow_offset: run.shadow_offset.or(self.shadow_offset),
}
}
/// Set this run to modify the font of the text within the range.
pub fn with_font(mut self, font: FontResource) -> Self {
self.font = Some(font);
+2
View File
@@ -220,6 +220,7 @@
#![allow(clippy::mutable_key_type)]
#![allow(mismatched_lifetime_syntaxes)]
pub use bbcode::*;
pub use copypasta;
pub use fyrox_core as core;
use message::TouchPhase;
@@ -227,6 +228,7 @@ use message::TouchPhase;
pub mod absm;
mod alignment;
pub mod animation;
mod bbcode;
pub mod bit;
pub mod border;
pub mod brush;
+122 -17
View File
@@ -25,6 +25,7 @@
use crate::formatted_text::{Run, RunSet};
use crate::style::StyledProperty;
use crate::BBCode;
use crate::{
brush::Brush,
core::{
@@ -43,6 +44,7 @@ use crate::{
use crate::message::MessageData;
use fyrox_core::algebra::Matrix3;
use fyrox_core::variable::InheritableVariable;
use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
use std::{
cell::RefCell,
@@ -52,6 +54,8 @@ use std::{
/// Possible messages that can be used to alternate [`Text`] widget state at runtime.
#[derive(Debug, Clone, PartialEq)]
pub enum TextMessage {
/// Used to set a new text and runs with BBCode tags.
BBCode(String),
/// Used to set a new text or to receive the changed text.
Text(String),
/// Used to set new text wrapping mode of the widget. See [Text](Text#text-alignment-and-word-wrapping) for usage
@@ -81,6 +85,11 @@ pub enum TextMessage {
impl MessageData for TextMessage {}
impl TextMessage {
define_constructor!(
/// Creates new [`TextMessage::BBCode`] message.
TextMessage:BBCode => fn bbcode(String)
);
define_constructor!(
/// Creates new [`TextMessage::Text`] message.
TextMessage:Text => fn text(String)
@@ -302,11 +311,57 @@ impl TextMessage {
/// }
/// ```
///
/// ## Runs
///
/// Formatting options such as fonts, shadows, sizes, and brushes can be independently controlled
/// for each character in text by adding formatting runs to the text with [`Run`].
/// Each run has a range of `char` positions within the text and fields to control formatting.
///
/// ```rust,no_run
/// # use fyrox_ui::{
/// # core::{algebra::Vector2, color::Color, pool::Handle},
/// # brush::Brush, text::TextBuilder, widget::WidgetBuilder, UiNode, UserInterface,
/// # formatted_text::Run,
/// # };
/// #
/// fn create_text_with_red_run(ui: &mut UserInterface, text: &str) -> Handle<UiNode> {
/// TextBuilder::new(WidgetBuilder::new())
/// .with_text(text)
/// .with_run(Run::new(5..22).with_brush(Brush::Solid(Color::RED)).with_shadow(true))
/// .build(&mut ui.build_ctx())
/// }
/// ```
///
/// ## BBCode
///
/// Text widget supports BBCode to add runs of various formatting to text.
/// The available tags are:
/// * `[b]` **bold text** `[/b]`
/// * `[i]` *italic text* `[/i]`
/// * `[color=red]` red text `[/color]` (can be shortened to `[c=red]`... `[/c]`, and can use hex color as in `[color=#FF0000]`)
/// * `[size=24]` large text `[/size]` (can be shortened to `[s=24]` ... `[/s]`)
/// * `[shadow]` shadowed text `[/shadow]` (can be shortened to `[sh]` ... `[/sh]` and can change shadow color with `[shadow=blue]`)
/// * `[br]` for a line break.
///
/// ```rust,no_run
/// # use fyrox_ui::{
/// # core::{algebra::Vector2, color::Color, pool::Handle},
/// # brush::Brush, text::TextBuilder, widget::WidgetBuilder, UiNode, UserInterface
/// # };
/// #
/// fn create_text_with_bbcode(ui: &mut UserInterface) -> Handle<UiNode> {
/// TextBuilder::new(WidgetBuilder::new())
/// .with_bbcode("BBCode example: [b][c=blue]bold and blue[/c][/b]")
/// .build(&mut ui.build_ctx())
/// }
/// ```
///
/// ## Messages
///
/// Text widget can accept the following list of messages at runtime (respective constructors are name with small letter -
/// `TextMessage::Text -> TextMessage::text(widget_handle, direction, text)`):
///
/// - [`TextMessage::BBCode`] - sets the text and formatting runs using BBCode.
/// - [`TextMessage::Text`] - sets new text for a `Text` widget.
/// - [`TextMessage::Wrap`] - sets new [wrapping mode](Text#text-alignment-and-word-wrapping).
/// - [`TextMessage::Font`] - sets new [font](Text#fonts-and-colors)
@@ -316,6 +371,7 @@ impl TextMessage {
/// - [`TextMessage::ShadowDilation`] - sets "thickness" of the shadows under the tex.
/// - [`TextMessage::ShadowBrush`] - sets shadow brush (allows you to change color and even make shadow with color gradients).
/// - [`TextMessage::ShadowOffset`] - sets offset of the shadows.
/// - [`TextMessage::Runs`] - sets the formatting runs for the text.
///
/// An example of changing text at runtime could be something like this:
///
@@ -342,6 +398,17 @@ impl TextMessage {
pub struct Text {
/// Base widget of the Text widget.
pub widget: Widget,
/// Text that may have BBCode tags to automatically generate formatting runs.
/// The available tags are:
/// * `[b]` **bold text** `[/b]`
/// * `[i]` *italic text* `[/i]`
/// * `[color=red]` red text `[/color]` (can be shortened to `[c=red]`... `[/c]`, and can use hex color as in `[color=#FF0000]`)
/// * `[size=24]` large text `[/size]` (can be shortened to `[s=24]` ... `[/s]`)
/// * `[shadow]` shadowed text `[/shadow]` (can be shortened to `[sh]` ... `[/sh]` and can change shadow color with `[shadow=blue]`)
/// * `[br]` for a line break.
#[visit(optional)]
#[reflect(hidden)]
pub bbcode: InheritableVariable<String>,
/// [`FormattedText`] instance that is used to layout text and generate drawing commands.
pub formatted_text: RefCell<FormattedText>,
}
@@ -405,6 +472,10 @@ impl Control for Text {
if let Some(msg) = message.data::<TextMessage>() {
let mut text_ref = self.formatted_text.borrow_mut();
match msg {
TextMessage::BBCode(text) => {
drop(text_ref);
self.set_bbcode(text.clone());
}
TextMessage::Text(text) => {
text_ref.set_text(text);
drop(text_ref);
@@ -485,6 +556,23 @@ impl Control for Text {
}
impl Text {
/// Modifies the content of the text, with BBCode tags used to set the formatting runs.
/// The available tags are:
/// * `[b]` **bold text** `[/b]`
/// * `[i]` *italic text* `[/i]`
/// * `[color=red]` red text `[/color]` (can be shortened to `[c=red]`... `[/c]`, and can use hex color as in `[color=#FF0000]`)
/// * `[size=24]` large text `[/size]` (can be shortened to `[s=24]` ... `[/s]`)
/// * `[shadow]` shadowed text `[/shadow]` (can be shortened to `[sh]` ... `[/sh]` and can change shadow color with `[shadow=blue]`)
/// * `[br]` for a line break.
pub fn set_bbcode(&mut self, code: String) {
self.bbcode.set_value_and_mark_modified(code);
let code: BBCode = self.bbcode.parse().unwrap();
let mut formatted = self.formatted_text.borrow_mut();
let font = formatted.get_font();
formatted.set_runs(code.build_runs(&font));
formatted.set_text(code.text);
self.invalidate_layout();
}
/// Returns current text wrapping mode of the widget.
pub fn wrap_mode(&self) -> WrapMode {
self.formatted_text.borrow().wrap_mode()
@@ -514,6 +602,7 @@ impl Text {
/// TextBuilder is used to create instances of [`Text`] widget and register them in the user interface.
pub struct TextBuilder {
widget_builder: WidgetBuilder,
bbcode: Option<String>,
text: Option<String>,
font: Option<FontResource>,
vertical_text_alignment: VerticalAlignment,
@@ -532,6 +621,7 @@ impl TextBuilder {
pub fn new(widget_builder: WidgetBuilder) -> Self {
Self {
widget_builder,
bbcode: None,
text: None,
font: None,
vertical_text_alignment: VerticalAlignment::Top,
@@ -546,6 +636,14 @@ impl TextBuilder {
}
}
/// Sets the desired text of the widget, with BBcode tags that will
/// automatically generate the formatting runs and replace any other
/// runs set through this builder.
pub fn with_bbcode<P: Into<String>>(mut self, text: P) -> Self {
self.bbcode = Some(text.into());
self
}
/// Sets the desired text of the widget.
pub fn with_text<P: Into<String>>(mut self, text: P) -> Self {
self.text = Some(text.into());
@@ -641,25 +739,32 @@ impl TextBuilder {
self.widget_builder.foreground = Some(ctx.style.property(Style::BRUSH_TEXT));
}
let text_builder = if let Some(bbcode) = &self.bbcode {
let code: BBCode = bbcode.parse().unwrap();
code.build_formatted_text(font)
} else {
FormattedTextBuilder::new(font)
.with_text(self.text.unwrap_or_default())
.with_runs(self.runs)
};
let formatted_text = text_builder
.with_vertical_alignment(self.vertical_text_alignment)
.with_horizontal_alignment(self.horizontal_text_alignment)
.with_wrap(self.wrap)
.with_shadow(self.shadow)
.with_shadow_brush(self.shadow_brush)
.with_shadow_dilation(self.shadow_dilation)
.with_shadow_offset(self.shadow_offset)
.with_font_size(
self.font_size
.unwrap_or_else(|| ctx.style.property(Style::FONT_SIZE)),
)
.build();
let text = Text {
widget: self.widget_builder.build(ctx),
formatted_text: RefCell::new(
FormattedTextBuilder::new(font)
.with_text(self.text.unwrap_or_default())
.with_vertical_alignment(self.vertical_text_alignment)
.with_horizontal_alignment(self.horizontal_text_alignment)
.with_wrap(self.wrap)
.with_shadow(self.shadow)
.with_shadow_brush(self.shadow_brush)
.with_shadow_dilation(self.shadow_dilation)
.with_shadow_offset(self.shadow_offset)
.with_font_size(
self.font_size
.unwrap_or_else(|| ctx.style.property(Style::FONT_SIZE)),
)
.with_runs(self.runs)
.build(),
),
bbcode: self.bbcode.unwrap_or_default().into(),
formatted_text: RefCell::new(formatted_text),
};
ctx.add_node(UiNode::new(text))
}
+1
View File
@@ -1344,6 +1344,7 @@ impl Control for TextBox {
let mut text = self.formatted_text.borrow_mut();
match msg {
TextMessage::BBCode(_) => (),
TextMessage::Text(new_text) => {
fn text_equals(
formatted_text: &FormattedText,
+2 -2
View File
@@ -32,6 +32,7 @@ use crate::{manager::ProjectManager, settings::DATA_DIR, utils::make_button};
use fyrox::asset::io::FsResourceIo;
use fyrox::core::Uuid;
use fyrox::engine::ApplicationLoopController;
use fyrox::gui::font::FontStyles;
use fyrox::{
asset::{manager::ResourceManager, untyped::ResourceKind},
core::{
@@ -103,8 +104,7 @@ fn main() {
Font::from_memory(
include_bytes!("../resources/Roboto-Regular.ttf").to_vec(),
1024,
None,
None,
FontStyles::default(),
Vec::default(),
)
.unwrap(),