diff --git a/Sources/ObjectivelyMVC/Renderer.c b/Sources/ObjectivelyMVC/Renderer.c index c60aa77c..be0b108f 100644 --- a/Sources/ObjectivelyMVC/Renderer.c +++ b/Sources/ObjectivelyMVC/Renderer.c @@ -22,6 +22,7 @@ */ #include +#include #include #include @@ -158,6 +159,87 @@ static SDL_FRect textureRegion(const Texture *texture, const SDL_Rect *src) { return (SDL_FRect) { src->x / w, src->y / h, src->w / w, src->h / h }; } +/** + * @brief The color at `x, y` for a gradient of `angle` degrees across `bounds`. + * @details `angle` is measured clockwise from north, so `0` fills upwards and `90` to the + * right. The interpolant is the point's projection onto the gradient axis, normalized by the + * extent of `bounds` along that axis, which makes the color an affine function of position: + * barycentric interpolation across the triangle then reproduces the gradient exactly, at any + * angle, from colors assigned only at the vertices. + */ +static SDL_Color gradientColor(float x, float y, const SDL_FRect *bounds, int angle, + const SDL_Color *from, const SDL_Color *to) { + + const float radians = float_radians((float) angle); + const float dx = sinf(radians), dy = -cosf(radians); + + const float cx = bounds->x + bounds->w * 0.5f; + const float cy = bounds->y + bounds->h * 0.5f; + + // the extent of the bounds along the axis, which is the box's support in that direction + const float extent = fabsf(bounds->w * dx) + fabsf(bounds->h * dy); + + float t = 0.5f; + if (extent > 0.001f) { + t = 0.5f + ((x - cx) * dx + (y - cy) * dy) / extent; + t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t); + } + + return (SDL_Color) { + (Uint8) (from->r + (to->r - from->r) * t + 0.5f), + (Uint8) (from->g + (to->g - from->g) * t + 0.5f), + (Uint8) (from->b + (to->b - from->b) * t + 0.5f), + (Uint8) (from->a + (to->a - from->a) * t + 0.5f), + }; +} + +/** + * @brief Appends the quads stroking `points`, of `width`, optionally closing the loop. + * @remarks Each segment is stroked as its own quad, centered on the segment. The joints are + * therefore not mitered; at the one and two pixel widths borders actually use, the notch is + * smaller than a pixel. + */ +static void strokePolyline(const Renderer *self, const SDL_Point *points, size_t count, + bool closed, float width, const SDL_Color *color) { + + const size_t segments = closed ? count : count - 1; + + MVC_Vertex verts[16 * 6]; + const size_t batchSize = lengthof(verts) / 6; + + for (size_t s = 0; s < segments; ) { + + const size_t batch = min(segments - s, batchSize); + + for (size_t i = 0; i < batch; i++, s++) { + + const SDL_Point *a = &points[s], *b = &points[(s + 1) % count]; + + const float ax = (float) a->x, ay = (float) a->y; + const float bx = (float) b->x, by = (float) b->y; + + const float dx = bx - ax, dy = by - ay; + const float len = sqrtf(dx * dx + dy * dy); + + float nx = 0.0f, ny = 0.0f; + if (len > 0.001f) { + nx = (-dy / len) * width * 0.5f; + ny = ( dx / len) * width * 0.5f; + } + + MVC_Vertex *v = &verts[i * 6]; + v[0] = (MVC_Vertex) { { { ax - nx, ay - ny } }, { { 0.0f, 0.0f } }, { 0 } }; + v[1] = (MVC_Vertex) { { { ax + nx, ay + ny } }, { { 0.0f, 0.0f } }, { 0 } }; + v[2] = (MVC_Vertex) { { { bx - nx, by - ny } }, { { 0.0f, 0.0f } }, { 0 } }; + v[3] = (MVC_Vertex) { { { ax + nx, ay + ny } }, { { 0.0f, 0.0f } }, { 0 } }; + v[4] = (MVC_Vertex) { { { bx + nx, by + ny } }, { { 0.0f, 0.0f } }, { 0 } }; + v[5] = (MVC_Vertex) { { { bx - nx, by - ny } }, { { 0.0f, 0.0f } }, { 0 } }; + } + + $(self, pushDrawArrays, verts, batch * 6, NULL, color); + } +} + /** * @fn void Renderer::drawBevel(const Renderer *self, const SDL_Rect *rect, int radius, int width, const SDL_Color *topLeft, const SDL_Color *bottomRight) * @memberof Renderer @@ -201,39 +283,78 @@ static void drawLines(const Renderer *self, const SDL_Point *points, size_t coun return; } - const size_t segments = count - 1; + strokePolyline(self, points, count, false, 1.f, color); +} - MVC_Vertex verts[16 * 6]; - const size_t batchSize = lengthof(verts) / 6; +/** + * @fn void Renderer::drawPolygon(const Renderer *self, const SDL_Point *points, size_t count, int width, const SDL_Color *color) + * @memberof Renderer + */ +static void drawPolygon(const Renderer *self, const SDL_Point *points, size_t count, int width, + const SDL_Color *color) { - for (size_t s = 0; s < segments; ) { + assert(points); + assert(color); - const size_t batch = min(segments - s, batchSize); + if (count < 3 || width < 1) { + return; + } - for (size_t i = 0; i < batch; i++, s++) { + strokePolyline(self, points, count, true, (float) width, color); +} - const float ax = (float) points[s].x, ay = (float) points[s].y; - const float bx = (float) points[s + 1].x, by = (float) points[s + 1].y; +/** + * @fn void Renderer::drawPolygonFilled(const Renderer *self, const SDL_Point *points, size_t count, int angle, const SDL_Color *from, const SDL_Color *to) + * @memberof Renderer + */ +static void drawPolygonFilled(const Renderer *self, const SDL_Point *points, size_t count, + int angle, const SDL_Color *from, const SDL_Color *to) { - const float dx = bx - ax, dy = by - ay; - const float len = sqrtf(dx * dx + dy * dy); + assert(points); + assert(from); - float nx = 0.0f, ny = 0.0f; - if (len > 0.001f) { - nx = (-dy / len) * 0.5f; - ny = ( dx / len) * 0.5f; - } + if (count < 3) { + return; + } - MVC_Vertex *v = &verts[i * 6]; - v[0] = (MVC_Vertex) { { { ax - nx, ay - ny } }, { { 0.0f, 0.0f } }, { 0 } }; - v[1] = (MVC_Vertex) { { { ax + nx, ay + ny } }, { { 0.0f, 0.0f } }, { 0 } }; - v[2] = (MVC_Vertex) { { { bx - nx, by - ny } }, { { 0.0f, 0.0f } }, { 0 } }; - v[3] = (MVC_Vertex) { { { ax + nx, ay + ny } }, { { 0.0f, 0.0f } }, { 0 } }; - v[4] = (MVC_Vertex) { { { bx + nx, by + ny } }, { { 0.0f, 0.0f } }, { 0 } }; - v[5] = (MVC_Vertex) { { { bx - nx, by - ny } }, { { 0.0f, 0.0f } }, { 0 } }; + SDL_FRect bounds = { (float) points[0].x, (float) points[0].y, 0.f, 0.f }; + float x2 = bounds.x, y2 = bounds.y; + + for (size_t i = 1; i < count; i++) { + bounds.x = min(bounds.x, (float) points[i].x); + bounds.y = min(bounds.y, (float) points[i].y); + x2 = max(x2, (float) points[i].x); + y2 = max(y2, (float) points[i].y); + } + + bounds.w = x2 - bounds.x; + bounds.h = y2 - bounds.y; + + // a convex polygon triangulates as a fan from its first vertex + const size_t triangles = count - 2; + + MVC_Vertex verts[16 * 3]; + const size_t batchSize = lengthof(verts) / 3; + + for (size_t t = 0; t < triangles; ) { + + const size_t batch = min(triangles - t, batchSize); + + for (size_t i = 0; i < batch; i++, t++) { + + const SDL_Point *p[3] = { &points[0], &points[t + 1], &points[t + 2] }; + + for (size_t j = 0; j < 3; j++) { + + const float x = (float) p[j]->x, y = (float) p[j]->y; + + verts[i * 3 + j] = (MVC_Vertex) { { { x, y } }, { { 0.f, 0.f } }, + to ? gradientColor(x, y, &bounds, angle, from, to) : *from + }; + } } - $(self, pushDrawArrays, verts, batch * 6, NULL, color); + $(self, pushDrawArrays, verts, batch * 3, NULL, NULL); } } @@ -313,6 +434,30 @@ static void drawRoundedRectFilled(const Renderer *self, const SDL_Rect *rect, in $(self, pushDrawArrays, verts, 6, NULL, color); } +/** + * @fn void Renderer::drawRoundedRectGradientFilled(const Renderer *self, const SDL_Rect *rect, int radius, int angle, const SDL_Color *from, const SDL_Color *to) + * @memberof Renderer + */ +static void drawRoundedRectGradientFilled(const Renderer *self, const SDL_Rect *rect, int radius, + int angle, const SDL_Color *from, const SDL_Color *to) { + + assert(rect); + assert(from); + assert(to); + + SDL_FRect frect; + SDL_RectToFRect(rect, &frect); + + MVC_Vertex verts[6]; + roundedRectVertices(verts, &frect, radius, 0, NULL); + + for (size_t i = 0; i < lengthof(verts); i++) { + verts[i].color = gradientColor(verts[i].position.x, verts[i].position.y, &frect, angle, from, to); + } + + $(self, pushDrawArrays, verts, lengthof(verts), NULL, NULL); +} + /** * @fn void Renderer::drawRoundedTexture(const Renderer *self, Texture *texture, const SDL_FRect *dest, int radius, const SDL_Color *color) * @memberof Renderer @@ -461,18 +606,24 @@ static void endFrame(Renderer *self) { $(self->commands, pushVertexUniformData, 0, projection.f, sizeof(projection)); $(renderPass, bindPipeline, self->pipeline); - $(renderPass, bindVertexBuffers, 0, &(SDL_GPUBufferBinding) { .buffer = self->vertexBuffer->buffer }, 1); - for (size_t i = 0; i < self->drawArrays->count; i++) { - const MVC_DrawArrays *draw = VectorElement(self->drawArrays, MVC_DrawArrays, i); + // the vertex buffer is created only once something has been drawn into the frame, so a + // frame that drew nothing has none to bind + if (vertexCount) { + + $(renderPass, bindVertexBuffers, 0, &(SDL_GPUBufferBinding) { .buffer = self->vertexBuffer->buffer }, 1); + + for (size_t i = 0; i < self->drawArrays->count; i++) { + const MVC_DrawArrays *draw = VectorElement(self->drawArrays, MVC_DrawArrays, i); - $(renderPass, setScissor, &draw->scissor); + $(renderPass, setScissor, &draw->scissor); - $(renderPass, bindFragmentSamplers, 0, &(SDL_GPUTextureSamplerBinding) { - .texture = draw->texture->texture, .sampler = self->sampler->sampler, - }, 1); + $(renderPass, bindFragmentSamplers, 0, &(SDL_GPUTextureSamplerBinding) { + .texture = draw->texture->texture, .sampler = self->sampler->sampler, + }, 1); - $(renderPass, drawPrimitives, draw->vertexCount, 1, draw->firstVertex, 0); + $(renderPass, drawPrimitives, draw->vertexCount, 1, draw->firstVertex, 0); + } } release(renderPass); @@ -509,7 +660,6 @@ static Renderer *initWithDevice(Renderer *self, RenderDevice *device) { static void pushDrawArrays(const Renderer *self, const MVC_Vertex *verts, size_t count, Texture *texture, const SDL_Color *color) { assert(verts); - assert(color); const MVC_DrawArrays draw = { .firstVertex = (Uint32) self->vertices->count, @@ -527,7 +677,9 @@ static void pushDrawArrays(const Renderer *self, const MVC_Vertex *verts, size_t MVC_Vertex *out = VectorElement(vertices, MVC_Vertex, vertices->count); for (size_t i = 0; i < count; i++) { out[i] = verts[i]; - out[i].color = *color; + if (color) { + out[i].color = *color; + } } vertices->count += count; @@ -709,10 +861,13 @@ static void initialize(Class *clazz) { ((RendererInterface *) clazz->interface)->drawBevel = drawBevel; ((RendererInterface *) clazz->interface)->drawLine = drawLine; ((RendererInterface *) clazz->interface)->drawLines = drawLines; + ((RendererInterface *) clazz->interface)->drawPolygon = drawPolygon; + ((RendererInterface *) clazz->interface)->drawPolygonFilled = drawPolygonFilled; ((RendererInterface *) clazz->interface)->drawRect = drawRect; ((RendererInterface *) clazz->interface)->drawRectFilled = drawRectFilled; ((RendererInterface *) clazz->interface)->drawRoundedRect = drawRoundedRect; ((RendererInterface *) clazz->interface)->drawRoundedRectFilled = drawRoundedRectFilled; + ((RendererInterface *) clazz->interface)->drawRoundedRectGradientFilled = drawRoundedRectGradientFilled; ((RendererInterface *) clazz->interface)->drawRoundedTexture = drawRoundedTexture; ((RendererInterface *) clazz->interface)->drawRoundedTextureRegion = drawRoundedTextureRegion; ((RendererInterface *) clazz->interface)->drawTexture = drawTexture; diff --git a/Sources/ObjectivelyMVC/Renderer.h b/Sources/ObjectivelyMVC/Renderer.h index 009615ae..0dcb4d65 100644 --- a/Sources/ObjectivelyMVC/Renderer.h +++ b/Sources/ObjectivelyMVC/Renderer.h @@ -231,6 +231,39 @@ struct RendererInterface { */ void (*drawLines)(const Renderer *self, const SDL_Point *points, size_t count, const SDL_Color *color); + /** + * @fn void Renderer::drawPolygon(const Renderer *self, const SDL_Point *points, size_t count, int width, const SDL_Color *color) + * @brief Records a closed polygon outline of the given width. + * @details The stroke is centered on each edge. Joints are not mitered, so a width well + * above the one or two pixels a border uses will notch at sharp corners. + * @param self The Renderer. + * @param points The points, in order; the last is joined back to the first. + * @param count The number of points; fewer than three draws nothing. + * @param width The stroke width. + * @param color The outline color. + * @memberof Renderer + */ + void (*drawPolygon)(const Renderer *self, const SDL_Point *points, size_t count, int width, + const SDL_Color *color); + + /** + * @fn void Renderer::drawPolygonFilled(const Renderer *self, const SDL_Point *points, size_t count, int angle, const SDL_Color *from, const SDL_Color *to) + * @brief Records a filled convex polygon, optionally with a linear gradient. + * @details The polygon is triangulated as a fan from its first vertex, so it MUST be + * convex. It is drawn without the rounded rectangle shader, and so without its + * anti-aliasing: diagonal edges are hard. + * @param self The Renderer. + * @param points The points, in order. + * @param count The number of points; fewer than three draws nothing. + * @param angle The gradient angle in degrees, clockwise from north. Ignored when `to` is + * `NULL`. + * @param from The fill color, and the near color of the gradient. + * @param to The far color of the gradient, or `NULL` to fill flat with `from`. + * @memberof Renderer + */ + void (*drawPolygonFilled)(const Renderer *self, const SDL_Point *points, size_t count, + int angle, const SDL_Color *from, const SDL_Color *to); + /** * @fn void Renderer::drawRect(const Renderer *self, const SDL_Rect *rect, const SDL_Color *color) * @brief Records a rectangle outline. @@ -274,6 +307,21 @@ struct RendererInterface { */ void (*drawRoundedRectFilled)(const Renderer *self, const SDL_Rect *rect, int radius, const SDL_Color *color); + /** + * @fn void Renderer::drawRoundedRectGradientFilled(const Renderer *self, const SDL_Rect *rect, int radius, int angle, const SDL_Color *from, const SDL_Color *to) + * @brief Records a rounded rectangle filled with a linear gradient. + * @param self The Renderer. + * @param rect The rectangle. + * @param radius The corner radius. + * @param angle The gradient angle in degrees, clockwise from north: `0` fills upwards, + * `90` to the right, `180` downwards. + * @param from The near color of the gradient. + * @param to The far color of the gradient. + * @memberof Renderer + */ + void (*drawRoundedRectGradientFilled)(const Renderer *self, const SDL_Rect *rect, int radius, + int angle, const SDL_Color *from, const SDL_Color *to); + /** * @fn void Renderer::drawRoundedTexture(const Renderer *self, Texture *texture, const SDL_FRect *dest, int radius, const SDL_Color *color) * @brief Records a textured quad clipped to a rounded rectangle. @@ -374,7 +422,9 @@ struct RendererInterface { * @param verts The vertices to append (in logical screen coordinates). * @param count The number of vertices. * @param texture The texture to bind, or `NULL` to use the 1×1 white fallback. - * @param color The color multiplier applied in the fragment shader. + * @param color The color to apply to every vertex, or `NULL` to keep the colors the + * vertices already carry. MVC_Vertex::color is interpolated across the triangle, so + * `NULL` is how a caller draws a gradient. * @memberof Renderer */ void (*pushDrawArrays)(const Renderer *self, const MVC_Vertex *verts, size_t count, diff --git a/Sources/ObjectivelyMVC/StackView.c b/Sources/ObjectivelyMVC/StackView.c index b3ef98a1..3922d59e 100644 --- a/Sources/ObjectivelyMVC/StackView.c +++ b/Sources/ObjectivelyMVC/StackView.c @@ -51,6 +51,7 @@ static void applyStyle(View *self, const Style *style) { const Inlet inlets[] = MakeInlets( MakeInlet("axis", InletTypeEnum, &this->axis, (ident) StackViewAxisNames), + MakeInlet("reversed", InletTypeBool, &this->reversed, NULL), MakeInlet("distribution", InletTypeEnum, &this->distribution, (ident) StackViewDistributionNames), MakeInlet("spacing", InletTypeInteger, &this->spacing, NULL) ); @@ -113,7 +114,9 @@ static void layoutSubviews(View *self) { const float scale = requestedSize ? availableSize / (float) requestedSize : 1.f; - for (size_t i = 0; i < subviews->count; i++) { + for (size_t j = 0; j < subviews->count; j++) { + + const size_t i = this->reversed ? subviews->count - 1 - j : j; View *subview = $(subviews, objectAtIndex, i); diff --git a/Sources/ObjectivelyMVC/StackView.h b/Sources/ObjectivelyMVC/StackView.h index 706e785d..59807fe3 100644 --- a/Sources/ObjectivelyMVC/StackView.h +++ b/Sources/ObjectivelyMVC/StackView.h @@ -88,6 +88,13 @@ struct StackView { */ StackViewDistribution distribution; + /** + * @brief If true, subviews are laid out in reverse order along View::axis. + * @remarks The order of View::subviews, and so of hit testing and drawing, is unchanged; + * only the positions along the axis are mirrored. + */ + bool reversed; + /** * @brief The subview spacing. */ diff --git a/Sources/ObjectivelyMVC/View.c b/Sources/ObjectivelyMVC/View.c index 02d1db10..a561a7ff 100644 --- a/Sources/ObjectivelyMVC/View.c +++ b/Sources/ObjectivelyMVC/View.c @@ -266,10 +266,17 @@ static void applyStyle(View *self, const Style *style) { MakeInlet("alignment", InletTypeEnum, &self->alignment, (ident) ViewAlignmentNames), MakeInlet("autoresizing-mask", InletTypeEnum, &self->autoresizingMask, (ident) ViewAutoresizingNames), MakeInlet("background-color", InletTypeColor, &self->backgroundColor, NULL), + MakeInlet("background-gradient-angle", InletTypeInteger, &self->backgroundGradientAngle, NULL), + MakeInlet("background-gradient-color", InletTypeColor, &self->backgroundGradientColor, NULL), MakeInlet("border-color", InletTypeColor, &self->borderColor, NULL), MakeInlet("border-radius", InletTypeInteger, &self->borderRadius, NULL), MakeInlet("border-width", InletTypeInteger, &self->borderWidth, NULL), MakeInlet("clips-subviews", InletTypeBool, &self->clipsSubviews, NULL), + MakeInlet("corner-cut", InletTypeRectangle, &self->cornerCut, NULL), + MakeInlet("corner-cut-top-left", InletTypeInteger, &self->cornerCut.topLeft, NULL), + MakeInlet("corner-cut-top-right", InletTypeInteger, &self->cornerCut.topRight, NULL), + MakeInlet("corner-cut-bottom-right", InletTypeInteger, &self->cornerCut.bottomRight, NULL), + MakeInlet("corner-cut-bottom-left", InletTypeInteger, &self->cornerCut.bottomLeft, NULL), MakeInlet("frame", InletTypeRectangle, &self->frame, NULL), MakeInlet("hidden", InletTypeBool, &self->hidden, NULL), MakeInlet("height", InletTypeInteger, &self->frame.h, NULL), @@ -1040,6 +1047,7 @@ static View *initWithFrame(View *self, const SDL_Rect *frame) { self->warnings = $$(Array, arrayWithCapacity, 0); assert(self->warnings); + self->backgroundGradientAngle = 180; self->maxSize = MakeSize(INT32_MAX, INT32_MAX); self->pointerEvents = true; @@ -1446,6 +1454,32 @@ static void removeSubview(View *self, View *subview) { } } +/** + * @brief Writes the four corners of `rect`, inset by `cut`, in clockwise order. + * @details Each inset pulls its corner in along the x axis, so an inset on one of a pair of + * vertically adjacent corners slants the edge between them. The two insets on an edge are + * clamped against each other as well as the width, so however large a cut is the edge closes + * to a point rather than crossing over itself. + */ +static void cornerCutPoints(SDL_Point *points, const SDL_Rect *rect, const ViewCornerCut *cut) { + + const int x1 = rect->x, y1 = rect->y; + const int x2 = rect->x + rect->w, y2 = rect->y + rect->h; + + const int w = rect->w; + + const int topLeft = clamp(cut->topLeft, 0, w); + const int topRight = clamp(cut->topRight, 0, w - topLeft); + + const int bottomLeft = clamp(cut->bottomLeft, 0, w); + const int bottomRight = clamp(cut->bottomRight, 0, w - bottomLeft); + + points[0] = MakePoint(x1 + topLeft, y1); + points[1] = MakePoint(x2 - topRight, y1); + points[2] = MakePoint(x2 - bottomRight, y2); + points[3] = MakePoint(x1 + bottomLeft, y2); +} + /** * @fn void View::render(View *self, Renderer *renderer) * @memberof View @@ -1458,10 +1492,24 @@ static void render(View *self, Renderer *renderer) { SDL_TriggerBreakpoint(); } - if (self->backgroundColor.a) { + const bool cut = !CornerCutIsEmpty(self->cornerCut); + + if (self->backgroundColor.a || self->backgroundGradientColor.a) { const SDL_Rect frame = $(self, renderFrame); - if (self->borderRadius > 0) { + const SDL_Color *gradient = self->backgroundGradientColor.a ? &self->backgroundGradientColor : NULL; + + if (cut) { + SDL_Point points[4]; + cornerCutPoints(points, &frame, &self->cornerCut); + + $(renderer, drawPolygonFilled, points, lengthof(points), self->backgroundGradientAngle, + &self->backgroundColor, gradient); + + } else if (gradient) { + $(renderer, drawRoundedRectGradientFilled, &frame, self->borderRadius, + self->backgroundGradientAngle, &self->backgroundColor, gradient); + } else if (self->borderRadius > 0) { $(renderer, drawRoundedRectFilled, &frame, self->borderRadius, &self->backgroundColor); } else { $(renderer, drawRectFilled, &frame, &self->backgroundColor); @@ -1472,7 +1520,22 @@ static void render(View *self, Renderer *renderer) { SDL_Rect frame = $(self, renderFrame); - if (self->borderRadius > 0) { + if (cut) { + + // The stroke is centered on the outline, so grow by half of it to sit outside the fill + const int inset = self->borderWidth / 2; + + frame.x -= inset; + frame.y -= inset; + frame.w += inset * 2; + frame.h += inset * 2; + + SDL_Point points[4]; + cornerCutPoints(points, &frame, &self->cornerCut); + + $(renderer, drawPolygon, points, lengthof(points), self->borderWidth, &self->borderColor); + + } else if (self->borderRadius > 0) { // The border grows outward, so its inner edge shares the background's radius frame.x -= self->borderWidth; diff --git a/Sources/ObjectivelyMVC/View.h b/Sources/ObjectivelyMVC/View.h index 6089a15a..d553f454 100644 --- a/Sources/ObjectivelyMVC/View.h +++ b/Sources/ObjectivelyMVC/View.h @@ -154,6 +154,28 @@ typedef struct { #define AddPadding(a, b) \ MakePadding(a.top + b.top, a.right + b.right, a.bottom + b.bottom, a.left + b.left) +/** + * @brief The horizontal inset of each of a View's corners. + * @details A non-zero value draws that corner pulled in along the x axis, slanting the edge + * it shares with the corner below or above it. This is an inset, not a 45 degree chamfer: + * the slant runs the full height of the View, however small the inset. + */ +typedef struct { + int topLeft, topRight, bottomRight, bottomLeft; +} ViewCornerCut; + +/** + * @brief Creates a ViewCornerCut with the given insets. + */ +#define MakeCornerCut(topLeft, topRight, bottomRight, bottomLeft) \ + (ViewCornerCut) { (topLeft), (topRight), (bottomRight), (bottomLeft) } + +/** + * @return True if `cut` insets no corner. + */ +#define CornerCutIsEmpty(cut) \ + ((cut).topLeft == 0 && (cut).topRight == 0 && (cut).bottomRight == 0 && (cut).bottomLeft == 0) + /** * @brief Relative positioning of subviews within their superview. */ @@ -200,6 +222,19 @@ struct View { */ SDL_Color backgroundColor; + /** + * @brief The angle of the background gradient, in degrees, clockwise from north: `0` fills + * upwards, `90` to the right, `180` (the default) downwards. + */ + int backgroundGradientAngle; + + /** + * @brief The far color of the background gradient, which runs from View::backgroundColor. + * @remarks The gradient is drawn only when this color has a non-zero alpha; otherwise the + * background is a flat View::backgroundColor. + */ + SDL_Color backgroundGradientColor; + /** * @brief The border color. */ @@ -241,6 +276,13 @@ struct View { */ Style *computedStyle; + /** + * @brief The horizontal inset of each corner of the background and border. + * @remarks A View with any corner inset is drawn as a polygon rather than through the + * rounded rectangle shader, so View::borderRadius does not apply to it. + */ + ViewCornerCut cornerCut; + /** * @brief The frame, relative to the superview. */ diff --git a/configure.ac b/configure.ac index 6ce03cd3..6532c7eb 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,6 @@ AC_PREREQ(2.69) -AC_INIT([ObjectivelyMVC], [2.4.6], [jay@jaydolan.com]) +AC_INIT([ObjectivelyMVC], [2.4.7], [jay@jaydolan.com]) AC_SUBST([RELEASE_VERSION], [m4_bpatsubst(AC_PACKAGE_VERSION, [\.[0-9]*$], [])]) AC_CONFIG_HEADERS([Sources/ObjectivelyMVC/Config.h])