// 7 september 2015 #include "uipriv_windows.h" // TODO // - write a test for transform followed by clip and clip followed by transform to make sure they work the same as on gtk+ and cocoa 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; // according to Rick Brewster, some drivers will misbehave if we don't specify this (see http://stackoverflow.com/a/33222983/3408572) hprops.presentOptions = D2D1_PRESENT_OPTIONS_RETAIN_CONTENTS; 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; p = uiNew(uiDrawPath); hr = ID2D1Factory_CreatePathGeometry(d2dfactory, &(p->path)); 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; } 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); uiFree(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; } // Direct2D arcs require a little explanation. // An arc in Direct2D is defined by the chord between the endpoints. // There are four possible arcs with the same two endpoints that you can draw this way. // See https://www.youtube.com/watch?v=ATS0ANW1UxQ for a demonstration. // There is a property rotationAngle which deals with the rotation /of the entire ellipse that forms an ellpitical arc/ - it's effectively a transformation on the arc. // That is to say, it's NOT THE SWEEP. // The sweep is defined by the start and end points and whether the arc is "large". // As a result, this design does not allow for full circles or ellipses with a single arc; they have to be simulated with two. struct arc { double xCenter; double yCenter; double radius; double startAngle; double sweep; int negative; }; // this is used for the comparison below // if it falls apart it can be changed later #define aerMax 6 * DBL_EPSILON static void drawArc(uiDrawPath *p, struct arc *a, void (*startFunction)(uiDrawPath *, double, double)) { double sinx, cosx; double startX, startY; double endX, endY; D2D1_ARC_SEGMENT as; BOOL fullCircle; double absSweep; // as above, we can't do a full circle with one arc // simulate it with two half-circles // of course, we have a dragon: equality on floating-point values! // I've chosen to do the AlmostEqualRelative() technique in https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ fullCircle = FALSE; // use the absolute value to tackle both ≥2π and ≤-2π at the same time absSweep = fabs(a->sweep); if (absSweep > (2 * M_PI)) // this part is easy fullCircle = TRUE; else { double aerDiff; aerDiff = fabs(absSweep - (2 * M_PI)); // if we got here then we know a->sweep is larger (or the same!) fullCircle = aerDiff <= absSweep * aerMax; } // TODO make sure this works right for the negative direction if (fullCircle) { a->sweep = M_PI; drawArc(p, a, startFunction); a->startAngle += M_PI; drawArc(p, a, NULL); return; } // first, figure out the arc's endpoints // 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... sinx = sin(a->startAngle); cosx = cos(a->startAngle); startX = a->xCenter + a->radius * cosx; startY = a->yCenter + a->radius * sinx; sinx = sin(a->startAngle + a->sweep); cosx = cos(a->startAngle + a->sweep); endX = a->xCenter + a->radius * cosx; endY = a->yCenter + a->radius * sinx; // now do the initial step to get the current point to be the start point // this is either creating a new figure, drawing a line, or (in the case of our full circle code above) doing nothing if (startFunction != NULL) (*startFunction)(p, startX, startY); // now we can draw the arc as.point.x = endX; as.point.y = endY; as.size.width = a->radius; as.size.height = a->radius; as.rotationAngle = 0; // as above, not relevant for circles if (a->negative) as.sweepDirection = D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE; else as.sweepDirection = D2D1_SWEEP_DIRECTION_CLOCKWISE; // TODO explain the outer if if (!a->negative) if (a->sweep > M_PI) as.arcSize = D2D1_ARC_SIZE_LARGE; else as.arcSize = D2D1_ARC_SIZE_SMALL; else // TODO especially this part if (a->sweep > M_PI) as.arcSize = D2D1_ARC_SIZE_SMALL; else as.arcSize = D2D1_ARC_SIZE_LARGE; ID2D1GeometrySink_AddArc(p->sink, &as); } void uiDrawPathNewFigureWithArc(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative) { struct arc a; a.xCenter = xCenter; a.yCenter = yCenter; a.radius = radius; a.startAngle = startAngle; a.sweep = sweep; a.negative = negative; drawArc(p, &a, uiDrawPathNewFigure); } 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 sweep, int negative) { struct arc a; a.xCenter = xCenter; a.yCenter = yCenter; a.radius = radius; a.startAngle = startAngle; a.sweep = sweep; a.negative = negative; drawArc(p, &a, uiDrawPathLineTo); } 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; struct ptrArray *states; ID2D1PathGeometry *currentClip; }; // declared below static void initStates(uiDrawContext *); static void uninitStates(uiDrawContext *); static void resetTarget(ID2D1RenderTarget *rt) { D2D1_MATRIX_3X2_F dm; // transformations persist // reset to the identity matrix ZeroMemory(&dm, sizeof (D2D1_MATRIX_3X2_F)); dm._11 = 1; dm._22 = 1; ID2D1RenderTarget_SetTransform(rt, &dm); } uiDrawContext *newContext(ID2D1RenderTarget *rt) { uiDrawContext *c; c = uiNew(uiDrawContext); c->rt = rt; initStates(c); resetTarget(c->rt); return c; } void freeContext(uiDrawContext *c) { if (c->currentClip != NULL) ID2D1PathGeometry_Release(c->currentClip); uninitStates(c); uiFree(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; stops = uiAlloc(b->NumStops * sizeof (D2D1_GRADIENT_STOP), "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); uiFree(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 } complain("invalid brush type %d in makeBrush()", b->Type); return NULL; // make compiler happy } // how clipping works: // every fill and stroke is done on a temporary layer with the clip geometry applied to it // this is really the only way to clip in Direct2D that doesn't involve opacity images // reference counting: // - initially the clip is NULL, which means do not use a layer // - the first time uiDrawClip() is called, we take a reference on the path passed in (this is also why uiPathEnd() is needed) // - every successive time, we create a new PathGeometry and merge the current clip with the new path, releasing the reference we took earlier and taking a reference to the new one // - in Save, we take another reference; in Restore we drop the refernece to the existing path geometry and transfer that saved ref to the new path geometry over to the context // uiDrawFreePath() doesn't destroy the path geometry, it just drops the reference count, so a clip can exist independent of its path static ID2D1Layer *applyClip(uiDrawContext *c) { ID2D1Layer *layer; D2D1_LAYER_PARAMETERS params; HRESULT hr; // if no clip, don't do anything if (c->currentClip == NULL) return NULL; // create a layer for clipping // we have to explicitly make the layer because we're still targeting Windows 7 // TODO MINGW BUG // this macro is supposed to take three parameters hr = (*(c->rt->lpVtbl->CreateLayer))(c->rt, NULL, &layer); if (hr != S_OK) logHRESULT("error creating clip layer in applyClip()", hr); // apply it as the clip ZeroMemory(¶ms, sizeof (D2D1_LAYER_PARAMETERS)); // this is the equivalent of InfiniteRect() in d2d1helper.h params.contentBounds.left = -FLT_MAX; params.contentBounds.top = -FLT_MAX; params.contentBounds.right = FLT_MAX; params.contentBounds.bottom = FLT_MAX; params.geometricMask = (ID2D1Geometry *) (c->currentClip); // TODO is this correct? params.maskAntialiasMode = ID2D1RenderTarget_GetAntialiasMode(c->rt); // identity matrix params.maskTransform._11 = 1; params.maskTransform._22 = 1; params.opacity = 1.0; params.opacityBrush = NULL; params.layerOptions = D2D1_LAYER_OPTIONS_NONE; // TODO is this correct? if (ID2D1RenderTarget_GetTextAntialiasMode(c->rt) == D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE) params.layerOptions = D2D1_LAYER_OPTIONS_INITIALIZE_FOR_CLEARTYPE; ID2D1RenderTarget_PushLayer(c->rt, ¶ms, layer); // return the layer so it can be freed later return layer; } static void unapplyClip(uiDrawContext *c, ID2D1Layer *layer) { if (layer == NULL) return; ID2D1RenderTarget_PopLayer(c->rt); ID2D1Layer_Release(layer); } void uiDrawStroke(uiDrawContext *c, uiDrawPath *p, uiDrawBrush *b, uiDrawStrokeParams *sp) { ID2D1Brush *brush; ID2D1StrokeStyle *style; D2D1_STROKE_STYLE_PROPERTIES dsp; FLOAT *dashes; size_t i; ID2D1Layer *cliplayer; 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; dashes = NULL; // note that dash widths and the dash phase are scaled up by the thickness by Direct2D // TODO be sure to formally document this if (sp->NumDashes != 0) { dsp.dashStyle = D2D1_DASH_STYLE_CUSTOM; dashes = (FLOAT *) uiAlloc(sp->NumDashes * sizeof (FLOAT), "FLOAT[]"); for (i = 0; i < sp->NumDashes; i++) dashes[i] = sp->Dashes[i] / sp->Thickness; } dsp.dashOffset = sp->DashPhase / sp->Thickness; hr = ID2D1Factory_CreateStrokeStyle(d2dfactory, &dsp, dashes, sp->NumDashes, &style); if (hr != S_OK) logHRESULT("error creating stroke style in uiDrawStroke()", hr); if (sp->NumDashes != 0) uiFree(dashes); cliplayer = applyClip(c); ID2D1RenderTarget_DrawGeometry(c->rt, (ID2D1Geometry *) (p->path), brush, sp->Thickness, style); unapplyClip(c, cliplayer); ID2D1StrokeStyle_Release(style); ID2D1Brush_Release(brush); } void uiDrawFill(uiDrawContext *c, uiDrawPath *p, uiDrawBrush *b) { ID2D1Brush *brush; ID2D1Layer *cliplayer; brush = makeBrush(b, c->rt); cliplayer = applyClip(c); ID2D1RenderTarget_FillGeometry(c->rt, (ID2D1Geometry *) (p->path), brush, NULL); unapplyClip(c, cliplayer); ID2D1Brush_Release(brush); } static void m2d(uiDrawMatrix *m, D2D1_MATRIX_3X2_F *d) { d->_11 = m->M11; d->_12 = m->M12; d->_21 = m->M21; d->_22 = m->M22; d->_31 = m->M31; d->_32 = m->M32; } static void d2m(D2D1_MATRIX_3X2_F *d, uiDrawMatrix *m) { m->M11 = d->_11; m->M12 = d->_12; m->M21 = d->_21; m->M22 = d->_22; m->M31 = d->_31; m->M32 = d->_32; } void uiDrawMatrixSetIdentity(uiDrawMatrix *m) { setIdentity(m); } // frustratingly all of the operations on a matrix except rotation and skeweing are provided by the C++-only d2d1helper.h // we'll have to recreate their functionalities here // the implementations are all in the main matrix.c file void uiDrawMatrixTranslate(uiDrawMatrix *m, double x, double y) { fallbackTranslate(m, x, y); } void uiDrawMatrixScale(uiDrawMatrix *m, double xCenter, double yCenter, double x, double y) { fallbackScale(m, xCenter, yCenter, x, y); } void uiDrawMatrixRotate(uiDrawMatrix *m, double x, double y, double amount) { D2D1_POINT_2F center; D2D1_MATRIX_3X2_F dm; uiDrawMatrix rm; amount *= 180 / M_PI; // must be in degrees center.x = x; center.y = y; D2D1MakeRotateMatrix(amount, center, &dm); d2m(&dm, &rm); uiDrawMatrixMultiply(m, &rm); } void uiDrawMatrixSkew(uiDrawMatrix *m, double x, double y, double xamount, double yamount) { D2D1_POINT_2F center; D2D1_MATRIX_3X2_F dm; uiDrawMatrix sm; xamount *= 180 / M_PI; // must be in degrees yamount *= 180 / M_PI; // must be in degrees center.x = x; center.y = y; D2D1MakeSkewMatrix(xamount, yamount, center, &dm); d2m(&dm, &sm); uiDrawMatrixMultiply(m, &sm); } void uiDrawMatrixMultiply(uiDrawMatrix *dest, uiDrawMatrix *src) { fallbackMultiply(dest, src); } int uiDrawMatrixInvertible(uiDrawMatrix *m) { D2D1_MATRIX_3X2_F d; m2d(m, &d); return D2D1IsMatrixInvertible(&d) != FALSE; } int uiDrawMatrixInvert(uiDrawMatrix *m) { D2D1_MATRIX_3X2_F d; m2d(m, &d); if (D2D1InvertMatrix(&d) == FALSE) return 0; d2m(&d, m); return 1; } void uiDrawMatrixTransformPoint(uiDrawMatrix *m, double *x, double *y) { fallbackTransformPoint(m, x, y); } void uiDrawMatrixTransformSize(uiDrawMatrix *m, double *x, double *y) { fallbackTransformSize(m, x, y); } void uiDrawTransform(uiDrawContext *c, uiDrawMatrix *m) { D2D1_MATRIX_3X2_F dm; uiDrawMatrix already; uiDrawMatrix temp; ID2D1RenderTarget_GetTransform(c->rt, &dm); d2m(&dm, &already); temp = *m; // don't modify m // you would think we have to do already * m, right? // WRONG! we have to do m * already // why? a few reasons // a) this lovely comment in cairo's source - http://cgit.freedesktop.org/cairo/tree/src/cairo-matrix.c?id=0537479bd1d4c5a3bc0f6f41dec4deb98481f34a#n330 // Direct2D uses column vectors and I don't know if this is even documented // b) that's what Core Graphics does // TODO see if Microsoft says to do this uiDrawMatrixMultiply(&temp, &already); m2d(&temp, &dm); ID2D1RenderTarget_SetTransform(c->rt, &dm); } // TODO for this, stroke, and fill, make sure that an open path causes error state void uiDrawClip(uiDrawContext *c, uiDrawPath *path) { ID2D1PathGeometry *newPath; ID2D1GeometrySink *newSink; HRESULT hr; // if there's no current clip, borrow the path if (c->currentClip == NULL) { c->currentClip = path->path; // we have to take our own reference to that clip ID2D1PathGeometry_AddRef(c->currentClip); return; } // otherwise we have to intersect the current path with the new one // we do that into a new path, and then replace c->currentClip with that new path hr = ID2D1Factory_CreatePathGeometry(d2dfactory, &newPath); if (hr != S_OK) logHRESULT("error creating new path in uiDrawClip()", hr); hr = ID2D1PathGeometry_Open(newPath, &newSink); if (hr != S_OK) logHRESULT("error opening new path in uiDrawClip()", hr); // TODO BUG IN MINGW // the macro is supposed to take 6 parameters hr = (*(c->currentClip->lpVtbl->Base.CombineWithGeometry))( (ID2D1Geometry *) (c->currentClip), (ID2D1Geometry *) (path->path), D2D1_COMBINE_MODE_INTERSECT, NULL, // TODO is this correct or can this be set per target? D2D1_DEFAULT_FLATTENING_TOLERANCE, (ID2D1SimplifiedGeometrySink *) newSink); if (hr != S_OK) logHRESULT("error intersecting old path with new path in uiDrawClip()", hr); hr = ID2D1GeometrySink_Close(newSink); if (hr != S_OK) logHRESULT("error closing new path in uiDrawClip()", hr); ID2D1GeometrySink_Release(newSink); // okay we have the new clip; we just need to replace the old one with it ID2D1PathGeometry_Release(c->currentClip); c->currentClip = newPath; // we have a reference already; no need for another } struct state { ID2D1DrawingStateBlock *dsb; ID2D1PathGeometry *clip; }; static void initStates(uiDrawContext *c) { c->states = newPtrArray(); } static void uninitStates(uiDrawContext *c) { // have a helpful diagnostic here if (c->states->len > 0) complain("imbalanced save/restore count in uiDrawContext in uninitStates(); did you save without restoring?"); ptrArrayDestroy(c->states); } void uiDrawSave(uiDrawContext *c) { struct state *state; ID2D1DrawingStateBlock *dsb; HRESULT hr; hr = ID2D1Factory_CreateDrawingStateBlock(d2dfactory, // TODO verify that these are correct NULL, NULL, &dsb); if (hr != S_OK) logHRESULT("error creating drawing state block in uiDrawSave()", hr); ID2D1RenderTarget_SaveDrawingState(c->rt, dsb); // if we have a clip, we need to hold another reference to it if (c->currentClip != NULL) ID2D1PathGeometry_AddRef(c->currentClip); state = uiNew(struct state); state->dsb = dsb; state->clip = c->currentClip; ptrArrayAppend(c->states, state); } void uiDrawRestore(uiDrawContext *c) { struct state *state; state = ptrArrayIndex(c->states, struct state *, c->states->len - 1); ID2D1RenderTarget_RestoreDrawingState(c->rt, state->dsb); ID2D1DrawingStateBlock_Release(state->dsb); // if we have a current clip, we need to drop it if (c->currentClip != NULL) ID2D1PathGeometry_Release(c->currentClip); // no need to explicitly addref or release; just transfer the ref c->currentClip = state->clip; uiFree(state); ptrArrayDelete(c->states, c->states->len - 1); } // TODO document that fully opaque black is the default text color; figure out whether this is upheld in various scenarios on other platforms void uiDrawText(uiDrawContext *c, double x, double y, uiDrawTextLayout *layout) { uiDrawBrush brush; ID2D1Brush *black; ZeroMemory(&brush, sizeof (uiDrawBrush)); brush.Type = uiDrawBrushTypeSolid; brush.R = 0.0; brush.G = 0.0; brush.B = 0.0; brush.A = 1.0; black = makeBrush(&brush, c->rt); doDrawText(c->rt, black, x, y, layout); ID2D1Brush_Release(black); }