use std::marker::PhantomData;
use bevy::{ecs::query::QuerySingleError, prelude::*, ui::UiSystem, window::PrimaryWindow};
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum AnchorUiSystemSet {
MoveUiNodes,
}
#[derive(Default, Reflect, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum VerticalAnchor {
Top,
#[default]
Mid,
Bottom,
}
#[derive(Default, Reflect, Debug, Clone, Copy, PartialEq, Eq)]
pub enum HorizontalAnchor {
Left,
#[default]
Mid,
Right,
}
#[derive(Default, Reflect, Debug, Clone, Copy, PartialEq, Eq)]
pub struct AnchorPoint {
pub horizontal: HorizontalAnchor,
pub vertical: VerticalAnchor,
}
impl AnchorPoint {
pub fn topleft() -> Self {
Self {
horizontal: HorizontalAnchor::Left,
vertical: VerticalAnchor::Top,
}
}
pub fn topright() -> Self {
Self {
horizontal: HorizontalAnchor::Right,
vertical: VerticalAnchor::Top,
}
}
pub fn bottomleft() -> Self {
Self {
horizontal: HorizontalAnchor::Left,
vertical: VerticalAnchor::Bottom,
}
}
pub fn bottomright() -> Self {
Self {
horizontal: HorizontalAnchor::Right,
vertical: VerticalAnchor::Bottom,
}
}
pub fn middle() -> Self {
Self {
horizontal: HorizontalAnchor::Mid,
vertical: VerticalAnchor::Mid,
}
}
}
#[derive(Component, Reflect, Clone, Debug, PartialEq)]
#[relationship_target(relationship = AnchorUiNode, linked_spawn)]
pub struct AnchoredUiNodes(Vec<Entity>);
#[derive(Component, Reflect, Clone, Debug, PartialEq)]
#[relationship(relationship_target = AnchoredUiNodes)]
#[require(AnchorUiConfig, Node)]
pub struct AnchorUiNode {
#[relationship]
pub target: Entity,
}
#[derive(Component, Reflect, Clone, Debug, PartialEq, Default)]
pub struct AnchorUiConfig {
pub anchorpoint: AnchorPoint,
pub offset: Option<Vec3>,
}
impl AnchorUiConfig {
pub fn with_offset(mut self, offset: Vec3) -> Self {
self.offset = Some(offset);
self
}
pub fn with_horizontal_anchoring(mut self, horizontal: HorizontalAnchor) -> Self {
self.anchorpoint.horizontal = horizontal;
self
}
pub fn with_vertical_anchoring(mut self, vertical: VerticalAnchor) -> Self {
self.anchorpoint.vertical = vertical;
self
}
}
impl AnchorUiNode {
pub fn to_entity(entity: Entity) -> Self {
Self { target: entity }
}
}
pub struct AnchorUiPlugin<SingleCameraMarker: Component> {
_component: PhantomData<SingleCameraMarker>,
}
impl<SingleCameraMarker: Component> AnchorUiPlugin<SingleCameraMarker> {
pub fn new() -> Self {
Self {
_component: PhantomData::default(),
}
}
}
impl<SingleCameraMarker: Component> Plugin for AnchorUiPlugin<SingleCameraMarker> {
fn build(&self, app: &mut App) {
app.configure_sets(
PostUpdate,
AnchorUiSystemSet::MoveUiNodes
.before(TransformSystem::TransformPropagate)
.before(UiSystem::Layout),
);
app.add_systems(
PostUpdate,
system_move_ui_nodes::<SingleCameraMarker>.in_set(AnchorUiSystemSet::MoveUiNodes),
);
app.register_type::<AnchorUiNode>();
}
}
fn system_move_ui_nodes<C: Component>(
cameras: Query<(Entity, &Camera), With<C>>,
window: Query<&Window, With<PrimaryWindow>>,
mut uinodes: Query<(
Entity,
&mut Node,
&ComputedNode,
&AnchorUiNode,
&AnchorUiConfig,
)>,
transformhelper: TransformHelper,
) {
let window = match window.single() {
Ok(window) => window,
Err(QuerySingleError::NoEntities(_)) => return,
Err(err @ QuerySingleError::MultipleEntities(_)) => {
bevy::log::error!("more than one primary window: {err}");
return;
}
};
let (camera_entity, main_camera) = match cameras.single() {
Ok(camera) => camera,
Err(QuerySingleError::NoEntities(_)) => return,
Err(err @ QuerySingleError::MultipleEntities(_)) => {
bevy::log::error!("more than one camera with the specified marker component: {err}");
return;
}
};
let Ok(main_camera_transform) = transformhelper.compute_global_transform(camera_entity) else {
warn!("Failed computing global transform for Camera Entity");
return;
};
for (uientity, mut node, computed_node, uinode, uianchorconf) in uinodes.iter_mut() {
if node.display == Display::None {
continue;
}
let world_location = if let Ok(gt) = transformhelper.compute_global_transform(uinode.target)
{
gt.translation()
} else {
warn!("AnchorTarget({}) failed to compute global transform, uinode: {uientity} will not be updated", uinode.target);
continue;
};
let world_location = if let Some(offset) = uianchorconf.offset {
world_location + offset
} else {
world_location
};
let Ok(position) =
main_camera.world_to_viewport_with_depth(&main_camera_transform, world_location)
else {
bevy::log::debug!("world location is offscreen, and thus we dont change the position");
continue;
};
if node.as_ref().position_type != PositionType::Absolute {
node.position_type = PositionType::Absolute;
}
let nodewidth = if let Val::Px(width) = node.width {
width
} else {
computed_node.size().x * computed_node.inverse_scale_factor()
};
let leftpos = match uianchorconf.anchorpoint.horizontal {
HorizontalAnchor::Left => Val::Px(position.x),
HorizontalAnchor::Mid => Val::Px(position.x - nodewidth / 2.0),
HorizontalAnchor::Right => Val::Px(position.x - nodewidth),
};
node.left = leftpos;
let window_height = window.height();
let nodeheight = if let Val::Px(height) = node.height {
height
} else {
computed_node.size().y * computed_node.inverse_scale_factor()
};
let newheight = match uianchorconf.anchorpoint.vertical {
VerticalAnchor::Top => Val::Px(window_height - position.y - nodeheight),
VerticalAnchor::Mid => Val::Px(window_height - position.y - nodeheight / 2.0),
VerticalAnchor::Bottom => Val::Px(window_height - position.y),
};
node.bottom = newheight;
}
}