// 7 september 2015 #include "area.h" static ID2D1Factory *d2dfactory = NULL; HRESULT initDraw(void) { D2D1_FACTORY_OPTIONS opts; ZeroMemory(&opts, sizeof (D2D1_FACTORY_OPTIONS)); // TODO make this an option opts.debugLevel = D2D1_DEBUG_LEVEL_NONE; return D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &IID_ID2D1Factory, &opts, (void **) (&d2dfactory)); } void uninitDraw(void) { ID2D1Factory_Release(d2dfactory); } ID2D1HwndRenderTarget *makeHWNDRenderTarget(HWND hwnd) { D2D1_RENDER_TARGET_PROPERTIES props; D2D1_HWND_RENDER_TARGET_PROPERTIES hprops; HDC dc; RECT r; ID2D1HwndRenderTarget *rt; HRESULT hr; // we need a DC for the DPI // we *could* just use the screen DPI but why when we have a window handle and its DC has a DPI dc = GetDC(hwnd); if (dc == NULL) logLastError("error getting DC to find DPI in makeHWNDRenderTarget()"); ZeroMemory(&props, sizeof (D2D1_RENDER_TARGET_PROPERTIES)); props.type = D2D1_RENDER_TARGET_TYPE_DEFAULT; props.pixelFormat.format = DXGI_FORMAT_UNKNOWN; props.pixelFormat.alphaMode = D2D1_ALPHA_MODE_UNKNOWN; props.dpiX = GetDeviceCaps(dc, LOGPIXELSX); props.dpiY = GetDeviceCaps(dc, LOGPIXELSY); props.usage = D2D1_RENDER_TARGET_USAGE_NONE; props.minLevel = D2D1_FEATURE_LEVEL_DEFAULT; if (ReleaseDC(hwnd, dc) == 0) logLastError("error releasing DC for finding DPI in makeHWNDRenderTarget()"); if (GetClientRect(hwnd, &r) == 0) logLastError("error getting current size of window in makeHWNDRenderTarget()"); ZeroMemory(&hprops, sizeof (D2D1_HWND_RENDER_TARGET_PROPERTIES)); hprops.hwnd = hwnd; hprops.pixelSize.width = r.right - r.left; hprops.pixelSize.height = r.bottom - r.top; hprops.presentOptions = D2D1_PRESENT_OPTIONS_NONE; hr = ID2D1Factory_CreateHwndRenderTarget(d2dfactory, &props, &hprops, &rt); if (hr != S_OK) logHRESULT("error creating area HWND render target in makeHWNDRenderTarget()", hr); return rt; } struct uiDrawPath { ID2D1PathGeometry *path; ID2D1GeometrySink *sink; BOOL inFigure; }; uiDrawPath *uiDrawNewPath(uiDrawFillMode fillmode) { uiDrawPath *p; HRESULT hr; // TODO uiNew p = malloc(sizeof (uiDrawPath)); hr = ID2D1Factory_CreatePathGeometry(d2dfactory, &(p->path)); // TODO make sure this is the only success code if (hr != S_OK) logHRESULT("error creating path in uiDrawNewPath()", hr); hr = ID2D1PathGeometry_Open(p->path, &(p->sink)); if (hr != S_OK) logHRESULT("error opening path in uiDrawNewPath()", hr); switch (fillmode) { case uiDrawFillModeWinding: ID2D1GeometrySink_SetFillMode(p->sink, D2D1_FILL_MODE_WINDING); break; case uiDrawFillModeAlternate: ID2D1GeometrySink_SetFillMode(p->sink, D2D1_FILL_MODE_ALTERNATE); break; } p->inFigure = FALSE; return p; } void uiDrawFreePath(uiDrawPath *p) { if (p->inFigure) ID2D1GeometrySink_EndFigure(p->sink, D2D1_FIGURE_END_OPEN); if (p->sink != NULL) // TODO close sink first? ID2D1GeometrySink_Release(p->sink); ID2D1PathGeometry_Release(p->path); // TODO uiFree free(p); } void uiDrawPathNewFigure(uiDrawPath *p, double x, double y) { D2D1_POINT_2F pt; if (p->inFigure) ID2D1GeometrySink_EndFigure(p->sink, D2D1_FIGURE_END_OPEN); pt.x = x; pt.y = y; ID2D1GeometrySink_BeginFigure(p->sink, pt, D2D1_FIGURE_BEGIN_FILLED); p->inFigure = TRUE; } static void arcEndpoint(double xCenter, double yCenter, double radius, double startAngle, double *startX, double *startY) { FLOAT sinStart, cosStart; // unfortunately D2D1SinCos() is only defined on Windows 8 and newer // the MSDN page doesn't say this, but says it requires d2d1_1.h, which is listed as only supported on Windows 8 and newer elsewhere on MSDN // so we must use sin() and cos() and hope it's right... sinStart = sin(startAngle); cosStart = cos(startAngle); *startX = xCenter + radius * cosStart; *startY = yCenter + radius * sinStart; } // An arc in Direct2D is defined by the chord between its endpoints, not solely by the sweep angle. // There are four possible arcs with the same sweep amount that you can draw this way. // See https://www.youtube.com/watch?v=ATS0ANW1UxQ for a demonstration. // TODO stress test this, compare to cairo and Core Graphics; handle the case where sweep >= 2π static void doDrawArc(ID2D1GeometrySink *sink, double startX, double startY, double endX, double endY, double radius, double sweep) { D2D1_ARC_SEGMENT as; as.point.x = endX; as.point.y = endY; as.size.width = radius; as.size.height = radius; as.rotationAngle = sweep * (180.0 / M_PI); // TODO explain this if (sweep < 0) as.sweepDirection = D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE; else as.sweepDirection = D2D1_SWEEP_DIRECTION_CLOCKWISE; if (fabs(sweep) > M_PI) as.arcSize = D2D1_ARC_SIZE_LARGE; else as.arcSize = D2D1_ARC_SIZE_SMALL; ID2D1GeometrySink_AddArc(sink, &as); } void uiDrawPathNewFigureWithArc(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double endAngle) { double startX, startY; double endX, endY; // make the new figure arcEndpoint(xCenter, yCenter, radius, startAngle, &startX, &startY); uiDrawPathNewFigure(p, startX, startY); // draw the arc arcEndpoint(xCenter, yCenter, radius, endAngle, &endX, &endY); doDrawArc(p->sink, startX, startY, endX, endY, radius, endAngle - startAngle); } void uiDrawPathLineTo(uiDrawPath *p, double x, double y) { D2D1_POINT_2F pt; pt.x = x; pt.y = y; ID2D1GeometrySink_AddLine(p->sink, pt); } void uiDrawPathArcTo(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double endAngle) { double startX, startY; double endX, endY; // draw the starting line arcEndpoint(xCenter, yCenter, radius, startAngle, &startX, &startY); uiDrawPathLineTo(p, startX, startY); // draw the arc arcEndpoint(xCenter, yCenter, radius, endAngle, &endX, &endY); doDrawArc(p->sink, startX, startY, endX, endY, radius, endAngle - startAngle); } void uiDrawPathBezierTo(uiDrawPath *p, double c1x, double c1y, double c2x, double c2y, double endX, double endY) { D2D1_BEZIER_SEGMENT s; s.point1.x = c1x; s.point1.y = c1y; s.point2.x = c2x; s.point2.y = c2y; s.point3.x = endX; s.point3.y = endY; ID2D1GeometrySink_AddBezier(p->sink, &s); } void uiDrawPathCloseFigure(uiDrawPath *p) { ID2D1GeometrySink_EndFigure(p->sink, D2D1_FIGURE_END_CLOSED); p->inFigure = FALSE; } void uiDrawPathAddRectangle(uiDrawPath *p, double x, double y, double width, double height) { // this is the same algorithm used by cairo and Core Graphics, according to their documentations uiDrawPathNewFigure(p, x, y); uiDrawPathLineTo(p, x + width, y); uiDrawPathLineTo(p, x + width, y + height); uiDrawPathLineTo(p, x, y + height); uiDrawPathCloseFigure(p); } void uiDrawPathEnd(uiDrawPath *p) { HRESULT hr; if (p->inFigure) { ID2D1GeometrySink_EndFigure(p->sink, D2D1_FIGURE_END_OPEN); // needed for uiDrawFreePath() p->inFigure = FALSE; } hr = ID2D1GeometrySink_Close(p->sink); if (hr != S_OK) logHRESULT("error closing path in uiDrawPathEnd()", hr); ID2D1GeometrySink_Release(p->sink); p->sink = NULL; } struct uiDrawContext { ID2D1RenderTarget *rt; }; uiDrawContext *newContext(ID2D1RenderTarget *rt) { uiDrawContext *c; // TODO use uiNew c = (uiDrawContext *) malloc(sizeof (uiDrawContext)); c->rt = rt; return c; } static ID2D1Brush *makeSolidBrush(uiDrawBrush *b, ID2D1RenderTarget *rt, D2D1_BRUSH_PROPERTIES *props) { D2D1_COLOR_F color; ID2D1SolidColorBrush *brush; HRESULT hr; color.r = b->R; color.g = b->G; color.b = b->B; color.a = b->A; hr = ID2D1RenderTarget_CreateSolidColorBrush(rt, &color, props, &brush); if (hr != S_OK) logHRESULT("error creating solid brush in makeSolidBrush()", hr); return (ID2D1Brush *) brush; } static ID2D1GradientStopCollection *mkstops(uiDrawBrush *b, ID2D1RenderTarget *rt) { ID2D1GradientStopCollection *s; D2D1_GRADIENT_STOP *stops; size_t i; HRESULT hr; // TODO uiAlloc stops = malloc(b->NumStops * sizeof (D2D1_GRADIENT_STOP)); for (i = 0; i < b->NumStops; i++) { stops[i].position = b->Stops[i].Pos; stops[i].color.r = b->Stops[i].R; stops[i].color.g = b->Stops[i].G; stops[i].color.b = b->Stops[i].B; stops[i].color.a = b->Stops[i].A; } // TODO BUG IN MINGW // the Microsoft headers give this all 6 parameters // the MinGW headers use the 4-parameter version hr = (*(rt->lpVtbl->CreateGradientStopCollection))(rt, stops, b->NumStops, D2D1_GAMMA_2_2, // this is the default for the C++-only overload of ID2D1RenderTarget::GradientStopCollection() D2D1_EXTEND_MODE_CLAMP, &s); if (hr != S_OK) logHRESULT("error creating stop collection in mkstops()", hr); // TODO uiFree free(stops); return s; } static ID2D1Brush *makeLinearBrush(uiDrawBrush *b, ID2D1RenderTarget *rt, D2D1_BRUSH_PROPERTIES *props) { ID2D1LinearGradientBrush *brush; D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES gprops; ID2D1GradientStopCollection *stops; HRESULT hr; ZeroMemory(&gprops, sizeof (D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES)); gprops.startPoint.x = b->X0; gprops.startPoint.y = b->Y0; gprops.endPoint.x = b->X1; gprops.endPoint.y = b->Y1; stops = mkstops(b, rt); hr = ID2D1RenderTarget_CreateLinearGradientBrush(rt, &gprops, props, stops, &brush); if (hr != S_OK) logHRESULT("error creating gradient brush in makeLinearBrush()", hr); // the example at https://msdn.microsoft.com/en-us/library/windows/desktop/dd756682%28v=vs.85%29.aspx says this is safe to do now ID2D1GradientStopCollection_Release(stops); return (ID2D1Brush *) brush; } static ID2D1Brush *makeRadialBrush(uiDrawBrush *b, ID2D1RenderTarget *rt, D2D1_BRUSH_PROPERTIES *props) { ID2D1RadialGradientBrush *brush; D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES gprops; ID2D1GradientStopCollection *stops; HRESULT hr; ZeroMemory(&gprops, sizeof (D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES)); gprops.gradientOriginOffset.x = b->X0 - b->X1; gprops.gradientOriginOffset.y = b->Y0 - b->Y1; gprops.center.x = b->X1; gprops.center.y = b->Y1; gprops.radiusX = b->OuterRadius; gprops.radiusY = b->OuterRadius; stops = mkstops(b, rt); hr = ID2D1RenderTarget_CreateRadialGradientBrush(rt, &gprops, props, stops, &brush); if (hr != S_OK) logHRESULT("error creating gradient brush in makeRadialBrush()", hr); ID2D1GradientStopCollection_Release(stops); return (ID2D1Brush *) brush; } static ID2D1Brush *makeBrush(uiDrawBrush *b, ID2D1RenderTarget *rt) { D2D1_BRUSH_PROPERTIES props; ZeroMemory(&props, sizeof (D2D1_BRUSH_PROPERTIES)); props.opacity = 1.0; // identity matrix props.transform._11 = 1; props.transform._22 = 1; switch (b->Type) { case uiDrawBrushTypeSolid: return makeSolidBrush(b, rt, &props); case uiDrawBrushTypeLinearGradient: return makeLinearBrush(b, rt, &props); case uiDrawBrushTypeRadialGradient: return makeRadialBrush(b, rt, &props); // case uiDrawBrushTypeImage: // TODO } //TODO complain("invalid brush type %d in makeBrush()", b->Type); return NULL; // make compiler happy } void uiDrawStroke(uiDrawContext *c, uiDrawPath *p, uiDrawBrush *b, uiDrawStrokeParams *sp) { ID2D1Brush *brush; ID2D1StrokeStyle *style; D2D1_STROKE_STYLE_PROPERTIES dsp; HRESULT hr; brush = makeBrush(b, c->rt); ZeroMemory(&dsp, sizeof (D2D1_STROKE_STYLE_PROPERTIES)); switch (sp->Cap) { case uiDrawLineCapFlat: dsp.startCap = D2D1_CAP_STYLE_FLAT; dsp.endCap = D2D1_CAP_STYLE_FLAT; dsp.dashCap = D2D1_CAP_STYLE_FLAT; break; case uiDrawLineCapRound: dsp.startCap = D2D1_CAP_STYLE_ROUND; dsp.endCap = D2D1_CAP_STYLE_ROUND; dsp.dashCap = D2D1_CAP_STYLE_ROUND; break; case uiDrawLineCapSquare: dsp.startCap = D2D1_CAP_STYLE_SQUARE; dsp.endCap = D2D1_CAP_STYLE_SQUARE; dsp.dashCap = D2D1_CAP_STYLE_SQUARE; break; } switch (sp->Join) { case uiDrawLineJoinMiter: dsp.lineJoin = D2D1_LINE_JOIN_MITER_OR_BEVEL; dsp.miterLimit = sp->MiterLimit; break; case uiDrawLineJoinRound: dsp.lineJoin = D2D1_LINE_JOIN_ROUND; break; case uiDrawLineJoinBevel: dsp.lineJoin = D2D1_LINE_JOIN_BEVEL; break; } dsp.dashStyle = D2D1_DASH_STYLE_SOLID; dsp.dashOffset = 0; hr = ID2D1Factory_CreateStrokeStyle(d2dfactory, &dsp, NULL, 0, &style); if (hr != S_OK) logHRESULT("error creating stroke style in uiDrawStroke()", hr); ID2D1RenderTarget_DrawGeometry(c->rt, (ID2D1Geometry *) (p->path), brush, sp->Thickness, style); ID2D1StrokeStyle_Release(style); ID2D1Brush_Release(brush); } void uiDrawFill(uiDrawContext *c, uiDrawPath *p, uiDrawBrush *b) { ID2D1Brush *brush; brush = makeBrush(b, c->rt); ID2D1RenderTarget_FillGeometry(c->rt, (ID2D1Geometry *) (p->path), brush, NULL); ID2D1Brush_Release(brush); }