use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use windows::core::{Interface, HRESULT, PCWSTR};
use windows::Win32::Foundation::{D2DERR_RECREATE_TARGET, HWND};
use windows::Win32::Graphics::Direct2D::Common::{
D2D1_ALPHA_MODE_PREMULTIPLIED, D2D1_COLOR_F, D2D1_COMPOSITE_MODE_SOURCE_OVER,
D2D1_GRADIENT_STOP, D2D1_PIXEL_FORMAT, D2D_RECT_F, D2D_SIZE_U,
};
use windows::Win32::Graphics::Direct2D::{
CLSID_D2D1Shadow, D2D1CreateFactory, ID2D1Bitmap1, ID2D1Brush, ID2D1Device, ID2D1DeviceContext,
ID2D1Effect, ID2D1Factory1, ID2D1SolidColorBrush, D2D1_ANTIALIAS_MODE_ALIASED,
D2D1_ANTIALIAS_MODE_PER_PRIMITIVE, D2D1_BITMAP_OPTIONS_CANNOT_DRAW, D2D1_BITMAP_OPTIONS_NONE,
D2D1_BITMAP_OPTIONS_TARGET, D2D1_BITMAP_PROPERTIES1, D2D1_BUFFER_PRECISION_8BPC_UNORM,
D2D1_COLOR_INTERPOLATION_MODE_STRAIGHT, D2D1_COLOR_SPACE_SRGB,
D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT, D2D1_ELLIPSE, D2D1_EXTEND_MODE_CLAMP,
D2D1_FACTORY_TYPE_SINGLE_THREADED, D2D1_INTERPOLATION_MODE_LINEAR, D2D1_LAYER_OPTIONS1_NONE,
D2D1_LAYER_PARAMETERS1, D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES, D2D1_PROPERTY_TYPE_FLOAT,
D2D1_PROPERTY_TYPE_VECTOR4, D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES, D2D1_ROUNDED_RECT,
D2D1_SHADOW_PROP_BLUR_STANDARD_DEVIATION, D2D1_SHADOW_PROP_COLOR,
D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE, D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE,
};
use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_FEATURE_LEVEL};
use windows::Win32::Graphics::Direct3D11::{
D3D11CreateDevice, ID3D11Device, D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_SDK_VERSION,
};
use windows::Win32::Graphics::DirectWrite::{
DWriteCreateFactory, IDWriteFactory, IDWriteTextFormat, IDWriteTextLayout,
DWRITE_FACTORY_TYPE_SHARED, DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_WEIGHT, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_PARAGRAPH_ALIGNMENT_NEAR,
DWRITE_TEXT_ALIGNMENT_CENTER, DWRITE_TEXT_ALIGNMENT_LEADING, DWRITE_TEXT_ALIGNMENT_TRAILING,
};
use windows::Win32::Graphics::Dxgi::Common::{
DXGI_ALPHA_MODE_IGNORE, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM,
DXGI_FORMAT_UNKNOWN, DXGI_SAMPLE_DESC,
};
use windows::Win32::Graphics::Dxgi::{
IDXGIDevice, IDXGIFactory2, IDXGISurface, IDXGISwapChain1, DXGI_ERROR_DEVICE_REMOVED,
DXGI_ERROR_DEVICE_RESET, DXGI_PRESENT, DXGI_SCALING_STRETCH, DXGI_SWAP_CHAIN_DESC1,
DXGI_SWAP_CHAIN_FLAG, DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL, DXGI_USAGE_RENDER_TARGET_OUTPUT,
};
use windows::Win32::Graphics::Gdi::ValidateRect;
use windows_numerics::{Matrix3x2, Vector2};
use super::{AppHandler, WinRenderBackend};
use crate::geometry::{Color, Size};
use crate::render::{Canvas, Gradient, Paint, RenderTarget};
pub(super) struct D2DBackend {
#[allow(dead_code)]
d3d_device: ID3D11Device,
#[allow(dead_code)]
d2d_factory: ID2D1Factory1,
#[allow(dead_code)]
d2d_device: ID2D1Device,
swapchain: IDXGISwapChain1,
context: ID2D1DeviceContext,
target_bitmap: Option<ID2D1Bitmap1>,
solid: ID2D1SolidColorBrush,
grad_cache: HashMap<GradKey, ID2D1Brush>,
dwrite_factory: IDWriteFactory,
format_cache: HashMap<(String, u32, u16), IDWriteTextFormat>,
layout_cache: HashMap<LayoutKey, IDWriteTextLayout>,
image_cache: HashMap<usize, ID2D1Bitmap1>,
bake_ctx: ID2D1DeviceContext,
shadow_effect: Option<ID2D1Effect>,
shadow_cache: HashMap<ShadowKey, ID2D1Bitmap1>,
device_gen: u64,
lost: bool,
recreate_fails: u32,
}
const MAX_RECREATE_FAILS: u32 = 3;
type LayoutKey = (String, String, u32, u16, u32, u32);
type ShadowKey = (u32, u32, u32, u32, u32);
#[derive(Clone, PartialEq, Eq, Hash)]
struct GradKey {
kind: u8,
a: (i32, i32),
b: (i32, i32),
stops: Vec<(i32, u32)>,
}
#[derive(Clone)]
struct SharedDevice {
d3d_device: ID3D11Device,
dxgi_factory: IDXGIFactory2,
d2d_factory: ID2D1Factory1,
d2d_device: ID2D1Device,
dwrite_factory: IDWriteFactory,
generation: u64,
}
thread_local! {
static SHARED_DEVICE: RefCell<Option<SharedDevice>> = const { RefCell::new(None) };
static DEVICE_GEN: Cell<u64> = const { Cell::new(0) };
}
unsafe fn shared_device() -> Option<SharedDevice> {
SHARED_DEVICE.with(|cell| {
if let Some(d) = cell.borrow().as_ref() {
return Some(d.clone());
}
let d = create_shared_device()?;
*cell.borrow_mut() = Some(d.clone());
Some(d)
})
}
pub(super) fn release_shared_device() {
SHARED_DEVICE.with(|cell| {
*cell.borrow_mut() = None;
});
}
unsafe fn invalidate_shared_device(gen: u64) {
SHARED_DEVICE.with(|cell| {
let stale = cell
.borrow()
.as_ref()
.map(|d| d.generation == gen)
.unwrap_or(false);
if stale {
*cell.borrow_mut() = None;
}
});
}
unsafe fn create_shared_device() -> Option<SharedDevice> {
let mut d3d_device: Option<ID3D11Device> = None;
let mut feature_level = D3D_FEATURE_LEVEL::default();
D3D11CreateDevice(
None, D3D_DRIVER_TYPE_HARDWARE,
Default::default(), D3D11_CREATE_DEVICE_BGRA_SUPPORT,
None, D3D11_SDK_VERSION,
Some(&mut d3d_device),
Some(&mut feature_level),
None, )
.ok()?;
let d3d_device = d3d_device?;
let dxgi_device: IDXGIDevice = d3d_device.cast().ok()?;
let adapter = dxgi_device.GetAdapter().ok()?;
let dxgi_factory: IDXGIFactory2 = adapter.GetParent().ok()?;
let d2d_factory: ID2D1Factory1 =
D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, None).ok()?;
let d2d_device = d2d_factory.CreateDevice(&dxgi_device).ok()?;
let dwrite_factory: IDWriteFactory = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED).ok()?;
let generation = DEVICE_GEN.with(|g| {
let n = g.get() + 1;
g.set(n);
n
});
Some(SharedDevice {
d3d_device,
dxgi_factory,
d2d_factory,
d2d_device,
dwrite_factory,
generation,
})
}
pub(super) fn try_create(hwnd: HWND, w: i32, h: i32) -> Option<D2DBackend> {
unsafe { try_create_inner(hwnd, w, h) }
}
unsafe fn try_create_inner(hwnd: HWND, w: i32, h: i32) -> Option<D2DBackend> {
let shared = shared_device()?;
let desc = DXGI_SWAP_CHAIN_DESC1 {
Width: w.max(1) as u32,
Height: h.max(1) as u32,
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
Stereo: false.into(),
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,
BufferCount: 2,
Scaling: DXGI_SCALING_STRETCH,
SwapEffect: DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL,
AlphaMode: DXGI_ALPHA_MODE_IGNORE,
Flags: 0,
};
let swapchain = shared
.dxgi_factory
.CreateSwapChainForHwnd(&shared.d3d_device, hwnd, &desc, None, None)
.ok()?;
let context = shared
.d2d_device
.CreateDeviceContext(Default::default())
.ok()?;
let bake_ctx = shared
.d2d_device
.CreateDeviceContext(Default::default())
.ok()?;
let solid = context
.CreateSolidColorBrush(&d2d_color(Color::rgba(0, 0, 0, 255)), None)
.ok()?;
context.SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE);
let mut backend = D2DBackend {
d3d_device: shared.d3d_device.clone(),
d2d_factory: shared.d2d_factory.clone(),
d2d_device: shared.d2d_device.clone(),
swapchain,
context,
target_bitmap: None,
solid,
grad_cache: HashMap::new(),
dwrite_factory: shared.dwrite_factory.clone(),
format_cache: HashMap::new(),
layout_cache: HashMap::new(),
image_cache: HashMap::new(),
bake_ctx,
shadow_effect: None,
shadow_cache: HashMap::new(),
device_gen: shared.generation,
lost: false,
recreate_fails: 0,
};
backend.bind_target().ok()?;
Some(backend)
}
impl D2DBackend {
unsafe fn bind_target(&mut self) -> windows::core::Result<()> {
if self.target_bitmap.is_none() {
let surface: IDXGISurface = self.swapchain.GetBuffer(0)?;
let props = D2D1_BITMAP_PROPERTIES1 {
pixelFormat: D2D1_PIXEL_FORMAT {
format: DXGI_FORMAT_B8G8R8A8_UNORM,
alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED,
},
dpiX: 96.0,
dpiY: 96.0,
bitmapOptions: D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW,
colorContext: std::mem::ManuallyDrop::new(None),
};
let bitmap: ID2D1Bitmap1 = self
.context
.CreateBitmapFromDxgiSurface(&surface, Some(&props))?;
self.target_bitmap = Some(bitmap);
}
if let Some(bitmap) = &self.target_bitmap {
self.context.SetTarget(bitmap);
}
Ok(())
}
unsafe fn try_recreate(&mut self, hwnd: HWND) -> bool {
invalidate_shared_device(self.device_gen);
let mut rc = windows::Win32::Foundation::RECT::default();
let _ = windows::Win32::UI::WindowsAndMessaging::GetClientRect(hwnd, &mut rc);
let (w, h) = (rc.right - rc.left, rc.bottom - rc.top);
match try_create_inner(hwnd, w, h) {
Some(fresh) => {
*self = fresh;
true
}
None => {
self.recreate_fails += 1;
false
}
}
}
}
fn is_device_lost(hr: HRESULT) -> bool {
hr == D2DERR_RECREATE_TARGET || hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET
}
impl WinRenderBackend for D2DBackend {
fn resize(&mut self, w: i32, h: i32) {
unsafe {
self.context.SetTarget(None);
self.target_bitmap = None;
let r = self.swapchain.ResizeBuffers(
0, w.max(1) as u32,
h.max(1) as u32,
DXGI_FORMAT_UNKNOWN, DXGI_SWAP_CHAIN_FLAG(0), );
if let Err(e) = r {
if is_device_lost(e.code()) {
self.lost = true;
return;
}
debug_assert!(false, "ResizeBuffers 失败: {e:?}");
return;
}
let _ = self.bind_target();
}
}
unsafe fn paint(&mut self, hwnd: HWND, bg: Color, handler: &mut dyn AppHandler) -> bool {
if self.lost && !self.try_recreate(hwnd) {
return self.recreate_fails >= MAX_RECREATE_FAILS;
}
if self.bind_target().is_err() {
return false;
}
let mut rc = windows::Win32::Foundation::RECT::default();
let _ = windows::Win32::UI::WindowsAndMessaging::GetClientRect(hwnd, &mut rc);
let size = Size::new(rc.right - rc.left, rc.bottom - rc.top);
self.context.BeginDraw();
self.context.Clear(Some(&d2d_color(bg)));
{
let mut target = D2DTarget {
ctx: &self.context,
solid: &self.solid,
grad_cache: &mut self.grad_cache,
dwrite_factory: &self.dwrite_factory,
format_cache: &mut self.format_cache,
layout_cache: &mut self.layout_cache,
image_cache: &mut self.image_cache,
bake_ctx: &self.bake_ctx,
shadow_effect: &mut self.shadow_effect,
shadow_cache: &mut self.shadow_cache,
};
handler.render(&mut target, size);
self.context.SetTransform(&Matrix3x2::identity());
}
if let Err(e) = self.context.EndDraw(None, None) {
if is_device_lost(e.code()) {
self.lost = true;
return false;
}
}
let hr = self.swapchain.Present(1, DXGI_PRESENT(0));
if is_device_lost(hr) {
self.lost = true;
return false;
}
let _ = ValidateRect(Some(hwnd), None);
false
}
}
struct D2DTarget<'a> {
ctx: &'a ID2D1DeviceContext,
solid: &'a ID2D1SolidColorBrush,
grad_cache: &'a mut HashMap<GradKey, ID2D1Brush>,
dwrite_factory: &'a IDWriteFactory,
format_cache: &'a mut HashMap<(String, u32, u16), IDWriteTextFormat>,
layout_cache: &'a mut HashMap<LayoutKey, IDWriteTextLayout>,
image_cache: &'a mut HashMap<usize, ID2D1Bitmap1>,
bake_ctx: &'a ID2D1DeviceContext,
shadow_effect: &'a mut Option<ID2D1Effect>,
shadow_cache: &'a mut HashMap<ShadowKey, ID2D1Bitmap1>,
}
impl RenderTarget for D2DTarget<'_> {
fn make_canvas<'a>(
&'a mut self,
_engine: &'a mut dyn crate::text::TextEngine,
scale: f32,
) -> Box<dyn Canvas + 'a> {
unsafe {
self.ctx.SetTransform(&Matrix3x2::scale(scale, scale));
}
Box::new(D2DCanvas {
ctx: self.ctx,
solid: self.solid,
grad_cache: self.grad_cache,
dwrite_factory: self.dwrite_factory,
format_cache: self.format_cache,
layout_cache: self.layout_cache,
image_cache: self.image_cache,
bake_ctx: self.bake_ctx,
shadow_effect: self.shadow_effect,
shadow_cache: self.shadow_cache,
saves: Vec::new(),
pushed_clips: 0,
pushed_layers: 0,
scale,
})
}
}
struct D2DCanvas<'a> {
ctx: &'a ID2D1DeviceContext,
solid: &'a ID2D1SolidColorBrush,
grad_cache: &'a mut HashMap<GradKey, ID2D1Brush>,
dwrite_factory: &'a IDWriteFactory,
format_cache: &'a mut HashMap<(String, u32, u16), IDWriteTextFormat>,
layout_cache: &'a mut HashMap<LayoutKey, IDWriteTextLayout>,
image_cache: &'a mut HashMap<usize, ID2D1Bitmap1>,
bake_ctx: &'a ID2D1DeviceContext,
shadow_effect: &'a mut Option<ID2D1Effect>,
shadow_cache: &'a mut HashMap<ShadowKey, ID2D1Bitmap1>,
saves: Vec<u32>,
pushed_clips: u32,
pushed_layers: u32,
scale: f32,
}
impl D2DCanvas<'_> {
fn text_layout(
&mut self,
text: &str,
family: Option<&str>,
size: f32,
maxw: f32,
maxh: f32,
) -> Option<IDWriteTextLayout> {
let fam = family.unwrap_or(DEFAULT_FAMILY).to_string();
let weight = crate::text::current_weight();
let key: LayoutKey = (
fam,
text.to_string(),
size.to_bits(),
weight,
maxw.to_bits(),
maxh.to_bits(),
);
if let Some(l) = self.layout_cache.get(&key) {
return Some(l.clone());
}
let format = self.text_format(family, size)?;
let text_w = wide(text);
let layout = unsafe {
self.dwrite_factory
.CreateTextLayout(&text_w, &format, maxw, maxh)
}
.ok()?;
if self.layout_cache.len() > 512 {
self.layout_cache.clear();
}
self.layout_cache.insert(key, layout.clone());
Some(layout)
}
fn image_bitmap(&mut self, img: &crate::render::image::Image) -> Option<ID2D1Bitmap1> {
let id = img.cache_id();
if let Some(b) = self.image_cache.get(&id) {
return Some(b.clone());
}
let pm = img.pixmap(); let (w, h) = (img.width(), img.height());
if w == 0 || h == 0 {
return None;
}
let props = D2D1_BITMAP_PROPERTIES1 {
pixelFormat: D2D1_PIXEL_FORMAT {
format: DXGI_FORMAT_R8G8B8A8_UNORM,
alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED,
},
dpiX: 96.0,
dpiY: 96.0,
bitmapOptions: D2D1_BITMAP_OPTIONS_NONE,
colorContext: std::mem::ManuallyDrop::new(None),
};
let size = D2D_SIZE_U {
width: w,
height: h,
};
let bitmap = unsafe {
self.ctx
.CreateBitmap(size, Some(pm.data().as_ptr() as *const _), w * 4, &props)
}
.ok()?;
if self.image_cache.len() > 64 {
self.image_cache.clear();
}
self.image_cache.insert(id, bitmap.clone());
Some(bitmap)
}
fn build_mask(&self, wpx: u32, hpx: u32, r: f32) -> Option<ID2D1Bitmap1> {
let mut pm = tiny_skia::Pixmap::new(wpx, hpx)?;
if let Some(path) = crate::render::rounded_rect_path(0.0, 0.0, wpx as f32, hpx as f32, r) {
let mut sp = tiny_skia::Paint::default();
sp.set_color_rgba8(255, 255, 255, 255);
sp.anti_alias = true;
pm.fill_path(
&path,
&sp,
tiny_skia::FillRule::Winding,
tiny_skia::Transform::identity(),
None,
);
}
let props = D2D1_BITMAP_PROPERTIES1 {
pixelFormat: D2D1_PIXEL_FORMAT {
format: DXGI_FORMAT_R8G8B8A8_UNORM,
alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED,
},
dpiX: 96.0,
dpiY: 96.0,
bitmapOptions: D2D1_BITMAP_OPTIONS_NONE,
colorContext: std::mem::ManuallyDrop::new(None),
};
let size = D2D_SIZE_U {
width: wpx,
height: hpx,
};
unsafe {
self.bake_ctx
.CreateBitmap(size, Some(pm.data().as_ptr() as *const _), wpx * 4, &props)
}
.ok()
}
fn bake_effect(&mut self) -> Option<ID2D1Effect> {
if let Some(e) = self.shadow_effect.as_ref() {
return Some(e.clone());
}
let e = unsafe { self.bake_ctx.CreateEffect(&CLSID_D2D1Shadow) }.ok()?;
*self.shadow_effect = Some(e.clone());
Some(e)
}
fn bake_shadow(
&mut self,
wpx: u32,
hpx: u32,
r: f32,
sigma: f32,
margin: u32,
color: Color,
) -> Option<ID2D1Bitmap1> {
let mask = self.build_mask(wpx, hpx, r)?;
let effect = self.bake_effect()?;
let (ow, oh) = (wpx + 2 * margin, hpx + 2 * margin);
let props = D2D1_BITMAP_PROPERTIES1 {
pixelFormat: D2D1_PIXEL_FORMAT {
format: DXGI_FORMAT_B8G8R8A8_UNORM,
alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED,
},
dpiX: 96.0,
dpiY: 96.0,
bitmapOptions: D2D1_BITMAP_OPTIONS_TARGET,
colorContext: std::mem::ManuallyDrop::new(None),
};
let target: ID2D1Bitmap1 = unsafe {
self.bake_ctx.CreateBitmap(
D2D_SIZE_U {
width: ow,
height: oh,
},
None,
0,
&props,
)
}
.ok()?;
let col = [
color.r as f32 / 255.0,
color.g as f32 / 255.0,
color.b as f32 / 255.0,
color.a as f32 / 255.0,
];
unsafe {
effect.SetInput(0, &mask, true);
let _ = effect.SetValue(
D2D1_SHADOW_PROP_BLUR_STANDARD_DEVIATION.0 as u32,
D2D1_PROPERTY_TYPE_FLOAT,
&sigma.to_le_bytes(),
);
let col_bytes =
std::slice::from_raw_parts(col.as_ptr() as *const u8, std::mem::size_of_val(&col));
let _ = effect.SetValue(
D2D1_SHADOW_PROP_COLOR.0 as u32,
D2D1_PROPERTY_TYPE_VECTOR4,
col_bytes,
);
let out = effect.GetOutput().ok()?;
self.bake_ctx.SetTarget(&target);
self.bake_ctx.BeginDraw();
self.bake_ctx.Clear(None); let off = vec2(margin as f32, margin as f32);
self.bake_ctx.DrawImage(
&out,
Some(&off as *const _),
None,
D2D1_INTERPOLATION_MODE_LINEAR,
D2D1_COMPOSITE_MODE_SOURCE_OVER,
);
let r = self.bake_ctx.EndDraw(None, None);
self.bake_ctx.SetTarget(None); r.ok()?;
}
Some(target)
}
fn fill_brush(&mut self, paint: &Paint, x: f32, y: f32, w: f32, h: f32) -> ID2D1Brush {
match paint.gradient.as_ref() {
Some(g) => match self.gradient_brush(g, x, y, w, h) {
Some(b) => b,
None => self.solid_brush(paint.color),
},
None => self.solid_brush(paint.color),
}
}
fn solid_brush(&self, color: Color) -> ID2D1Brush {
unsafe { self.solid.SetColor(&d2d_color(color)) };
self.solid
.cast()
.expect("ID2D1SolidColorBrush is ID2D1Brush")
}
fn stroke_brush(&self, paint: &Paint) -> ID2D1Brush {
self.solid_brush(paint.color)
}
fn gradient_brush(
&mut self,
g: &Gradient,
x: f32,
y: f32,
w: f32,
h: f32,
) -> Option<ID2D1Brush> {
let stops = g.stops();
if stops.len() < 2 {
return None;
}
let q = |v: f32| (v * 1000.0).round() as i32;
let rgba = |c: Color| {
((c.r as u32) << 24) | ((c.g as u32) << 16) | ((c.b as u32) << 8) | c.a as u32
};
let stop_keys: Vec<(i32, u32)> = stops
.iter()
.map(|s| (q(s.offset.clamp(0.0, 1.0)), rgba(s.color)))
.collect();
let key = match g {
Gradient::Linear { start, end, .. } => GradKey {
kind: 0,
a: (q(start.0), q(start.1)),
b: (q(end.0), q(end.1)),
stops: stop_keys,
},
Gradient::Radial { center, radius, .. } => {
let (cx, cy) = (x + center.0 * w, y + center.1 * h);
let r = (radius * w.min(h)).max(0.01);
GradKey {
kind: 1,
a: (q(cx), q(cy)),
b: (q(r), 0),
stops: stop_keys,
}
}
};
let brush = match self.grad_cache.get(&key) {
Some(b) => b.clone(),
None => {
if self.grad_cache.len() > 256 {
self.grad_cache.clear();
}
let b = self.build_gradient_brush(g, x, y, w, h)?;
self.grad_cache.insert(key, b.clone());
b
}
};
match g {
Gradient::Linear { .. } => unsafe {
brush.SetTransform(&Matrix3x2 {
M11: w,
M12: 0.0,
M21: 0.0,
M22: h,
M31: x,
M32: y,
});
},
Gradient::Radial { .. } => unsafe {
brush.SetTransform(&Matrix3x2::identity());
},
}
Some(brush)
}
fn build_gradient_brush(
&self,
g: &Gradient,
x: f32,
y: f32,
w: f32,
h: f32,
) -> Option<ID2D1Brush> {
let d2d_stops: Vec<D2D1_GRADIENT_STOP> = g
.stops()
.iter()
.map(|s| D2D1_GRADIENT_STOP {
position: s.offset.clamp(0.0, 1.0),
color: d2d_color(s.color),
})
.collect();
unsafe {
let coll = self
.ctx
.CreateGradientStopCollection(
&d2d_stops,
D2D1_COLOR_SPACE_SRGB,
D2D1_COLOR_SPACE_SRGB,
D2D1_BUFFER_PRECISION_8BPC_UNORM,
D2D1_EXTEND_MODE_CLAMP,
D2D1_COLOR_INTERPOLATION_MODE_STRAIGHT,
)
.ok()?;
let brush: ID2D1Brush = match g {
Gradient::Linear { start, end, .. } => {
let props = D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES {
startPoint: vec2(start.0, start.1),
endPoint: vec2(end.0, end.1),
};
self.ctx
.CreateLinearGradientBrush(&props, None, &coll)
.ok()?
.cast()
.ok()?
}
Gradient::Radial { center, radius, .. } => {
let c = vec2(x + center.0 * w, y + center.1 * h);
let r = (radius * w.min(h)).max(0.01);
let props = D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES {
center: c,
gradientOriginOffset: vec2(0.0, 0.0),
radiusX: r,
radiusY: r,
};
self.ctx
.CreateRadialGradientBrush(&props, None, &coll)
.ok()?
.cast()
.ok()?
}
};
Some(brush)
}
}
fn text_format(&mut self, family: Option<&str>, size: f32) -> Option<IDWriteTextFormat> {
let fam = family.unwrap_or(DEFAULT_FAMILY).to_string();
let weight = crate::text::current_weight();
let key = (fam.clone(), size.to_bits(), weight);
if let Some(f) = self.format_cache.get(&key) {
return Some(f.clone());
}
let dw_weight = if weight == crate::text::WEIGHT_NORMAL {
DWRITE_FONT_WEIGHT_NORMAL
} else {
DWRITE_FONT_WEIGHT(weight as i32)
};
let fam_w = wide_nul(&fam);
let locale = wide_nul("zh-cn");
let format = unsafe {
self.dwrite_factory
.CreateTextFormat(
PCWSTR(fam_w.as_ptr()),
None,
dw_weight,
DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_STRETCH_NORMAL,
size, PCWSTR(locale.as_ptr()),
)
.ok()?
};
self.format_cache.insert(key, format.clone());
Some(format)
}
}
impl Drop for D2DCanvas<'_> {
fn drop(&mut self) {
debug_assert_eq!(
self.pushed_clips, 0,
"EndDraw 前裁剪栈应已平衡(pushed_clips==0),残留说明上层漏 restore"
);
while self.pushed_clips > 0 {
unsafe { self.ctx.PopAxisAlignedClip() };
self.pushed_clips -= 1;
}
debug_assert_eq!(
self.pushed_layers, 0,
"EndDraw 前合成层应已平衡(pushed_layers==0),残留说明上层 push_layer/pop_layer 不平衡"
);
while self.pushed_layers > 0 {
unsafe { self.ctx.PopLayer() };
self.pushed_layers -= 1;
}
}
}
impl Canvas for D2DCanvas<'_> {
fn dpi_scale(&self) -> f32 {
self.scale
}
fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32, paint: &Paint) {
let brush = self.fill_brush(paint, x, y, w, h);
unsafe { self.ctx.FillRectangle(&rect_f(x, y, w, h), &brush) };
}
fn fill_round_rect(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32, paint: &Paint) {
let brush = self.fill_brush(paint, x, y, w, h);
let r = radius.min(w / 2.0).min(h / 2.0).max(0.0);
let rr = D2D1_ROUNDED_RECT {
rect: rect_f(x, y, w, h),
radiusX: r,
radiusY: r,
};
unsafe { self.ctx.FillRoundedRectangle(&rr, &brush) };
}
fn stroke_round_rect(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
radius: f32,
width: f32,
paint: &Paint,
) {
let s = self.scale;
let x0 = (x * s).round() / s;
let y0 = (y * s).round() / s;
let x1 = ((x + w) * s).round() / s;
let y1 = ((y + h) * s).round() / s;
let (x, y, w, h) = (x0, y0, x1 - x0, y1 - y0);
let width = width.min(w / 2.0).min(h / 2.0).max(0.0);
let half = width / 2.0;
let r = (radius - half).max(0.0);
let rr = D2D1_ROUNDED_RECT {
rect: rect_f(x + half, y + half, w - width, h - width),
radiusX: r,
radiusY: r,
};
let brush = self.stroke_brush(paint);
unsafe { self.ctx.DrawRoundedRectangle(&rr, &brush, width, None) };
}
fn draw_line(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, width: f32, paint: &Paint) {
let brush = self.stroke_brush(paint);
unsafe {
self.ctx
.DrawLine(vec2(x0, y0), vec2(x1, y1), &brush, width, None)
};
}
fn fill_circle(&mut self, cx: f32, cy: f32, r: f32, paint: &Paint) {
let brush = self.fill_brush(paint, cx - r, cy - r, 2.0 * r, 2.0 * r);
let ellipse = D2D1_ELLIPSE {
point: vec2(cx, cy),
radiusX: r,
radiusY: r,
};
unsafe { self.ctx.FillEllipse(&ellipse, &brush) };
}
fn draw_shadow(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
radius: f32,
blur: f32,
color: Color,
) {
if color.a == 0 || w <= 0.0 || h <= 0.0 || crate::render::skia::shadows_disabled() {
return;
}
let wpx = w.ceil().max(1.0) as u32;
let hpx = h.ceil().max(1.0) as u32;
let r = radius.min(w / 2.0).min(h / 2.0).max(0.0);
let sigma = blur.max(0.0);
let margin = (sigma * 3.0).ceil().max(0.0) as u32;
let rgba = ((color.r as u32) << 24)
| ((color.g as u32) << 16)
| ((color.b as u32) << 8)
| color.a as u32;
let key: ShadowKey = (wpx, hpx, r.round() as u32, sigma.to_bits(), rgba);
let baked = match self.shadow_cache.get(&key) {
Some(b) => b.clone(),
None => {
let Some(b) = self.bake_shadow(wpx, hpx, r, sigma, margin, color) else {
return;
};
if self.shadow_cache.len() > 128 {
self.shadow_cache.clear();
}
self.shadow_cache.insert(key, b.clone());
b
}
};
let dest = rect_f(
x - margin as f32,
y - margin as f32,
(wpx + 2 * margin) as f32,
(hpx + 2 * margin) as f32,
);
unsafe {
self.ctx.DrawBitmap(
&baked,
Some(&dest),
1.0,
D2D1_INTERPOLATION_MODE_LINEAR,
None,
None,
);
}
}
fn draw_image(
&mut self,
img: &crate::render::image::Image,
dst: crate::geometry::Rect,
fit: crate::render::image::Fit,
radius: f32,
opacity: f32,
) {
use crate::render::image::Fit;
let opacity = opacity.clamp(0.0, 1.0);
if opacity <= 0.0 || dst.is_empty() {
return;
}
let (iw, ih) = (img.width() as f32, img.height() as f32);
if iw <= 0.0 || ih <= 0.0 {
return;
}
let Some(bitmap) = self.image_bitmap(img) else {
return;
};
let (dw0, dh0) = (dst.w as f32, dst.h as f32);
let (sx, sy) = match fit {
Fit::Fill => (dw0 / iw, dh0 / ih),
Fit::Contain => {
let s = (dw0 / iw).min(dh0 / ih);
(s, s)
}
Fit::Cover => {
let s = (dw0 / iw).max(dh0 / ih);
(s, s)
}
Fit::None => (1.0, 1.0),
};
let (dw, dh) = (iw * sx, ih * sy);
let tx = dst.x as f32 + (dw0 - dw) / 2.0;
let ty = dst.y as f32 + (dh0 - dh) / 2.0;
let dest_rect = D2D_RECT_F {
left: tx,
top: ty,
right: tx + dw,
bottom: ty + dh,
};
let r = radius.min(dw0 / 2.0).min(dh0 / 2.0).max(0.0);
if r <= 0.0 {
let clip = rect_f(dst.x as f32, dst.y as f32, dw0, dh0);
unsafe {
self.ctx
.PushAxisAlignedClip(&clip, D2D1_ANTIALIAS_MODE_ALIASED);
self.ctx.DrawBitmap(
&bitmap,
Some(&dest_rect),
opacity,
D2D1_INTERPOLATION_MODE_LINEAR,
None, None, );
self.ctx.PopAxisAlignedClip();
}
} else {
let rr = D2D1_ROUNDED_RECT {
rect: rect_f(dst.x as f32, dst.y as f32, dw0, dh0),
radiusX: r,
radiusY: r,
};
let geom = unsafe {
let factory = match self.ctx.GetFactory() {
Ok(f) => f,
Err(_) => return,
};
match factory.CreateRoundedRectangleGeometry(&rr) {
Ok(g) => g,
Err(_) => return,
}
};
let params = D2D1_LAYER_PARAMETERS1 {
contentBounds: INFINITE_RECT,
geometricMask: std::mem::ManuallyDrop::new(Some(geom.into())),
maskAntialiasMode: D2D1_ANTIALIAS_MODE_PER_PRIMITIVE,
maskTransform: Matrix3x2::identity(),
opacity: 1.0,
opacityBrush: std::mem::ManuallyDrop::new(None),
layerOptions: D2D1_LAYER_OPTIONS1_NONE,
};
unsafe {
self.ctx.PushLayer(¶ms, None);
self.ctx.DrawBitmap(
&bitmap,
Some(&dest_rect),
opacity,
D2D1_INTERPOLATION_MODE_LINEAR,
None, None, );
self.ctx.PopLayer();
}
}
}
fn draw_text(
&mut self,
text: &str,
rect: crate::geometry::Rect,
color: Color,
align: crate::spec::Align,
family: Option<&str>,
size: f32,
) {
if text.is_empty() || rect.is_empty() {
return;
}
let Some(layout) = self.text_layout(text, family, size, rect.w as f32, f32::MAX) else {
return;
};
let h_align = match align {
crate::spec::Align::Start | crate::spec::Align::Stretch => {
DWRITE_TEXT_ALIGNMENT_LEADING
}
crate::spec::Align::Center => DWRITE_TEXT_ALIGNMENT_CENTER,
crate::spec::Align::End => DWRITE_TEXT_ALIGNMENT_TRAILING,
};
let mut m = windows::Win32::Graphics::DirectWrite::DWRITE_TEXT_METRICS::default();
let th = if unsafe { layout.GetMetrics(&mut m) }.is_ok() {
m.height
} else {
0.0
};
unsafe {
let _ = layout.SetTextAlignment(h_align);
let _ = layout.SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR);
}
let oy = rect.y as f32 + (rect.h as f32 - th).max(0.0) / 2.0;
let origin = vec2(rect.x as f32, oy);
let brush = self.solid_brush(color);
let semi = color.a < 255;
unsafe {
if semi {
let opacity = color.a as f32 / 255.0;
let params = D2D1_LAYER_PARAMETERS1 {
contentBounds: INFINITE_RECT,
geometricMask: std::mem::ManuallyDrop::new(None),
maskAntialiasMode: D2D1_ANTIALIAS_MODE_PER_PRIMITIVE,
maskTransform: Matrix3x2::identity(),
opacity,
opacityBrush: std::mem::ManuallyDrop::new(None),
layerOptions: D2D1_LAYER_OPTIONS1_NONE,
};
self.ctx.PushLayer(¶ms, None);
self.pushed_layers += 1;
self.ctx
.SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
}
self.ctx.DrawTextLayout(
origin,
&layout,
&brush,
D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT,
);
if semi {
self.ctx
.SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE);
self.ctx.PopLayer();
self.pushed_layers -= 1;
}
}
}
fn measure_text(
&mut self,
text: &str,
family: Option<&str>,
size: f32,
) -> crate::geometry::Size {
if text.is_empty() {
return crate::geometry::Size::new(0, size.ceil() as i32);
}
let fallback = || {
crate::geometry::Size::new(
(text.chars().count() as f32 * size * 0.6).ceil() as i32,
size.ceil() as i32,
)
};
let Some(layout) = self.text_layout(text, family, size, f32::MAX, f32::MAX) else {
return fallback();
};
let mut m = windows::Win32::Graphics::DirectWrite::DWRITE_TEXT_METRICS::default();
if unsafe { layout.GetMetrics(&mut m) }.is_err() {
return fallback();
}
crate::geometry::Size::new(
m.widthIncludingTrailingWhitespace.ceil() as i32,
m.height.ceil() as i32,
)
}
fn measure_text_wrapped(
&mut self,
text: &str,
family: Option<&str>,
size: f32,
max_width: f32,
) -> crate::geometry::Size {
if text.is_empty() {
return crate::geometry::Size::new(0, size.ceil() as i32);
}
let fallback = || {
let per_line = ((max_width / (size * 0.6)).floor() as usize).max(1);
let chars = text.chars().count().max(1);
let lines = chars.div_ceil(per_line).max(1);
crate::geometry::Size::new(max_width.ceil() as i32, (size.ceil() as i32) * lines as i32)
};
let Some(layout) = self.text_layout(text, family, size, max_width, f32::MAX) else {
return fallback();
};
let mut m = windows::Win32::Graphics::DirectWrite::DWRITE_TEXT_METRICS::default();
if unsafe { layout.GetMetrics(&mut m) }.is_err() {
return fallback();
}
crate::geometry::Size::new(m.width.ceil() as i32, m.height.ceil() as i32)
}
fn push_layer(&mut self, opacity: f32) {
let params = D2D1_LAYER_PARAMETERS1 {
contentBounds: INFINITE_RECT,
geometricMask: std::mem::ManuallyDrop::new(None),
maskAntialiasMode: D2D1_ANTIALIAS_MODE_PER_PRIMITIVE,
maskTransform: Matrix3x2::identity(),
opacity: opacity.clamp(0.0, 1.0),
opacityBrush: std::mem::ManuallyDrop::new(None),
layerOptions: D2D1_LAYER_OPTIONS1_NONE,
};
unsafe { self.ctx.PushLayer(¶ms, None) };
self.pushed_layers += 1;
}
fn pop_layer(&mut self) {
if self.pushed_layers > 0 {
unsafe { self.ctx.PopLayer() };
self.pushed_layers -= 1;
}
}
fn save(&mut self) {
self.saves.push(self.pushed_clips);
}
fn restore(&mut self) {
if let Some(target) = self.saves.pop() {
while self.pushed_clips > target {
unsafe { self.ctx.PopAxisAlignedClip() };
self.pushed_clips -= 1;
}
}
}
fn clip_rect(&mut self, r: crate::geometry::Rect) {
debug_assert!(
!self.saves.is_empty(),
"clip_rect 必须在 save() 之后调用,以与 restore() 配对"
);
let rect = rect_f(r.x as f32, r.y as f32, r.w as f32, r.h as f32);
unsafe {
self.ctx
.PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED)
};
self.pushed_clips += 1;
}
}
const INFINITE_RECT: D2D_RECT_F = D2D_RECT_F {
left: -1e7,
top: -1e7,
right: 1e7,
bottom: 1e7,
};
const DEFAULT_FAMILY: &str = "Microsoft YaHei UI";
fn wide(s: &str) -> Vec<u16> {
s.encode_utf16().collect()
}
fn wide_nul(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
fn rect_f(x: f32, y: f32, w: f32, h: f32) -> D2D_RECT_F {
D2D_RECT_F {
left: x,
top: y,
right: x + w,
bottom: y + h,
}
}
fn vec2(x: f32, y: f32) -> Vector2 {
Vector2 { X: x, Y: y }
}
fn d2d_color(c: Color) -> D2D1_COLOR_F {
D2D1_COLOR_F {
r: c.r as f32 / 255.0,
g: c.g as f32 / 255.0,
b: c.b as f32 / 255.0,
a: c.a as f32 / 255.0,
}
}