Created
February 19, 2021 23:16
-
-
Save Ciantic/f99479d4988c83fa8437fec9e26fcf69 to your computer and use it in GitHub Desktop.
set_blend_mode, get_blend_mode
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//! Convenience wrappers for Direct2D objects. | |
//! | |
//! These also function as safety boundaries (though determining the | |
//! exact safety guarantees is work in progress). | |
// TODO: get rid of this when we actually do use everything | |
#![allow(unused)] | |
use std::ffi::c_void; | |
use std::fmt::{Debug, Display, Formatter}; | |
use std::marker::PhantomData; | |
use std::ops::Deref; | |
use std::ptr::{null, null_mut}; | |
use piet::kurbo::{Circle, Line, Rect, RoundedRect}; | |
use wio::com::ComPtr; | |
use winapi::shared::dxgi::{IDXGIDevice, IDXGISurface}; | |
use winapi::shared::dxgiformat::DXGI_FORMAT_R8G8B8A8_UNORM; | |
use winapi::shared::minwindef::TRUE; | |
use winapi::shared::winerror::{HRESULT, SUCCEEDED}; | |
use winapi::um::d2d1::{ | |
D2D1CreateFactory, ID2D1Bitmap, ID2D1BitmapRenderTarget, ID2D1Brush, ID2D1EllipseGeometry, | |
ID2D1Geometry, ID2D1GeometrySink, ID2D1GradientStopCollection, ID2D1Image, ID2D1Layer, | |
ID2D1PathGeometry, ID2D1RectangleGeometry, ID2D1RoundedRectangleGeometry, ID2D1SolidColorBrush, | |
ID2D1StrokeStyle, D2D1_ANTIALIAS_MODE_PER_PRIMITIVE, D2D1_BEZIER_SEGMENT, | |
D2D1_BITMAP_INTERPOLATION_MODE, D2D1_BRUSH_PROPERTIES, D2D1_COLOR_F, | |
D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE, D2D1_DEBUG_LEVEL_WARNING, D2D1_DRAW_TEXT_OPTIONS, | |
D2D1_EXTEND_MODE_CLAMP, D2D1_FACTORY_OPTIONS, D2D1_FACTORY_TYPE_MULTI_THREADED, | |
D2D1_FIGURE_BEGIN_FILLED, D2D1_FIGURE_BEGIN_HOLLOW, D2D1_FIGURE_END_CLOSED, | |
D2D1_FIGURE_END_OPEN, D2D1_FILL_MODE_ALTERNATE, D2D1_FILL_MODE_WINDING, D2D1_GAMMA_2_2, | |
D2D1_GRADIENT_STOP, D2D1_LAYER_OPTIONS_NONE, D2D1_LAYER_PARAMETERS, | |
D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES, D2D1_MATRIX_3X2_F, D2D1_POINT_2F, | |
D2D1_QUADRATIC_BEZIER_SEGMENT, D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES, D2D1_RECT_F, D2D1_SIZE_F, | |
D2D1_SIZE_U, D2D1_STROKE_STYLE_PROPERTIES, | |
}; | |
use winapi::um::d2d1_1::{ | |
ID2D1Bitmap1, ID2D1Device, ID2D1DeviceContext, ID2D1Effect, ID2D1Factory1, | |
D2D1_BITMAP_OPTIONS_NONE, D2D1_BITMAP_OPTIONS_TARGET, D2D1_BITMAP_PROPERTIES1, | |
D2D1_COMPOSITE_MODE, D2D1_DEVICE_CONTEXT_OPTIONS_NONE, D2D1_INTERPOLATION_MODE, | |
D2D1_PROPERTY_TYPE_FLOAT, | |
}; | |
use winapi::um::d2d1_1::{D2D1_PRIMITIVE_BLEND_COPY, D2D1_PRIMITIVE_BLEND_SOURCE_OVER}; | |
use winapi::um::d2d1effects::{CLSID_D2D1GaussianBlur, D2D1_GAUSSIANBLUR_PROP_STANDARD_DEVIATION}; | |
use winapi::um::dcommon::{D2D1_ALPHA_MODE, D2D1_ALPHA_MODE_PREMULTIPLIED, D2D1_PIXEL_FORMAT}; | |
use winapi::Interface; | |
use crate::conv::{circle_to_d2d, rect_to_rectf, rounded_rect_to_d2d, to_point2f}; | |
use crate::dwrite::TextLayout; | |
pub(crate) enum BlendMode { | |
SourceOver, | |
Copy, | |
Unknown(u32), | |
} | |
pub enum FillRule { | |
EvenOdd, | |
NonZero, | |
} | |
pub enum Error { | |
WinapiError(HRESULT), | |
} | |
/// A Direct2D factory object. | |
/// | |
/// This struct is public only to use for system integration in piet_common and druid-shell. It is not intended | |
/// that end-users directly use this struct. | |
pub struct D2DFactory(ComPtr<ID2D1Factory1>); | |
/// A Direct2D device. | |
pub struct D2DDevice(ComPtr<ID2D1Device>); | |
// Microsoft's API docs suggest that it's safe to access D2D factories, and anything coming out of | |
// them, from multiple threads. (In fact, if there's no underlying D3D resources, they're even | |
// `Sync`.) https://docs.microsoft.com/en-us/windows/win32/direct2d/multi-threaded-direct2d-apps | |
unsafe impl Send for D2DFactory {} | |
unsafe impl Send for D2DDevice {} | |
/// The main context that takes drawing operations. | |
/// | |
/// This type is a thin wrapper for | |
/// [ID2D1DeviceContext](https://docs.microsoft.com/en-us/windows/win32/api/d2d1_1/nn-d2d1_1-id2d1devicecontext). | |
/// | |
/// This struct is public only to use for system integration in piet_common and druid-shell. It is not intended | |
/// that end-users directly use this struct. | |
#[derive(Clone)] | |
pub struct DeviceContext(ComPtr<ID2D1DeviceContext>); | |
pub struct PathGeometry(ComPtr<ID2D1PathGeometry>); | |
pub struct RectangleGeometry(ComPtr<ID2D1RectangleGeometry>); | |
pub struct RoundedRectangleGeometry(ComPtr<ID2D1RoundedRectangleGeometry>); | |
pub struct EllipseGeometry(ComPtr<ID2D1EllipseGeometry>); | |
pub struct Geometry(ComPtr<ID2D1Geometry>); | |
pub struct GeometrySink<'a> { | |
ptr: ComPtr<ID2D1GeometrySink>, | |
// The PhantomData keeps us from doing stuff like having | |
// two GeometrySink objects open on the same PathGeometry. | |
// | |
// It's conservative, but helps avoid logic errors. | |
marker: PhantomData<&'a mut PathGeometry>, | |
} | |
pub struct GradientStopCollection(ComPtr<ID2D1GradientStopCollection>); | |
// TODO: consider not building this at all, but just Brush. | |
pub struct SolidColorBrush(ComPtr<ID2D1SolidColorBrush>); | |
pub struct StrokeStyle(ComPtr<ID2D1StrokeStyle>); | |
pub struct Layer(ComPtr<ID2D1Layer>); | |
#[derive(Clone)] | |
pub struct Brush(ComPtr<ID2D1Brush>); | |
pub struct Bitmap { | |
inner: ComPtr<ID2D1Bitmap1>, | |
pub(crate) empty_image: bool, | |
} | |
pub struct Effect(ComPtr<ID2D1Effect>); | |
// Note: there may be an opportunity here to combine with the version in | |
// piet-common direct2d_back, but the use cases are somewhat different. | |
pub struct BitmapRenderTarget(ComPtr<ID2D1BitmapRenderTarget>); | |
impl From<HRESULT> for Error { | |
fn from(hr: HRESULT) -> Error { | |
Error::WinapiError(hr) | |
} | |
} | |
impl Debug for Error { | |
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { | |
match self { | |
Error::WinapiError(hr) => write!(f, "hresult {:x}", hr), | |
} | |
} | |
} | |
impl Display for Error { | |
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { | |
match self { | |
Error::WinapiError(hr) => write!(f, "hresult {:x}", hr), | |
} | |
} | |
} | |
impl std::error::Error for Error { | |
fn description(&self) -> &str { | |
"winapi error" | |
} | |
} | |
impl From<Error> for piet::Error { | |
fn from(e: Error) -> piet::Error { | |
piet::Error::BackendError(Box::new(e)) | |
} | |
} | |
impl From<PathGeometry> for Geometry { | |
fn from(pg: PathGeometry) -> Self { | |
Geometry(pg.0.up()) | |
} | |
} | |
impl From<RectangleGeometry> for Geometry { | |
fn from(pg: RectangleGeometry) -> Self { | |
Geometry(pg.0.up()) | |
} | |
} | |
impl From<RoundedRectangleGeometry> for Geometry { | |
fn from(pg: RoundedRectangleGeometry) -> Self { | |
Geometry(pg.0.up()) | |
} | |
} | |
impl From<EllipseGeometry> for Geometry { | |
fn from(pg: EllipseGeometry) -> Self { | |
Geometry(pg.0.up()) | |
} | |
} | |
unsafe fn wrap<T, U, F>(hr: HRESULT, ptr: *mut T, f: F) -> Result<U, Error> | |
where | |
F: Fn(ComPtr<T>) -> U, | |
T: Interface, | |
{ | |
if SUCCEEDED(hr) { | |
Ok(f(ComPtr::from_raw(ptr))) | |
} else { | |
Err(hr.into()) | |
} | |
} | |
pub(crate) fn wrap_unit(hr: HRESULT) -> Result<(), Error> { | |
if SUCCEEDED(hr) { | |
Ok(()) | |
} else { | |
Err(hr.into()) | |
} | |
} | |
fn optional<T>(val: &Option<T>) -> *const T { | |
val.as_ref().map(|x| x as *const T).unwrap_or(null()) | |
} | |
fn stroke_style_to_d2d(style: Option<&StrokeStyle>) -> *mut ID2D1StrokeStyle { | |
style.map(|ss| ss.0.as_raw()).unwrap_or(null_mut()) | |
} | |
impl D2DFactory { | |
/// Create a new Direct2D factory. | |
/// | |
/// This requires Windows 7 platform update, and can also fail if | |
/// resources are unavailable. | |
pub fn new() -> Result<D2DFactory, Error> { | |
unsafe { | |
let mut ptr: *mut ID2D1Factory1 = null_mut(); | |
let hr = D2D1CreateFactory( | |
D2D1_FACTORY_TYPE_MULTI_THREADED, | |
&ID2D1Factory1::uuidof(), | |
&D2D1_FACTORY_OPTIONS { | |
debugLevel: D2D1_DEBUG_LEVEL_WARNING, | |
}, | |
&mut ptr as *mut _ as *mut _, | |
); | |
wrap(hr, ptr, D2DFactory) | |
} | |
} | |
// Would it be safe to take &ComPtr<IDXGIDevice> here? | |
/// Create D2D Device | |
/// # Safety | |
/// TODO | |
pub unsafe fn create_device(&self, dxgi_device: *mut IDXGIDevice) -> Result<D2DDevice, Error> { | |
let mut ptr = null_mut(); | |
let hr = self.0.CreateDevice(dxgi_device, &mut ptr); | |
wrap(hr, ptr, D2DDevice) | |
} | |
/// Get the raw pointer | |
pub fn get_raw(&self) -> *mut ID2D1Factory1 { | |
self.0.as_raw() | |
} | |
pub fn create_path_geometry(&self) -> Result<PathGeometry, Error> { | |
unsafe { | |
let mut ptr = null_mut(); | |
let hr = self.0.deref().deref().CreatePathGeometry(&mut ptr); | |
wrap(hr, ptr, PathGeometry) | |
} | |
} | |
pub fn create_rect_geometry(&self, rect: Rect) -> Result<RectangleGeometry, Error> { | |
unsafe { | |
let mut ptr = null_mut(); | |
let hr = self | |
.0 | |
.deref() | |
.deref() | |
.CreateRectangleGeometry(&rect_to_rectf(rect), &mut ptr); | |
wrap(hr, ptr, RectangleGeometry) | |
} | |
} | |
pub fn create_round_rect_geometry( | |
&self, | |
rect: Rect, | |
radius: f64, | |
) -> Result<RoundedRectangleGeometry, Error> { | |
unsafe { | |
let mut ptr = null_mut(); | |
let hr = self | |
.0 | |
.deref() | |
.deref() | |
.CreateRoundedRectangleGeometry(&rounded_rect_to_d2d(rect, radius), &mut ptr); | |
wrap(hr, ptr, RoundedRectangleGeometry) | |
} | |
} | |
pub fn create_circle_geometry(&self, circle: Circle) -> Result<EllipseGeometry, Error> { | |
unsafe { | |
let mut ptr = null_mut(); | |
let hr = self | |
.0 | |
.deref() | |
.deref() | |
.CreateEllipseGeometry(&circle_to_d2d(circle), &mut ptr); | |
wrap(hr, ptr, EllipseGeometry) | |
} | |
} | |
pub fn create_stroke_style( | |
&self, | |
props: &D2D1_STROKE_STYLE_PROPERTIES, | |
dashes: Option<&[f32]>, | |
) -> Result<StrokeStyle, Error> { | |
unsafe { | |
let mut ptr = null_mut(); | |
let dashes_len = dashes.map(|d| d.len()).unwrap_or(0); | |
assert!(dashes_len <= 0xffff_ffff); | |
let hr = self.0.deref().deref().CreateStrokeStyle( | |
props, | |
dashes.map(|d| d.as_ptr()).unwrap_or(null()), | |
dashes_len as u32, | |
&mut ptr, | |
); | |
wrap(hr, ptr, StrokeStyle) | |
} | |
} | |
} | |
impl D2DDevice { | |
/// Create a new device context from the device. | |
/// | |
/// This is a wrapper for | |
/// [ID2D1Device::CreateDeviceContext](https://docs.microsoft.com/en-us/windows/win32/api/d2d1_1/nf-d2d1_1-id2d1device-createdevicecontext). | |
pub fn create_device_context(&mut self) -> Result<DeviceContext, Error> { | |
unsafe { | |
let mut ptr = null_mut(); | |
let options = D2D1_DEVICE_CONTEXT_OPTIONS_NONE; | |
let hr = self.0.CreateDeviceContext(options, &mut ptr); | |
wrap(hr, ptr, DeviceContext) | |
} | |
} | |
} | |
const IDENTITY_MATRIX_3X2_F: D2D1_MATRIX_3X2_F = D2D1_MATRIX_3X2_F { | |
matrix: [[1.0, 0.0], [0.0, 1.0], [0.0, 0.0]], | |
}; | |
const DEFAULT_BRUSH_PROPERTIES: D2D1_BRUSH_PROPERTIES = D2D1_BRUSH_PROPERTIES { | |
opacity: 1.0, | |
transform: IDENTITY_MATRIX_3X2_F, | |
}; | |
impl DeviceContext { | |
/// Create a new device context from an existing COM object. | |
/// | |
/// Marked as unsafe because the device must be in a good state. | |
/// This *might* be overly conservative. | |
/// | |
/// # Safety | |
/// TODO | |
pub unsafe fn new(ptr: ComPtr<ID2D1DeviceContext>) -> DeviceContext { | |
DeviceContext(ptr) | |
} | |
/// Get the raw pointer | |
pub fn get_raw(&self) -> *mut ID2D1DeviceContext { | |
self.0.as_raw() | |
} | |
/// Get the Com ptr | |
/// TODO rename to `inner`, like for D3D11Device? | |
pub fn get_comptr(&self) -> &ComPtr<ID2D1DeviceContext> { | |
&self.0 | |
} | |
/// Create a bitmap from a DXGI surface. | |
/// | |
/// Most often, this bitmap will be used to set the target of a | |
/// DeviceContext. | |
/// | |
/// Assumes RGBA8 format and premultiplied alpha. | |
/// | |
/// The `unsafe` might be conservative, but we assume the `dxgi` | |
/// argument is in good shape to be a target. | |
/// | |
/// # Safety | |
/// TODO | |
pub unsafe fn create_bitmap_from_dxgi( | |
&self, | |
dxgi: &ComPtr<IDXGISurface>, | |
dpi_scale: f32, | |
) -> Result<Bitmap, Error> { | |
let mut ptr = null_mut(); | |
let props = D2D1_BITMAP_PROPERTIES1 { | |
pixelFormat: D2D1_PIXEL_FORMAT { | |
format: DXGI_FORMAT_R8G8B8A8_UNORM, | |
alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED, | |
}, | |
dpiX: 96.0 * dpi_scale, | |
dpiY: 96.0 * dpi_scale, | |
bitmapOptions: D2D1_BITMAP_OPTIONS_TARGET, | |
colorContext: null_mut(), | |
}; | |
let hr = self | |
.0 | |
.CreateBitmapFromDxgiSurface(dxgi.as_raw(), &props, &mut ptr); | |
wrap(hr, ptr, |ptr| Bitmap { | |
inner: ptr, | |
// I'm pretty sure an empty dxgi surface will be invalid, so we can be sure the image | |
// is not empty. | |
empty_image: false, | |
}) | |
} | |
/// Set the target for the device context. | |
/// | |
/// Useful for rendering into bitmaps. | |
pub fn set_target(&mut self, target: &Bitmap) { | |
assert!(!target.empty_image); | |
unsafe { self.0.SetTarget(target.inner.as_raw() as *mut ID2D1Image) } | |
} | |
/// Set the dpi scale. | |
/// | |
/// Mostly useful when rendering into bitmaps. | |
pub fn set_dpi_scale(&mut self, dpi_scale: f32) { | |
unsafe { | |
self.0.SetDpi(96. * dpi_scale, 96. * dpi_scale); | |
} | |
} | |
/// Begin drawing. | |
/// | |
/// This must be done before any piet drawing operations. | |
/// | |
/// There may be safety concerns (not clear what happens if the sequence | |
/// is not followed). | |
pub fn begin_draw(&mut self) { | |
unsafe { | |
self.0.BeginDraw(); | |
} | |
} | |
/// End drawing. | |
pub fn end_draw(&mut self) -> Result<(), Error> { | |
unsafe { | |
let mut tag1 = 0; | |
let mut tag2 = 0; | |
let hr = self.0.EndDraw(&mut tag1, &mut tag2); | |
wrap_unit(hr) | |
} | |
} | |
/// Set the blend mode for alpha channel | |
pub(crate) fn set_blend_mode(&mut self, blend_mode: BlendMode) { | |
unsafe { | |
self.0.SetPrimitiveBlend(match blend_mode { | |
BlendMode::Copy => D2D1_PRIMITIVE_BLEND_COPY, | |
BlendMode::SourceOver => D2D1_PRIMITIVE_BLEND_COPY, | |
BlendMode::Unknown(u) => u, | |
}) | |
} | |
} | |
/// Get the blend mode for alpha channel | |
pub(crate) fn get_blend_mode(&mut self) -> BlendMode { | |
unsafe { | |
match self.0.GetPrimitiveBlend() { | |
u if u == D2D1_PRIMITIVE_BLEND_COPY => BlendMode::Copy, | |
u if u == D2D1_PRIMITIVE_BLEND_SOURCE_OVER => BlendMode::SourceOver, | |
u => BlendMode::Unknown(u), | |
} | |
} | |
} | |
/// Clip axis aligned clip | |
/// | |
/// Currently this is used for clipping just RenderContext::clear(). If this | |
/// is used for clipping drawing primitives it requires proper stack to not | |
/// interfere with clearing. | |
pub(crate) fn push_aligned_axis_clip(&mut self, rect: Rect) { | |
unsafe { | |
self.0 | |
.PushAxisAlignedClip(&rect_to_rectf(rect), D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); | |
} | |
} | |
/// Pop axis aligned clip | |
/// | |
/// Currently this is used for just clearing. | |
pub(crate) fn pop_axis_aligned_clip(&mut self) { | |
unsafe { | |
self.0.PopAxisAlignedClip(); | |
} | |
} | |
pub(crate) fn clear(&mut self, color: D2D1_COLOR_F) { | |
unsafe { | |
self.0.Clear(&color); | |
} | |
} | |
pub(crate) fn set_transform(&mut self, transform: &D2D1_MATRIX_3X2_F) { | |
unsafe { | |
self.0.SetTransform(transform); | |
} | |
} | |
pub(crate) fn fill_geometry( | |
&mut self, | |
geom: &Geometry, | |
brush: &Brush, | |
opacity_brush: Option<&Brush>, | |
) { | |
unsafe { | |
self.0.FillGeometry( | |
geom.0.as_raw(), | |
brush.as_raw(), | |
opacity_brush.map(|b| b.as_raw()).unwrap_or(null_mut()), | |
); | |
} | |
} | |
pub(crate) fn draw_geometry( | |
&mut self, | |
geom: &Geometry, | |
brush: &Brush, | |
width: f32, | |
style: Option<&StrokeStyle>, | |
) { | |
unsafe { | |
self.0.DrawGeometry( | |
geom.0.as_raw(), | |
brush.as_raw(), | |
width, | |
style.map(|ss| ss.0.as_raw()).unwrap_or(null_mut()), | |
); | |
} | |
} | |
pub(crate) fn draw_line( | |
&self, | |
line: Line, | |
brush: &Brush, | |
width: f32, | |
style: Option<&StrokeStyle>, | |
) { | |
unsafe { | |
self.0.DrawLine( | |
to_point2f(line.p0), | |
to_point2f(line.p1), | |
brush.as_raw(), | |
width, | |
stroke_style_to_d2d(style), | |
); | |
} | |
} | |
pub(crate) fn draw_rect( | |
&self, | |
rect: Rect, | |
brush: &Brush, | |
width: f32, | |
style: Option<&StrokeStyle>, | |
) { | |
unsafe { | |
self.0.DrawRectangle( | |
&rect_to_rectf(rect), | |
brush.as_raw(), | |
width, | |
stroke_style_to_d2d(style), | |
); | |
} | |
} | |
pub(crate) fn draw_rounded_rect( | |
&mut self, | |
rect: Rect, | |
radius: f64, | |
brush: &Brush, | |
width: f32, | |
style: Option<&StrokeStyle>, | |
) { | |
let d2d_rounded_rect = rounded_rect_to_d2d(rect, radius); | |
unsafe { | |
self.0.DrawRoundedRectangle( | |
&d2d_rounded_rect, | |
brush.as_raw(), | |
width, | |
stroke_style_to_d2d(style), | |
); | |
} | |
} | |
pub(crate) fn draw_circle( | |
&self, | |
circle: Circle, | |
brush: &Brush, | |
width: f32, | |
style: Option<&StrokeStyle>, | |
) { | |
unsafe { | |
self.0.DrawEllipse( | |
&circle_to_d2d(circle), | |
brush.as_raw(), | |
width, | |
stroke_style_to_d2d(style), | |
); | |
} | |
} | |
pub(crate) fn fill_rect(&self, rect: Rect, brush: &Brush) { | |
unsafe { | |
self.0.FillRectangle(&rect_to_rectf(rect), brush.as_raw()); | |
} | |
} | |
pub(crate) fn fill_rounded_rect(&mut self, rect: Rect, radius: f64, brush: &Brush) { | |
let d2d_rounded_rect = rounded_rect_to_d2d(rect, radius); | |
unsafe { | |
self.0 | |
.FillRoundedRectangle(&d2d_rounded_rect, brush.as_raw()); | |
} | |
} | |
pub(crate) fn fill_circle(&self, circle: Circle, brush: &Brush) { | |
unsafe { | |
self.0.FillEllipse(&circle_to_d2d(circle), brush.as_raw()); | |
} | |
} | |
pub(crate) fn create_layer(&mut self, size: Option<D2D1_SIZE_F>) -> Result<Layer, Error> { | |
unsafe { | |
let mut ptr = null_mut(); | |
let hr = self.0.CreateLayer(optional(&size), &mut ptr); | |
wrap(hr, ptr, Layer) | |
} | |
} | |
// Should be &mut layer? | |
pub(crate) fn push_layer_mask(&mut self, mask: &Geometry, layer: &Layer) { | |
unsafe { | |
let params = D2D1_LAYER_PARAMETERS { | |
contentBounds: D2D1_RECT_F { | |
left: std::f32::NEG_INFINITY, | |
top: std::f32::NEG_INFINITY, | |
right: std::f32::INFINITY, | |
bottom: std::f32::INFINITY, | |
}, | |
geometricMask: mask.0.as_raw(), | |
maskAntialiasMode: D2D1_ANTIALIAS_MODE_PER_PRIMITIVE, | |
maskTransform: IDENTITY_MATRIX_3X2_F, | |
opacity: 1.0, | |
opacityBrush: null_mut(), | |
layerOptions: D2D1_LAYER_OPTIONS_NONE, | |
}; | |
self.0.deref().deref().PushLayer(¶ms, layer.0.as_raw()); | |
} | |
} | |
pub(crate) fn pop_layer(&mut self) { | |
unsafe { | |
self.0.PopLayer(); | |
} | |
} | |
/// This method should not be called directly. Callers should instead call | |
/// D2DRenderContext::solid_brush so values can be cached. | |
pub(crate) fn create_solid_color(&mut self, color: D2D1_COLOR_F) -> Result<Brush, Error> { | |
unsafe { | |
let mut ptr = null_mut(); | |
let hr = self | |
.0 | |
.CreateSolidColorBrush(&color, &DEFAULT_BRUSH_PROPERTIES, &mut ptr); | |
wrap(hr, ptr, |p| Brush(p.up())) | |
} | |
} | |
pub(crate) fn create_gradient_stops( | |
&mut self, | |
stops: &[D2D1_GRADIENT_STOP], | |
) -> Result<GradientStopCollection, Error> { | |
unsafe { | |
// Should this assert or should we return an overflow error? Super | |
// unlikely in either case. | |
assert!(stops.len() <= 0xffff_ffff); | |
let mut ptr = null_mut(); | |
// The `deref` is because there is a method of the same name in DeviceContext | |
// (with fancier color space controls). We'll take the vanilla one for now. | |
let hr = self.0.deref().deref().CreateGradientStopCollection( | |
stops.as_ptr(), | |
stops.len() as u32, | |
D2D1_GAMMA_2_2, | |
D2D1_EXTEND_MODE_CLAMP, | |
&mut ptr, | |
); | |
wrap(hr, ptr, GradientStopCollection) | |
} | |
} | |
#[allow(clippy::trivially_copy_pass_by_ref)] | |
pub(crate) fn create_linear_gradient( | |
&mut self, | |
props: &D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES, | |
stops: &GradientStopCollection, | |
) -> Result<Brush, Error> { | |
unsafe { | |
let mut ptr = null_mut(); | |
let hr = self.0.CreateLinearGradientBrush( | |
props, | |
&DEFAULT_BRUSH_PROPERTIES, | |
stops.0.as_raw(), | |
&mut ptr, | |
); | |
wrap(hr, ptr, |p| Brush(p.up())) | |
} | |
} | |
pub(crate) fn create_radial_gradient( | |
&mut self, | |
props: &D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES, | |
stops: &GradientStopCollection, | |
) -> Result<Brush, Error> { | |
unsafe { | |
let mut ptr = null_mut(); | |
let hr = self.0.CreateRadialGradientBrush( | |
props, | |
&DEFAULT_BRUSH_PROPERTIES, | |
stops.0.as_raw(), | |
&mut ptr, | |
); | |
wrap(hr, ptr, |p| Brush(p.up())) | |
} | |
} | |
// Buf is always interpreted as RGBA32 premultiplied. | |
pub(crate) fn create_bitmap( | |
&mut self, | |
width: usize, | |
height: usize, | |
buf: &[u8], | |
alpha_mode: D2D1_ALPHA_MODE, | |
) -> Result<Bitmap, Error> { | |
// Maybe using TryInto would be more Rust-like. | |
// Note: value is set so that multiplying by 4 (for pitch) is valid. | |
assert!(width != 0 && width <= 0x3fff_ffff); | |
assert!(height != 0 && height <= 0xffff_ffff); | |
let size = D2D1_SIZE_U { | |
width: width as u32, | |
height: height as u32, | |
}; | |
let format = D2D1_PIXEL_FORMAT { | |
format: DXGI_FORMAT_R8G8B8A8_UNORM, | |
alphaMode: alpha_mode, | |
}; | |
let props = D2D1_BITMAP_PROPERTIES1 { | |
pixelFormat: format, | |
dpiX: 96.0, | |
dpiY: 96.0, | |
bitmapOptions: D2D1_BITMAP_OPTIONS_NONE, | |
colorContext: null_mut(), | |
}; | |
let pitch = (width * 4) as u32; | |
unsafe { | |
let mut ptr = null_mut(); | |
let hr = self.0.deref().CreateBitmap( | |
size, | |
buf.as_ptr() as *const c_void, | |
pitch, | |
&props, | |
&mut ptr, | |
); | |
wrap(hr, ptr, |ptr| Bitmap { | |
inner: ptr, | |
empty_image: false, | |
}) | |
} | |
} | |
/// Create a valid empty image | |
/// | |
/// The image will actually be a 1x1 transparent pixel, but with the `empty_image` flag set so | |
/// rendering can be skipped. | |
pub(crate) fn create_empty_bitmap(&mut self) -> Result<Bitmap, Error> { | |
unsafe { | |
let mut ptr = null_mut(); | |
let hr = self.0.deref().CreateBitmap( | |
D2D1_SIZE_U { | |
width: 1, | |
height: 1, | |
}, | |
[0, 0, 0, 0].as_ptr() as *const c_void, | |
4, | |
&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: null_mut(), | |
}, | |
&mut ptr, | |
); | |
wrap(hr, ptr, |ptr| Bitmap { | |
inner: ptr, | |
empty_image: true, | |
}) | |
} | |
} | |
pub(crate) fn draw_text_layout( | |
&mut self, | |
origin: D2D1_POINT_2F, | |
layout: &TextLayout, | |
brush: &Brush, | |
options: D2D1_DRAW_TEXT_OPTIONS, | |
) { | |
unsafe { | |
self.0 | |
.DrawTextLayout(origin, layout.get_raw(), brush.as_raw(), options); | |
} | |
} | |
#[allow(clippy::trivially_copy_pass_by_ref)] | |
pub(crate) fn draw_bitmap( | |
&mut self, | |
bitmap: &Bitmap, | |
dst_rect: &D2D1_RECT_F, | |
opacity: f32, | |
interp_mode: D2D1_BITMAP_INTERPOLATION_MODE, | |
src_rect: Option<&D2D1_RECT_F>, | |
) { | |
unsafe { | |
// derefs are so we get RenderTarget method rather than DeviceContext method. | |
// pointer casts are partly to undo that :) | |
self.0.deref().deref().DrawBitmap( | |
bitmap.inner.as_raw() as *mut ID2D1Bitmap, | |
dst_rect, | |
opacity, | |
interp_mode, | |
src_rect.map(|r| r as *const _).unwrap_or(null()), | |
); | |
} | |
} | |
// Discussion question: should we be using stddev instead of radius? | |
pub(crate) fn create_blur_effect(&mut self, radius: f64) -> Result<Effect, Error> { | |
unsafe { | |
let mut ptr = null_mut(); | |
let hr = self | |
.0 | |
.deref() | |
.CreateEffect(&CLSID_D2D1GaussianBlur, &mut ptr); | |
let effect = wrap(hr, ptr, Effect)?; | |
let val = radius as f32; | |
let hr = effect.0.SetValue( | |
D2D1_GAUSSIANBLUR_PROP_STANDARD_DEVIATION, | |
D2D1_PROPERTY_TYPE_FLOAT, | |
&val as *const _ as *const _, | |
std::mem::size_of_val(&val) as u32, | |
); | |
wrap_unit(hr)?; | |
Ok(effect) | |
} | |
} | |
// This is basically equivalent to an override of ID2D1DeviceContext::DrawImage method | |
// https://docs.microsoft.com/en-us/windows/win32/api/d2d1_1/nf-d2d1_1-id2d1devicecontext-drawimage(id2d1effect_constd2d1_point_2f_constd2d1_rect_f_d2d1_interpolation_mode_d2d1_composite_mode) | |
pub(crate) fn draw_image_effect( | |
&mut self, | |
effect: &Effect, | |
target_offset: Option<D2D1_POINT_2F>, | |
image_rect: Option<D2D1_RECT_F>, | |
interpolation_mode: D2D1_INTERPOLATION_MODE, | |
composite_mode: D2D1_COMPOSITE_MODE, | |
) { | |
unsafe { | |
let mut ptr = null_mut(); | |
effect.0.GetOutput(&mut ptr); | |
let output = ComPtr::from_raw(ptr); | |
self.0.DrawImage( | |
output.as_raw(), | |
optional(&target_offset), | |
optional(&image_rect), | |
interpolation_mode, | |
composite_mode, | |
); | |
} | |
} | |
// Note: the pixel size is not specified. As a potential future optimization, | |
// we can be more sophisticated in choosing a pixel size. | |
pub(crate) fn create_compatible_render_target( | |
&mut self, | |
width_f: f32, | |
height_f: f32, | |
) -> Result<BitmapRenderTarget, Error> { | |
unsafe { | |
let mut ptr = null_mut(); | |
let size_f = D2D1_SIZE_F { | |
width: width_f, | |
height: height_f, | |
}; | |
// It might be slightly cleaner to not specify the format, but we want | |
// premultiplied alpha even if for whatever reason the parent render target | |
// doesn't have that. | |
let format = D2D1_PIXEL_FORMAT { | |
format: DXGI_FORMAT_R8G8B8A8_UNORM, | |
alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED, | |
}; | |
let options = D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE; | |
let hr = | |
self.0 | |
.CreateCompatibleRenderTarget(&size_f, null(), &format, options, &mut ptr); | |
wrap(hr, ptr, BitmapRenderTarget) | |
} | |
} | |
} | |
impl PathGeometry { | |
pub fn open(&mut self) -> Result<GeometrySink, Error> { | |
unsafe { | |
let mut ptr = null_mut(); | |
let hr = (self.0).Open(&mut ptr); | |
wrap(hr, ptr, |ptr| GeometrySink { | |
ptr, | |
marker: Default::default(), | |
}) | |
} | |
} | |
} | |
// Note: this impl has not been audited for safety. It might be possible | |
// to provoke a crash by doing things in the wrong order. | |
impl<'a> GeometrySink<'a> { | |
pub fn set_fill_mode(&mut self, fill_rule: FillRule) { | |
let fill_mode = match fill_rule { | |
FillRule::EvenOdd => D2D1_FILL_MODE_ALTERNATE, | |
FillRule::NonZero => D2D1_FILL_MODE_WINDING, | |
}; | |
unsafe { | |
self.ptr.SetFillMode(fill_mode); | |
} | |
} | |
pub fn add_bezier( | |
&mut self, | |
point1: D2D1_POINT_2F, | |
point2: D2D1_POINT_2F, | |
point3: D2D1_POINT_2F, | |
) { | |
let seg = D2D1_BEZIER_SEGMENT { | |
point1, | |
point2, | |
point3, | |
}; | |
unsafe { | |
self.ptr.AddBezier(&seg); | |
} | |
} | |
pub fn add_quadratic_bezier(&mut self, point1: D2D1_POINT_2F, point2: D2D1_POINT_2F) { | |
let seg = D2D1_QUADRATIC_BEZIER_SEGMENT { point1, point2 }; | |
unsafe { | |
self.ptr.AddQuadraticBezier(&seg); | |
} | |
} | |
pub fn add_line(&mut self, point: D2D1_POINT_2F) { | |
unsafe { | |
self.ptr.AddLine(point); | |
} | |
} | |
pub fn begin_figure(&mut self, start: D2D1_POINT_2F, is_filled: bool) { | |
unsafe { | |
let figure_end = if is_filled { | |
D2D1_FIGURE_BEGIN_FILLED | |
} else { | |
D2D1_FIGURE_BEGIN_HOLLOW | |
}; | |
self.ptr.BeginFigure(start, figure_end); | |
} | |
} | |
pub fn end_figure(&mut self, is_closed: bool) { | |
unsafe { | |
let figure_end = if is_closed { | |
D2D1_FIGURE_END_CLOSED | |
} else { | |
D2D1_FIGURE_END_OPEN | |
}; | |
self.ptr.EndFigure(figure_end); | |
} | |
} | |
// A case can be made for doing this in the drop instead. | |
pub fn close(self) -> Result<(), Error> { | |
unsafe { wrap_unit(self.ptr.Close()) } | |
} | |
} | |
/// This might not be needed. | |
impl Bitmap { | |
pub fn get_size(&self) -> D2D1_SIZE_F { | |
unsafe { self.inner.GetSize() } | |
} | |
} | |
impl Effect { | |
/// Set the effect's input. | |
/// | |
/// Safety concern: is this capturing the lifetime of the input? What happens | |
/// if the input is deallocated before the effect is actually run? This is not | |
/// adequately documented, but we will be conservative in actual usage. | |
pub(crate) fn set_input(&self, index: u32, input: &ID2D1Image) { | |
unsafe { | |
self.0.SetInput(index, input, TRUE); | |
} | |
} | |
} | |
impl BitmapRenderTarget { | |
// Get the bitmap. | |
// | |
// We could return a wrapped object, but it doesn't seem worth it. | |
pub(crate) fn get_bitmap(&self) -> Result<ComPtr<ID2D1Bitmap>, Error> { | |
unsafe { | |
let mut ptr = null_mut(); | |
let hr = self.0.GetBitmap(&mut ptr); | |
wrap(hr, ptr, |com_ptr| com_ptr) | |
} | |
} | |
} | |
// Note: this approach is a bit different than other wrapped types; it's basically | |
// optimized for usage in unsafe code. | |
impl Deref for BitmapRenderTarget { | |
type Target = ID2D1BitmapRenderTarget; | |
fn deref(&self) -> &Self::Target { | |
&self.0 | |
} | |
} | |
impl Brush { | |
// This impl is provided for blurred rectangle drawing. There are other ways | |
// to factor this (for example, by making methods available on the bitmap | |
// render target). | |
pub(crate) fn as_raw(&self) -> *mut ID2D1Brush { | |
self.0.as_raw() | |
} | |
} | |
mod tests { | |
use super::*; | |
#[test] | |
fn geom_builder() { | |
let mut factory = D2DFactory::new().unwrap(); | |
let mut p = factory.create_path_geometry().unwrap(); | |
let mut s1 = p.open().unwrap(); | |
// Note: if the next two lines are swapped, it's a compile | |
// error. | |
s1.close(); | |
if let Ok(mut s2) = p.open() { | |
s2.close(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment