mirror of
https://github.com/FyroxEngine/Fyrox.git
synced 2026-10-12 02:02:20 +00:00
BBCode
This commit is contained in:
+23
-15
@@ -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,
|
||||
¤t_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(
|
||||
¤t_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>>();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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.
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user