use bevy::platform::collections::HashMap;
use bevy::prelude::*;
use bevy::ui::BorderColor;
use bevy_pf_xaml::value as v;
use crate::convert;
use crate::resources::PfValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ValueSource {
Default = 1,
Inherited = 2,
ThemeStyle = 3,
ThemeStyleTrigger = 4,
Style = 5,
TemplateTrigger = 6,
StyleTrigger = 7,
ImplicitReference = 8,
ParentTemplate = 9,
ParentTemplateTrigger = 10,
Local = 11,
Animation = 12,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PropertyTarget {
Background,
BorderBrush,
BorderThickness,
Foreground,
Fill,
FontSize,
Margin,
Padding,
CornerRadius,
Width,
Height,
Visibility,
Opacity,
}
pub fn property_target_for(property: &str) -> Option<PropertyTarget> {
Some(match property {
"Background" => PropertyTarget::Background,
"BorderBrush" => PropertyTarget::BorderBrush,
"BorderThickness" => PropertyTarget::BorderThickness,
"Foreground" => PropertyTarget::Foreground,
"Fill" => PropertyTarget::Fill,
"FontSize" => PropertyTarget::FontSize,
"Margin" => PropertyTarget::Margin,
"Padding" => PropertyTarget::Padding,
"CornerRadius" => PropertyTarget::CornerRadius,
"Width" => PropertyTarget::Width,
"Height" => PropertyTarget::Height,
"Opacity" => PropertyTarget::Opacity,
"Visibility" => PropertyTarget::Visibility,
_ => return None,
})
}
pub type StoredValue = Option<PfValue>;
#[derive(Component, Debug, Default, Clone)]
pub struct PfPropertyStore {
entries: HashMap<PropertyTarget, Vec<(ValueSource, StoredValue)>>,
}
impl PfPropertyStore {
pub fn set(&mut self, target: PropertyTarget, source: ValueSource, value: StoredValue) {
let slot = self.entries.entry(target).or_default();
slot.retain(|(s, _)| *s != source);
slot.push((source, value));
}
pub fn clear(&mut self, target: PropertyTarget, source: ValueSource) {
if let Some(slot) = self.entries.get_mut(&target) {
slot.retain(|(s, _)| *s != source);
}
}
pub fn clear_tier(&mut self, source: ValueSource) -> Vec<PropertyTarget> {
let mut affected = Vec::new();
for (target, slot) in self.entries.iter_mut() {
let before = slot.len();
slot.retain(|(s, _)| *s != source);
if slot.len() != before {
affected.push(*target);
}
}
affected
}
pub fn effective(&self, target: PropertyTarget) -> Option<&(ValueSource, StoredValue)> {
self.entries
.get(&target)?
.iter()
.max_by_key(|(s, _)| *s)
}
pub fn effective_source(&self, target: PropertyTarget) -> Option<ValueSource> {
self.effective(target).map(|(s, _)| *s)
}
pub fn effective_below(
&self,
target: PropertyTarget,
ceiling: ValueSource,
) -> Option<&(ValueSource, StoredValue)> {
self.entries
.get(&target)?
.iter()
.filter(|(s, _)| *s < ceiling)
.max_by_key(|(s, _)| *s)
}
}
pub fn set_local(
world: &mut World,
entity: Entity,
target: PropertyTarget,
value: PfValue,
) {
store_and_apply(world, entity, target, ValueSource::Local, Some(value));
}
pub fn store_and_apply(
world: &mut World,
entity: Entity,
target: PropertyTarget,
source: ValueSource,
value: StoredValue,
) {
{
let Ok(mut e) = world.get_entity_mut(entity) else {
return;
};
if let Some(mut store) = e.get_mut::<PfPropertyStore>() {
store.set(target, source, value);
} else {
let mut store = PfPropertyStore::default();
store.set(target, source, value);
e.insert(store);
}
}
apply_effective(world, entity, target);
}
pub fn apply_effective(world: &mut World, entity: Entity, target: PropertyTarget) {
let value = world
.get::<PfPropertyStore>(entity)
.and_then(|s| s.effective(target).cloned());
match value {
Some((_, Some(v))) => {
apply_value(world, entity, target, &v);
after_apply_value(world, entity, target);
}
Some((_, None)) | None => apply_unset(world, entity, target),
}
forward_template_dependents(world, entity, target);
}
fn forward_template_dependents(world: &mut World, entity: Entity, target: PropertyTarget) {
let Some(deps) = world.get::<PfTemplateBindingDependents>(entity) else {
return;
};
let matching: Vec<(Entity, PropertyTarget)> = deps
.0
.iter()
.filter(|(src, child, _)| *src == target && *child != entity)
.map(|(_, child, dst)| (*child, *dst))
.collect();
if matching.is_empty() {
return;
}
let value = world
.get::<PfPropertyStore>(entity)
.and_then(|s| s.effective(target))
.map(|(_, v)| v.clone());
let mut stale: Vec<Entity> = Vec::new();
for (child, dst) in matching {
if world.get_entity(child).is_err() {
stale.push(child);
continue;
}
match &value {
Some(stored) => {
store_and_apply(world, child, dst, ValueSource::ParentTemplate, stored.clone());
}
None => {
if let Some(mut store) = world.get_mut::<PfPropertyStore>(child) {
store.clear(dst, ValueSource::ParentTemplate);
}
apply_effective(world, child, dst);
}
}
}
if !stale.is_empty()
&& let Some(mut deps) = world.get_mut::<PfTemplateBindingDependents>(entity)
{
deps.0.retain(|(_, child, _)| !stale.contains(child));
}
}
pub(crate) fn collect_text_entities(world: &World, root: Entity) -> Vec<Entity> {
if world.get::<bevy::text::TextColor>(root).is_some() {
return vec![root];
}
let mut out = Vec::new();
let mut stack: Vec<Entity> = world
.get::<Children>(root)
.map(|c| c.iter().collect())
.unwrap_or_default();
while let Some(e) = stack.pop() {
if world.get::<bevy::text::TextColor>(e).is_some() {
out.push(e);
}
if let Some(children) = world.get::<Children>(e) {
stack.extend(children.iter());
}
}
out
}
#[derive(Component, Debug, Default)]
pub struct PfOpacity {
pub value: f32,
originals: bevy::platform::collections::HashMap<Entity, OriginalAlphas>,
}
#[derive(Debug, Clone, Copy, Default)]
struct OriginalAlphas {
bg: Option<f32>,
border: Option<[f32; 4]>,
cursor: Option<[f32; 2]>,
text: Option<f32>,
}
#[derive(Resource, Default)]
pub(crate) struct PfOpacityCount(pub(crate) usize);
fn set_alpha(color: &mut Color, alpha: f32) {
let mut c = color.to_srgba();
c.alpha = alpha;
*color = Color::Srgba(c);
}
fn apply_subtree_opacity(world: &mut World, root: Entity, value: f32) {
let had = world.get::<PfOpacity>(root).is_some();
let mut state = world
.entity_mut(root)
.take::<PfOpacity>()
.unwrap_or_default();
if !had {
let mut count = world.get_resource_or_insert_with(PfOpacityCount::default);
count.0 += 1;
}
state.value = value;
let mut stack = vec![root];
while let Some(e) = stack.pop() {
let orig = state.originals.entry(e).or_default();
if let Some(mut bg) = world.get_mut::<bevy::ui::BackgroundColor>(e) {
let base = *orig.bg.get_or_insert_with(|| bg.0.alpha());
set_alpha(&mut bg.0, base * value);
}
if let Some(mut border) = world.get_mut::<bevy::ui::BorderColor>(e) {
let base = *orig.border.get_or_insert([
border.top.alpha(),
border.right.alpha(),
border.bottom.alpha(),
border.left.alpha(),
]);
set_alpha(&mut border.top, base[0] * value);
set_alpha(&mut border.right, base[1] * value);
set_alpha(&mut border.bottom, base[2] * value);
set_alpha(&mut border.left, base[3] * value);
}
if let Some(mut text) = world.get_mut::<bevy::text::TextColor>(e) {
let base = *orig.text.get_or_insert_with(|| text.0.alpha());
set_alpha(&mut text.0, base * value);
}
if let Some(mut cursor) = world.get_mut::<bevy::text::TextCursorStyle>(e) {
let base = *orig
.cursor
.get_or_insert([cursor.color.alpha(), cursor.selection_color.alpha()]);
set_alpha(&mut cursor.color, base[0] * value);
set_alpha(&mut cursor.selection_color, base[1] * value);
}
if let Some(children) = world.get::<Children>(e) {
stack.extend(children.iter());
}
}
world.entity_mut(root).insert(state);
}
pub(crate) fn reapply_opacity(world: &mut World, root: Entity) {
if let Some(value) = world.get::<PfOpacity>(root).map(|o| o.value) {
apply_subtree_opacity(world, root, value);
}
}
fn unset_subtree_opacity(world: &mut World, root: Entity) {
let Some(state) = world.entity_mut(root).take::<PfOpacity>() else {
return;
};
if let Some(mut count) = world.get_resource_mut::<PfOpacityCount>() {
count.0 = count.0.saturating_sub(1);
}
for (e, orig) in state.originals {
if world.get_entity(e).is_err() {
continue;
}
if let (Some(alpha), Some(mut bg)) = (orig.bg, world.get_mut::<bevy::ui::BackgroundColor>(e)) {
set_alpha(&mut bg.0, alpha);
}
if let Some(sides) = orig.border
&& let Some(mut border) = world.get_mut::<bevy::ui::BorderColor>(e)
{
set_alpha(&mut border.top, sides[0]);
set_alpha(&mut border.right, sides[1]);
set_alpha(&mut border.bottom, sides[2]);
set_alpha(&mut border.left, sides[3]);
}
if let (Some(alpha), Some(mut text)) = (orig.text, world.get_mut::<bevy::text::TextColor>(e)) {
set_alpha(&mut text.0, alpha);
}
}
}
fn rescale_after_color_writes(world: &mut World, entity: Entity, changed: &[Entity]) {
if world
.get_resource::<PfOpacityCount>()
.is_none_or(|c| c.0 == 0)
{
return;
}
let mut holder = entity;
loop {
if world.get::<PfOpacity>(holder).is_some() {
break;
}
match world.get::<ChildOf>(holder) {
Some(parent) => holder = parent.parent(),
None => return,
}
}
let value = world.get::<PfOpacity>(holder).map(|o| o.value).unwrap_or(1.0);
if let Some(mut state) = world.entity_mut(holder).take::<PfOpacity>() {
for e in changed {
state.originals.remove(e);
}
world.entity_mut(holder).insert(state);
}
apply_subtree_opacity(world, holder, value);
}
#[derive(Component, Debug, Default)]
pub struct PfTemplateBindingDependents(
pub Vec<(PropertyTarget, Entity, PropertyTarget)>,
);
pub fn is_template_consumed(target: PropertyTarget) -> bool {
matches!(
target,
PropertyTarget::Background
| PropertyTarget::BorderBrush
| PropertyTarget::BorderThickness
| PropertyTarget::Padding
| PropertyTarget::CornerRadius
)
}
fn template_suppressed(world: &World, entity: Entity, target: PropertyTarget) -> bool {
is_template_consumed(target)
&& world
.get::<crate::components::PfTemplatedControl>(entity)
.is_some()
}
pub(crate) fn mark_background_assigned(world: &mut World, entity: Entity, assigned: bool) {
let governed = world
.get::<crate::components::PfElementKind>(entity)
.is_some_and(|k| crate::hit_test::background_governs_hit_testing(&k.0));
let mut e = world.entity_mut(entity);
if assigned {
e.insert(crate::hit_test::PfBackgroundSet);
if governed {
e.insert(bevy::picking::Pickable::default());
}
} else {
e.remove::<crate::hit_test::PfBackgroundSet>();
if governed {
e.insert(bevy::picking::Pickable::IGNORE);
}
}
}
pub(crate) fn apply_value(world: &mut World, entity: Entity, target: PropertyTarget, value: &PfValue) {
if template_suppressed(world, entity, target) {
return;
}
let as_brush = || -> Option<v::PfBrush> {
match value {
PfValue::Brush(b) => Some(b.clone()),
PfValue::Color(c) => Some(v::PfBrush::Solid(*c)),
PfValue::String(s) => s.parse().ok(),
_ => None,
}
};
let as_f32 = || -> Option<f32> {
match value {
PfValue::Double(d) => Some(*d as f32),
PfValue::String(s) => s.trim().parse().ok(),
_ => None,
}
};
let as_thickness = || -> Option<v::Thickness> {
match value {
PfValue::Thickness(t) => Some(*t),
PfValue::Double(d) => Some(v::Thickness::uniform(*d as f32)),
PfValue::String(s) => s.parse().ok(),
_ => None,
}
};
match target {
PropertyTarget::Background => {
let Some(brush) = as_brush() else { return };
mark_background_assigned(world, entity, true);
match convert::brush_to_background(&brush) {
Ok(bg) => {
if let Some(mut visual) =
world.get_mut::<crate::components::ButtonVisual>(entity)
{
visual.normal_bg = bg.0;
}
world
.entity_mut(entity)
.remove::<bevy::ui::BackgroundGradient>()
.insert(bg);
}
Err(gradient) => {
world.entity_mut(entity).insert(gradient);
}
}
}
PropertyTarget::BorderBrush => {
if let Some(v::PfBrush::Solid(c)) = as_brush() {
let color = convert::color(c);
if let Some(mut visual) =
world.get_mut::<crate::components::ButtonVisual>(entity)
{
visual.normal_border = color;
}
world.entity_mut(entity).insert(BorderColor::all(color));
}
}
PropertyTarget::BorderThickness => {
if let Some(t) = as_thickness()
&& let Some(mut node) = world.get_mut::<Node>(entity) {
node.border = convert::thickness(t);
}
}
PropertyTarget::Fill => {
if let Some(brush) = as_brush() {
let brush = brush.clone();
let mut e = world.entity_mut(entity);
if let Some(mut shape) = e.get_mut::<crate::shapes::PfShape>() {
shape.fill = Some(brush);
e.remove::<crate::shapes::PfShapeRendered>();
}
}
}
PropertyTarget::Foreground => {
if let Some(v::PfBrush::Solid(c)) = as_brush() {
let color = convert::color(c);
for text_entity in collect_text_entities(world, entity) {
world
.entity_mut(text_entity)
.insert(bevy::text::TextColor(color));
}
}
}
PropertyTarget::FontSize => {
let Some(px) = as_f32() else { return };
for text_entity in collect_text_entities(world, entity) {
if let Some(mut font) = world.get_mut::<bevy::text::TextFont>(text_entity) {
font.font_size = bevy::text::FontSize::Px(px);
}
}
}
PropertyTarget::Margin => {
if let Some(t) = as_thickness()
&& let Some(mut node) = world.get_mut::<Node>(entity) {
node.margin = convert::thickness(t);
}
}
PropertyTarget::Padding => {
if let Some(t) = as_thickness()
&& let Some(mut node) = world.get_mut::<Node>(entity) {
node.padding = convert::thickness(t);
}
}
PropertyTarget::CornerRadius => {
let radius = match value {
PfValue::CornerRadius(r) => Some(*r),
PfValue::Double(d) => Some(v::CornerRadius::uniform(*d as f32)),
PfValue::String(s) => s.parse().ok(),
_ => None,
};
if let Some(r) = radius
&& let Some(mut node) = world.get_mut::<Node>(entity) {
node.border_radius = convert::corner_radius(r);
}
}
PropertyTarget::Width | PropertyTarget::Height => {
let Some(px) = as_f32() else { return };
if let Some(mut node) = world.get_mut::<Node>(entity) {
let dim = convert::dimension(px);
if target == PropertyTarget::Width {
node.width = dim;
node.min_width = dim;
node.max_width = dim;
} else {
node.height = dim;
node.min_height = dim;
node.max_height = dim;
}
}
}
PropertyTarget::Opacity => {
let Some(v) = as_f32() else { return };
apply_subtree_opacity(world, entity, v.clamp(0.0, 1.0));
}
PropertyTarget::Visibility => {
let vis = match value {
PfValue::String(s) => s.parse::<v::Visibility>().ok(),
PfValue::Bool(b) => Some(if *b {
v::Visibility::Visible
} else {
v::Visibility::Collapsed
}),
_ => None,
};
if let Some(vis) = vis {
apply_wpf_visibility(world, entity, vis);
}
}
}
}
pub(crate) fn apply_wpf_visibility(world: &mut World, entity: Entity, vis: v::Visibility) {
use crate::components::PfCollapsedDisplay;
let (visibility, display) = convert::visibility(vis);
world.entity_mut(entity).insert(visibility);
if display == Some(Display::None) {
let current = world.get::<Node>(entity).map(|n| n.display);
if let Some(current) = current
&& current != Display::None
{
world.entity_mut(entity).insert(PfCollapsedDisplay(current));
}
if let Some(mut node) = world.get_mut::<Node>(entity) {
node.display = Display::None;
}
} else {
let saved = world
.entity_mut(entity)
.take::<PfCollapsedDisplay>()
.map(|s| s.0);
if let Some(mut node) = world.get_mut::<Node>(entity)
&& node.display == Display::None
{
node.display = saved.unwrap_or(Display::DEFAULT);
}
}
}
fn after_apply_value(world: &mut World, entity: Entity, target: PropertyTarget) {
let changed: Vec<Entity> = match target {
PropertyTarget::Background | PropertyTarget::BorderBrush => vec![entity],
PropertyTarget::Foreground => collect_text_entities(world, entity),
_ => Vec::new(),
};
if !changed.is_empty() {
rescale_after_color_writes(world, entity, &changed);
}
}
fn apply_unset(world: &mut World, entity: Entity, target: PropertyTarget) {
if template_suppressed(world, entity, target) {
return;
}
match target {
PropertyTarget::Fill => {
let mut e = world.entity_mut(entity);
if let Some(mut shape) = e.get_mut::<crate::shapes::PfShape>() {
shape.fill = None;
e.remove::<crate::shapes::PfShapeRendered>();
}
}
PropertyTarget::Background => {
mark_background_assigned(world, entity, false);
world
.entity_mut(entity)
.remove::<bevy::ui::BackgroundGradient>()
.insert(BackgroundColor(Color::NONE));
}
PropertyTarget::BorderBrush => {
world
.entity_mut(entity)
.insert(BorderColor::all(Color::NONE));
}
PropertyTarget::BorderThickness => {
if let Some(mut node) = world.get_mut::<Node>(entity) {
node.border = UiRect::ZERO;
}
}
PropertyTarget::Margin => {
if let Some(mut node) = world.get_mut::<Node>(entity) {
node.margin = UiRect::ZERO;
}
}
PropertyTarget::Padding => {
if let Some(mut node) = world.get_mut::<Node>(entity) {
node.padding = UiRect::ZERO;
}
}
PropertyTarget::Opacity => {
unset_subtree_opacity(world, entity);
}
PropertyTarget::CornerRadius => {
if let Some(mut node) = world.get_mut::<Node>(entity) {
node.border_radius = BorderRadius::ZERO;
}
}
PropertyTarget::Width => {
if let Some(mut node) = world.get_mut::<Node>(entity) {
node.width = Val::Auto;
node.min_width = Val::Auto;
node.max_width = Val::Auto;
}
}
PropertyTarget::Height => {
if let Some(mut node) = world.get_mut::<Node>(entity) {
node.height = Val::Auto;
node.min_height = Val::Auto;
node.max_height = Val::Auto;
}
}
PropertyTarget::Visibility => {
apply_wpf_visibility(world, entity, v::Visibility::Visible);
}
PropertyTarget::Foreground | PropertyTarget::FontSize => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn precedence_order_is_wpf_verbatim() {
assert!(ValueSource::Local > ValueSource::ParentTemplateTrigger);
assert!(ValueSource::ParentTemplateTrigger > ValueSource::ParentTemplate);
assert!(ValueSource::ParentTemplate > ValueSource::ImplicitReference);
assert!(ValueSource::ImplicitReference > ValueSource::StyleTrigger);
assert!(ValueSource::StyleTrigger > ValueSource::TemplateTrigger);
assert!(ValueSource::TemplateTrigger > ValueSource::Style);
assert!(ValueSource::Style > ValueSource::ThemeStyleTrigger);
assert!(ValueSource::ThemeStyleTrigger > ValueSource::ThemeStyle);
assert!(ValueSource::ThemeStyle > ValueSource::Inherited);
assert!(ValueSource::Inherited > ValueSource::Default);
}
#[test]
fn store_effective_and_revert() {
let mut store = PfPropertyStore::default();
let bg = PropertyTarget::Background;
let red = Some(PfValue::String("Red".into()));
let blue = Some(PfValue::String("Blue".into()));
let green = Some(PfValue::String("Green".into()));
store.set(bg, ValueSource::Style, red.clone());
assert_eq!(store.effective_source(bg), Some(ValueSource::Style));
store.set(bg, ValueSource::StyleTrigger, blue.clone());
assert_eq!(store.effective_source(bg), Some(ValueSource::StyleTrigger));
store.set(bg, ValueSource::Local, green.clone());
assert_eq!(store.effective_source(bg), Some(ValueSource::Local));
store.clear(bg, ValueSource::Local);
assert_eq!(store.effective_source(bg), Some(ValueSource::StyleTrigger));
store.clear(bg, ValueSource::StyleTrigger);
assert_eq!(store.effective_source(bg), Some(ValueSource::Style));
store.clear(bg, ValueSource::Style);
assert_eq!(store.effective_source(bg), None);
}
#[test]
fn same_tier_overwrites() {
let mut store = PfPropertyStore::default();
let bg = PropertyTarget::Background;
store.set(bg, ValueSource::StyleTrigger, Some(PfValue::Bool(true)));
store.set(bg, ValueSource::StyleTrigger, Some(PfValue::Bool(false)));
let (_, v) = store.effective(bg).unwrap();
assert!(matches!(v, Some(PfValue::Bool(false))));
}
#[test]
fn explicit_null_masks_lower_tiers() {
let mut store = PfPropertyStore::default();
let bg = PropertyTarget::Background;
store.set(bg, ValueSource::Style, Some(PfValue::Bool(true)));
store.set(bg, ValueSource::StyleTrigger, None); let (source, v) = store.effective(bg).unwrap();
assert_eq!(*source, ValueSource::StyleTrigger);
assert!(v.is_none());
}
#[test]
fn clear_tier_reports_affected() {
let mut store = PfPropertyStore::default();
store.set(
PropertyTarget::Background,
ValueSource::StyleTrigger,
Some(PfValue::Bool(true)),
);
store.set(
PropertyTarget::Width,
ValueSource::StyleTrigger,
Some(PfValue::Double(1.0)),
);
store.set(PropertyTarget::Height, ValueSource::Local, Some(PfValue::Double(2.0)));
let mut affected = store.clear_tier(ValueSource::StyleTrigger);
affected.sort_by_key(|t| format!("{t:?}"));
assert_eq!(affected.len(), 2);
assert_eq!(store.effective_source(PropertyTarget::Height), Some(ValueSource::Local));
}
}