use bevy::prelude::*;
use bevy::ui::{ComputedNode, ScrollPosition};
use super::apply::resolve;
use super::stamps::{
apply_animated, apply_pointer_handlers, apply_scroll_listener, apply_scroll_step,
apply_style_variants_delta, apply_wheel_listener, update_controlled_scroll, variant_keys,
};
use crate::bridge::{JsBridge, ReactNode, SpanKind, StyleVariants};
use crate::element::{AttrDirty, Common};
use crate::ext::TextRole;
use crate::plugin::Fonts;
use crate::protocol::{NodeId, props::Props};
use crate::style::props::OPACITY;
use crate::style::{Invalidation, NodeCtx, Style, WriterCtx};
use crate::transition::ScrollTransitionState;
use crate::ui_map::{
apply_resolved_text_style, apply_style_masked, resolve_text_color, resolve_text_font,
};
#[allow(clippy::too_many_arguments)]
pub(super) fn apply_update(
commands: &mut Commands,
bridge: &mut JsBridge,
assets: &AssetServer,
fonts: &Fonts,
children: &Query<&Children>,
rnodes: &Query<&ReactNode>,
scroll_query: &mut Query<(
&mut ScrollPosition,
&ComputedNode,
Option<&mut ScrollTransitionState>,
)>,
id: NodeId,
props: Box<Props>,
unset: Vec<String>,
style_unset: Vec<String>,
) {
let Some(e) = resolve(bridge, id) else {
return;
};
let _diag = crate::diag::node_scope(id);
let registry = bridge.ext.clone();
let kind: std::borrow::Cow<'static, str> = bridge.shared_tags.kind_cow(id);
let kind: &str = &kind;
let info = registry.element_or_fallback(kind);
let flags = info.decl.flags;
let mut cached = bridge.props_cache.remove(&id).unwrap_or_else(|| {
tracing::warn!("delta update for uncached node {id}; merging onto defaults");
Box::default()
});
let old_name = dirty_name(&props, &unset).then(|| cached.name.clone());
let old_tag = dirty_shared_tag(&props, &unset).then(|| cached.shared_tag.clone());
let old_variant_keys = variant_keys(&cached);
let (dirty, ev) = cached.merge_delta(props, &unset, &style_unset, info);
let props = cached;
if let Some(old_name) = old_name {
crate::names::apply_name(
&mut commands.entity(e),
&mut bridge.names,
old_name.as_deref(),
props.name.as_deref(),
);
}
if let Some(old_tag) = old_tag {
bridge
.shared_tags
.apply(id, old_tag.as_deref(), props.shared_tag.as_deref());
}
let style = &props.style;
let styles = registry.styles();
let touched = if props.hover_style.is_some()
|| props.press_style.is_some()
|| props.focus_style.is_some()
{
dirty.style
} else {
dirty.style.union(old_variant_keys)
};
let promoted = bridge.promoted_layers.contains(&id);
let invalidation = styles
.invalidation(
&touched,
&dirty.style_old,
style.as_ref().unwrap_or(Style::empty()),
&NodeCtx { promoted, kind },
)
.union(info.attr_invalidation(dirty.attrs));
if invalidation.contains(Invalidation::PROMOTION)
|| dirty.hover_style
|| dirty.press_style
|| dirty.focus_style
{
bridge.layer_dirty.insert(id);
}
let global = styles.writers_for(&touched);
let own = info.writers_for(&touched, dirty.attrs.union(ev.attrs.keys()));
let is_text_block = matches!(flags.text, TextRole::Block | TextRole::Span);
let text_promoted = flags.text == TextRole::Block && promoted;
let color_half = is_text_block && global.intersects(styles.masks.text_color);
let font_half = is_text_block && global.intersects(styles.masks.text_font);
let resolved = (color_half || font_half)
.then(|| {
let entry = bridge.text_styles.get_mut(&id)?;
if color_half {
entry.0 = resolve_text_color(style.as_ref(), text_promoted);
}
if font_half {
let (font, line, spacing) = resolve_text_font(style.as_ref(), fonts);
(entry.1, entry.2, entry.3) = (font, line, spacing);
}
Some(entry.clone())
})
.flatten();
if flags.text == TextRole::Span {
super::stamps::warn_span_ignored(&props);
}
if dirty.style.intersects(&info.ignored_styles) {
super::stamps::warn_ignored_styles(info, styles, &props);
}
let wctx = WriterCtx {
promoted: if flags.text == TextRole::Block {
text_promoted
} else {
promoted
},
fresh: false,
kind,
flags,
assets,
fonts,
styles,
element: info,
attrs: &props.attrs,
events: &ev.attrs,
id,
};
let mut ec = commands.entity(e);
apply_style_masked(&mut ec, style, global, own, &wctx, invalidation);
let common = info.decl.common;
if common.contains(Common::VARIANTS) {
apply_style_variants_delta(&mut ec, &props, style, &dirty);
}
if common.contains(Common::POINTER) && dirty.pointer {
apply_pointer_handlers(&mut ec, &props, flags, false);
}
if common.contains(Common::SCROLL) {
if dirty.scroll_listener {
apply_scroll_listener(&mut ec, &props, false);
}
if dirty.scroll_step {
apply_scroll_step(&mut ec, &props, false);
}
}
if common.contains(Common::WHEEL) && dirty.wheel {
apply_wheel_listener(&mut ec, &props, false);
}
if dirty.style.any() || dirty.attrs != AttrDirty::NONE {
apply_animated(&mut ec, &mut bridge.animated, id, &props, false);
}
if dirty.handlers {
let events = props.handler_events(info);
if events.is_empty() {
ec.remove::<crate::element::EventSubscriptions>();
} else {
ec.insert(crate::element::EventSubscriptions(events));
}
}
if common.contains(Common::SCROLL) {
update_controlled_scroll(
bridge,
&mut ec,
scroll_query,
e,
id,
ev.scroll_left,
ev.scroll_top,
);
}
if let Some(resolved) = resolved
&& let Ok(kids) = children.get(e)
{
for child in kids.iter() {
if let Ok(rnode) = rnodes.get(child)
&& bridge.spans.get(&rnode.0) == Some(&SpanKind::RawInherited)
{
apply_resolved_text_style(
&mut commands.entity(child),
&resolved,
color_half,
font_half,
);
}
}
}
bridge.props_cache.insert(id, props);
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn reapply_opacity_outputs(
commands: &mut Commands,
entity: Entity,
props: &Props,
wctx: &WriterCtx,
style_variants: &mut Query<&mut StyleVariants>,
) {
if let Ok(mut variants) = style_variants.get_mut(entity) {
variants.restyle = crate::bridge::Restyle::Full;
return;
}
let mut ec = commands.entity(entity);
let styles = wctx.styles;
let mask = styles
.readers_of(&OPACITY)
.without(styles.masks.transition.union(styles.masks.text_color));
let own = wctx
.element
.writers_for(&styles.dirty_of(&[&OPACITY]), AttrDirty::NONE);
apply_style_masked(&mut ec, &props.style, mask, own, wctx, Invalidation::PAINT);
}
fn dirty_name(delta: &Props, unset: &[String]) -> bool {
delta.name.is_some() || unset.iter().any(|u| u == "name")
}
fn dirty_shared_tag(delta: &Props, unset: &[String]) -> bool {
delta.shared_tag.is_some() || unset.iter().any(|u| u == "sharedTag")
}
#[cfg(test)]
mod tests;