Add rounded-rect fill, arc stroke and clip-rect drawing primitives

The UI subsystem's render executor needs all three -- clay emits rectangles
with corner radii, per-side borders, and scissor commands -- but they earn
their place in draw.h on the same terms as everything else there: a rounded
panel, a ring gauge and a clipped viewport are things a game wants with or
without a layout engine.

akgl_draw_filled_rounded_rect decomposes into three band fills plus four
quarter-circle triangle fans through SDL_RenderGeometry, with the radius
clamped to half the shorter side. akgl_draw_arc strokes between an outer and
inner radius as one triangle strip, stepping in proportion to the swept angle
under a fixed vertex cap -- no VLAs, unlike clay's own SDL3 reference
renderer. akgl_draw_set_clip wraps SDL_SetRenderClipRect and is the one draw
entry point where a NULL rectangle is legal, because that is how a clip is
cleared.

Pixel-readback tests cover the corner geometry (inside the arc filled,
outside it untouched), the radius clamp, the zero-radius fall-through, ring
and quarter-arc coverage at mid-stroke, sweep bounds, and clip set/clear
round trips.

Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
This commit is contained in:
2026-08-02 10:57:23 -04:00
parent b7ff54b09f
commit 064e6569e8
3 changed files with 507 additions and 0 deletions

View File

