use glam::{Mat4, Vec2, Vec3};
use super::retained::Ui;
use crate::render::batch::PanelDraw;
use crate::{Ctx, Entity, MouseButton, Texture, Transform3D};
pub struct UiPanel3D {
pub ui: Ui,
pub texture: Texture,
pub size: Vec2,
pub interactive: bool,
pointer: Option<Vec2>,
}
impl std::fmt::Debug for UiPanel3D {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UiPanel3D")
.field("texture", &self.texture)
.field("size", &self.size)
.field("interactive", &self.interactive)
.field("pointer", &self.pointer)
.finish_non_exhaustive()
}
}
impl UiPanel3D {
#[must_use]
pub fn new(texture: Texture, size: Vec2) -> Self {
Self {
ui: Ui::default(),
texture,
size,
interactive: true,
pointer: None,
}
}
#[must_use]
pub fn mesh(&self) -> crate::MeshData {
let (x, y) = (self.size.x * 0.5, self.size.y * 0.5);
crate::MeshData {
positions: vec![
Vec3::new(-x, y, 0.0),
Vec3::new(x, y, 0.0),
Vec3::new(x, -y, 0.0),
Vec3::new(-x, -y, 0.0),
],
normals: vec![Vec3::Z; 4],
uvs: vec![Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y],
indices: vec![0, 2, 1, 0, 3, 2],
..Default::default()
}
}
#[must_use]
pub fn pointer(&self) -> Option<Vec2> {
self.pointer
}
fn hit(&self, matrix: Mat4, origin: Vec3, direction: Vec3) -> Option<(f32, Vec2)> {
let to_local = matrix.inverse();
if !to_local.is_finite() {
return None;
}
let (o, d) = (to_local.transform_point3(origin), to_local.transform_vector3(direction));
if d.z.abs() < 1e-9 {
return None;
}
let k = -o.z / d.z;
let at = o + d * k;
let half = self.size * 0.5;
if k <= 0.0 || at.x.abs() > half.x || at.y.abs() > half.y {
return None;
}
let distance = (matrix.transform_point3(at) - origin).length();
let pixels = Vec2::new(self.texture.width() as f32, self.texture.height() as f32);
let uv = Vec2::new(at.x / self.size.x + 0.5, 0.5 - at.y / self.size.y);
Some((distance, uv * pixels))
}
}
#[derive(Default)]
pub(crate) struct Panels {
pub ray: Option<(Vec3, Vec3)>,
active: Option<Entity>,
entities: Vec<Entity>,
pub draws: Vec<PanelDraw>,
used: usize,
}
impl Panels {
pub fn frame_draws(&mut self) -> &mut [PanelDraw] {
&mut self.draws[..self.used]
}
}
impl Ctx {
pub fn create_ui_panel(&mut self, width: u32, height: u32, size: Vec2) -> (UiPanel3D, crate::Model) {
self.try_create_ui_panel(width, height, size)
.unwrap_or_else(|message| panic!("{message}"))
}
#[doc(hidden)]
pub fn try_create_ui_panel(
&mut self,
width: u32,
height: u32,
size: Vec2,
) -> Result<(UiPanel3D, crate::Model), String> {
if !size.is_finite() || size.x <= 0.0 || size.y <= 0.0 {
return Err("panel size must be positive and finite".into());
}
let texture = self.try_create_render_target(width, height)?;
let panel = UiPanel3D::new(texture, size);
let material = self.create_material(crate::MaterialDesc {
color: crate::Color::BLACK,
texture: Some(texture),
emissive: crate::Color::WHITE,
emissive_map: Some(texture),
blend: crate::Blend::Transparent,
..Default::default()
});
let mesh = self.create_mesh(&panel.mesh(), material);
Ok((panel, crate::Model::new(mesh)))
}
pub fn set_ui_pointer_ray(&mut self, ray: Option<(Vec3, Vec3)>) {
self.ui.panels.ray = ray;
}
pub(crate) fn update_ui_panels(&mut self) {
let mut entities = std::mem::take(&mut self.ui.panels.entities);
entities.clear();
self.world
.query::<(Entity, &UiPanel3D, &Transform3D)>()
.for_each(|(e, _, _)| entities.push(e));
if entities.is_empty() {
self.ui.panels.entities = entities;
return;
}
let (origin, direction) = self
.ui
.panels
.ray
.unwrap_or_else(|| self.camera3d.screen_ray(self.mouse_pos()));
let screen_has_it = self.ui_retained.wants_pointer();
let mut nearest: Option<(Entity, f32, Vec2)> = None;
for &e in &entities {
let (Some(panel), Some(at)) = (self.world.get::<UiPanel3D>(e), self.world.get::<Transform3D>(e)) else {
continue;
};
if !panel.interactive || screen_has_it {
continue;
}
if let Some((distance, pixel)) = panel.hit(at.matrix(), origin, direction)
&& nearest.is_none_or(|n| distance < n.1)
{
nearest = Some((e, distance, pixel));
}
}
if self.mouse_pressed(MouseButton::Left) {
self.ui.panels.active = nearest.map(|n| n.0);
}
let active = self.ui.panels.active;
for &e in &entities {
let Some(mut ui) = self.world.get_mut::<UiPanel3D>(e).map(|p| std::mem::take(&mut p.ui)) else {
continue;
};
let pointer = nearest.filter(|n| n.0 == e).map(|n| n.2);
let full = self.retained_input(&ui, String::new());
let mut input = if active == Some(e) {
full
} else {
crate::UiInput {
held: full.held,
released: full.released,
..Default::default()
}
};
input.pointer = pointer.unwrap_or(Vec2::splat(-1e9));
if pointer.is_none() {
input.pressed = false;
input.wheel = Vec2::ZERO;
}
let Some(panel) = self.world.get_mut::<UiPanel3D>(e) else {
continue;
};
let viewport = Vec2::new(panel.texture.width() as f32, panel.texture.height() as f32);
panel.pointer = pointer;
ui.frame_with_text(viewport, &input, &mut self.text);
if let Some(panel) = self.world.get_mut::<UiPanel3D>(e) {
panel.ui = ui;
}
}
self.ui.panels.entities = entities;
}
pub(crate) fn draw_ui_panels(&mut self) {
self.ui.panels.used = 0;
if self.headless {
return;
}
let mut entities = std::mem::take(&mut self.ui.panels.entities);
entities.clear();
self.world
.query::<(Entity, &UiPanel3D)>()
.for_each(|(e, _)| entities.push(e));
let window_scale = std::mem::replace(&mut self.scale_factor, 1.0);
for &e in &entities {
let Some((mut ui, texture)) = self
.world
.get_mut::<UiPanel3D>(e)
.map(|p| (std::mem::take(&mut p.ui), p.texture))
else {
continue;
};
let used = self.ui.panels.used;
if used == self.ui.panels.draws.len() {
self.ui.panels.draws.push(PanelDraw::default());
}
let mut draw = std::mem::take(&mut self.ui.panels.draws[used]);
draw.texture = texture.id;
draw.size = [texture.width(), texture.height()];
draw.list.clear();
std::mem::swap(&mut self.ui_list, &mut draw.list);
self.draw_ui_tree(&mut ui, Vec2::new(texture.width() as f32, texture.height() as f32));
std::mem::swap(&mut self.ui_list, &mut draw.list);
self.ui.panels.draws[used] = draw;
self.ui.panels.used += 1;
if let Some(panel) = self.world.get_mut::<UiPanel3D>(e) {
panel.ui = ui;
}
}
self.scale_factor = window_scale;
self.ui.panels.entities = entities;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{UiEvent, UiWidget, Vec2 as V2, vec3};
fn panel() -> UiPanel3D {
UiPanel3D::new(Texture::new(1, 200, 100), V2::new(2.0, 1.0))
}
#[test]
fn rays_hit_panels_at_their_pixels() {
let p = panel();
let at = Transform3D::at(vec3(0.0, 0.0, -5.0));
let (distance, pixel) = p.hit(at.matrix(), Vec3::ZERO, -Vec3::Z).unwrap();
assert!((distance - 5.0).abs() < 1e-5);
assert!(pixel.abs_diff_eq(V2::new(100.0, 50.0), 1e-3));
let (_, pixel) = p.hit(at.matrix(), vec3(-0.5, 0.25, 0.0), -Vec3::Z).unwrap();
assert!(pixel.abs_diff_eq(V2::new(50.0, 25.0), 1e-3), "{pixel}");
assert!(p.hit(at.matrix(), vec3(3.0, 0.0, 0.0), -Vec3::Z).is_none());
assert!(p.hit(at.matrix(), Vec3::ZERO, Vec3::Z).is_none());
assert!(p.hit(at.matrix(), Vec3::ZERO, Vec3::X).is_none());
}
#[test]
fn turned_panels_are_hit_on_their_own_plane() {
let p = panel();
let mut at = Transform3D::at(vec3(3.0, 0.0, 0.0));
at.rotation = glam::Quat::from_rotation_y(std::f32::consts::FRAC_PI_2);
let (distance, pixel) = p.hit(at.matrix(), vec3(6.0, 0.0, 0.0), -Vec3::X).unwrap();
assert!((distance - 3.0).abs() < 1e-5);
assert!(pixel.abs_diff_eq(V2::new(100.0, 50.0), 1e-3));
}
#[test]
fn the_mouse_clicks_buttons_on_the_nearest_panel() {
let mut ctx = Ctx::new(1.0 / 60.0, V2::new(800.0, 600.0));
ctx.camera3d = crate::Camera3D::new(V2::new(800.0, 600.0));
ctx.camera3d.position = Vec3::ZERO;
ctx.camera3d.look_at(-Vec3::Z);
let mut spawn = |z: f32| {
let mut p = UiPanel3D::new(Texture::new(1, 200, 100), V2::new(2.0, 1.0));
let style = taffy::Style {
size: taffy::Size::from_percent(1.0, 1.0),
..Default::default()
};
let button = p.ui.add(UiWidget::Button, style);
p.ui.push_screen(button, false);
(ctx.world.spawn((Transform3D::at(vec3(0.0, 0.0, z)), p)), button)
};
let (near, near_button) = spawn(-3.0);
let (far, _) = spawn(-6.0);
let mut ctx = ctx;
ctx.update_ui_panels();
ctx.input.set_mouse_pos(V2::new(400.0, 300.0));
ctx.input.set_mouse_button(MouseButton::Left, true);
ctx.update_ui_panels();
ctx.input.end_frame();
ctx.input.set_mouse_button(MouseButton::Left, false);
ctx.update_ui_panels();
let events: Vec<UiEvent> = ctx.world.get_mut::<UiPanel3D>(near).unwrap().ui.events().collect();
assert!(
events
.iter()
.any(|e| matches!(e, UiEvent::Clicked(id) if *id == near_button)),
"{events:?}"
);
let far_events = ctx.world.get_mut::<UiPanel3D>(far).unwrap().ui.events().count();
assert_eq!(far_events, 0, "the far panel is behind the near one");
assert!(ctx.world.get::<UiPanel3D>(near).unwrap().pointer().is_some());
assert!(ctx.world.get::<UiPanel3D>(far).unwrap().pointer().is_none());
assert_eq!(ctx.ui.panels.active, Some(near), "clicked last: it gets the keyboard");
ctx.set_ui_pointer_ray(Some((Vec3::ZERO, Vec3::X)));
ctx.update_ui_panels();
assert!(ctx.world.get::<UiPanel3D>(near).unwrap().pointer().is_none());
}
}