use bevy::prelude::*;
use bevy::text::FontSize as BevyFontSize;
use crate::bridge::{JsBridge, ResolvedTextStyle, SpanKind};
use crate::plugin::Fonts;
use crate::protocol::{NodeId, units::FontSize};
use crate::style::Style;
use crate::style::props::FONT_SIZE;
use crate::ui_map::{apply_resolved_text_style, resolve_text_font_in};
#[allow(clippy::too_many_arguments)]
pub(crate) fn cascade(
commands: &mut Commands,
bridge: &JsBridge,
fonts: &Fonts,
id: NodeId,
resolved: &ResolvedTextStyle,
color_half: bool,
font_half: bool,
rebased: &mut Vec<(NodeId, ResolvedTextStyle)>,
) {
for kid in bridge.children_of(id) {
match bridge.spans.get(&kid) {
Some(SpanKind::RawInherited) => {
if let Some(&entity) = bridge.nodes.get(&kid) {
apply_resolved_text_style(
&mut commands.entity(entity),
resolved,
color_half,
font_half,
);
}
}
Some(SpanKind::InlineStyled) if font_half => {
rebase(commands, bridge, fonts, kid, resolved.1.font_size, rebased);
}
_ => {}
}
}
}
pub(super) fn em_sized(style: Option<&Style>) -> bool {
style.is_some_and(|s| matches!(s.get(&FONT_SIZE), Some(FontSize::Em(_))))
}
pub(crate) fn rebase(
commands: &mut Commands,
bridge: &JsBridge,
fonts: &Fonts,
id: NodeId,
inherited: BevyFontSize,
rebased: &mut Vec<(NodeId, ResolvedTextStyle)>,
) {
let style = bridge.props_cache.get(&id).and_then(|p| p.style.as_ref());
if !em_sized(style) {
return;
}
let (Some(&entity), Some(cached)) = (bridge.nodes.get(&id), bridge.text_styles.get(&id)) else {
return;
};
let (font, line, spacing) = resolve_text_font_in(style, fonts, inherited);
let own = (cached.0, font, line, spacing);
apply_resolved_text_style(&mut commands.entity(entity), &own, false, true);
cascade(commands, bridge, fonts, id, &own, false, true, rebased);
rebased.push((id, own));
}
#[cfg(test)]
mod tests {
use super::super::test_util::{create, ent, op_app, update};
use super::*;
use crate::bridge::ReactInteraction;
use crate::protocol::{ROOT_ID, op::Op};
use serde_json::json;
fn append(parent: NodeId, child: NodeId) -> Op {
Op::Append { parent, child }
}
fn run(id: NodeId) -> Op {
Op::CreateTextSpan {
id,
text: "run".into(),
}
}
fn font_size(app: &App, id: NodeId) -> BevyFontSize {
let font = app.world().entity(ent(app, id)).get::<TextFont>();
font.expect("a text font").font_size
}
fn nested() -> (App, crossbeam_channel::Sender<Vec<Op>>) {
let (mut app, tx) = op_app();
tx.send(vec![
create(1, "text", json!({ "style": { "fontSize": 30 } })),
create(2, "textSpan", json!({ "style": { "fontSize": "0.5em" } })),
run(3),
create(4, "textSpan", json!({ "style": { "fontSize": "2em" } })),
append(2, 3),
append(2, 4),
append(1, 2),
append(ROOT_ID, 1),
])
.unwrap();
app.update();
(app, tx)
}
#[test]
fn em_span_resolves_against_its_parent() {
let (mut app, tx) = nested();
assert_eq!(font_size(&app, 2), BevyFontSize::Px(15.0));
assert_eq!(font_size(&app, 3), BevyFontSize::Px(15.0), "its run");
assert_eq!(font_size(&app, 4), BevyFontSize::Px(30.0), "its em span");
tx.send(vec![run(5), append(2, 5)]).unwrap();
app.update();
assert_eq!(font_size(&app, 5), BevyFontSize::Px(15.0));
}
#[test]
fn em_span_follows_parent_delta() {
let (mut app, tx) = nested();
tx.send(vec![update(
1,
"text",
json!({ "style": { "fontSize": 40 } }),
&[],
&[],
)])
.unwrap();
app.update();
assert_eq!(font_size(&app, 2), BevyFontSize::Px(20.0));
assert_eq!(font_size(&app, 3), BevyFontSize::Px(20.0));
assert_eq!(font_size(&app, 4), BevyFontSize::Px(40.0));
}
#[test]
fn em_span_own_delta() {
let (mut app, tx) = nested();
let restyle = |size: serde_json::Value| {
update(
2,
"textSpan",
json!({ "style": { "fontSize": size } }),
&[],
&[],
)
};
tx.send(vec![restyle(json!("0.75em"))]).unwrap();
app.update();
assert_eq!(font_size(&app, 2), BevyFontSize::Px(22.5));
assert_eq!(font_size(&app, 3), BevyFontSize::Px(22.5), "its run");
assert_eq!(font_size(&app, 4), BevyFontSize::Px(45.0), "its em span");
tx.send(vec![restyle(json!(12))]).unwrap();
app.update();
tx.send(vec![update(
1,
"text",
json!({ "style": { "fontSize": 40 } }),
&[],
&[],
)])
.unwrap();
app.update();
assert_eq!(font_size(&app, 2), BevyFontSize::Px(12.0));
assert_eq!(font_size(&app, 4), BevyFontSize::Px(24.0));
}
#[test]
fn em_stays_in_the_parent_unit() {
let (mut app, tx) = op_app();
tx.send(vec![
create(1, "text", json!({ "style": { "fontSize": "1.5em" } })),
create(2, "textSpan", json!({ "style": { "fontSize": "0.5em" } })),
append(1, 2),
append(ROOT_ID, 1),
])
.unwrap();
app.update();
assert_eq!(font_size(&app, 1), BevyFontSize::Rem(1.5));
assert_eq!(font_size(&app, 2), BevyFontSize::Rem(0.75));
}
#[test]
fn run_appended_before_a_same_batch_delta_follows_it() {
let (mut app, tx) = op_app();
tx.send(vec![
create(1, "text", json!({ "style": { "fontSize": 30 } })),
append(ROOT_ID, 1),
])
.unwrap();
app.update();
tx.send(vec![
run(2),
append(1, 2),
update(1, "text", json!({ "style": { "fontSize": 40 } }), &[], &[]),
])
.unwrap();
app.update();
assert_eq!(font_size(&app, 2), BevyFontSize::Px(40.0));
}
#[test]
fn em_span_follows_a_hover_font_size() {
let (mut app, tx) = op_app();
app.add_systems(
Update,
crate::reconcile::apply_interaction_styles.after(crate::reconcile::apply_js_ops),
);
tx.send(vec![
create(
1,
"text",
json!({ "style": { "fontSize": 30 }, "hoverStyle": { "fontSize": 40 } }),
),
create(2, "textSpan", json!({ "style": { "fontSize": "0.5em" } })),
append(1, 2),
append(ROOT_ID, 1),
])
.unwrap();
app.update();
let block = ent(&app, 1);
assert_eq!(font_size(&app, 2), BevyFontSize::Px(15.0));
app.world_mut()
.entity_mut(block)
.insert(ReactInteraction::Hovered);
app.update();
assert_eq!(font_size(&app, 2), BevyFontSize::Px(20.0));
app.world_mut()
.entity_mut(block)
.insert(ReactInteraction::None);
app.update();
assert_eq!(font_size(&app, 2), BevyFontSize::Px(15.0));
}
}