@@ -36,6 +36,25 @@
*/
#define AKGL_DRAW_MAX_FLOOD_SPANS 4096
/**
* @brief Triangles each corner of akgl_draw_filled_rounded_rect() is built from.
*
* A quarter circle approximated by this many segments is visually round at any
* radius a UI panel plausibly uses; the vertex arrays are fixed at this size,
* so it is a compile-time cost, not a per-call one.
*/
#define AKGL_DRAW_ROUNDED_RECT_SEGMENTS 16
/**
* @brief Most vertices akgl_draw_arc() will spend on one arc.
*
* An arc is a triangle strip between its inner and outer edge, two vertices
* per step, so half of these are positions along the curve. The step count
* scales with the swept angle and is clamped to fit this array -- a full
* circle gets all of them, a sliver gets a few -- rather than allocating.
*/
#define AKGL_DRAW_ARC_MAX_POINTS 64
/**
* @brief Paint an 8x8 grey checkerboard over a region, the way an image editor shows transparency.
*
@@ -242,4 +261,86 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *sel
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y);
/**
* @brief Fill a rectangle whose corners are rounded off with quarter circles.
*
* The body is three axis-aligned fills and the corners are triangle fans
* through `SDL_RenderGeometry` -- #AKGL_DRAW_ROUNDED_RECT_SEGMENTS triangles
* each -- so nothing here is anti-aliased, matching every other primitive in
* this header.
*
* @param self The backend to draw through. Required, along with its
* `sdl_renderer`.
* @param rect The rectangle, in render-target pixels. Required. A zero or
* negative width or height fills nothing and is not reported.
* @param radius Corner radius in pixels. Zero or negative falls through to
* akgl_draw_filled_rect() -- a square corner is not an error.
* Larger than half the shorter side is clamped to it, which at
* the limit turns a square into a circle rather than folding the
* corners over each other.
* @param color Colour to fill with, including alpha.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self, `self->sdl_renderer`, or @p rect is
* `NULL`.
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if a band
* or corner cannot be drawn. A failure partway leaves the shape
* partially drawn.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rounded_rect(akgl_RenderBackend *self, SDL_FRect *rect, float32_t radius, SDL_Color color);
/**
* @brief Stroke a circular arc of a given thickness between two angles.
*
* Angles are in degrees, 0 pointing along +x (three o'clock) and increasing
* clockwise on screen -- the same convention as the rotation argument the
* render backend's `draw_texture` takes. The stroke runs from the arc's outer
* edge at @p radius inward by @p thickness, drawn as one triangle strip, not
* anti-aliased.
*
* @param self The backend to draw through. Required, along with its
* `sdl_renderer`.
* @param x Horizontal position of the arc's centre.
* @param y Vertical position of the arc's centre.
* @param radius Outer radius in pixels. Must be positive.
* @param start_deg Angle the arc starts at.
* @param end_deg Angle the arc ends at. At or below @p start_deg nothing is
* drawn and nothing is reported, matching the zero-area
* rectangle fills above. Neither angle is normalized, so an
* arc crossing three o'clock is 350 to 370, not 350 to 10; a
* sweep beyond 360 degrees is clamped to one full turn.
* @param thickness Stroke width in pixels, measured inward from @p radius.
* Must be positive; at or above @p radius the arc becomes a
* filled wedge to the centre.
* @param color Colour to stroke in, including alpha.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p radius or @p thickness is not positive. The
* message reports the offending value.
* @throws AKGL_ERR_SDL If the strip cannot be drawn.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_arc(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, float32_t start_deg, float32_t end_deg, float32_t thickness, SDL_Color color);
/**
* @brief Restrict every subsequent draw to a rectangle of the render target.
*
* Everything drawn through @p self after this -- primitives here, textures,
* text -- is clipped to @p rect until the restriction is cleared. This is the
* only entry point in this header where a `NULL` rectangle is legal rather
* than an error: it means "clear the restriction", matching what
* `SDL_SetRenderClipRect` does with it, and there is no other value that
* could.
*
* The clip is renderer state, not saved and restored around anything: a
* caller that sets it owns putting it back, the way a `CLEANUP` block puts
* back a draw colour. Leaving it set clips the next frame's world too.
*
* @param self The backend to clip. Required, along with its `sdl_renderer`.
* @param rect Rectangle to clip to, in render-target pixels -- integer, since
* a clip boundary is a pixel boundary. `NULL` clears the clip.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
* @throws AKGL_ERR_SDL If the clip cannot be set or cleared.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_set_clip(akgl_RenderBackend *self, SDL_Rect *rect);
#endif //_AKGL_DRAW_H_

View File

@@ -619,3 +619,229 @@ akerr_ErrorContext *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief Fill one quarter-circle corner as a triangle fan.
*
* `SDL_RenderGeometry` colours from its vertices rather than the renderer's
* draw colour, so this neither pushes nor pops it.
*
* @param self The backend. Assumed non-`NULL` with a live `sdl_renderer`;
* the caller has already checked both.
* @param cx Horizontal position of the corner's centre -- the point the
* fan radiates from, one radius inside the rectangle.
* @param cy Vertical position of the centre.
* @param radius Radius of the quarter circle. Assumed positive.
* @param start_deg Angle the quarter starts at; it sweeps 90 degrees clockwise
* from there.
* @param color Colour to fill with.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL If the fan cannot be drawn.
*/
static akerr_ErrorContext *fill_corner_fan(akgl_RenderBackend *self, float32_t cx, float32_t cy, float32_t radius, float32_t start_deg, SDL_Color color)
{
SDL_Vertex vertices[AKGL_DRAW_ROUNDED_RECT_SEGMENTS + 2];
int indices[AKGL_DRAW_ROUNDED_RECT_SEGMENTS * 3];
SDL_FColor fcolor;
float32_t angle = 0.0f;
int i = 0;
PREPARE_ERROR(errctx);
fcolor.r = (float)color.r / 255.0f;
fcolor.g = (float)color.g / 255.0f;
fcolor.b = (float)color.b / 255.0f;
fcolor.a = (float)color.a / 255.0f;
vertices[0].position.x = cx;
vertices[0].position.y = cy;
vertices[0].color = fcolor;
vertices[0].tex_coord.x = 0.0f;
vertices[0].tex_coord.y = 0.0f;
for ( i = 0; i <= AKGL_DRAW_ROUNDED_RECT_SEGMENTS; i++ ) {
angle = (start_deg + ((90.0f / AKGL_DRAW_ROUNDED_RECT_SEGMENTS) * (float)i)) * (SDL_PI_F / 180.0f);
vertices[i + 1].position.x = cx + (radius * SDL_cosf(angle));
vertices[i + 1].position.y = cy + (radius * SDL_sinf(angle));
vertices[i + 1].color = fcolor;
vertices[i + 1].tex_coord.x = 0.0f;
vertices[i + 1].tex_coord.y = 0.0f;
}
for ( i = 0; i < AKGL_DRAW_ROUNDED_RECT_SEGMENTS; i++ ) {
indices[(i * 3) + 0] = 0;
indices[(i * 3) + 1] = i + 1;
indices[(i * 3) + 2] = i + 2;
}
FAIL_ZERO_RETURN(
errctx,
SDL_RenderGeometry(
self->sdl_renderer,
NULL,
vertices,
AKGL_DRAW_ROUNDED_RECT_SEGMENTS + 2,
indices,
AKGL_DRAW_ROUNDED_RECT_SEGMENTS * 3),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_draw_filled_rounded_rect(akgl_RenderBackend *self, SDL_FRect *rect, float32_t radius, SDL_Color color)
{
SDL_Color previous;
SDL_FRect band;
float32_t half = 0.0f;
bool pushed = false;
bool drawfailed = false;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "NULL rectangle");
if ( (rect->w <= 0.0f) || (rect->h <= 0.0f) ) {
SUCCEED_RETURN(errctx);
}
if ( radius <= 0.0f ) {
PASS(errctx, akgl_draw_filled_rect(self, rect, color));
SUCCEED_RETURN(errctx);
}
half = (((rect->w < rect->h) ? rect->w : rect->h) / 2.0f);
if ( radius > half ) {
radius = half;
}
ATTEMPT {
CATCH(errctx, push_draw_color(self, color, &previous));
pushed = true;
// The body is three bands: one the full width between the corner rows,
// and one between the corners along each of the top and bottom edges.
// A radius of exactly half the shorter side leaves one or more of them
// with no area, which SDL fills as nothing -- at the limit the whole
// shape is the four fans.
band.x = rect->x;
band.y = rect->y + radius;
band.w = rect->w;
band.h = rect->h - (radius * 2.0f);
if ( SDL_RenderFillRect(self->sdl_renderer, &band) == false ) {
drawfailed = true;
}
band.x = rect->x + radius;
band.y = rect->y;
band.w = rect->w - (radius * 2.0f);
band.h = radius;
if ( SDL_RenderFillRect(self->sdl_renderer, &band) == false ) {
drawfailed = true;
}
band.y = rect->y + rect->h - radius;
if ( SDL_RenderFillRect(self->sdl_renderer, &band) == false ) {
drawfailed = true;
}
FAIL_NONZERO_BREAK(errctx, drawfailed, AKGL_ERR_SDL, "%s", SDL_GetError());
CATCH(errctx, fill_corner_fan(self, rect->x + radius, rect->y + radius, radius, 180.0f, color));
CATCH(errctx, fill_corner_fan(self, rect->x + rect->w - radius, rect->y + radius, radius, 270.0f, color));
CATCH(errctx, fill_corner_fan(self, rect->x + rect->w - radius, rect->y + rect->h - radius, radius, 0.0f, color));
CATCH(errctx, fill_corner_fan(self, rect->x + radius, rect->y + rect->h - radius, radius, 90.0f, color));
} CLEANUP {
if ( pushed == true ) {
IGNORE(pop_draw_color(self, &previous));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_draw_arc(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, float32_t start_deg, float32_t end_deg, float32_t thickness, SDL_Color color)
{
SDL_Vertex vertices[AKGL_DRAW_ARC_MAX_POINTS];
int indices[(AKGL_DRAW_ARC_MAX_POINTS - 2) * 3];
SDL_FColor fcolor;
float32_t span = 0.0f;
float32_t inner = 0.0f;
float32_t angle = 0.0f;
int segments = 0;
int base = 0;
int i = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
FAIL_NONZERO_RETURN(errctx, (radius <= 0.0f), AKERR_OUTOFBOUNDS, "Arc radius %f is not positive", (double)radius);
FAIL_NONZERO_RETURN(errctx, (thickness <= 0.0f), AKERR_OUTOFBOUNDS, "Arc thickness %f is not positive", (double)thickness);
span = end_deg - start_deg;
if ( span <= 0.0f ) {
SUCCEED_RETURN(errctx);
}
if ( span > 360.0f ) {
span = 360.0f;
}
inner = radius - thickness;
if ( inner < 0.0f ) {
inner = 0.0f;
}
// Steps in proportion to the angle swept, so a sliver of arc does not
// spend the whole vertex budget and a full circle uses all of it: the
// quarter-circle density of AKGL_DRAW_ROUNDED_RECT_SEGMENTS, capped by
// what AKGL_DRAW_ARC_MAX_POINTS vertices can carry at two per step.
segments = (int)((span / 90.0f) * (float)AKGL_DRAW_ROUNDED_RECT_SEGMENTS) + 1;
if ( segments > ((AKGL_DRAW_ARC_MAX_POINTS / 2) - 1) ) {
segments = (AKGL_DRAW_ARC_MAX_POINTS / 2) - 1;
}
fcolor.r = (float)color.r / 255.0f;
fcolor.g = (float)color.g / 255.0f;
fcolor.b = (float)color.b / 255.0f;
fcolor.a = (float)color.a / 255.0f;
for ( i = 0; i <= segments; i++ ) {
angle = (start_deg + ((span / (float)segments) * (float)i)) * (SDL_PI_F / 180.0f);
vertices[i * 2].position.x = x + (radius * SDL_cosf(angle));
vertices[i * 2].position.y = y + (radius * SDL_sinf(angle));
vertices[i * 2].color = fcolor;
vertices[i * 2].tex_coord.x = 0.0f;
vertices[i * 2].tex_coord.y = 0.0f;
vertices[(i * 2) + 1].position.x = x + (inner * SDL_cosf(angle));
vertices[(i * 2) + 1].position.y = y + (inner * SDL_sinf(angle));
vertices[(i * 2) + 1].color = fcolor;
vertices[(i * 2) + 1].tex_coord.x = 0.0f;
vertices[(i * 2) + 1].tex_coord.y = 0.0f;
}
for ( i = 0; i < segments; i++ ) {
base = i * 2;
indices[(i * 6) + 0] = base;
indices[(i * 6) + 1] = base + 2;
indices[(i * 6) + 2] = base + 1;
indices[(i * 6) + 3] = base + 1;
indices[(i * 6) + 4] = base + 2;
indices[(i * 6) + 5] = base + 3;
}
FAIL_ZERO_RETURN(
errctx,
SDL_RenderGeometry(
self->sdl_renderer,
NULL,
vertices,
(segments + 1) * 2,
indices,
segments * 6),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_draw_set_clip(akgl_RenderBackend *self, SDL_Rect *rect)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
// NULL rect deliberately passes straight through: it is how the clip is
// cleared, and the header says so.
FAIL_ZERO_RETURN(
errctx,
SDL_SetRenderClipRect(self->sdl_renderer, rect),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
SUCCEED_RETURN(errctx);
}

View File

@@ -628,6 +628,183 @@ akerr_ErrorContext *test_draw_background(void)
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_draw_filled_rounded_rect(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
SDL_FRect rect = { 8.0f, 8.0f, 40.0f, 24.0f };
SDL_FRect square = { 40.0f, 40.0f, 16.0f, 16.0f };
ATTEMPT {
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_filled_rounded_rect(akgl_renderer, &rect, 8.0f, testred),
"filling a rounded rectangle");
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 28, 20, testred),
"the middle of the rounded rectangle is unfilled");
TEST_ASSERT(errctx, pixel_is(shot, 28, 8, testred),
"the top edge between the corners is unfilled");
TEST_ASSERT(errctx, pixel_is(shot, 8, 20, testred),
"the left edge between the corners is unfilled");
// 12,12 is inside the corner's quarter circle (5.7px from its centre
// at 16,16); 8,8 and 9,9 are outside it (11.3px and 9.9px). The fan's
// triangles are chords of the circle, so they cover nothing beyond it.
TEST_ASSERT(errctx, pixel_is(shot, 12, 12, testred),
"the inside of the rounded corner is unfilled");
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testblack),
"the square corner pixel was filled despite the rounding");
TEST_ASSERT(errctx, pixel_is(shot, 9, 9, testblack),
"a pixel outside the corner arc was filled");
SDL_DestroySurface(shot);
shot = NULL;
// A radius beyond half the shorter side clamps to it, which on a
// square is a circle: centre filled, corners untouched.
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_filled_rounded_rect(akgl_renderer, &square, 100.0f, testgreen),
"filling a rounded rectangle with an oversized radius");
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 48, 48, testgreen),
"the centre of the clamped-radius square is unfilled");
TEST_ASSERT(errctx, pixel_is(shot, 41, 41, testblack),
"clamping the radius still filled the square's corner");
// Radius zero is the plain fill, corners and all.
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_filled_rounded_rect(akgl_renderer, &rect, 0.0f, testred),
"a zero radius falling through to the plain fill");
SDL_DestroySurface(shot);
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred),
"a zero radius did not fill the square corner");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_filled_rounded_rect(NULL, &rect, 4.0f, testred),
"rounded fill through a NULL backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_filled_rounded_rect(akgl_renderer, NULL, 4.0f, testred),
"rounded fill of a NULL rectangle");
} CLEANUP {
if ( shot != NULL ) {
SDL_DestroySurface(shot);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_draw_arc(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
ATTEMPT {
// A full ring: outer radius 20, stroke 4, so mid-stroke is radius 18.
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 20.0f, 0.0f, 360.0f, 4.0f, testred),
"stroking a full circle");
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 50, 32, testred),
"the ring is missing at three o'clock");
TEST_ASSERT(errctx, pixel_is(shot, 32, 50, testred),
"the ring is missing at six o'clock");
TEST_ASSERT(errctx, pixel_is(shot, 14, 32, testred),
"the ring is missing at nine o'clock");
TEST_ASSERT(errctx, pixel_is(shot, 32, 14, testred),
"the ring is missing at twelve o'clock");
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testblack),
"the centre of the ring was filled");
TEST_ASSERT(errctx, pixel_is(shot, 42, 32, testblack),
"the stroke bled inside its inner radius");
SDL_DestroySurface(shot);
shot = NULL;
// A quarter arc sweeps clockwise from three o'clock to six o'clock;
// 44,44 is on its mid-stroke at 45 degrees, twelve o'clock is not in
// the sweep at all.
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 20.0f, 0.0f, 90.0f, 4.0f, testgreen),
"stroking a quarter arc");
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 44, 44, testgreen),
"the quarter arc is missing at its midpoint");
TEST_ASSERT(errctx, pixel_is(shot, 32, 14, testblack),
"the quarter arc drew outside its sweep");
// An empty sweep draws nothing and reports nothing.
TEST_EXPECT_OK(errctx, akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 20.0f, 90.0f, 90.0f, 4.0f, testred),
"an arc with no sweep");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 0.0f, 0.0f, 90.0f, 4.0f, testred),
"an arc with no radius");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 20.0f, 0.0f, 90.0f, -1.0f, testred),
"an arc with a negative thickness");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_arc(NULL, 32.0f, 32.0f, 20.0f, 0.0f, 90.0f, 4.0f, testred),
"an arc through a NULL backend");
} CLEANUP {
if ( shot != NULL ) {
SDL_DestroySurface(shot);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_draw_set_clip(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
SDL_Rect clip = { 0, 0, 16, 16 };
SDL_FRect everything = { 0.0f, 0.0f, (float)TEST_TARGET_SIZE, (float)TEST_TARGET_SIZE };
ATTEMPT {
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_set_clip(akgl_renderer, &clip),
"setting the clip rectangle");
TEST_EXPECT_OK(errctx, akgl_draw_filled_rect(akgl_renderer, &everything, testred),
"filling the whole target under a clip");
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred),
"the fill did not reach inside the clip rectangle");
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testblack),
"the fill escaped the clip rectangle");
SDL_DestroySurface(shot);
shot = NULL;
TEST_EXPECT_OK(errctx, akgl_draw_set_clip(akgl_renderer, NULL),
"clearing the clip rectangle");
TEST_EXPECT_OK(errctx, akgl_draw_filled_rect(akgl_renderer, &everything, testgreen),
"filling the whole target after clearing the clip");
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testgreen),
"clearing the clip did not restore the full target");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_set_clip(NULL, &clip),
"clipping through a NULL backend");
} CLEANUP {
// The clip is renderer state; leaving it set would clip every test
// after this one.
IGNORE(akgl_draw_set_clip(akgl_renderer, NULL));
if ( shot != NULL ) {
SDL_DestroySurface(shot);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
@@ -668,6 +845,9 @@ int main(void)
CATCH(errctx, test_draw_preserves_render_draw_color());
CATCH(errctx, test_draw_backend_without_a_renderer());
CATCH(errctx, test_draw_background());
CATCH(errctx, test_draw_filled_rounded_rect());
CATCH(errctx, test_draw_arc());
CATCH(errctx, test_draw_set_clip());
} CLEANUP {
SDL_Quit();
} PROCESS(errctx) {