#![allow(clippy::too_many_arguments)]
use crate::error::Result;
use windows::Foundation::Numerics::Matrix3x2;
use windows::Win32::Foundation::HWND;
use windows::Win32::Graphics::Direct2D::Common::{
D2D1_ALPHA_MODE_PREMULTIPLIED, D2D1_COLOR_F, D2D1_PIXEL_FORMAT, D2D_POINT_2F, D2D_RECT_F,
D2D_SIZE_U,
};
use windows::Win32::Graphics::Direct2D::{
D2D1CreateFactory, ID2D1Factory, ID2D1HwndRenderTarget, ID2D1SolidColorBrush,
D2D1_BRUSH_PROPERTIES, D2D1_DRAW_TEXT_OPTIONS_NONE, D2D1_ELLIPSE, D2D1_FACTORY_OPTIONS,
D2D1_FACTORY_TYPE_SINGLE_THREADED, D2D1_HWND_RENDER_TARGET_PROPERTIES,
D2D1_PRESENT_OPTIONS_NONE, D2D1_RENDER_TARGET_PROPERTIES, D2D1_RENDER_TARGET_TYPE_DEFAULT,
D2D1_ROUNDED_RECT,
};
use windows::Win32::Graphics::DirectWrite::{
DWriteCreateFactory, IDWriteFactory, IDWriteTextFormat, DWRITE_FACTORY_TYPE_SHARED,
DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_MEASURING_MODE_NATURAL, DWRITE_PARAGRAPH_ALIGNMENT_CENTER,
DWRITE_PARAGRAPH_ALIGNMENT_FAR, DWRITE_PARAGRAPH_ALIGNMENT_NEAR, DWRITE_TEXT_ALIGNMENT_CENTER,
DWRITE_TEXT_ALIGNMENT_JUSTIFIED, DWRITE_TEXT_ALIGNMENT_LEADING, DWRITE_TEXT_ALIGNMENT_TRAILING,
};
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_B8G8R8A8_UNORM;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Color {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Color {
pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
Self { r, g, b, a: 1.0 }
}
pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
}
pub fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
Self {
r: r as f32 / 255.0,
g: g as f32 / 255.0,
b: b as f32 / 255.0,
a: 1.0,
}
}
pub fn from_hex(hex: u32) -> Self {
Self::from_rgb8(
((hex >> 16) & 0xFF) as u8,
((hex >> 8) & 0xFF) as u8,
(hex & 0xFF) as u8,
)
}
pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
pub const RED: Self = Self::rgb(1.0, 0.0, 0.0);
pub const GREEN: Self = Self::rgb(0.0, 1.0, 0.0);
pub const BLUE: Self = Self::rgb(0.0, 0.0, 1.0);
pub const YELLOW: Self = Self::rgb(1.0, 1.0, 0.0);
pub const CYAN: Self = Self::rgb(0.0, 1.0, 1.0);
pub const MAGENTA: Self = Self::rgb(1.0, 0.0, 1.0);
pub const GRAY: Self = Self::rgb(0.5, 0.5, 0.5);
pub const TRANSPARENT: Self = Self::rgba(0.0, 0.0, 0.0, 0.0);
fn as_d2d1(&self) -> D2D1_COLOR_F {
D2D1_COLOR_F {
r: self.r,
g: self.g,
b: self.b,
a: self.a,
}
}
}
impl Default for Color {
fn default() -> Self {
Self::BLACK
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TextAlignment {
#[default]
Left,
Right,
Center,
Justified,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ParagraphAlignment {
#[default]
Top,
Bottom,
Center,
}
pub struct D2DFactory {
factory: ID2D1Factory,
}
impl D2DFactory {
pub fn new() -> Result<Self> {
let options = D2D1_FACTORY_OPTIONS::default();
let factory: ID2D1Factory =
unsafe { D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, Some(&options))? };
Ok(Self { factory })
}
pub fn create_hwnd_render_target(&self, hwnd: HWND) -> Result<RenderTarget> {
let mut rect = windows::Win32::Foundation::RECT::default();
unsafe {
windows::Win32::UI::WindowsAndMessaging::GetClientRect(hwnd, &mut rect)?;
}
let size = D2D_SIZE_U {
width: (rect.right - rect.left) as u32,
height: (rect.bottom - rect.top) as u32,
};
let render_target_properties = D2D1_RENDER_TARGET_PROPERTIES {
r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT,
pixelFormat: D2D1_PIXEL_FORMAT {
format: DXGI_FORMAT_B8G8R8A8_UNORM,
alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED,
},
dpiX: 0.0,
dpiY: 0.0,
..Default::default()
};
let hwnd_render_target_properties = D2D1_HWND_RENDER_TARGET_PROPERTIES {
hwnd,
pixelSize: size,
presentOptions: D2D1_PRESENT_OPTIONS_NONE,
};
let render_target = unsafe {
self.factory
.CreateHwndRenderTarget(&render_target_properties, &hwnd_render_target_properties)?
};
Ok(RenderTarget {
target: render_target,
})
}
}
pub struct RenderTarget {
target: ID2D1HwndRenderTarget,
}
impl RenderTarget {
pub fn resize(&self, width: u32, height: u32) -> Result<()> {
let size = D2D_SIZE_U { width, height };
unsafe {
self.target.Resize(&size)?;
}
Ok(())
}
pub fn begin_draw(&self) {
unsafe {
self.target.BeginDraw();
}
}
pub fn end_draw(&self) -> Result<()> {
unsafe {
self.target.EndDraw(None, None)?;
}
Ok(())
}
pub fn clear(&self, color: Color) {
unsafe {
self.target.Clear(Some(&color.as_d2d1()));
}
}
pub fn create_solid_brush(&self, color: Color) -> Result<SolidBrush> {
let props = D2D1_BRUSH_PROPERTIES {
opacity: 1.0,
transform: Matrix3x2::identity(),
};
let brush = unsafe {
self.target
.CreateSolidColorBrush(&color.as_d2d1(), Some(&props))?
};
Ok(SolidBrush { brush })
}
pub fn draw_line(
&self,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
brush: &SolidBrush,
stroke_width: f32,
) {
let p1 = D2D_POINT_2F { x: x1, y: y1 };
let p2 = D2D_POINT_2F { x: x2, y: y2 };
unsafe {
self.target
.DrawLine(p1, p2, &brush.brush, stroke_width, None);
}
}
pub fn draw_rect(
&self,
x: f32,
y: f32,
width: f32,
height: f32,
brush: &SolidBrush,
stroke_width: f32,
) {
let rect = D2D_RECT_F {
left: x,
top: y,
right: x + width,
bottom: y + height,
};
unsafe {
self.target
.DrawRectangle(&rect, &brush.brush, stroke_width, None);
}
}
pub fn fill_rect(&self, x: f32, y: f32, width: f32, height: f32, brush: &SolidBrush) {
let rect = D2D_RECT_F {
left: x,
top: y,
right: x + width,
bottom: y + height,
};
unsafe {
self.target.FillRectangle(&rect, &brush.brush);
}
}
pub fn draw_rounded_rect(
&self,
x: f32,
y: f32,
width: f32,
height: f32,
radius_x: f32,
radius_y: f32,
brush: &SolidBrush,
stroke_width: f32,
) {
let rect = D2D1_ROUNDED_RECT {
rect: D2D_RECT_F {
left: x,
top: y,
right: x + width,
bottom: y + height,
},
radiusX: radius_x,
radiusY: radius_y,
};
unsafe {
self.target
.DrawRoundedRectangle(&rect, &brush.brush, stroke_width, None);
}
}
pub fn fill_rounded_rect(
&self,
x: f32,
y: f32,
width: f32,
height: f32,
radius_x: f32,
radius_y: f32,
brush: &SolidBrush,
) {
let rect = D2D1_ROUNDED_RECT {
rect: D2D_RECT_F {
left: x,
top: y,
right: x + width,
bottom: y + height,
},
radiusX: radius_x,
radiusY: radius_y,
};
unsafe {
self.target.FillRoundedRectangle(&rect, &brush.brush);
}
}
pub fn draw_ellipse(
&self,
center_x: f32,
center_y: f32,
radius_x: f32,
radius_y: f32,
brush: &SolidBrush,
stroke_width: f32,
) {
let ellipse = D2D1_ELLIPSE {
point: D2D_POINT_2F {
x: center_x,
y: center_y,
},
radiusX: radius_x,
radiusY: radius_y,
};
unsafe {
self.target
.DrawEllipse(&ellipse, &brush.brush, stroke_width, None);
}
}
pub fn fill_ellipse(
&self,
center_x: f32,
center_y: f32,
radius_x: f32,
radius_y: f32,
brush: &SolidBrush,
) {
let ellipse = D2D1_ELLIPSE {
point: D2D_POINT_2F {
x: center_x,
y: center_y,
},
radiusX: radius_x,
radiusY: radius_y,
};
unsafe {
self.target.FillEllipse(&ellipse, &brush.brush);
}
}
pub fn draw_text(
&self,
text: &str,
format: &TextFormat,
x: f32,
y: f32,
width: f32,
height: f32,
brush: &SolidBrush,
) {
let wide: Vec<u16> = text.encode_utf16().collect();
let rect = D2D_RECT_F {
left: x,
top: y,
right: x + width,
bottom: y + height,
};
unsafe {
self.target.DrawText(
&wide,
&format.format,
&rect,
&brush.brush,
D2D1_DRAW_TEXT_OPTIONS_NONE,
DWRITE_MEASURING_MODE_NATURAL,
);
}
}
pub fn size(&self) -> (f32, f32) {
let size = unsafe { self.target.GetSize() };
(size.width, size.height)
}
}
pub struct SolidBrush {
brush: ID2D1SolidColorBrush,
}
impl SolidBrush {
pub fn set_color(&self, color: Color) {
unsafe {
self.brush.SetColor(&color.as_d2d1());
}
}
pub fn color(&self) -> Color {
let c = unsafe { self.brush.GetColor() };
Color {
r: c.r,
g: c.g,
b: c.b,
a: c.a,
}
}
pub fn set_opacity(&self, opacity: f32) {
unsafe {
self.brush.SetOpacity(opacity);
}
}
}
pub struct DWriteFactory {
factory: IDWriteFactory,
}
impl DWriteFactory {
pub fn new() -> Result<Self> {
let factory: IDWriteFactory = unsafe { DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)? };
Ok(Self { factory })
}
pub fn create_text_format(&self, font_family: &str, font_size: f32) -> Result<TextFormat> {
let font_family_wide: Vec<u16> = font_family
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let locale_wide: Vec<u16> = "en-US".encode_utf16().chain(std::iter::once(0)).collect();
let format = unsafe {
self.factory.CreateTextFormat(
windows::core::PCWSTR(font_family_wide.as_ptr()),
None,
DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_STRETCH_NORMAL,
font_size,
windows::core::PCWSTR(locale_wide.as_ptr()),
)?
};
Ok(TextFormat { format })
}
}
pub struct TextFormat {
format: IDWriteTextFormat,
}
impl TextFormat {
pub fn set_text_alignment(&self, alignment: TextAlignment) -> Result<()> {
let align = match alignment {
TextAlignment::Left => DWRITE_TEXT_ALIGNMENT_LEADING,
TextAlignment::Right => DWRITE_TEXT_ALIGNMENT_TRAILING,
TextAlignment::Center => DWRITE_TEXT_ALIGNMENT_CENTER,
TextAlignment::Justified => DWRITE_TEXT_ALIGNMENT_JUSTIFIED,
};
unsafe {
self.format.SetTextAlignment(align)?;
}
Ok(())
}
pub fn set_paragraph_alignment(&self, alignment: ParagraphAlignment) -> Result<()> {
let align = match alignment {
ParagraphAlignment::Top => DWRITE_PARAGRAPH_ALIGNMENT_NEAR,
ParagraphAlignment::Bottom => DWRITE_PARAGRAPH_ALIGNMENT_FAR,
ParagraphAlignment::Center => DWRITE_PARAGRAPH_ALIGNMENT_CENTER,
};
unsafe {
self.format.SetParagraphAlignment(align)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_rgb() {
let c = Color::rgb(0.5, 0.25, 0.75);
assert_eq!(c.r, 0.5);
assert_eq!(c.g, 0.25);
assert_eq!(c.b, 0.75);
assert_eq!(c.a, 1.0);
}
#[test]
fn test_color_from_hex() {
let c = Color::from_hex(0xFF8040);
assert!((c.r - 1.0).abs() < 0.01);
assert!((c.g - 0.5).abs() < 0.01);
assert!((c.b - 0.25).abs() < 0.01);
}
#[test]
fn test_color_constants() {
assert_eq!(Color::BLACK.r, 0.0);
assert_eq!(Color::WHITE.r, 1.0);
assert_eq!(Color::RED.r, 1.0);
assert_eq!(Color::RED.g, 0.0);
}
#[test]
fn test_d2d_factory_creation() {
let _ = D2DFactory::new();
}
#[test]
fn test_dwrite_factory_creation() {
let _ = DWriteFactory::new();
}
}