use bevy::image::TRANSPARENT_IMAGE_HANDLE;
use bevy::prelude::*;
use bevy::ui::widget::NodeImageMode;
#[derive(Component, Clone, Debug)]
pub struct RBackgroundTexture(pub String);
#[derive(Component, Clone, Copy, Debug)]
pub struct BackgroundTileScale(pub f32);
pub fn bind_background_textures(
mut commands: Commands,
targets: Res<crate::render_target::RenderTargets>,
mut nodes: Query<(Entity, &RBackgroundTexture, &mut ImageNode)>,
) {
for (entity, marker, mut node) in &mut nodes {
let desired = targets.get(&marker.0).unwrap_or(TRANSPARENT_IMAGE_HANDLE);
if node.image != desired {
node.image = desired;
crate::layer::mark_content_dirty(&mut commands.entity(entity));
}
}
}
#[allow(clippy::type_complexity)]
pub fn sync_background_tile_scale(
mut nodes: Query<
(&ComputedNode, &BackgroundTileScale, &mut ImageNode),
Or<(
Changed<ComputedNode>,
Changed<BackgroundTileScale>,
Changed<ImageNode>,
)>,
>,
) {
for (computed, tile, mut node) in &mut nodes {
let scale_factor = computed.inverse_scale_factor().recip();
if !scale_factor.is_finite() || scale_factor <= 0.0 {
continue;
}
let want = tile.0 * scale_factor;
let current = match &node.image_mode {
NodeImageMode::Tiled { stretch_value, .. } => *stretch_value,
_ => continue,
};
if (current - want).abs() <= 1e-4 {
continue;
}
if let NodeImageMode::Tiled {
ref mut stretch_value,
..
} = node.image_mode
{
*stretch_value = want;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render_target::{RenderTargetSpec, RenderTargets};
use bevy::asset::AssetPlugin;
fn test_app() -> App {
let mut app = App::new();
app.add_plugins((MinimalPlugins, AssetPlugin::default()));
app.init_asset::<Image>();
app.init_resource::<RenderTargets>();
app.add_systems(
Update,
(bind_background_textures, sync_background_tile_scale),
);
app
}
#[test]
fn binds_and_reverts_to_placeholder() {
let mut app = test_app();
let node = app
.world_mut()
.spawn((
RBackgroundTexture("minimap".into()),
ImageNode::new(TRANSPARENT_IMAGE_HANDLE),
))
.id();
app.update();
assert_eq!(
app.world().entity(node).get::<ImageNode>().unwrap().image,
TRANSPARENT_IMAGE_HANDLE,
"an unregistered name shows the transparent placeholder"
);
let target_handle =
app.world_mut()
.resource_scope(|world, mut targets: Mut<RenderTargets>| {
let mut images = world.resource_mut::<Assets<Image>>();
targets
.create(&mut images, "minimap", RenderTargetSpec::default())
.handle
});
app.update();
assert_eq!(
app.world().entity(node).get::<ImageNode>().unwrap().image,
target_handle,
"the background binds once the target registers"
);
app.world_mut()
.resource_mut::<RenderTargets>()
.remove("minimap");
app.update();
assert_eq!(
app.world().entity(node).get::<ImageNode>().unwrap().image,
TRANSPARENT_IMAGE_HANDLE,
"a removed target reverts to the placeholder"
);
}
#[test]
fn tile_scale_tracks_dpi() {
let mut app = test_app();
let computed = ComputedNode {
inverse_scale_factor: 0.5, ..Default::default()
};
let node = app
.world_mut()
.spawn((
computed,
BackgroundTileScale(2.0),
ImageNode::new(TRANSPARENT_IMAGE_HANDLE).with_mode(NodeImageMode::Tiled {
tile_x: true,
tile_y: true,
stretch_value: 2.0,
}),
))
.id();
app.update();
let stretch = |app: &App| match app
.world()
.entity(node)
.get::<ImageNode>()
.unwrap()
.image_mode
{
NodeImageMode::Tiled { stretch_value, .. } => stretch_value,
_ => panic!("expected Tiled"),
};
assert_eq!(stretch(&app), 4.0, "scale 2 on a 2× display → stretch 4");
app.update();
assert_eq!(stretch(&app), 4.0, "the corrected value settles");
app.world_mut()
.entity_mut(node)
.get_mut::<ComputedNode>()
.unwrap()
.inverse_scale_factor = 1.0;
app.update();
assert_eq!(stretch(&app), 2.0, "a DPI change re-derives the stretch");
}
}