use crate::com::d2d::{D2D1CommandListExt, D2D1DeviceContextExt};
use crate::ecs::Visual;
use crate::ecs::graphics::{GraphicsCommandList, GraphicsCore, format_entity_name};
use crate::ecs::layout::Arrangement;
use crate::ecs::widget::brushes::{Brushes, DEFAULT_FOREGROUND};
use bevy_ecs::component::Component;
use bevy_ecs::lifecycle::HookContext;
use bevy_ecs::name::Name;
use bevy_ecs::prelude::*;
use bevy_ecs::world::DeferredWorld;
use tracing::{trace, warn};
use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F;
use windows_numerics::Matrix3x2;
pub type Color = D2D1_COLOR_F;
#[deprecated(since = "0.1.0", note = "Use Brush::XXX constants instead")]
pub mod colors {
use super::Color;
pub const TRANSPARENT: Color = Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.0,
};
pub const BLACK: Color = Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
pub const WHITE: Color = Color {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
};
pub const RED: Color = Color {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
pub const GREEN: Color = Color {
r: 0.0,
g: 1.0,
b: 0.0,
a: 1.0,
};
pub const BLUE: Color = Color {
r: 0.0,
g: 0.0,
b: 1.0,
a: 1.0,
};
}
const TRANSPARENT_COLOR: Color = Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.0,
};
#[derive(Component, Debug, Clone, Default)]
#[component(on_add = on_rectangle_add, on_remove = on_rectangle_remove)]
pub struct Rectangle;
impl Rectangle {
pub fn new() -> Self {
Self
}
}
fn on_rectangle_add(mut world: DeferredWorld, hook: HookContext) {
if world.get::<Visual>(hook.entity).is_some() {
return;
}
world
.commands()
.entity(hook.entity)
.insert(Visual::default());
}
fn on_rectangle_remove(mut world: DeferredWorld, hook: HookContext) {
let entity = hook.entity;
if let Some(mut cmd_list) = world.get_mut::<GraphicsCommandList>(entity) {
cmd_list.set_if_neq(GraphicsCommandList::empty());
}
}
pub fn draw_rectangles(
mut commands: Commands,
query: Query<
(
Entity,
&Rectangle,
&Arrangement,
&Brushes,
Option<&GraphicsCommandList>,
Option<&Name>,
),
Or<(Changed<Rectangle>, Changed<Arrangement>, Changed<Brushes>)>,
>,
graphics_core: Option<Res<GraphicsCore>>,
) {
let Some(graphics_core) = graphics_core else {
warn!("GraphicsCore not available, skipping draw_rectangles");
return;
};
for (entity, _rectangle, arrangement, brushes, cmd_list_opt, name) in query.iter() {
let entity_name = format_entity_name(entity, name);
trace!(
entity = %entity_name,
width = arrangement.size.width,
height = arrangement.size.height,
"Drawing rectangle"
);
let color = brushes
.foreground
.as_color()
.unwrap_or_else(|| DEFAULT_FOREGROUND.as_color().unwrap());
let dc = match graphics_core.device_context() {
Some(dc) => dc,
None => {
warn!(
entity = %entity_name,
"DeviceContext not available"
);
continue;
}
};
let command_list = match unsafe { dc.CreateCommandList() } {
Ok(cl) => cl,
Err(err) => {
warn!(
entity = %entity_name,
error = ?err,
"Failed to create CommandList"
);
continue;
}
};
unsafe {
dc.SetTarget(&command_list);
}
unsafe {
dc.SetTransform(&Matrix3x2::identity());
dc.BeginDraw();
dc.clear(Some(&TRANSPARENT_COLOR));
let rect = crate::ecs::Rect {
left: 0.0,
top: 0.0,
right: arrangement.size.width,
bottom: arrangement.size.height,
};
trace!(
entity = %entity_name,
left = rect.left,
top = rect.top,
right = rect.right,
bottom = rect.bottom,
r = format_args!("{:.2}", color.r),
g = format_args!("{:.2}", color.g),
b = format_args!("{:.2}", color.b),
a = format_args!("{:.2}", color.a),
"Rectangle details"
);
let brush = match dc.create_solid_color_brush(&color, None) {
Ok(b) => b,
Err(err) => {
warn!(
entity = %entity_name,
error = ?err,
"Failed to create brush"
);
let _ = dc.EndDraw(None, None);
continue;
}
};
dc.fill_rectangle(&rect.into(), &brush);
if let Err(err) = dc.EndDraw(None, None) {
warn!(
entity = %entity_name,
error = ?err,
"EndDraw failed"
);
continue;
}
}
if let Err(err) = command_list.close() {
warn!(
entity = %entity_name,
error = ?err,
"Failed to close CommandList"
);
continue;
}
let new_cmd_list = GraphicsCommandList::new(command_list);
match cmd_list_opt {
Some(existing) if *existing != new_cmd_list => {
commands.entity(entity).insert(new_cmd_list);
trace!(
entity = %entity_name,
"CommandList updated"
);
}
None => {
commands.entity(entity).insert(new_cmd_list);
trace!(
entity = %entity_name,
"CommandList created"
);
}
_ => {
trace!(
entity = %entity_name,
"CommandList unchanged"
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rectangle_new_equals_default() {
let a = Rectangle::new();
let b = Rectangle;
let c = Rectangle::default();
assert_eq!(format!("{a:?}"), format!("{b:?}"));
assert_eq!(format!("{a:?}"), format!("{c:?}"));
}
#[test]
fn test_transparent_color_constant_is_fully_transparent() {
assert!((TRANSPARENT_COLOR.r - 0.0).abs() < f32::EPSILON);
assert!((TRANSPARENT_COLOR.g - 0.0).abs() < f32::EPSILON);
assert!((TRANSPARENT_COLOR.b - 0.0).abs() < f32::EPSILON);
assert!((TRANSPARENT_COLOR.a - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_color_type_alias_is_d2d_color() {
let c: Color = Color {
r: 0.25,
g: 0.5,
b: 0.75,
a: 1.0,
};
let d: D2D1_COLOR_F = c;
assert!((d.r - 0.25).abs() < f32::EPSILON);
assert!((d.g - 0.5).abs() < f32::EPSILON);
assert!((d.b - 0.75).abs() < f32::EPSILON);
assert!((d.a - 1.0).abs() < f32::EPSILON);
}
#[test]
#[allow(deprecated)]
fn test_deprecated_colors_match_brush_constants() {
use crate::ecs::widget::brushes::Brush;
let pairs: [(Color, Brush); 6] = [
(colors::TRANSPARENT, Brush::TRANSPARENT),
(colors::BLACK, Brush::BLACK),
(colors::WHITE, Brush::WHITE),
(colors::RED, Brush::RED),
(colors::GREEN, Brush::GREEN),
(colors::BLUE, Brush::BLUE),
];
for (legacy, brush) in pairs {
let bc = brush.as_color().expect("brush constant is Solid");
assert!((legacy.r - bc.r).abs() < f32::EPSILON);
assert!((legacy.g - bc.g).abs() < f32::EPSILON);
assert!((legacy.b - bc.b).abs() < f32::EPSILON);
assert!((legacy.a - bc.a).abs() < f32::EPSILON);
}
}
}