use std::any::Any;
use std::sync::OnceLock;
use bevy_ecs::prelude::*;
use parking_lot::Mutex;
use parley::Alignment;
use parley::AlignmentOptions;
use parley::FontContext;
use parley::GenericFamily;
use parley::Layout;
use parley::LayoutContext;
use parley::LineHeight;
use parley::StyleProperty;
use crate::color::Color;
use crate::color::ColorExt;
use crate::layout::DisplayNode;
use crate::layout::TaffyStyle;
use crate::renderer::AppScaleFactor;
use crate::renderer::Command;
use crate::renderer::DrawCommand;
use crate::renderer::Measurable;
use crate::renderer::Measurer;
use crate::renderer::RenderTarget;
use crate::renderer::Renderable;
use crate::renderer::Renderer;
use crate::renderer::ScaleFactor;
use crate::style::Rectangle;
use crate::widget::Widget;
use crate::widget::WidgetNode;
static FONT_CONTEXT: OnceLock<Mutex<FontContext>> = OnceLock::new();
static LAYOUT_CONTEXT: OnceLock<Mutex<LayoutContext>> = OnceLock::new();
fn get_font_context() -> &'static Mutex<FontContext> {
FONT_CONTEXT.get_or_init(|| Mutex::new(FontContext::new()))
}
fn get_layout_context() -> &'static Mutex<LayoutContext> {
LAYOUT_CONTEXT.get_or_init(|| Mutex::new(LayoutContext::new()))
}
#[derive(Component, Debug)]
pub struct TextNode(pub String);
#[derive(Component, Debug)]
pub struct FontSizeNode(pub f32);
#[derive(Component, Debug)]
pub struct LineHeightNode(pub f32);
#[derive(Debug, Clone)]
struct TextNodeMeasurer {
text: String,
font_size: f32,
line_height: f32,
scale_factor: f32,
layout: Layout<[u8; 4]>,
inner_size: Rectangle,
}
fn create_layout(
text: &str,
font_size: f32,
line_height: f32,
scale_factor: f32,
) -> Layout<[u8; 4]> {
let mut font_context = get_font_context().lock();
let mut layout_context = get_layout_context().lock();
let mut builder = layout_context.ranged_builder(&mut font_context, &text, scale_factor, true);
builder.push_default(GenericFamily::SystemUi);
builder.push_default(LineHeight::Absolute(line_height));
builder.push_default(StyleProperty::FontSize(font_size));
let mut layout = builder.build(&text);
layout.break_all_lines(None);
layout.align(Alignment::Start, AlignmentOptions::default());
layout
}
impl TextNodeMeasurer {
pub fn new(
text: impl Into<String>,
font_size: f32,
line_height: f32,
scale_factor: f32,
) -> Self {
let text = text.into();
let layout = create_layout(&text, font_size, line_height, scale_factor);
Self {
text,
font_size,
line_height,
layout,
scale_factor,
inner_size: Rectangle::zero(),
}
}
fn sync(
&mut self,
text: impl Into<String>,
font_size: f32,
line_height: f32,
scale_factor: f32,
) -> bool {
let text = text.into();
let did_change = text != self.text
|| font_size != self.font_size
|| line_height != self.line_height
|| scale_factor != self.scale_factor;
if did_change {
let layout = create_layout(&text, font_size, line_height, scale_factor);
self.layout = layout;
}
self.text = text;
self.font_size = font_size;
self.line_height = line_height;
self.scale_factor = scale_factor;
did_change
}
fn sync_scale_factor(&mut self, scale_factor: f32) -> bool {
let did_change = scale_factor != self.scale_factor;
if did_change {
let layout = create_layout(&self.text, self.font_size, self.line_height, scale_factor);
self.layout = layout;
}
self.scale_factor = scale_factor;
did_change
}
fn set_inner_size(&mut self) {
let width = (self.layout.width() / self.scale_factor).ceil();
let height = (self.layout.height() / self.scale_factor).ceil();
self.inner_size = Rectangle::from_size(width, height);
}
}
impl Measurable for TextNodeMeasurer {
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn measure(&mut self, available_space: taffy::Size<taffy::AvailableSpace>) -> Rectangle {
let scale_factor = self.scale_factor;
match available_space.width {
taffy::AvailableSpace::Definite(pixels) => {
self.layout.break_all_lines(Some(pixels * scale_factor));
self.set_inner_size();
self.inner_size
}
taffy::AvailableSpace::MinContent => {
let content_widths = self.layout.calculate_content_widths();
Rectangle::from_size(
(content_widths.min / scale_factor).ceil(),
(self.layout.height() / scale_factor).ceil(),
)
}
taffy::AvailableSpace::MaxContent => {
let content_widths = self.layout.calculate_content_widths();
Rectangle::from_size(
(content_widths.max / scale_factor).ceil(),
(self.layout.height() / scale_factor).ceil(),
)
}
}
}
}
#[derive(Component, Clone)]
struct TextNodeRender {
color: Color,
text_measure: TextNodeMeasurer,
last_commands: Option<(RenderTarget, Vec<Command>)>,
}
impl TextNodeRender {
fn sync(
&mut self,
text: impl Into<String>,
font_size: f32,
line_height: f32,
scale_factor: f32,
color: Color,
) -> bool {
let did_change = self
.text_measure
.sync(text, font_size, line_height, scale_factor)
|| self.color != color;
self.color = color;
if did_change {
self.last_commands = None;
}
did_change
}
fn sync_scale_factor(&mut self, scale_factor: f32) -> bool {
let did_change = self.text_measure.sync_scale_factor(scale_factor);
if did_change {
self.last_commands = None;
}
did_change
}
}
impl Renderable for TextNodeRender {
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn render(&mut self, render_target: RenderTarget) -> Vec<DrawCommand> {
let width = render_target.rectangle.width() * self.text_measure.scale_factor;
if let Some((last_render_target, last_commands)) = &self.last_commands {
if *last_render_target == render_target {
return vec![DrawCommand {
render_target,
commands: last_commands.clone(),
}];
}
}
let mut commands = match self.last_commands.take() {
Some((_, mut commands)) => {
commands.clear();
commands
}
None => Vec::new(),
};
let mut layout = self.text_measure.layout.clone();
layout.break_all_lines(Some(width));
commands.push(Command::Text {
color: self.color,
layout,
});
self.last_commands = Some((render_target.clone(), commands.clone()));
vec![DrawCommand {
render_target,
commands,
}]
}
}
#[derive(Debug)]
pub struct TextStyle {
color: Color,
font_size: f32,
line_height: f32,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
color: Color::default(),
font_size: 20.0,
line_height: 23.0,
}
}
}
impl TextStyle {
pub fn font_size(mut self, font_size: f32) -> Self {
self.font_size = font_size;
self
}
pub fn line_height(mut self, line_height: f32) -> Self {
self.line_height = line_height;
self
}
fn bundle(&self) -> impl Bundle {
(
self.color.clone(),
FontSizeNode(self.font_size),
LineHeightNode(self.line_height),
)
}
}
pub struct Text<M> {
text: String,
style: TextStyle,
_marker: std::marker::PhantomData<M>,
}
impl<M> std::fmt::Debug for Text<M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Text")
.field("text", &self.text)
.field("style", &self.style)
.finish()
}
}
impl<M> Text<M> {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
style: TextStyle::default(),
_marker: std::marker::PhantomData,
}
}
pub fn font_size(mut self, font_size: f32) -> Self {
self.style = self.style.font_size(font_size);
self
}
pub fn line_height(mut self, line_height: f32) -> Self {
self.style = self.style.line_height(line_height);
self
}
}
impl<'widget, M: Clone + Send + Sync + 'static> Widget<'widget, M> for Text<M> {
fn spawn<'a, 'b>(&self, entity_commands: &'a mut EntityWorldMut<'b>)
where
'b: 'a,
{
entity_commands.insert((
WidgetNode,
TextNode(self.text.clone()),
DisplayNode(taffy::Display::Block),
self.style.bundle(),
));
}
}
impl<T> ColorExt for Text<T> {
fn color(mut self, color: impl Into<Color>) -> Self {
self.style.color = color.into();
self
}
}
pub(crate) fn text_setup(world: &mut World) {
world.add_observer(on_insert_text);
world.add_observer(on_scale_factor_change);
}
fn on_insert_text(
insert: On<Insert, TextNode>,
mut commands: Commands,
scale_factor: Res<ScaleFactor>,
mut texts: Query<(
&TextNode,
&Color,
&FontSizeNode,
&LineHeightNode,
Option<&mut TaffyStyle>,
Option<&mut Measurer>,
Option<&mut Renderer>,
)>,
) {
tracing::trace!("Text node inserted: {:?}", insert.entity);
commands.entity(insert.entity).insert(Name::new("Text"));
let (text, color, font_size, line_height, taffy_style, measurer, renderer) =
texts.get_mut(insert.entity).unwrap();
if let Some(mut measurer) = measurer
&& let Some(text_node_measurer) = measurer.0.as_any_mut().downcast_mut::<TextNodeMeasurer>()
{
let did_change =
text_node_measurer.sync(text.0.clone(), font_size.0, line_height.0, scale_factor.0);
if did_change && let Some(mut taffy_style) = taffy_style {
taffy_style.is_content_dirty = true;
}
} else {
let text_node_measure =
TextNodeMeasurer::new(text.0.clone(), font_size.0, line_height.0, scale_factor.0);
let measurer = Measurer(Box::new(text_node_measure.clone()));
commands.entity(insert.entity).insert(measurer);
}
if let Some(mut renderer) = renderer
&& let Some(text_node_renderer) = renderer.0.as_any_mut().downcast_mut::<TextNodeRender>()
{
text_node_renderer.sync(
text.0.clone(),
font_size.0,
line_height.0,
scale_factor.0,
*color,
);
} else {
let text_node_render = TextNodeRender {
color: *color,
text_measure: TextNodeMeasurer::new(
text.0.clone(),
font_size.0,
line_height.0,
scale_factor.0,
),
last_commands: None,
};
let renderer = Renderer(Box::new(text_node_render));
commands.entity(insert.entity).insert(renderer);
}
}
fn on_scale_factor_change(
scale_factor: On<AppScaleFactor>,
mut measurers: Query<&mut Measurer, With<TextNode>>,
mut renderers: Query<&mut Renderer, With<TextNode>>,
) {
for mut measurer in &mut measurers {
if let Some(text_node_measurer) = measurer.0.as_any_mut().downcast_mut::<TextNodeMeasurer>()
{
text_node_measurer.sync_scale_factor(scale_factor.0.0);
}
}
for mut renderer in &mut renderers {
if let Some(text_node_renderer) = renderer.0.as_any_mut().downcast_mut::<TextNodeRender>() {
text_node_renderer.sync_scale_factor(scale_factor.0.0);
}
}
}