diff --git a/include/akgl/draw.h b/include/akgl/draw.h index ced1e70..425461f 100644 --- a/include/akgl/draw.h +++ b/include/akgl/draw.h @@ -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_ diff --git a/src/draw.c b/src/draw.c index ae9c399..6d3eb58 100644 --- a/src/draw.c +++ b/src/draw.c @@ -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); +} diff --git a/tests/draw.c b/tests/draw.c index 8088b4e..401a99c 100644 --- a/tests/draw.c +++ b/tests/draw.c @@ -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) {