use bevy::a11y::AccessibilityNode;
use bevy::input_focus::tab_navigation::TabIndex;
use bevy::platform::collections::HashSet;
use bevy::prelude::*;
use bevy::ui::{ComputedNode, ScrollPosition};
use crate::animations::AnimatedNode;
use crate::bridge::{
ClickOwner, FocusState, HoverState, JsBridge, PointerHandlers, ReactInteraction, Restyle,
ScrollListener, ScrollStep, StyleVariants, WheelListener,
};
use crate::protocol::{
NodeId,
props::{Props, PropsDirty, UpdateEvents},
};
use crate::style::Style;
use crate::style::props::{BACKDROP_FILTER, CACHE, FILTER, MORPH_FILTER, TRANSFORM3D};
use crate::transition::ScrollTransitionState;
pub(crate) fn apply_animated(
ec: &mut EntityCommands,
animated: &mut HashSet<NodeId>,
id: NodeId,
props: &Props,
fresh: bool,
) {
crate::style_bindings::warn_variant_bindings(props);
let bindings = crate::style_bindings::derive_props_bindings(props);
crate::style_bindings::warn_gradient_transition_mix(props, bindings.as_ref());
match bindings {
Some(bindings) => {
if bindings.has_stage(crate::animations::props::PropStage::Ext) {
ec.insert_if_new(crate::ext::DrivenExtValues::default());
} else if !fresh {
ec.remove::<crate::ext::DrivenExtValues>();
}
ec.insert(AnimatedNode(bindings));
animated.insert(id);
}
None => {
if !fresh && !animated.is_empty() && animated.remove(&id) {
ec.remove::<(AnimatedNode, crate::ext::DrivenExtValues)>();
}
}
}
}
pub(super) fn apply_style_variants(
ec: &mut EntityCommands,
props: &Props,
base: &Option<Style>,
fresh: bool,
) {
if props.has_variant() {
ec.insert(StyleVariants {
base: base.clone(),
hover: props.hover_style.clone(),
press: props.press_style.clone(),
focus: props.focus_style.clone(),
focus_visible: props.focus_visible_style.clone(),
keys: variant_keys(props),
restyle: Restyle::Full,
});
if props.hover_style.is_some() || props.press_style.is_some() {
ec.insert_if_new(ReactInteraction::default());
}
if props.focus_style.is_some() || props.focus_visible_style.is_some() {
ec.insert_if_new(FocusState::default());
} else if !fresh {
ec.remove::<FocusState>();
}
} else if !fresh {
ec.remove::<(StyleVariants, FocusState)>();
}
}
pub(super) fn apply_style_variants_delta(
ec: &mut EntityCommands,
props: &Props,
base: &Option<Style>,
dirty: &PropsDirty,
) {
if !dirty.any_style_variant() {
return;
}
if dirty.variant() {
apply_style_variants(ec, props, base, false);
return;
}
if props.has_variant() {
let base = base.clone();
let mask = dirty.style;
ec.queue(move |mut entity: EntityWorldMut| {
if let Some(mut variants) = entity.get_mut::<StyleVariants>() {
variants.base = base;
variants.restyle.note_base_delta(mask);
}
});
}
}
pub(crate) fn apply_pointer_handlers(
ec: &mut EntityCommands,
props: &Props,
flags: crate::ext::ElementFlags,
fresh: bool,
) {
let any_pointer = props.on_pointer_down
|| props.on_pointer_move
|| props.on_pointer_up
|| props.on_pointer_enter
|| props.on_pointer_leave
|| props.on_pointer_hover;
if any_pointer {
let handlers = PointerHandlers {
down: props.on_pointer_down,
moved: props.on_pointer_move,
up: props.on_pointer_up,
enter: props.on_pointer_enter,
leave: props.on_pointer_leave,
hover: props.on_pointer_hover,
};
ec.insert(handlers);
} else if !fresh {
ec.remove::<PointerHandlers>();
}
if props.on_pointer_enter || props.on_pointer_leave {
ec.insert_if_new(HoverState::default());
} else if !fresh {
ec.remove::<HoverState>();
}
if props.on_click || any_pointer {
ec.insert_if_new((ReactInteraction::default(), ClickOwner));
} else if !fresh {
ec.remove::<ClickOwner>();
}
if flags.node_less {
node_less_pointer_slot(ec, props.on_click || any_pointer, fresh);
}
}
fn node_less_pointer_slot(ec: &mut EntityCommands, any: bool, fresh: bool) {
if any {
ec.insert_if_new(crate::ext::EventLocalPos::default());
} else if !fresh {
ec.remove::<(crate::ext::EventLocalPos, ReactInteraction)>();
}
}
pub(super) fn warn_span_ignored(props: &Props) {
for (present, name) in [
(
props.all_styles().any(|s| s.get(&FILTER).is_some()),
"filter",
),
(
props
.all_styles()
.any(|s| s.get(&BACKDROP_FILTER).is_some()),
"backdropFilter",
),
(
props.all_styles().any(|s| s.get(&MORPH_FILTER).is_some()),
"morphFilter",
),
(
props.all_styles().any(|s| s.get(&TRANSFORM3D).is_some()),
"transform3d",
),
(props.all_styles().any(|s| s.get(&CACHE).is_some()), "cache"),
] {
if present {
crate::diag::report(
"spanLayerStyle",
name,
&format!(
"`{name}` on a nested <text> span is ignored — a span has no \
layout box to capture; put it on the enclosing <text> (or a \
wrapping <node>)"
),
);
}
}
}
pub(super) fn warn_ignored_styles(
info: &crate::element::ElementInfo,
styles: &crate::style::StyleRegistry,
props: &Props,
) {
if !info.ignored_styles.any() {
return;
}
for style in props.all_styles() {
if !style.keys().intersects(&info.ignored_styles) {
continue;
}
for (property, _) in style.iter() {
if styles
.id_of(property)
.is_some_and(|id| info.ignored_styles.contains(id))
{
let name = property.name();
crate::diag::report(
"styleIgnored",
name,
&format!("`{name}` has no effect on <{}>", info.name()),
);
}
}
}
}
pub(crate) fn apply_focus(
ec: &mut EntityCommands,
props: &Props,
flags: crate::ext::ElementFlags,
fresh: bool,
) {
let disabled = props.attrs.get(&crate::elements::DISABLED) == Some(&true);
let implied = (flags.text == crate::ext::TextRole::Input).then_some(0);
match props.tab_index.or(implied).filter(|_| !disabled) {
Some(index) => {
ec.insert(TabIndex(index));
ec.insert_if_new(AccessibilityNode(accesskit::Node::new(
accesskit::Role::GenericContainer,
)));
}
None if !fresh => {
ec.remove::<TabIndex>();
}
None => {}
}
}
pub(super) fn apply_scroll_listener(ec: &mut EntityCommands, props: &Props, fresh: bool) {
if props.on_scroll {
ec.insert_if_new(ScrollListener);
} else if !fresh {
ec.remove::<ScrollListener>();
}
}
pub(super) fn apply_wheel_listener(ec: &mut EntityCommands, props: &Props, fresh: bool) {
if props.on_wheel {
ec.insert_if_new(WheelListener);
} else if !fresh {
ec.remove::<WheelListener>();
}
}
pub(super) fn apply_scroll_step(ec: &mut EntityCommands, props: &Props, fresh: bool) {
match props.scroll_step {
Some(step) => {
ec.insert(ScrollStep(step));
}
None if !fresh => {
ec.remove::<ScrollStep>();
}
None => {}
}
}
pub(crate) fn stamp_common(
ec: &mut EntityCommands,
animated: &mut HashSet<NodeId>,
id: NodeId,
props: &Props,
base: &Option<Style>,
common: crate::element::Common,
flags: crate::ext::ElementFlags,
) {
use crate::element::Common;
if common.contains(Common::VARIANTS) {
apply_style_variants(ec, props, base, true);
}
if common.contains(Common::POINTER) {
apply_pointer_handlers(ec, props, flags, true);
}
if common.contains(Common::FOCUS) {
apply_focus(ec, props, flags, true);
}
apply_animated(ec, animated, id, props, true);
}
pub(super) fn variant_keys(props: &Props) -> crate::style::StyleDirty {
[
&props.hover_style,
&props.press_style,
&props.focus_style,
&props.focus_visible_style,
]
.into_iter()
.flatten()
.fold(crate::style::StyleDirty::NONE, |keys, s| {
keys.union(s.keys())
})
}
pub(super) fn create_controlled_scroll(
bridge: &mut JsBridge,
ec: &mut EntityCommands,
id: NodeId,
props: &Props,
events: &UpdateEvents,
) {
if events.scroll_top.is_some() || events.scroll_left.is_some() {
let pos = Vec2::new(
events.scroll_left.unwrap_or(0.0),
events.scroll_top.unwrap_or(0.0),
);
ec.insert((
ScrollPosition(pos),
crate::scroll::PendingControlledScroll(pos),
));
bridge.scroll_positions.insert(id, pos);
} else if props.on_scroll {
bridge.scroll_positions.insert(id, Vec2::ZERO);
}
}
pub(super) fn update_controlled_scroll(
bridge: &mut JsBridge,
ec: &mut EntityCommands,
scroll_query: &mut Query<(
&mut ScrollPosition,
&ComputedNode,
Option<&mut ScrollTransitionState>,
)>,
e: Entity,
id: NodeId,
scroll_left: Option<f32>,
scroll_top: Option<f32>,
) {
if scroll_top.is_none() && scroll_left.is_none() {
return;
}
if let Ok((mut pos, computed, scroll_state)) = scroll_query.get_mut(e) {
let mut requested = crate::scroll::scroll_base(&pos, scroll_state.as_deref());
if let Some(x) = scroll_left {
requested.x = x;
}
if let Some(y) = scroll_top {
requested.y = y;
}
let clamped = requested.clamp(Vec2::ZERO, crate::scroll::scroll_range(computed));
crate::scroll::write_scroll(&mut pos, scroll_state, clamped);
if requested != clamped {
ec.insert(crate::scroll::PendingControlledScroll(requested));
}
bridge.scroll_positions.insert(id, requested);
}
}
#[cfg(test)]
mod tests;