use bevy::asset::AssetPlugin;
use bevy::prelude::*;
use bevy_pf::prelude::*;
use bevy_pf::resources::{
PfSetterValue, PfTriggerValue, PfValue, ResourceDictionary, ResourceScopes,
parse_resource_value,
};
use bevy_pf::{XamlEnv, instantiate_document_env};
fn parse_value(xaml: &str) -> (Option<PfValue>, Vec<String>) {
let doc = bevy_pf_xaml::parse(xaml).expect("parses");
let mut warnings = Vec::new();
let value = parse_resource_value(
&doc.root,
&ResourceScopes::default(),
&ResourceDictionary::new(),
&mut warnings,
)
.expect("no hard error");
(value, warnings)
}
fn test_app() -> App {
let mut app = App::new();
app.add_plugins((MinimalPlugins, AssetPlugin::default()));
app.add_plugins(PfUiPlugin);
app
}
fn spawn_collect_warnings(app: &mut App, xaml: &str) -> (Entity, Vec<String>) {
let doc = bevy_pf_xaml::parse(xaml).expect("parses");
let world = app.world_mut();
let root = world.spawn_empty().id();
let result =
instantiate_document_env(world, root, &doc, &XamlEnv::default()).expect("instantiates");
(root, result.warnings)
}
#[test]
fn keyed_control_template_parses_with_triggers_and_target_name() {
let (value, warnings) = parse_value(
r##"<ControlTemplate xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Key="T" TargetType="Button">
<Border x:Name="border" Background="#FFDDDDDD">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#FFBEE6FD"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Background" Value="#FFC4E5F6"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>"##,
);
assert_eq!(warnings, Vec::<String>::new());
let Some(PfValue::ControlTemplate(t)) = value else {
panic!("expected ControlTemplate, got {value:?}");
};
assert_eq!(t.target_type.as_deref(), Some("Button"));
assert_eq!(t.root.name, "Border");
assert_eq!(t.triggers.len(), 2);
assert_eq!(t.triggers[0].setters[0].target_name, None);
assert_eq!(
t.triggers[1].setters[0].target_name.as_deref(),
Some("border")
);
}
#[test]
fn style_setter_template_carries_control_template() {
for property in ["Template", "Control.Template"] {
let (value, warnings) = parse_value(&format!(
r##"<Style xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
TargetType="Button">
<Setter Property="{property}">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border><ContentPresenter/></Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>"##
));
assert_eq!(warnings, Vec::<String>::new(), "{property}");
let Some(PfValue::Style(style)) = value else {
panic!("expected Style");
};
assert_eq!(style.setters.len(), 1);
let setter = &style.setters[0];
assert_eq!(setter.owner, None, "{property} normalizes to unqualified");
assert_eq!(setter.property, "Template");
assert!(
matches!(
setter.value,
PfSetterValue::Value(PfValue::ControlTemplate(_))
),
"{property}: got {:?}",
setter.value
);
}
}
#[test]
fn template_kinds_reject_each_other() {
let mut app = test_app();
let (_, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<ControlTemplate x:Key="CT" TargetType="Button">
<Border/>
</ControlTemplate>
<DataTemplate x:Key="DT">
<TextBlock Text="row"/>
</DataTemplate>
</StackPanel.Resources>
<ItemsControl ItemTemplate="{StaticResource CT}"/>
<Button Template="{StaticResource DT}" Content="x"/>
</StackPanel>"##,
);
assert_eq!(warnings.len(), 2, "one rejection each: {warnings:?}");
assert!(warnings.iter().any(|w| w.contains("got a ControlTemplate")));
assert!(warnings.iter().any(|w| w.contains("got a DataTemplate")));
}
#[test]
fn style_template_delivery_is_warning_free() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border><ContentPresenter/></Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>
<Button x:Name="B" Content="hello"/>
</StackPanel>"##,
);
assert_eq!(warnings, Vec::<String>::new());
let button = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("B")
.unwrap();
assert!(
app.world().get::<bevy_pf::ButtonVisual>(button).is_none(),
"style-delivered template replaced the default chrome"
);
assert!(
app.world()
.get::<bevy_pf::components::PfTemplatedControl>(button)
.is_some()
);
}
#[test]
fn x_type_keys_meet_static_resource_type_lookups() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<Style x:Key="{x:Type TextBoxBase}" TargetType="TextBox">
<Setter Property="Foreground" Value="#FF112233"/>
</Style>
<Style x:Key="Derived" TargetType="TextBox"
BasedOn="{StaticResource {x:Type TextBoxBase}}">
<Setter Property="FontSize" Value="15"/>
</Style>
</StackPanel.Resources>
<TextBox x:Name="T" Style="{StaticResource Derived}"/>
</StackPanel>"##,
);
assert_eq!(
warnings,
Vec::<String>::new(),
"key spaces meet: {warnings:?}"
);
let root_names = app.world().get::<XamlNames>(root).unwrap();
let tb = root_names.get("T").unwrap();
let input = app
.world()
.get::<Children>(tb)
.and_then(|c| c.iter().next())
.unwrap();
let color = app.world().get::<bevy::text::TextColor>(input).unwrap().0;
assert_eq!(
bevy_pf::instantiate::color_to_hex(color),
"#112233",
"BasedOn {{x:Type}} base style applied"
);
}
#[test]
fn x_null_trigger_value_parses_and_skips_at_attach() {
let (value, warnings) = parse_value(
r##"<Style xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
TargetType="CheckBox">
<Style.Triggers>
<Trigger Property="IsChecked" Value="{x:Null}">
<Setter Property="Foreground" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>"##,
);
assert_eq!(warnings, Vec::<String>::new());
let Some(PfValue::Style(style)) = value else {
panic!("expected Style");
};
assert_eq!(style.triggers.len(), 1, "trigger retained at parse");
let bevy_pf::resources::PfTriggerCondition::Property { value, .. } =
&style.triggers[0].conditions[0]
else {
panic!("expected property condition");
};
assert_eq!(*value, PfTriggerValue::Null);
let mut app = test_app();
let (_, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<Style TargetType="CheckBox">
<Style.Triggers>
<Trigger Property="IsChecked" Value="{x:Null}">
<Setter Property="Foreground" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
</StackPanel.Resources>
<CheckBox Content="tri-state"/>
</StackPanel>"##,
);
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("three-state"), "{warnings:?}");
}
#[test]
fn style_trigger_target_name_still_skips_with_warning() {
let (value, warnings) = parse_value(
r##"<Style xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
TargetType="Button">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>"##,
);
assert!(matches!(value, Some(PfValue::Style(_))));
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("only valid in ControlTemplate"));
}
fn texts_in(app: &App, root: Entity) -> Vec<String> {
let mut out = Vec::new();
let mut stack = vec![root];
while let Some(e) = stack.pop() {
if let Some(t) = app.world().get::<bevy::ui::widget::Text>(e) {
out.push(t.0.clone());
}
if let Some(children) = app.world().get::<Children>(e) {
stack.extend(children.iter());
}
}
out
}
const BUTTON_TEMPLATE_PAGE: &str = r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Button x:Name="B" Content="Hi">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border x:Name="border" Background="Red" BorderThickness="2" CornerRadius="6">
<ContentPresenter/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</StackPanel>"##;
#[test]
fn inline_template_replaces_button_chrome() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(&mut app, BUTTON_TEMPLATE_PAGE);
assert_eq!(warnings, Vec::<String>::new());
let button = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("B")
.unwrap();
assert!(app.world().get::<bevy_pf::ButtonVisual>(button).is_none());
assert!(app.world().get::<BackgroundColor>(button).is_none());
let node = app.world().get::<Node>(button).unwrap();
assert_eq!(node.padding, UiRect::ZERO);
assert_eq!(node.border, UiRect::ZERO);
assert!(app.world().get::<Interaction>(button).is_some());
let templated = app
.world()
.get::<bevy_pf::components::PfTemplatedControl>(button)
.expect("marker present");
let border = templated.template_root;
assert_eq!(
app.world().get::<bevy_pf::PfElementKind>(border).unwrap().0,
"Border"
);
let bg = app.world().get::<BackgroundColor>(border).unwrap().0;
assert_eq!(bevy_pf::instantiate::color_to_hex(bg), "#FF0000");
let store = app
.world()
.get::<bevy_pf::PfPropertyStore>(border)
.expect("template child has a store");
assert_eq!(
store.effective_source(bevy_pf::provider::PropertyTarget::Background),
Some(bevy_pf::provider::ValueSource::ParentTemplate)
);
assert!(texts_in(&app, border).contains(&"Hi".to_string()));
assert!(
app.world()
.get::<XamlNames>(root)
.unwrap()
.get("border")
.is_none()
);
}
#[test]
fn style_delivered_template_expands_identically() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border" Background="#336699">
<ContentPresenter/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>
<Button x:Name="A" Content="one"/>
<Button x:Name="B" Content="two"/>
</StackPanel>"##,
);
assert_eq!(warnings, Vec::<String>::new());
let names = app.world().get::<XamlNames>(root).unwrap();
let (a, b) = (names.get("A").unwrap(), names.get("B").unwrap());
let ra = app
.world()
.get::<bevy_pf::components::PfTemplatedControl>(a)
.unwrap()
.template_root;
let rb = app
.world()
.get::<bevy_pf::components::PfTemplatedControl>(b)
.unwrap()
.template_root;
assert_ne!(ra, rb, "each expansion is its own subtree");
assert!(texts_in(&app, ra).contains(&"one".to_string()));
assert!(texts_in(&app, rb).contains(&"two".to_string()));
assert!(names.get("border").is_none(), "shared name never leaks");
}
#[test]
fn projected_content_keeps_page_namescope() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Button x:Name="B">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border><ContentPresenter/></Border>
</ControlTemplate>
</Button.Template>
<Button.Content>
<TextBlock x:Name="inner" Text="projected"/>
</Button.Content>
</Button>
</StackPanel>"##,
);
assert_eq!(warnings, Vec::<String>::new());
let names = app.world().get::<XamlNames>(root).unwrap();
let inner = names
.get("inner")
.expect("projected content x:Name is page-scoped");
assert_eq!(
app.world().get::<bevy::ui::widget::Text>(inner).unwrap().0,
"projected"
);
}
#[test]
fn templated_checkbox_keeps_behavior_loses_box_chrome() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<CheckBox x:Name="C" Content="opt" IsChecked="True">
<CheckBox.Template>
<ControlTemplate TargetType="CheckBox">
<Border Background="#EEEEEE"><ContentPresenter/></Border>
</ControlTemplate>
</CheckBox.Template>
</CheckBox>
</StackPanel>"##,
);
assert_eq!(warnings, Vec::<String>::new());
let cb = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("C")
.unwrap();
assert!(
app.world()
.get::<bevy_pf::components::PfCheckVisual>(cb)
.is_none(),
"box/glyph chrome replaced"
);
assert!(
app.world().get::<Interaction>(cb).is_some(),
"behavior kept"
);
assert!(
app.world().get::<bevy::ui::Checked>(cb).is_some(),
"IsChecked seed kept"
);
let templated = app
.world()
.get::<bevy_pf::components::PfTemplatedControl>(cb)
.unwrap();
assert!(texts_in(&app, templated.template_root).contains(&"opt".to_string()));
}
#[test]
fn template_consumed_paint_is_suppressed_on_root() {
let mut app = test_app();
let (root, _) = spawn_collect_warnings(&mut app, BUTTON_TEMPLATE_PAGE);
let button = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("B")
.unwrap();
bevy_pf::provider::set_local(
app.world_mut(),
button,
bevy_pf::provider::PropertyTarget::Background,
bevy_pf::resources::PfValue::Color(bevy_pf::xaml_ast::value::PfColor::rgb(0, 0, 255)),
);
let store = app.world().get::<bevy_pf::PfPropertyStore>(button).unwrap();
assert_eq!(
store.effective_source(bevy_pf::provider::PropertyTarget::Background),
Some(bevy_pf::provider::ValueSource::Local)
);
assert!(app.world().get::<BackgroundColor>(button).is_none());
bevy_pf::provider::set_local(
app.world_mut(),
button,
bevy_pf::provider::PropertyTarget::Padding,
bevy_pf::resources::PfValue::Thickness(bevy_pf::xaml_ast::value::Thickness::uniform(9.0)),
);
assert_eq!(
app.world().get::<Node>(button).unwrap().padding,
UiRect::ZERO
);
}
#[test]
fn templated_textbox_expands_and_strips_chrome() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<TextBox x:Name="T" Text="abc">
<TextBox.Template>
<ControlTemplate TargetType="TextBox"><Border/></ControlTemplate>
</TextBox.Template>
</TextBox>
</StackPanel>"##,
);
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("PART_ContentHost"));
let tb = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("T")
.unwrap();
assert!(
app.world().get::<BackgroundColor>(tb).is_none(),
"chrome stripped"
);
let node = app.world().get::<Node>(tb).unwrap();
assert_eq!(node.padding, UiRect::ZERO);
assert!(node.min_width != Val::Auto, "sizing defaults retained");
assert!(
app.world()
.get::<bevy_pf::components::PfTemplatedControl>(tb)
.is_some()
);
}
#[test]
fn nested_expansions_do_not_cross_contaminate() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<Style TargetType="CheckBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<Border x:Name="inner-chrome" Background="#DDEEFF">
<ContentPresenter/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>
<Button x:Name="B" Content="unused">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border x:Name="outer-chrome" Background="#112233">
<CheckBox Content="nested"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</StackPanel>"##,
);
assert_eq!(warnings, Vec::<String>::new());
let names = app.world().get::<XamlNames>(root).unwrap();
assert!(names.get("outer-chrome").is_none());
assert!(names.get("inner-chrome").is_none());
let button = names.get("B").unwrap();
let outer = app
.world()
.get::<bevy_pf::components::PfTemplatedControl>(button)
.unwrap()
.template_root;
assert!(texts_in(&app, outer).contains(&"nested".to_string()));
}
const TB_PAGE: &str = r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="#008000"/>
<Setter Property="Padding" Value="8"/>
<Setter Property="BorderThickness" Value="3"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter Margin="{TemplateBinding Padding}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>
<Button x:Name="B" Content="Go"/>
</StackPanel>"##;
fn tb_setup(app: &mut App) -> (Entity, Entity, Entity) {
let (root, warnings) = spawn_collect_warnings(app, TB_PAGE);
assert_eq!(warnings, Vec::<String>::new());
let button = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("B")
.unwrap();
let border = app
.world()
.get::<bevy_pf::components::PfTemplatedControl>(button)
.unwrap()
.template_root;
let presenter = app
.world()
.get::<Children>(border)
.and_then(|c| c.iter().next())
.unwrap();
(button, border, presenter)
}
#[test]
fn template_binding_forwards_initial_values_without_double_paint() {
let mut app = test_app();
let (button, border, presenter) = tb_setup(&mut app);
let bg = app.world().get::<BackgroundColor>(border).unwrap().0;
assert_eq!(bevy_pf::instantiate::color_to_hex(bg), "#008000");
assert_eq!(
app.world().get::<Node>(border).unwrap().border,
UiRect::all(Val::Px(3.0))
);
assert_eq!(
app.world().get::<Node>(presenter).unwrap().margin,
UiRect::all(Val::Px(8.0))
);
let store = app.world().get::<bevy_pf::PfPropertyStore>(border).unwrap();
assert_eq!(
store.effective_source(bevy_pf::provider::PropertyTarget::Background),
Some(bevy_pf::provider::ValueSource::ParentTemplate)
);
assert!(app.world().get::<BackgroundColor>(button).is_none());
let node = app.world().get::<Node>(button).unwrap();
assert_eq!(node.padding, UiRect::ZERO);
assert_eq!(node.border, UiRect::ZERO);
}
#[test]
fn template_binding_stays_live_and_reverts() {
let mut app = test_app();
let (button, border, _) = tb_setup(&mut app);
bevy_pf::provider::set_local(
app.world_mut(),
button,
bevy_pf::provider::PropertyTarget::Background,
bevy_pf::resources::PfValue::Color(bevy_pf::xaml_ast::value::PfColor::rgb(0, 0, 255)),
);
let bg = app.world().get::<BackgroundColor>(border).unwrap().0;
assert_eq!(bevy_pf::instantiate::color_to_hex(bg), "#0000FF");
assert!(app.world().get::<BackgroundColor>(button).is_none());
app.world_mut()
.get_mut::<bevy_pf::PfPropertyStore>(button)
.unwrap()
.clear(
bevy_pf::provider::PropertyTarget::Background,
bevy_pf::provider::ValueSource::Local,
);
bevy_pf::provider::apply_effective(
app.world_mut(),
button,
bevy_pf::provider::PropertyTarget::Background,
);
let bg = app.world().get::<BackgroundColor>(border).unwrap().0;
assert_eq!(bevy_pf::instantiate::color_to_hex(bg), "#008000");
}
#[test]
fn template_literal_paints_when_parent_value_is_unbound() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Button x:Name="B" Background="#008000" Content="x">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="Yellow"><ContentPresenter/></Border>
</ControlTemplate>
</Button.Template>
</Button>
</StackPanel>"##,
);
assert_eq!(warnings, Vec::<String>::new());
let button = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("B")
.unwrap();
let border = app
.world()
.get::<bevy_pf::components::PfTemplatedControl>(button)
.unwrap()
.template_root;
let bg = app.world().get::<BackgroundColor>(border).unwrap().0;
assert_eq!(bevy_pf::instantiate::color_to_hex(bg), "#FFFF00");
let button_bg = app.world().get::<BackgroundColor>(button);
assert!(button_bg.is_none());
}
#[test]
fn template_binding_outside_store_set_warns_once() {
let mut app = test_app();
let (_, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Button Content="x">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
<ContentPresenter/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</StackPanel>"##,
);
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("outside the store-managed property set"));
}
#[test]
fn dependent_despawn_is_pruned_not_panicked() {
let mut app = test_app();
let (button, border, _) = tb_setup(&mut app);
app.world_mut().entity_mut(border).despawn();
bevy_pf::provider::set_local(
app.world_mut(),
button,
bevy_pf::provider::PropertyTarget::Background,
bevy_pf::resources::PfValue::Color(bevy_pf::xaml_ast::value::PfColor::rgb(255, 0, 0)),
);
let deps = app
.world()
.get::<bevy_pf::provider::PfTemplateBindingDependents>(button)
.unwrap();
assert!(
deps.0.iter().all(|(_, child, _)| *child != border),
"stale links pruned"
);
}
#[test]
fn templated_textbox_edits_inside_part_content_host() {
#[derive(bevy::reflect::Reflect, Default)]
struct Vm {
query: String,
}
let mut app = test_app();
let vm = Bindable::new(Vm {
query: "start".into(),
});
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<TextBox x:Name="T" Text="{Binding query}">
<TextBox.Template>
<ControlTemplate TargetType="TextBox">
<Border x:Name="border" Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}">
<Border x:Name="PART_ContentHost"/>
</Border>
</ControlTemplate>
</TextBox.Template>
</TextBox>
</StackPanel>"##,
);
assert_eq!(warnings, Vec::<String>::new());
let tb = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("T")
.unwrap();
let parts = app
.world()
.get::<bevy_pf::components::PfTemplateParts>(tb)
.expect("parts map present");
let host = parts.get("PART_ContentHost").expect("part resolves");
let input = app
.world()
.get::<Children>(host)
.and_then(|c| c.iter().next())
.expect("editable child under the host");
assert!(app.world().get::<bevy::text::EditableText>(input).is_some());
app.world_mut()
.entity_mut(root)
.insert(DataContext(vm.clone()));
app.update();
let shown = app
.world()
.get::<bevy::text::EditableText>(input)
.unwrap()
.editor()
.text()
.to_string();
assert_eq!(shown, "start", "binding applied into templated input");
app.world_mut()
.get_mut::<bevy::text::EditableText>(input)
.unwrap()
.editor
.set_text("typed");
app.update();
let value = vm
.read_path("query")
.map(|v| v.to_display())
.unwrap_or_default();
assert_eq!(value, "typed", "write-back from templated input");
}
#[test]
fn templated_textbox_without_part_degrades_with_warnings() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<TextBox x:Name="T" Text="{Binding query}">
<TextBox.Template>
<ControlTemplate TargetType="TextBox"><Border/></ControlTemplate>
</TextBox.Template>
</TextBox>
</StackPanel>"##,
);
assert_eq!(warnings.len(), 2, "{warnings:?}");
assert!(warnings.iter().any(|w| w.contains("no PART_ContentHost")));
assert!(warnings.iter().any(|w| w.contains("Text binding skipped")));
let tb = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("T")
.unwrap();
assert!(
app.world()
.get::<bevy_pf::components::PfTemplatedControl>(tb)
.is_some()
);
assert!(app.world().get::<bevy_pf::PfBindings>(tb).is_none());
}
#[test]
fn template_parts_are_per_instance_and_take_any_name() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="HeaderSite"><ContentPresenter/></Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>
<Button x:Name="A" Content="a"/>
<Button x:Name="B" Content="b"/>
</StackPanel>"##,
);
assert_eq!(warnings, Vec::<String>::new());
let names = app.world().get::<XamlNames>(root).unwrap();
let (a, b) = (names.get("A").unwrap(), names.get("B").unwrap());
let pa = app
.world()
.get::<bevy_pf::components::PfTemplateParts>(a)
.unwrap()
.get("HeaderSite")
.unwrap();
let pb = app
.world()
.get::<bevy_pf::components::PfTemplateParts>(b)
.unwrap()
.get("HeaderSite")
.unwrap();
assert_ne!(pa, pb, "per-instance namescopes");
}
const TRIGGERED_TEMPLATE_PAGE: &str = r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Button x:Name="B" Content="Go">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border x:Name="border" Background="#E8E8E8">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="#BEE6FD"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Background" Value="#C4E5F6"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
</StackPanel>"##;
fn border_hex(app: &App, border: Entity) -> String {
bevy_pf::instantiate::color_to_hex(app.world().get::<BackgroundColor>(border).unwrap().0)
}
#[test]
fn template_triggers_drive_named_children_and_revert_to_template_value() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(&mut app, TRIGGERED_TEMPLATE_PAGE);
assert_eq!(warnings, Vec::<String>::new());
let button = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("B")
.unwrap();
let border = app
.world()
.get::<bevy_pf::components::PfTemplatedControl>(button)
.unwrap()
.template_root;
app.update();
assert_eq!(
border_hex(&app, border),
"#E8E8E8",
"rest state = template literal"
);
app.world_mut()
.entity_mut(button)
.insert(Interaction::Hovered);
app.update();
assert_eq!(border_hex(&app, border), "#BEE6FD");
app.world_mut()
.entity_mut(button)
.insert(Interaction::Pressed);
app.update();
assert_eq!(border_hex(&app, border), "#C4E5F6");
app.world_mut().entity_mut(button).insert(Interaction::None);
app.update();
assert_eq!(border_hex(&app, border), "#E8E8E8");
assert!(
app.world().get::<BackgroundColor>(button).is_none(),
"root never paints"
);
}
#[test]
fn style_and_template_trigger_blocks_merge_and_style_tier_wins() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<Style TargetType="Button">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#112233"/>
</Trigger>
</Style.Triggers>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border" Background="{TemplateBinding Background}">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#AABBCC"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>
<Button x:Name="B" Background="#008000" Content="x"/>
</StackPanel>"##,
);
assert_eq!(warnings, Vec::<String>::new());
let button = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("B")
.unwrap();
let border = app
.world()
.get::<bevy_pf::components::PfTemplatedControl>(button)
.unwrap()
.template_root;
let blocks = app.world().get::<bevy_pf::PfTriggers>(button).unwrap();
assert_eq!(blocks.triggers.len(), 2, "style + template trigger merged");
app.update();
assert_eq!(border_hex(&app, border), "#008000");
app.world_mut()
.entity_mut(button)
.insert(Interaction::Hovered);
app.update();
assert_eq!(border_hex(&app, border), "#008000");
assert!(app.world().get::<BackgroundColor>(button).is_none());
let store = app.world().get::<bevy_pf::PfPropertyStore>(button).unwrap();
assert_eq!(
store.effective_source(bevy_pf::provider::PropertyTarget::Background),
Some(bevy_pf::provider::ValueSource::Local)
);
}
#[test]
fn style_and_template_triggers_without_local_value() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="#008000"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#112233"/>
</Trigger>
</Style.Triggers>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border" Background="{TemplateBinding Background}">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#AABBCC"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>
<Button x:Name="B" Content="x"/>
</StackPanel>"##,
);
assert_eq!(warnings, Vec::<String>::new());
let button = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("B")
.unwrap();
let border = app
.world()
.get::<bevy_pf::components::PfTemplatedControl>(button)
.unwrap()
.template_root;
app.update();
assert_eq!(border_hex(&app, border), "#008000", "style setter at rest");
app.world_mut()
.entity_mut(button)
.insert(Interaction::Hovered);
app.update();
assert_eq!(
border_hex(&app, border),
"#112233",
"StyleTrigger(7) > TemplateTrigger(6)"
);
app.world_mut().entity_mut(button).insert(Interaction::None);
app.update();
assert_eq!(border_hex(&app, border), "#008000", "clean revert per tier");
}
#[test]
fn checked_template_trigger_on_templated_toggle() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<CheckBox x:Name="C" Content="opt">
<CheckBox.Template>
<ControlTemplate TargetType="CheckBox">
<Border x:Name="mark" Background="#FFFFFF">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="mark" Property="Background" Value="#0078D7"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</CheckBox.Template>
</CheckBox>
</StackPanel>"##,
);
assert_eq!(warnings, Vec::<String>::new());
let cb = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("C")
.unwrap();
let mark = app
.world()
.get::<bevy_pf::components::PfTemplatedControl>(cb)
.unwrap()
.template_root;
app.update();
assert_eq!(border_hex(&app, mark), "#FFFFFF");
app.world_mut().entity_mut(cb).insert(bevy::ui::Checked);
app.update();
assert_eq!(border_hex(&app, mark), "#0078D7", "IsChecked trigger fired");
app.world_mut().entity_mut(cb).remove::<bevy::ui::Checked>();
app.update();
assert_eq!(
border_hex(&app, mark),
"#FFFFFF",
"reverts to template literal"
);
}
#[test]
fn despawned_target_name_dest_is_pruned_without_panic() {
let mut app = test_app();
let (root, _) = spawn_collect_warnings(&mut app, TRIGGERED_TEMPLATE_PAGE);
let button = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("B")
.unwrap();
let border = app
.world()
.get::<bevy_pf::components::PfTemplatedControl>(button)
.unwrap()
.template_root;
app.update();
app.world_mut().entity_mut(border).despawn();
app.world_mut()
.entity_mut(button)
.insert(Interaction::Hovered);
app.update();
let blocks = app.world().get::<bevy_pf::PfTriggers>(button).unwrap();
assert!(
blocks
.triggers
.iter()
.all(|t| t.setters.iter().all(|s| s.dest != Some(border))),
"stale TargetName setters pruned"
);
}
#[test]
fn g1_aero2_button_fragment_instantiates_and_functions() {
let mut app = test_app();
app.update();
let dict_src = include_str!("fixtures/aero2/button.xaml");
let doc = bevy_pf_xaml::parse(dict_src).expect("fragment parses with zero errors");
let ingest_warnings =
bevy_pf::instantiate::set_application_resources(app.world_mut(), &doc, &XamlEnv::default());
for w in &ingest_warnings {
assert!(
w.contains("x:Static")
|| w.contains("unsupported DynamicResource key")
|| w.contains("Condition needs Property or a plain-path Binding")
|| w.contains("SystemParameters.HighContrast")
|| w.contains("RelativeSource")
|| w.contains("Stylus")
|| w.contains("not supported"),
"unexpected ingest warning class: {w}"
);
}
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Button x:Name="B" Content="Aero2"/>
</StackPanel>"##,
);
for w in &warnings {
assert!(
w.contains("FocusVisual")
|| w.contains("SnapsToDevicePixels")
|| w.contains("ContentAlignment")
|| w.contains("IsDefaulted")
|| w.contains("RecognizesAccessKey")
|| w.contains("Focusable")
|| w.contains("TextElement.Foreground")
|| w.contains("not supported"),
"unexpected instantiation warning class: {w}"
);
}
let button = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("B")
.unwrap();
let templated = app
.world()
.get::<bevy_pf::components::PfTemplatedControl>(button)
.expect("implicit Aero2 style templated the Button");
let border = templated.template_root;
app.update();
assert_eq!(
border_hex(&app, border),
"#DDDDDD",
"Button.Static.Background"
);
assert!(app.world().get::<BackgroundColor>(button).is_none());
assert!(texts_in(&app, border).contains(&"Aero2".to_string()));
app.world_mut()
.entity_mut(button)
.insert(Interaction::Hovered);
app.update();
assert_eq!(
border_hex(&app, border),
"#BEE6FD",
"Button.MouseOver.Background"
);
app.world_mut()
.entity_mut(button)
.insert(Interaction::Pressed);
app.update();
assert_eq!(
border_hex(&app, border),
"#C4E5F6",
"Button.Pressed.Background"
);
app.world_mut().entity_mut(button).insert(Interaction::None);
app.update();
assert_eq!(
border_hex(&app, border),
"#DDDDDD",
"revert to template value"
);
}
#[test]
fn g1_aero2_toggle_button_checked_trigger_works_unadapted() {
let mut app = test_app();
app.update();
let doc = bevy_pf_xaml::parse(include_str!("fixtures/aero2/button.xaml")).unwrap();
bevy_pf::instantiate::set_application_resources(app.world_mut(), &doc, &XamlEnv::default());
let (root, _) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ToggleButton x:Name="T" Content="pin"/>
</StackPanel>"##,
);
let toggle = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("T")
.unwrap();
let border = app
.world()
.get::<bevy_pf::components::PfTemplatedControl>(toggle)
.expect("ToggleButton typed-key style applied")
.template_root;
app.update();
assert_eq!(border_hex(&app, border), "#DDDDDD");
app.world_mut().entity_mut(toggle).insert(bevy::ui::Checked);
app.update();
assert_eq!(
border_hex(&app, border),
"#BCDDEE",
"Button.Checked.Background"
);
}
#[test]
fn templated_expander_toggles_via_two_way_templated_parent() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Expander x:Name="E" Header="More">
<Expander.Template>
<ControlTemplate TargetType="Expander">
<StackPanel>
<ToggleButton x:Name="head" Content="More"
IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"/>
<Border x:Name="body">
<ContentPresenter/>
</Border>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="False">
<Setter TargetName="body" Property="Visibility" Value="Collapsed"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Expander.Template>
<TextBlock Text="hidden treasure"/>
</Expander>
</StackPanel>"##,
);
assert_eq!(warnings, Vec::<String>::new());
app.update();
let names = app.world().get::<XamlNames>(root).unwrap();
let expander = names.get("E").unwrap();
let parts = app
.world()
.get::<bevy_pf::components::PfTemplateParts>(expander)
.expect("template parts");
let (head, body) = (parts.get("head").unwrap(), parts.get("body").unwrap());
app.update();
assert_eq!(
app.world().get::<Node>(body).unwrap().display,
Display::None,
"collapsed until expanded"
);
app.world_mut().entity_mut(head).insert(bevy::ui::Checked);
app.update();
app.update(); assert!(
app.world().get::<bevy::ui::Checked>(expander).is_some(),
"part checked the templated parent"
);
assert_ne!(
app.world().get::<Node>(body).unwrap().display,
Display::None,
"expanded content visible"
);
fn find_text(world: &World, e: Entity, needle: &str) -> bool {
if let Some(t) = world.get::<bevy::ui::widget::Text>(e)
&& t.0 == needle
{
return true;
}
world
.get::<Children>(e)
.into_iter()
.flat_map(|c| c.iter())
.any(|c| find_text(world, c, needle))
}
assert!(
find_text(app.world(), body, "hidden treasure"),
"content projected into the template"
);
app.world_mut()
.entity_mut(head)
.remove::<bevy::ui::Checked>();
app.update();
app.update();
assert_eq!(
app.world().get::<Node>(body).unwrap().display,
Display::None,
"collapsed again after un-toggle"
);
}
#[test]
fn templated_combo_box_keeps_popup_runtime_and_selection() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ComboBox x:Name="C" SelectedIndex="1">
<ComboBox.Template>
<ControlTemplate TargetType="ComboBox">
<Border x:Name="face" Background="#FF223344" CornerRadius="4" Padding="8,4">
<TextBlock x:Name="PART_SelectionBoxText"/>
</Border>
</ControlTemplate>
</ComboBox.Template>
<ComboBoxItem>Alpha</ComboBoxItem>
<ComboBoxItem>Beta</ComboBoxItem>
<ComboBoxItem>Gamma</ComboBoxItem>
</ComboBox>
</StackPanel>"##,
);
assert_eq!(warnings, Vec::<String>::new());
app.update();
let names = app.world().get::<XamlNames>(root).unwrap();
let combo = names.get("C").unwrap();
let parts = app
.world()
.get::<bevy_pf::components::PfTemplateParts>(combo)
.expect("template parts");
let selection_text = parts.get("PART_SelectionBoxText").unwrap();
let state = app
.world()
.get::<bevy_pf::components::PfComboBox>(combo)
.expect("combo runtime")
.clone();
assert_eq!(
state.text, selection_text,
"selection presenter is the part"
);
let items: Vec<Entity> = app
.world()
.get::<Children>(state.popup)
.unwrap()
.iter()
.collect();
assert_eq!(items.len(), 3, "static items in the popup");
assert_eq!(
app.world()
.get::<bevy::ui::widget::Text>(selection_text)
.unwrap()
.0,
"Beta"
);
bevy_pf::instantiate::select_combo_index(app.world_mut(), combo, 2);
assert_eq!(
app.world()
.get::<bevy::ui::widget::Text>(selection_text)
.unwrap()
.0,
"Gamma"
);
app.world_mut()
.get_mut::<bevy_pf::components::PfComboBox>(combo)
.unwrap()
.open = true;
app.update();
assert_ne!(
app.world().get::<Node>(state.popup).unwrap().display,
Display::None,
"popup opens"
);
}
#[test]
fn g1_aero2_check_box_fragment_instantiates_and_functions() {
let mut app = test_app();
app.update();
let dict_src = include_str!("fixtures/aero2/checkbox.xaml");
let doc = bevy_pf_xaml::parse(dict_src).expect("fragment parses with zero errors");
let ingest_warnings =
bevy_pf::instantiate::set_application_resources(app.world_mut(), &doc, &XamlEnv::default());
for w in &ingest_warnings {
assert!(
w.contains("x:Static")
|| w.contains("unsupported DynamicResource key")
|| w.contains("x:Null")
|| w.contains("not supported"),
"unexpected ingest warning class: {w}"
);
}
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<CheckBox x:Name="C" Content="Verbatim Aero2"/>
</StackPanel>"##,
);
for w in &warnings {
assert!(
w.contains("FocusVisual")
|| w.contains("SnapsToDevicePixels")
|| w.contains("ContentAlignment")
|| w.contains("RecognizesAccessKey")
|| w.contains("Focusable")
|| w.contains("HasContent")
|| w.contains("x:Null")
|| w.contains("not supported"),
"unexpected instantiation warning class: {w}"
);
}
let checkbox = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get("C")
.unwrap();
let templated = app
.world()
.get::<bevy_pf::components::PfTemplatedControl>(checkbox)
.expect("implicit Aero2 style templated the CheckBox");
let parts = app
.world()
.get::<bevy_pf::components::PfTemplateParts>(checkbox)
.expect("template parts");
let border = parts.get("checkBoxBorder").expect("checkBoxBorder part");
let mark = parts.get("optionMark").expect("optionMark part");
assert_eq!(parts.get("templateRoot"), Some(templated.template_root));
app.update();
assert_eq!(
border_hex(&app, border),
"#FFFFFF",
"OptionMark.Static.Background"
);
let glyph_opacity = |app: &App| -> Option<f64> {
let store = app.world().get::<bevy_pf::PfPropertyStore>(mark)?;
match store.effective(bevy_pf::PropertyTarget::Opacity) {
Some((_, Some(bevy_pf::resources::PfValue::Double(d)))) => Some(*d),
_ => None,
}
};
assert_eq!(glyph_opacity(&app), Some(0.0), "unchecked: glyph hidden");
app.world_mut()
.entity_mut(checkbox)
.insert(Interaction::Hovered);
app.update();
assert_eq!(
border_hex(&app, border),
"#F3F9FF",
"OptionMark.MouseOver.Background"
);
app.world_mut()
.entity_mut(checkbox)
.insert(Interaction::Pressed);
app.update();
assert_eq!(
border_hex(&app, border),
"#D9ECFF",
"OptionMark.Pressed.Background"
);
app.world_mut()
.entity_mut(checkbox)
.insert(Interaction::None);
app.update();
assert_eq!(
border_hex(&app, border),
"#FFFFFF",
"revert to template value"
);
app.world_mut()
.entity_mut(checkbox)
.insert(bevy::ui::Checked);
app.update();
assert_eq!(glyph_opacity(&app), Some(1.0), "checked: glyph visible");
app.world_mut()
.entity_mut(checkbox)
.insert(bevy::ui::InteractionDisabled);
app.update();
let fill = app
.world()
.get::<bevy_pf::shapes::PfShape>(mark)
.and_then(|s| s.fill.clone());
match fill {
Some(bevy_pf::xaml_ast::value::PfBrush::Solid(c)) => {
assert_eq!(
(c.r, c.g, c.b),
(0x70, 0x70, 0x70),
"OptionMark.Disabled.Glyph"
);
}
other => panic!("expected solid disabled glyph fill, got {other:?}"),
}
}
#[test]
fn templated_scroll_bar_wires_track_thumb_and_line_buttons() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ScrollBar x:Name="S" Minimum="0" Maximum="100" Value="40"
ViewportSize="25" SmallChange="10">
<ScrollBar.Template>
<ControlTemplate TargetType="ScrollBar">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<RepeatButton x:Name="up" Height="16" Background="#FFDADADA"/>
<Track x:Name="PART_Track" Grid.Row="1">
<Track.Thumb>
<Thumb x:Name="grip" Background="#FF606060"/>
</Track.Thumb>
</Track>
<RepeatButton x:Name="down" Grid.Row="2" Height="16" Background="#FFDADADA"/>
</Grid>
</ControlTemplate>
</ScrollBar.Template>
</ScrollBar>
</StackPanel>"##,
);
assert_eq!(warnings, Vec::<String>::new());
app.update();
let names = app.world().get::<XamlNames>(root).unwrap();
let bar = names.get("S").unwrap();
let parts = app
.world()
.get::<bevy_pf::components::PfTemplateParts>(bar)
.expect("template parts");
let (track, grip, up) = (
parts.get("PART_Track").unwrap(),
parts.get("grip").unwrap(),
parts.get("up").unwrap(),
);
let sb = app
.world()
.get::<bevy_pf::components::PfScrollBar>(bar)
.expect("scroll bar runtime");
assert_eq!(sb.track, track);
assert_eq!(sb.thumb, grip);
assert_eq!(
app.world()
.get::<bevy::ui_widgets::SliderValue>(bar)
.unwrap()
.0,
40.0,
"initial Value"
);
app.update();
let grip_node = app.world().get::<Node>(grip).unwrap();
match (grip_node.height, grip_node.top) {
(Val::Percent(len), Val::Percent(top)) => {
assert!((len - 20.0).abs() < 0.5, "thumb length {len}%");
assert!(
(top - 32.0).abs() < 0.5,
"thumb at 40% of (100-20)% = {top}%"
);
}
other => panic!("expected percent thumb geometry, got {other:?}"),
}
assert!(
app.world()
.get::<bevy_pf::components::PfRepeatButton>(up)
.is_some(),
"line buttons repeat while held"
);
bevy_pf::instantiate::scroll_bar_nudge(app.world_mut(), bar, -1.0);
assert_eq!(
app.world()
.get::<bevy::ui_widgets::SliderValue>(bar)
.unwrap()
.0,
30.0,
"nudge by SmallChange"
);
}
fn console_page(pressed_value: &str) -> String {
format!(
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<Style x:Key="Console" TargetType="Button">
<Setter Property="BorderBrush" Value="#00FFD4"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="frame" Background="#101010"><ContentPresenter/></Border>
<ControlTemplate.Triggers>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="frame" Property="Background" Value="{pressed_value}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ConsoleAmber" TargetType="Button" BasedOn="{{StaticResource Console}}">
<Setter Property="BorderBrush" Value="#FF8A50"/>
</Style>
</StackPanel.Resources>
<Button x:Name="Teal" Style="{{StaticResource Console}}" Content="A"/>
<Button x:Name="Amber" Style="{{StaticResource ConsoleAmber}}" Content="B"/>
</StackPanel>"##
)
}
fn press_and_read_frame(app: &mut App, root: Entity, name: &str) -> (String, String) {
let button = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get(name)
.unwrap();
let frame = app
.world()
.get::<bevy_pf::components::PfTemplatedControl>(button)
.unwrap()
.template_root;
app.update();
let rest = border_hex(app, frame);
app.world_mut()
.entity_mut(button)
.insert(Interaction::Pressed);
app.update();
let pressed = border_hex(app, frame);
app.world_mut().entity_mut(button).insert(Interaction::None);
app.update();
assert_eq!(
border_hex(app, frame),
rest,
"{name} must revert on release"
);
(rest, pressed)
}
#[test]
fn template_binding_trigger_setter_fills_with_each_instance_accent() {
let mut app = test_app();
let (root, warnings) =
spawn_collect_warnings(&mut app, &console_page("{TemplateBinding BorderBrush}"));
assert_eq!(warnings, Vec::<String>::new());
let (rest, pressed) = press_and_read_frame(&mut app, root, "Teal");
assert_eq!(rest, "#101010");
assert_eq!(pressed, "#00FFD4", "teal button presses to its own accent");
let (_, pressed) = press_and_read_frame(&mut app, root, "Amber");
assert_eq!(
pressed, "#FF8A50",
"the SAME template presses to the amber accent — one template, many colourways"
);
}
#[test]
fn wpf_spelling_of_the_templated_parent_binding_works_too() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
&console_page("{Binding BorderBrush, RelativeSource={RelativeSource TemplatedParent}}"),
);
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(press_and_read_frame(&mut app, root, "Amber").1, "#FF8A50");
}
#[test]
fn templated_parent_setter_value_parses_the_same_either_way() {
for spelling in [
"{TemplateBinding Background}",
"{Binding Background, RelativeSource={RelativeSource TemplatedParent}}",
"{Binding Path=Background, RelativeSource={RelativeSource Mode=TemplatedParent}}",
] {
let xaml = format!(
r##"<Style xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Key="S" TargetType="Button">
<Style.Triggers>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Foreground" Value="{spelling}"/>
</Trigger>
</Style.Triggers>
</Style>"##
);
let (value, warnings) = parse_value(&xaml);
assert_eq!(warnings, Vec::<String>::new(), "{spelling}");
let PfValue::Style(style) = value.expect("a style") else {
panic!("{spelling}: expected a Style");
};
assert!(
matches!(
&style.triggers[0].setters[0].value,
PfSetterValue::TemplatedParent(p) if p == "Background"
),
"{spelling} did not parse to TemplatedParent(Background)"
);
}
}
#[test]
fn a_plain_style_setter_cannot_read_a_templated_parent() {
let mut app = test_app();
let (_, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<Style x:Key="S" TargetType="Button">
<Setter Property="Background" Value="{TemplateBinding BorderBrush}"/>
</Style>
</StackPanel.Resources>
<Button Style="{StaticResource S}" Content="A"/>
</StackPanel>"##,
);
assert!(
warnings
.iter()
.any(|w| w.contains("ControlTemplate.Triggers")),
"expected a warning naming where it IS valid, got {warnings:?}"
);
}
#[test]
fn other_binding_flavours_in_a_setter_are_still_reported() {
let mut app = test_app();
let (_, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<Style x:Key="S" TargetType="Button">
<Setter Property="Background" Value="{Binding SomeProperty}"/>
</Style>
</StackPanel.Resources>
<Button Style="{StaticResource S}" Content="A"/>
</StackPanel>"##,
);
assert!(
warnings.iter().any(|w| w.contains("TemplatedParent")),
"expected the warning to point at the one supported flavour, got {warnings:?}"
);
}
#[test]
fn tag_forwards_into_a_template() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<Style x:Key="Hinted" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<TextBlock x:Name="hint" Text="{TemplateBinding Tag}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>
<Button x:Name="A" Style="{StaticResource Hinted}" Tag="F1"/>
<Button x:Name="B" Style="{StaticResource Hinted}" Tag="ESC"/>
<Button x:Name="C" Style="{StaticResource Hinted}"/>
</StackPanel>"##,
);
app.update();
assert_eq!(warnings, Vec::<String>::new());
let hint_of = |app: &App, name: &str| -> Option<String> {
let button = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get(name)
.unwrap();
let parts = app
.world()
.get::<bevy_pf::components::PfTemplateParts>(button)?;
let hint = parts.get("hint")?;
app.world()
.get::<bevy::ui::widget::Text>(hint)
.map(|t| t.0.clone())
};
assert_eq!(hint_of(&app, "A").as_deref(), Some("F1"));
assert_eq!(
hint_of(&app, "B").as_deref(),
Some("ESC"),
"the SAME template shows each instance's own tag"
);
assert_ne!(
hint_of(&app, "C").as_deref(),
Some("F1"),
"a button with no Tag must not inherit a sibling's"
);
}
#[test]
fn a_shape_stroke_binds_to_the_templated_parent() {
let mut app = test_app();
let (root, warnings) = spawn_collect_warnings(
&mut app,
r##"<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<Style x:Key="Slab" TargetType="Button">
<Setter Property="BorderBrush" Value="#00FFD4"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<Path x:Name="frame" Width="40" Height="20" Stretch="Fill"
Fill="{TemplateBinding Background}"
Stroke="{TemplateBinding BorderBrush}" StrokeThickness="1"
Data="M 0,0 L 40,0 L 40,20 L 0,20 Z"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Amber" TargetType="Button" BasedOn="{StaticResource Slab}">
<Setter Property="BorderBrush" Value="#FFB454"/>
</Style>
</StackPanel.Resources>
<Button x:Name="Teal" Style="{StaticResource Slab}"/>
<Button x:Name="Amber" Style="{StaticResource Amber}"/>
</StackPanel>"##,
);
app.update();
assert_eq!(
warnings,
Vec::<String>::new(),
"the stroke binding must not warn"
);
let stroke_of = |app: &App, name: &str| -> Option<String> {
let button = app
.world()
.get::<XamlNames>(root)
.unwrap()
.get(name)
.unwrap();
let frame = app
.world()
.get::<bevy_pf::components::PfTemplateParts>(button)?
.get("frame")?;
let shape = app.world().get::<bevy_pf::shapes::PfShape>(frame)?;
match shape.stroke.as_ref()? {
bevy_pf_xaml::value::PfBrush::Solid(c) => {
Some(format!("#{:02X}{:02X}{:02X}", c.r, c.g, c.b))
}
_ => None,
}
};
assert_eq!(stroke_of(&app, "Teal").as_deref(), Some("#00FFD4"));
assert_eq!(
stroke_of(&app, "Amber").as_deref(),
Some("#FFB454"),
"one template, each instance's own outline — the point of binding it"
);
}