diff --git a/include/iui.h b/include/iui.h index 7c36a06..b8d6fc6 100644 --- a/include/iui.h +++ b/include/iui.h @@ -3245,14 +3245,16 @@ int iui_a11y_describe(const iui_a11y_hint *hint, char *buf, size_t buf_size); /* Performance Optimization API * - * Three core performance systems (always available, disabled by default): + * Four core performance systems (always available, disabled by default): * 1. Draw Call Batching - buffers draw commands to reduce state changes * 2. Dirty Rectangle Tracking - skips redrawing unchanged regions - * 3. Text Width Caching - caches text measurement results + * 3. Ink-Bounds Tracking - tracks union bounding box of all draw calls + * 4. Text Width Caching - caches text measurement results * * Usage: * iui_batch_enable(ctx, true); // Enable draw batching * iui_dirty_enable(ctx, true); // Enable dirty rect tracking + * iui_ink_bounds_enable(ctx, true); // Enable ink-bounds tracking * iui_text_cache_enable(ctx, true); // Enable text cache */ @@ -3291,6 +3293,21 @@ void iui_dirty_invalidate_all(iui_context *ctx); */ int iui_dirty_count(const iui_context *ctx); +/* Ink-Bounds Tracking + * Tracks the union bounding box of all draw calls within a frame. + * When enabled, backends can query the drawn region to blit only the + * ink-bounds area instead of the full framebuffer. + */ +void iui_ink_bounds_enable(iui_context *ctx, bool enable); + +/* Query the ink-bounds rectangle for the current frame. + * Returns false if no draw calls were made this frame. + */ +bool iui_ink_bounds_get(const iui_context *ctx, iui_rect_t *out); + +/* Check if any draw calls were made this frame */ +bool iui_ink_bounds_valid(const iui_context *ctx); + /* Text Width Caching * Enable/disable text measurement caching. When enabled, text width * calculations are cached to avoid redundant measurements. diff --git a/src/appbar.c b/src/appbar.c index 7e78a8d..ee1341b 100644 --- a/src/appbar.c +++ b/src/appbar.c @@ -70,7 +70,7 @@ bool iui_top_app_bar(iui_context *ctx, /* Draw background (no corner radius for app bar) */ iui_rect_t bar_rect = {bar_x, bar_y, bar_width, bar_height}; - ctx->renderer.draw_box(bar_rect, 0.f, bg_color, ctx->renderer.user); + iui_emit_box(ctx, bar_rect, 0.f, bg_color); /* Draw elevation shadow if scrolled (Level 2) */ if (collapse_progress > 0.f) { @@ -108,13 +108,13 @@ bool iui_top_app_bar(iui_context *ctx, if (nav_pressed) { uint32_t state_color = iui_state_layer(icon_color, IUI_STATE_PRESS_ALPHA); - ctx->renderer.draw_box(nav_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f, - state_color, ctx->renderer.user); + iui_emit_box(ctx, nav_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f, + state_color); } else if (nav_hovered) { uint32_t state_color = iui_state_layer(icon_color, IUI_STATE_HOVER_ALPHA); - ctx->renderer.draw_box(nav_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f, - state_color, ctx->renderer.user); + iui_emit_box(ctx, nav_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f, + state_color); } /* Draw menu icon */ @@ -237,13 +237,13 @@ bool iui_top_app_bar_action(iui_context *ctx, const char *icon) if (pressed) { uint32_t state_color = iui_state_layer(icon_color, IUI_STATE_PRESS_ALPHA); - ctx->renderer.draw_box(action_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f, - state_color, ctx->renderer.user); + iui_emit_box(ctx, action_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f, + state_color); } else if (hovered) { uint32_t state_color = iui_state_layer(icon_color, IUI_STATE_HOVER_ALPHA); - ctx->renderer.draw_box(action_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f, - state_color, ctx->renderer.user); + iui_emit_box(ctx, action_rect, IUI_APPBAR_ICON_BUTTON_SIZE * 0.5f, + state_color); } /* Draw action icon */ diff --git a/src/basic.c b/src/basic.c index 722274f..9d2c858 100644 --- a/src/basic.c +++ b/src/basic.c @@ -41,9 +41,9 @@ void iui_segmented(iui_context *ctx, *selected = 0; /* MD3: Draw unified pill background (visible container for all segments) */ - ctx->renderer.draw_box( - (iui_rect_t) {seg_x_start, seg_y, ctx->layout.width, seg_height}, - pill_radius, ctx->colors.surface_container_highest, ctx->renderer.user); + iui_emit_box( + ctx, (iui_rect_t) {seg_x_start, seg_y, ctx->layout.width, seg_height}, + pill_radius, ctx->colors.surface_container_highest); /* Track component for MD3 validation */ IUI_MD3_TRACK_SEGMENTED( @@ -63,9 +63,8 @@ void iui_segmented(iui_context *ctx, if (*selected == 0 || *selected == num_entries - 1) corner = pill_radius; - ctx->renderer.draw_box( - (iui_rect_t) {sel_x, seg_y, seg_width, seg_height}, corner, - ctx->colors.secondary_container, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {sel_x, seg_y, seg_width, seg_height}, + corner, ctx->colors.secondary_container); } /* Draw each segment */ @@ -92,9 +91,9 @@ void iui_segmented(iui_context *ctx, uint32_t hover_color = iui_state_layer( ctx->colors.on_surface, iui_state_get_alpha(seg_state)); float corner = (i == 0 || i == num_entries - 1) ? pill_radius : 0.f; - ctx->renderer.draw_box( - (iui_rect_t) {seg_x, seg_y, seg_width, seg_height}, corner, - hover_color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {seg_x, seg_y, seg_width, seg_height}, + corner, hover_color); } /* Handle selection change */ @@ -288,16 +287,15 @@ float iui_slider_ex(iui_context *ctx, iui_get_component_state(ctx, touch_rect, disabled); /* Draw inactive track (full width, behind active track) */ - ctx->renderer.draw_box(track_rect, track_rect.height * .5f, inactive_color, - ctx->renderer.user); + iui_emit_box(ctx, track_rect, track_rect.height * .5f, inactive_color); /* Draw active track (left side up to thumb) */ float active_width = thumb_x - track_rect.x; if (active_width > 0) { - ctx->renderer.draw_box((iui_rect_t) {track_rect.x, track_rect.y, - active_width, track_rect.height}, - track_rect.height * .5f, active_color, - ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {track_rect.x, track_rect.y, active_width, + track_rect.height}, + track_rect.height * .5f, active_color); } /* Handle thumb interaction */ @@ -379,9 +377,9 @@ float iui_slider_ex(iui_context *ctx, uint8_t alpha = is_dragging ? IUI_STATE_DRAG_ALPHA : IUI_STATE_HOVER_ALPHA; uint32_t state_color = iui_state_layer(handle_color, alpha); - ctx->renderer.draw_box( - (iui_rect_t) {state_x, state_y, state_size, state_size}, - state_size * 0.5f, state_color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {state_x, state_y, state_size, state_size}, + state_size * 0.5f, state_color); } /* Draw value indicator bubble during drag */ @@ -415,10 +413,10 @@ float iui_slider_ex(iui_context *ctx, } /* Draw indicator background (pill shape with primary color) */ - ctx->renderer.draw_box((iui_rect_t) {indicator_x, indicator_y, - indicator_width, indicator_height}, - indicator_height * 0.5f, active_color, - ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {indicator_x, indicator_y, indicator_width, + indicator_height}, + indicator_height * 0.5f, active_color); /* Draw value text centered in indicator */ iui_rect_t indicator_text_rect = { @@ -439,8 +437,7 @@ float iui_slider_ex(iui_context *ctx, } /* Draw thumb (circle) */ - ctx->renderer.draw_box(thumb_rect, half_size, handle_color, - ctx->renderer.user); + iui_emit_box(ctx, thumb_rect, half_size, handle_color); iui_newline(ctx); @@ -636,16 +633,15 @@ bool iui_range_slider(iui_context *ctx, thumb_high_x = norm_high * track_rect.width + track_rect.x; /* Draw inactive track (full width) */ - ctx->renderer.draw_box(track_rect, track_height * 0.5f, inactive_color, - ctx->renderer.user); + iui_emit_box(ctx, track_rect, track_height * 0.5f, inactive_color); /* Draw active track (between thumbs) */ float active_x = thumb_low_x; float active_w = thumb_high_x - thumb_low_x; if (active_w > 0.f) { - ctx->renderer.draw_box( - (iui_rect_t) {active_x, track_rect.y, active_w, track_height}, - track_height * 0.5f, active_color, ctx->renderer.user); + iui_emit_box( + ctx, (iui_rect_t) {active_x, track_rect.y, active_w, track_height}, + track_height * 0.5f, active_color); } /* State layers on hover/drag */ @@ -660,9 +656,10 @@ bool iui_range_slider(iui_context *ctx, uint8_t alpha = dragging ? IUI_STATE_DRAG_ALPHA : IUI_STATE_HOVER_ALPHA; uint32_t sc = iui_state_layer(handle_color, alpha); - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {tx - ss * 0.5f, center_y - ss * 0.5f, ss, ss}, - ss * 0.5f, sc, ctx->renderer.user); + ss * 0.5f, sc); } } @@ -672,14 +669,16 @@ bool iui_range_slider(iui_context *ctx, float draw_size_low = draw_half_low * 2.f; float draw_size_high = draw_half_high * 2.f; - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {thumb_low_x - draw_half_low, center_y - draw_half_low, draw_size_low, draw_size_low}, - draw_half_low, handle_color, ctx->renderer.user); - ctx->renderer.draw_box( + draw_half_low, handle_color); + iui_emit_box( + ctx, (iui_rect_t) {thumb_high_x - draw_half_high, center_y - draw_half_high, draw_size_high, draw_size_high}, - draw_half_high, handle_color, ctx->renderer.user); + draw_half_high, handle_color); /* MD3 validation */ IUI_MD3_TRACK_SLIDER(touch_low, touch_low.height * 0.5f); @@ -883,17 +882,14 @@ bool iui_button_styled(iui_context *ctx, iui_draw_focus_ring(ctx, button_rect, corner); if (bg_color != 0) { - ctx->renderer.draw_box(button_rect, corner, bg_color, - ctx->renderer.user); + iui_emit_box(ctx, button_rect, corner, bg_color); } else if (is_focused && focus_layer != 0) { /* MD3: Show focus state layer for text/outlined buttons (no bg) */ - ctx->renderer.draw_box(button_rect, corner, focus_layer, - ctx->renderer.user); + iui_emit_box(ctx, button_rect, corner, focus_layer); } else if (state == IUI_STATE_HOVERED && hover_layer != 0) { /* MD3: Text buttons show state layer on hover (no bg, but visible * hover) */ - ctx->renderer.draw_box(button_rect, corner, hover_layer, - ctx->renderer.user); + iui_emit_box(ctx, button_rect, corner, hover_layer); } /* Draw border if specified (for outlined buttons) */ diff --git a/src/chips.c b/src/chips.c index 8cf43b8..ec23406 100644 --- a/src/chips.c +++ b/src/chips.c @@ -181,8 +181,7 @@ static bool iui_chip_internal(iui_context *ctx, /* Draw chip container */ if (draw_container) { - ctx->renderer.draw_box(chip_rect, corner_radius, container_color, - ctx->renderer.user); + iui_emit_box(ctx, chip_rect, corner_radius, container_color); } /* Draw outline */ @@ -190,62 +189,63 @@ static bool iui_chip_internal(iui_context *ctx, float outline_width = 1.f; if (ctx->renderer.draw_arc) { - /* Best quality: use arcs for smooth rounded corners */ + /* Best quality: use arcs for smooth rounded corners. + * Track ink-bounds for the full chip outline once. + * Expand by outline_width to include arc stroke thickness + * so dirty-region redraw does not miss corner pixels. + */ + iui_ink_bounds_extend(ctx, chip_rect.x - outline_width, + chip_rect.y - outline_width, + chip_rect.width + 2 * outline_width, + chip_rect.height + 2 * outline_width); /* Top edge */ iui_rect_t top_edge = {chip_rect.x + corner_radius, chip_rect.y, chip_rect.width - 2 * corner_radius, outline_width}; - ctx->renderer.draw_box(top_edge, 0.f, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, top_edge, 0.f, outline_color); /* Bottom edge */ iui_rect_t bottom_edge = { chip_rect.x + corner_radius, chip_rect.y + chip_rect.height - outline_width, chip_rect.width - 2 * corner_radius, outline_width}; - ctx->renderer.draw_box(bottom_edge, 0.f, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, bottom_edge, 0.f, outline_color); /* Left edge */ iui_rect_t left_edge = {chip_rect.x, chip_rect.y + corner_radius, outline_width, chip_rect.height - 2 * corner_radius}; - ctx->renderer.draw_box(left_edge, 0.f, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, left_edge, 0.f, outline_color); /* Right edge */ iui_rect_t right_edge = { chip_rect.x + chip_rect.width - outline_width, chip_rect.y + corner_radius, outline_width, chip_rect.height - 2 * corner_radius}; - ctx->renderer.draw_box(right_edge, 0.f, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, right_edge, 0.f, outline_color); /* Top-left corner arc */ - ctx->renderer.draw_arc(chip_rect.x + corner_radius, - chip_rect.y + corner_radius, corner_radius, - IUI_PI, 1.5f * IUI_PI, outline_width, - outline_color, ctx->renderer.user); + iui_draw_arc_soft(ctx, chip_rect.x + corner_radius, + chip_rect.y + corner_radius, corner_radius, + IUI_PI, 1.5f * IUI_PI, outline_width, + outline_color); /* Top-right corner arc */ - ctx->renderer.draw_arc( - chip_rect.x + chip_rect.width - corner_radius, + iui_draw_arc_soft( + ctx, chip_rect.x + chip_rect.width - corner_radius, chip_rect.y + corner_radius, corner_radius, 1.5f * IUI_PI, - 2.f * IUI_PI, outline_width, outline_color, ctx->renderer.user); + 2.f * IUI_PI, outline_width, outline_color); /* Bottom-right corner arc */ - ctx->renderer.draw_arc( - chip_rect.x + chip_rect.width - corner_radius, + iui_draw_arc_soft( + ctx, chip_rect.x + chip_rect.width - corner_radius, chip_rect.y + chip_rect.height - corner_radius, corner_radius, - 0.f, 0.5f * IUI_PI, outline_width, outline_color, - ctx->renderer.user); + 0.f, 0.5f * IUI_PI, outline_width, outline_color); /* Bottom-left corner arc */ - ctx->renderer.draw_arc( - chip_rect.x + corner_radius, - chip_rect.y + chip_rect.height - corner_radius, corner_radius, - 0.5f * IUI_PI, IUI_PI, outline_width, outline_color, - ctx->renderer.user); + iui_draw_arc_soft(ctx, chip_rect.x + corner_radius, + chip_rect.y + chip_rect.height - corner_radius, + corner_radius, 0.5f * IUI_PI, IUI_PI, + outline_width, outline_color); } else if (draw_container) { /* Fallback with container: overlay approach creates outline * by drawing outer rect with outline color and inner rect with * container color to simulate border */ - ctx->renderer.draw_box(chip_rect, corner_radius, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, chip_rect, corner_radius, outline_color); /* Inner rounded rect (container color) to create outline effect */ iui_rect_t inner_rect = {chip_rect.x + outline_width, chip_rect.y + outline_width, @@ -254,8 +254,7 @@ static bool iui_chip_internal(iui_context *ctx, float inner_corner = corner_radius > outline_width ? corner_radius - outline_width : 0.f; - ctx->renderer.draw_box(inner_rect, inner_corner, container_color, - ctx->renderer.user); + iui_emit_box(ctx, inner_rect, inner_corner, container_color); } else { /* Fallback without container: draw edges only (corners have small * gaps) @@ -264,25 +263,21 @@ static bool iui_chip_internal(iui_context *ctx, */ iui_rect_t top_edge_full = {chip_rect.x, chip_rect.y, chip_rect.width, outline_width}; - ctx->renderer.draw_box(top_edge_full, 0.f, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, top_edge_full, 0.f, outline_color); /* Bottom edge */ iui_rect_t bottom_edge_full = { chip_rect.x, chip_rect.y + chip_rect.height - outline_width, chip_rect.width, outline_width}; - ctx->renderer.draw_box(bottom_edge_full, 0.f, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, bottom_edge_full, 0.f, outline_color); /* Left edge */ iui_rect_t left_edge_full = {chip_rect.x, chip_rect.y, outline_width, chip_rect.height}; - ctx->renderer.draw_box(left_edge_full, 0.f, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, left_edge_full, 0.f, outline_color); /* Right edge */ iui_rect_t right_edge_full = { chip_rect.x + chip_rect.width - outline_width, chip_rect.y, outline_width, chip_rect.height}; - ctx->renderer.draw_box(right_edge_full, 0.f, outline_color, - ctx->renderer.user); + iui_emit_box(ctx, right_edge_full, 0.f, outline_color); } } diff --git a/src/container.c b/src/container.c index 2b7a1a7..f5270b2 100644 --- a/src/container.c +++ b/src/container.c @@ -90,9 +90,9 @@ void iui_progress_linear(iui_context *ctx, }; /* Draw background track */ - ctx->renderer.draw_box(bar_rect, - bar_rect.height * 0.5f, /* Pill-shaped corners */ - ctx->colors.surface_container, ctx->renderer.user); + iui_emit_box(ctx, bar_rect, + bar_rect.height * 0.5f, /* Pill-shaped corners */ + ctx->colors.surface_container); /* Draw progress indicator */ float progress_ratio = @@ -113,19 +113,19 @@ void iui_progress_linear(iui_context *ctx, float draw_x = fmaxf(anim_x, bar_rect.x); float draw_width = fminf(anim_x + bar_width, bar_rect.x + bar_rect.width) - draw_x; - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {draw_x, bar_rect.y, draw_width, bar_rect.height}, - bar_rect.height * 0.5f, ctx->colors.primary, - ctx->renderer.user); + bar_rect.height * 0.5f, ctx->colors.primary); } } else { /* Draw determinate progress */ if (filled_width > 0) { - ctx->renderer.draw_box( - (iui_rect_t) {bar_rect.x, bar_rect.y, filled_width, - bar_rect.height}, - bar_rect.height * 0.5f, /* Pill-shaped corners */ - ctx->colors.primary, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {bar_rect.x, bar_rect.y, filled_width, + bar_rect.height}, + bar_rect.height * 0.5f, /* Pill-shaped corners */ + ctx->colors.primary); } } @@ -250,8 +250,7 @@ bool iui_snackbar(iui_context *ctx, /* Draw snackbar background using inverse colors for contrast */ iui_rect_t bar_rect = {snackbar_x, snackbar_y, snackbar_width, snackbar_height}; - ctx->renderer.draw_box(bar_rect, corner_radius, ctx->colors.inverse_surface, - ctx->renderer.user); + iui_emit_box(ctx, bar_rect, corner_radius, ctx->colors.inverse_surface); /* Track component for MD3 validation */ IUI_MD3_TRACK_SNACKBAR(bar_rect, corner_radius); @@ -284,8 +283,7 @@ bool iui_snackbar(iui_context *ctx, pressed ? IUI_STATE_PRESS_ALPHA : IUI_STATE_HOVER_ALPHA; uint32_t layer_color = iui_state_layer(ctx->colors.inverse_primary, alpha); - ctx->renderer.draw_box(action_rect, corner_radius, layer_color, - ctx->renderer.user); + iui_emit_box(ctx, action_rect, corner_radius, layer_color); } /* Draw action text */ @@ -469,9 +467,8 @@ bool iui_scroll_end(iui_context *ctx, iui_scroll_state *state) thumb_rect.y = thumb_y; } - ctx->renderer.draw_box(track_rect, scrollbar_width * 0.5f, - ctx->colors.surface_container_high, - ctx->renderer.user); + iui_emit_box(ctx, track_rect, scrollbar_width * 0.5f, + ctx->colors.surface_container_high); uint32_t thumb_color = ctx->colors.outline; if (is_dragging) { @@ -479,8 +476,7 @@ bool iui_scroll_end(iui_context *ctx, iui_scroll_state *state) } else if (in_rect(&thumb_rect, ctx->mouse_pos)) { thumb_color = ctx->colors.on_surface_variant; } - ctx->renderer.draw_box(thumb_rect, scrollbar_width * 0.5f, thumb_color, - ctx->renderer.user); + iui_emit_box(ctx, thumb_rect, scrollbar_width * 0.5f, thumb_color); } /* Pop after scrollbar draw so the thumb stays bounded by the viewport */ @@ -576,10 +572,8 @@ bool iui_bottom_sheet_begin(iui_context *ctx, iui_rect_t scrim_rect = {0, 0, screen_width, screen_height}; uint8_t scrim_alpha = (uint8_t) (IUI_SCRIM_ALPHA * state->anim_progress); - ctx->renderer.draw_box( - scrim_rect, 0.f, - (scrim_alpha << 24) | (ctx->colors.scrim & 0x00FFFFFF), - ctx->renderer.user); + iui_emit_box(ctx, scrim_rect, 0.f, + (scrim_alpha << 24) | (ctx->colors.scrim & 0x00FFFFFF)); /* Push modal layer */ iui_push_layer(ctx, 100); @@ -593,9 +587,8 @@ bool iui_bottom_sheet_begin(iui_context *ctx, /* Draw sheet background with rounded top corners */ iui_rect_t sheet_rect = {0, sheet_y, screen_width, current_height + 100.f}; - ctx->renderer.draw_box(sheet_rect, IUI_BOTTOM_SHEET_CORNER_RADIUS, - ctx->colors.surface_container_low, - ctx->renderer.user); + iui_emit_box(ctx, sheet_rect, IUI_BOTTOM_SHEET_CORNER_RADIUS, + ctx->colors.surface_container_low); /* Track component for MD3 validation (use logical height, not padded rect) */ @@ -609,9 +602,8 @@ bool iui_bottom_sheet_begin(iui_context *ctx, iui_rect_t handle_rect = {handle_x, handle_y, IUI_BOTTOM_SHEET_DRAG_HANDLE_WIDTH, IUI_BOTTOM_SHEET_DRAG_HANDLE_HEIGHT}; - ctx->renderer.draw_box(handle_rect, - IUI_BOTTOM_SHEET_DRAG_HANDLE_HEIGHT * 0.5f, - ctx->colors.on_surface_variant, ctx->renderer.user); + iui_emit_box(ctx, handle_rect, IUI_BOTTOM_SHEET_DRAG_HANDLE_HEIGHT * 0.5f, + ctx->colors.on_surface_variant); /* Handle drag interaction */ iui_rect_t drag_area = {0, sheet_y, screen_width, 48.f}; @@ -719,8 +711,8 @@ void iui_tooltip(iui_context *ctx, const char *text) /* Draw tooltip background using inverse colors for contrast */ iui_rect_t tooltip_rect = {x, y, width, height}; - ctx->renderer.draw_box(tooltip_rect, IUI_TOOLTIP_CORNER_RADIUS, - ctx->colors.inverse_surface, ctx->renderer.user); + iui_emit_box(ctx, tooltip_rect, IUI_TOOLTIP_CORNER_RADIUS, + ctx->colors.inverse_surface); /* Track component for MD3 validation */ IUI_MD3_TRACK_TOOLTIP(tooltip_rect, IUI_TOOLTIP_CORNER_RADIUS); @@ -793,8 +785,8 @@ bool iui_tooltip_rich(iui_context *ctx, /* Draw background */ iui_rect_t tooltip_rect = {x, y, width, height}; - ctx->renderer.draw_box(tooltip_rect, IUI_TOOLTIP_CORNER_RADIUS, - ctx->colors.inverse_surface, ctx->renderer.user); + iui_emit_box(ctx, tooltip_rect, IUI_TOOLTIP_CORNER_RADIUS, + ctx->colors.inverse_surface); /* Draw content */ float text_y = y + IUI_TOOLTIP_PADDING; @@ -822,8 +814,7 @@ bool iui_tooltip_rich(iui_context *ctx, if (hovered) { uint32_t hover_color = iui_state_layer(ctx->colors.inverse_primary, IUI_STATE_HOVER_ALPHA); - ctx->renderer.draw_box(action_rect, 4.f, hover_color, - ctx->renderer.user); + iui_emit_box(ctx, action_rect, 4.f, hover_color); } iui_internal_draw_text(ctx, x + IUI_TOOLTIP_PADDING + 4.f, text_y, @@ -848,10 +839,10 @@ void iui_badge_dot(iui_context *ctx, float anchor_x, float anchor_y) float x = anchor_x - IUI_BADGE_OFFSET_X; float y = anchor_y + IUI_BADGE_OFFSET_Y; - ctx->renderer.draw_box( - (iui_rect_t) {x - radius, y - radius, IUI_BADGE_DOT_SIZE, - IUI_BADGE_DOT_SIZE}, - radius, ctx->colors.error, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {x - radius, y - radius, IUI_BADGE_DOT_SIZE, + IUI_BADGE_DOT_SIZE}, + radius, ctx->colors.error); } void iui_badge_number(iui_context *ctx, @@ -886,8 +877,8 @@ void iui_badge_number(iui_context *ctx, float y = anchor_y + IUI_BADGE_OFFSET_Y - height * 0.5f; /* Draw badge background */ - ctx->renderer.draw_box((iui_rect_t) {x, y, width, height}, radius, - ctx->colors.error, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {x, y, width, height}, radius, + ctx->colors.error); /* Draw badge text centered */ float text_x = x + (width - text_width) * 0.5f; @@ -920,7 +911,7 @@ static bool draw_banner_action(iui_context *ctx, if (hovered) { uint32_t hover = iui_state_layer(ctx->colors.primary, IUI_STATE_HOVER_ALPHA); - ctx->renderer.draw_box(btn_rect, 4.f, hover, ctx->renderer.user); + iui_emit_box(ctx, btn_rect, 4.f, hover); if (ctx->mouse_released & IUI_MOUSE_LEFT) clicked = true; } @@ -947,17 +938,16 @@ int iui_banner(iui_context *ctx, const iui_banner_options *options) height}; /* Draw banner background */ - ctx->renderer.draw_box(banner_rect, 0.f, ctx->colors.surface_container, - ctx->renderer.user); + iui_emit_box(ctx, banner_rect, 0.f, ctx->colors.surface_container); /* Track component for MD3 validation */ IUI_MD3_TRACK_BANNER(banner_rect, 0.f); /* Draw divider at bottom */ - ctx->renderer.draw_box( - (iui_rect_t) {banner_rect.x, banner_rect.y + height - 1.f, - banner_rect.width, 1.f}, - 0.f, ctx->colors.outline_variant, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {banner_rect.x, banner_rect.y + height - 1.f, + banner_rect.width, 1.f}, + 0.f, ctx->colors.outline_variant); float content_x = banner_rect.x + IUI_BANNER_PADDING; @@ -1093,8 +1083,7 @@ void iui_table_header(iui_context *ctx, /* Draw header cell background */ iui_rect_t cell_rect = {state->current_x, state->row_y, width, height}; - ctx->renderer.draw_box(cell_rect, 0.f, ctx->colors.surface_container, - ctx->renderer.user); + iui_emit_box(ctx, cell_rect, 0.f, ctx->colors.surface_container); /* Draw header text with label_large style (bold) */ float text_x = state->current_x + IUI_TABLE_CELL_PADDING; @@ -1109,11 +1098,11 @@ void iui_table_header(iui_context *ctx, if (state->current_col >= state->cols) { state->row_y += height; /* Draw divider below header */ - ctx->renderer.draw_box( - (iui_rect_t) {state->start_x, - state->row_y - IUI_TABLE_DIVIDER_HEIGHT, - ctx->layout.width, IUI_TABLE_DIVIDER_HEIGHT}, - 0.f, ctx->colors.outline_variant, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {state->start_x, + state->row_y - IUI_TABLE_DIVIDER_HEIGHT, + ctx->layout.width, IUI_TABLE_DIVIDER_HEIGHT}, + 0.f, ctx->colors.outline_variant); state->in_header = false; } } @@ -1157,9 +1146,7 @@ void iui_table_cell(iui_context *ctx, /* Alternate row background for zebra striping */ if (state->row_index % 2 == 1) { iui_rect_t cell_rect = {state->current_x, state->row_y, width, height}; - ctx->renderer.draw_box(cell_rect, 0.f, - ctx->colors.surface_container_low, - ctx->renderer.user); + iui_emit_box(ctx, cell_rect, 0.f, ctx->colors.surface_container_low); } /* Draw cell text */ @@ -1180,10 +1167,11 @@ void iui_table_row_end(iui_context *ctx, iui_table_state *state) state->row_y += IUI_TABLE_ROW_HEIGHT; /* Draw row divider */ - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {state->start_x, state->row_y - IUI_TABLE_DIVIDER_HEIGHT, ctx->layout.width, IUI_TABLE_DIVIDER_HEIGHT}, - 0.f, ctx->colors.outline_variant, ctx->renderer.user); + 0.f, ctx->colors.outline_variant); } void iui_table_end(iui_context *ctx, const iui_table_state *state) @@ -1240,8 +1228,7 @@ bool iui_carousel_item(iui_context *ctx, uint32_t bg_color = ctx->colors.surface_container; /* Draw background */ - ctx->renderer.draw_box(item_rect, IUI_CAROUSEL_CORNER_RADIUS, bg_color, - ctx->renderer.user); + iui_emit_box(ctx, item_rect, IUI_CAROUSEL_CORNER_RADIUS, bg_color); /* State layer */ iui_draw_state_layer(ctx, item_rect, IUI_CAROUSEL_CORNER_RADIUS, diff --git a/src/core.c b/src/core.c index 5e140eb..bfd6f69 100644 --- a/src/core.c +++ b/src/core.c @@ -542,13 +542,15 @@ static void iui_emit_glyph(iui_context *ctx, float mx = (x + nx) * 0.5f, my = (y + ny) * 0.5f; float w = fmaxf(len, 1.f), h = fmaxf(pen_w, 1.f); if (fabsf(dx) > fabsf(dy)) - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {fminf(x, nx), my - h * 0.5f, w, h}, 0, - color, ctx->renderer.user); + color); else - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {mx - h * 0.5f, fminf(y, ny), h, len}, - 0, color, ctx->renderer.user); + 0, color); } } x = nx, y = ny; @@ -592,15 +594,17 @@ static void iui_emit_glyph(iui_context *ctx, if (len > 0.5f) { float w = fmaxf(len, 1.f), h = fmaxf(pen_w, 1.f); if (fabsf(dx) > fabsf(dy)) - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {fminf(x, nx), (y + ny) * 0.5f - h * 0.5f, w, h}, - 0, color, ctx->renderer.user); + 0, color); else - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {(x + nx) * 0.5f - h * 0.5f, fminf(y, ny), h, len}, - 0, color, ctx->renderer.user); + 0, color); } x = nx; y = ny; @@ -804,6 +808,7 @@ iui_context *iui_init(const iui_config_t *config) /* Initialize performance systems (disabled by default) */ iui_batch_init(ctx); iui_dirty_init(ctx); + iui_ink_bounds_init(ctx); iui_text_cache_init(ctx); return ctx; } @@ -998,7 +1003,7 @@ void iui_draw_focus_ring(iui_context *ctx, uint32_t ring_color = ctx->colors.primary; /* Draw outer ring (full box) */ - ctx->renderer.draw_box(ring, outer_corner, ring_color, ctx->renderer.user); + iui_emit_box(ctx, ring, outer_corner, ring_color); /* Draw inner cutout with surface color to create ring effect */ iui_rect_t inner = { @@ -1010,8 +1015,7 @@ void iui_draw_focus_ring(iui_context *ctx, float inner_corner = corner_radius + offset; /* Use surface color to "cut out" the inner area */ - ctx->renderer.draw_box(inner, inner_corner, ctx->colors.surface, - ctx->renderer.user); + iui_emit_box(ctx, inner, inner_corner, ctx->colors.surface); } #else /* Stub: Focus ring disabled - draw nothing */ diff --git a/src/dialog.c b/src/dialog.c index ded43e2..10b6ae1 100644 --- a/src/dialog.c +++ b/src/dialog.c @@ -170,17 +170,16 @@ int iui_dialog(iui_context *ctx, /* Draw scrim (semi-transparent overlay) */ uint32_t scrim_color = ctx->colors.scrim; - ctx->renderer.draw_box((iui_rect_t) {0, 0, screen_width, screen_height}, 0, - scrim_color, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {0, 0, screen_width, screen_height}, 0, + scrim_color); /* Draw dialog shadow (elevation_3 for dialogs per MD3) */ float corner = IUI_DIALOG_CORNER_RADIUS; iui_draw_shadow(ctx, dialog_bounds, corner, IUI_ELEVATION_3); /* Draw dialog background */ - ctx->renderer.draw_box(dialog_bounds, corner, - ctx->colors.surface_container_high, - ctx->renderer.user); + iui_emit_box(ctx, dialog_bounds, corner, + ctx->colors.surface_container_high); /* Track component for MD3 validation */ IUI_MD3_TRACK_DIALOG(dialog_bounds, corner); @@ -239,8 +238,7 @@ int iui_dialog(iui_context *ctx, /* Primary button (rightmost) - filled style */ uint32_t bg_color = ctx->colors.primary; text_color = ctx->colors.on_primary; - ctx->renderer.draw_box(btn_rect, btn_corner, bg_color, - ctx->renderer.user); + iui_emit_box(ctx, btn_rect, btn_corner, bg_color); } /* Draw state layer for hover/press */ @@ -341,8 +339,7 @@ bool iui_fullscreen_dialog_begin(iui_context *ctx, iui_register_blocking_region(ctx, screen_bounds); /* Draw full-screen surface background */ - ctx->renderer.draw_box(screen_bounds, 0.f, ctx->colors.surface, - ctx->renderer.user); + iui_emit_box(ctx, screen_bounds, 0.f, ctx->colors.surface); /* Header bar dimensions */ float header_h = IUI_FULLSCREEN_DIALOG_HEADER_HEIGHT; @@ -436,8 +433,7 @@ bool iui_fullscreen_dialog_action(iui_context *ctx, if (dialog->action_count == 0) { /* Primary action - filled button */ - ctx->renderer.draw_box(btn_rect, corner, ctx->colors.primary, - ctx->renderer.user); + iui_emit_box(ctx, btn_rect, corner, ctx->colors.primary); text_color = ctx->colors.on_primary; } else { /* Secondary action - text button */ diff --git a/src/draw.c b/src/draw.c index eaa74f0..9d20af6 100644 --- a/src/draw.c +++ b/src/draw.c @@ -1,5 +1,200 @@ #include "internal.h" +#include + +/* Ink-bounds tracking */ + +void iui_ink_bounds_init(iui_context *ctx) +{ + if (!ctx) + return; + ctx->ink_bounds.min_x = FLT_MAX; + ctx->ink_bounds.min_y = FLT_MAX; + ctx->ink_bounds.max_x = -FLT_MAX; + ctx->ink_bounds.max_y = -FLT_MAX; + ctx->ink_bounds.valid = false; + ctx->ink_bounds.enabled = false; +} + +void iui_ink_bounds_reset(iui_context *ctx) +{ + if (!ctx) + return; + ctx->ink_bounds.min_x = FLT_MAX; + ctx->ink_bounds.min_y = FLT_MAX; + ctx->ink_bounds.max_x = -FLT_MAX; + ctx->ink_bounds.max_y = -FLT_MAX; + ctx->ink_bounds.valid = false; +} + +void iui_ink_bounds_enable(iui_context *ctx, bool enable) +{ + if (!ctx) + return; + ctx->ink_bounds.enabled = enable; + if (enable) + iui_ink_bounds_reset(ctx); +} + +bool iui_ink_bounds_get(const iui_context *ctx, iui_rect_t *out) +{ + if (!ctx || !ctx->ink_bounds.valid || !out) + return false; + out->x = ctx->ink_bounds.min_x; + out->y = ctx->ink_bounds.min_y; + out->width = ctx->ink_bounds.max_x - ctx->ink_bounds.min_x; + out->height = ctx->ink_bounds.max_y - ctx->ink_bounds.min_y; + return true; +} + +bool iui_ink_bounds_valid(const iui_context *ctx) +{ + return ctx && ctx->ink_bounds.valid; +} + +/* Draw call batch add functions */ + +bool iui_batch_add_rect(iui_context *ctx, + float x, + float y, + float w, + float h, + float radius, + uint32_t color) +{ + if (!ctx || !ctx->batch.enabled) + return false; + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) { + /* Buffer full: flush and retry */ + iui_batch_flush(ctx); + } + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + return false; + + iui_draw_cmd *cmd = &ctx->batch.commands[ctx->batch.count++]; + cmd->type = IUI_CMD_RECT; + cmd->data.rect.x = x; + cmd->data.rect.y = y; + cmd->data.rect.w = w; + cmd->data.rect.h = h; + cmd->data.rect.radius = radius; + cmd->color = color; + cmd->clip = ctx->current_clip; + return true; +} + +bool iui_batch_add_text(iui_context *ctx, + float x, + float y, + const char *text, + uint32_t color) +{ + if (!ctx || !ctx->batch.enabled || !text) + return false; + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + iui_batch_flush(ctx); + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + return false; + + iui_draw_cmd *cmd = &ctx->batch.commands[ctx->batch.count++]; + cmd->type = IUI_CMD_TEXT; + cmd->data.text.x = x; + cmd->data.text.y = y; + /* Truncate text to inline buffer size */ + size_t len = strlen(text); + if (len >= sizeof(cmd->data.text.text)) + len = sizeof(cmd->data.text.text) - 1; + memcpy(cmd->data.text.text, text, len); + cmd->data.text.text[len] = '\0'; + cmd->color = color; + cmd->clip = ctx->current_clip; + return true; +} + +bool iui_batch_add_line(iui_context *ctx, + float x0, + float y0, + float x1, + float y1, + float width, + uint32_t color) +{ + if (!ctx || !ctx->batch.enabled) + return false; + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + iui_batch_flush(ctx); + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + return false; + + iui_draw_cmd *cmd = &ctx->batch.commands[ctx->batch.count++]; + cmd->type = IUI_CMD_LINE; + cmd->data.line.x0 = x0; + cmd->data.line.y0 = y0; + cmd->data.line.x1 = x1; + cmd->data.line.y1 = y1; + cmd->data.line.width = width; + cmd->color = color; + cmd->clip = ctx->current_clip; + return true; +} + +bool iui_batch_add_circle(iui_context *ctx, + float cx, + float cy, + float radius, + uint32_t fill_color, + uint32_t stroke_color, + float stroke_width) +{ + if (!ctx || !ctx->batch.enabled) + return false; + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + iui_batch_flush(ctx); + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + return false; + + iui_draw_cmd *cmd = &ctx->batch.commands[ctx->batch.count++]; + cmd->type = IUI_CMD_CIRCLE; + cmd->data.circle.cx = cx; + cmd->data.circle.cy = cy; + cmd->data.circle.radius = radius; + cmd->data.circle.fill_color = fill_color; + cmd->data.circle.stroke_width = stroke_width; + cmd->color = fill_color; + cmd->color2 = stroke_color; + cmd->clip = ctx->current_clip; + return true; +} + +bool iui_batch_add_arc(iui_context *ctx, + float cx, + float cy, + float radius, + float start_angle, + float end_angle, + float width, + uint32_t color) +{ + if (!ctx || !ctx->batch.enabled) + return false; + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + iui_batch_flush(ctx); + if (ctx->batch.count >= IUI_DRAW_CMD_BUFFER_SIZE) + return false; + + iui_draw_cmd *cmd = &ctx->batch.commands[ctx->batch.count++]; + cmd->type = IUI_CMD_ARC; + cmd->data.arc.cx = cx; + cmd->data.arc.cy = cy; + cmd->data.arc.radius = radius; + cmd->data.arc.start_angle = start_angle; + cmd->data.arc.end_angle = end_angle; + cmd->data.arc.width = width; + cmd->color = color; + cmd->clip = ctx->current_clip; + return true; +} + /* Text width caching */ void iui_text_cache_init(iui_context *ctx) @@ -26,14 +221,27 @@ void iui_text_cache_clear(iui_context *ctx) memset(ctx->text_cache.entries, 0, sizeof(ctx->text_cache.entries)); } +/* Mix font_height bits into text hash to prevent cross-size cache poisoning. + * Typography paths temporarily mutate ctx->font_height, so same text at + * different sizes must hash to different slots. + */ +static uint32_t text_cache_hash(const char *text, float font_height) +{ + uint32_t h = iui_hash_str(text); + uint32_t fh; + memcpy(&fh, &font_height, sizeof(fh)); + h ^= fh * 2654435761u; /* Knuth multiplicative hash mixing */ + if (h == 0) + h = 1; + return h; +} + static bool text_cache_get(iui_context *ctx, const char *text, float *width) { if (!ctx->text_cache.enabled || !text || !width) return false; - uint32_t hash = iui_hash_str(text); - if (hash == 0) - hash = 1; + uint32_t hash = text_cache_hash(text, ctx->font_height); /* Bitwise AND for power-of-2 cache size (faster than modulo) */ int start = hash & (IUI_TEXT_CACHE_SIZE - 1); @@ -45,8 +253,9 @@ static bool text_cache_get(iui_context *ctx, const char *text, float *width) ctx->text_cache.misses++; return false; } - /* Compare both hash and pointer to handle collisions */ - if (entry->hash == hash && entry->text == text) { + /* Compare hash, pointer, and font size to handle collisions */ + if (entry->hash == hash && entry->text == text && + entry->font_height == ctx->font_height) { *width = entry->width; if (entry->hits < 255) entry->hits++; @@ -63,9 +272,7 @@ static void text_cache_put(iui_context *ctx, const char *text, float width) if (!ctx->text_cache.enabled || !text) return; - uint32_t hash = iui_hash_str(text); - if (hash == 0) - hash = 1; + uint32_t hash = text_cache_hash(text, ctx->font_height); /* Bitwise AND for power-of-2 cache size (faster than modulo) */ int start = hash & (IUI_TEXT_CACHE_SIZE - 1), evict_idx = -1; @@ -75,10 +282,12 @@ static void text_cache_put(iui_context *ctx, const char *text, float width) int idx = (start + i) & (IUI_TEXT_CACHE_SIZE - 1); iui_text_cache_entry *entry = &ctx->text_cache.entries[idx]; - /* Empty slot or exact match (same hash and pointer) */ - if (entry->hash == 0 || (entry->hash == hash && entry->text == text)) { + /* Empty slot or exact match (same hash, pointer, and font size) */ + if (entry->hash == 0 || (entry->hash == hash && entry->text == text && + entry->font_height == ctx->font_height)) { entry->hash = hash; entry->text = text; + entry->font_height = ctx->font_height; entry->width = width; entry->hits = 1; return; @@ -94,6 +303,7 @@ static void text_cache_put(iui_context *ctx, const char *text, float width) iui_text_cache_entry *entry = &ctx->text_cache.entries[evict_idx]; entry->hash = hash; entry->text = text; + entry->font_height = ctx->font_height; entry->width = width; entry->hits = 1; } @@ -128,6 +338,9 @@ void iui_text_cache_frame_end(iui_context *ctx) /* Text Width and Drawing */ float iui_get_text_width(iui_context *ctx, const char *text) { + if (!ctx || !text) + return 0.f; + /* Check cache first */ float cached_width; if (text_cache_get(ctx, text, &cached_width)) @@ -187,6 +400,28 @@ void iui_internal_draw_text(iui_context *ctx, const char *text, uint32_t color) { + /* Track text bounding box in ink-bounds: (x, y) to (x + width, y + h) */ + if (ctx->ink_bounds.enabled && text && *text) { + float tw = iui_get_text_width(ctx, text); + iui_ink_bounds_extend(ctx, x, y, tw, ctx->font_height); + } + + /* Route through batch to preserve draw order with boxes. + * Only batch when the backend has draw_text; vector font rendering + * goes through iui_emit_box which is already batch-aware. + * Fall back to direct draw for text exceeding inline buffer (64 bytes) + * to avoid silent truncation producing different rendering. + */ + if (ctx->batch.enabled && ctx->renderer.draw_text) { + if (text && strlen(text) >= 64) { + iui_batch_flush(ctx); + ctx->renderer.draw_text(x, y, text, color, ctx->renderer.user); + return; + } + iui_batch_add_text(ctx, x, y, text, color); + return; + } + if (ctx->renderer.draw_text) ctx->renderer.draw_text(x, y, text, color, ctx->renderer.user); else @@ -330,8 +565,8 @@ static void iui_divider_internal(iui_context *ctx, /* Guard against negative width in narrow containers */ if (w > 0.f) { float x = ctx->layout.x + left_inset; - ctx->renderer.draw_box((iui_rect_t) {x, ctx->layout.y, w, 1.f}, 0.f, - ctx->colors.outline_variant, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {x, ctx->layout.y, w, 1.f}, 0.f, + ctx->colors.outline_variant); } /* MD3: 8dp vertical margin below (advance by 1dp line + 8dp margin) */ @@ -363,20 +598,22 @@ void iui_draw_rect_outline(iui_context *ctx, uint32_t color) { /* Top */ - ctx->renderer.draw_box((iui_rect_t) {rect.x, rect.y, rect.width, width}, - 0.f, color, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {rect.x, rect.y, rect.width, width}, 0.f, + color); /* Bottom */ - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {rect.x, rect.y + rect.height - width, rect.width, width}, - 0.f, color, ctx->renderer.user); + 0.f, color); /* Left */ - ctx->renderer.draw_box((iui_rect_t) {rect.x, rect.y, width, rect.height}, - 0.f, color, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {rect.x, rect.y, width, rect.height}, 0.f, + color); /* Right */ - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {rect.x + rect.width - width, rect.y, width, rect.height}, - 0.f, color, ctx->renderer.user); + 0.f, color); } /* Internal: Draw line with fallback to box */ @@ -389,6 +626,15 @@ void iui_draw_line_soft(iui_context *ctx, uint32_t color) { if (ctx->renderer.draw_line) { + /* Conservative AABB: bounding box of endpoints + half-width */ + float hw = width * 0.5f; + iui_ink_bounds_extend(ctx, fminf(x0, x1) - hw, fminf(y0, y1) - hw, + fabsf(x1 - x0) + width, fabsf(y1 - y0) + width); + /* Route through batch to preserve draw order with boxes */ + if (ctx->batch.enabled) { + iui_batch_add_line(ctx, x0, y0, x1, y1, width, color); + return; + } ctx->renderer.draw_line(x0, y0, x1, y1, width, color, ctx->renderer.user); } else { @@ -399,15 +645,15 @@ void iui_draw_line_soft(iui_context *ctx, /* For simple horizontal/vertical lines, draw_box is perfect */ if (fabsf(dy) < 0.1f) { - ctx->renderer.draw_box( - (iui_rect_t) {fminf(x0, x1), y0 - width * 0.5f, fabsf(dx), - width}, - 0.f, color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {fminf(x0, x1), y0 - width * 0.5f, + fabsf(dx), width}, + 0.f, color); } else if (fabsf(dx) < 0.1f) { - ctx->renderer.draw_box( - (iui_rect_t) {x0 - width * 0.5f, fminf(y0, y1), width, - fabsf(dy)}, - 0.f, color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {x0 - width * 0.5f, fminf(y0, y1), width, + fabsf(dy)}, + 0.f, color); } else { /* For diagonal lines without rotation support, approximate with * bounding box or thin rects. @@ -423,16 +669,16 @@ void iui_draw_line_soft(iui_context *ctx, */ if (fabsf(dx) > fabsf(dy)) { float my = (y0 + y1) * 0.5f; - ctx->renderer.draw_box( - (iui_rect_t) {fminf(x0, x1), my - width * 0.5f, fabsf(dx), - width}, - 0.f, color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {fminf(x0, x1), my - width * 0.5f, + fabsf(dx), width}, + 0.f, color); } else { float mx = (x0 + x1) * 0.5f; - ctx->renderer.draw_box( - (iui_rect_t) {mx - width * 0.5f, fminf(y0, y1), width, - fabsf(dy)}, - 0.f, color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {mx - width * 0.5f, fminf(y0, y1), + width, fabsf(dy)}, + 0.f, color); } } } @@ -448,14 +694,24 @@ void iui_draw_circle_soft(iui_context *ctx, float stroke_width) { if (ctx->renderer.draw_circle) { + float outer = radius + stroke_width; + iui_ink_bounds_extend(ctx, cx - outer, cy - outer, outer * 2.f, + outer * 2.f); + /* Route through batch to preserve draw order with boxes */ + if (ctx->batch.enabled) { + iui_batch_add_circle(ctx, cx, cy, radius, fill_color, stroke_color, + stroke_width); + return; + } ctx->renderer.draw_circle(cx, cy, radius, fill_color, stroke_color, stroke_width, ctx->renderer.user); } else { /* Fallback: draw box (square) approximating circle */ if (fill_color) { - ctx->renderer.draw_box((iui_rect_t) {cx - radius, cy - radius, - radius * 2.f, radius * 2.f}, - radius, fill_color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {cx - radius, cy - radius, radius * 2.f, + radius * 2.f}, + radius, fill_color); } /* If stroked (border), simulate with two boxes or just one filled box * with stroke color if no fill. 'draw_box' does not support borders @@ -466,9 +722,10 @@ void iui_draw_circle_soft(iui_context *ctx, */ if (stroke_color && !fill_color) { /* Crude approximation: just draw a filled square for the outline */ - ctx->renderer.draw_box((iui_rect_t) {cx - radius, cy - radius, - radius * 2.f, radius * 2.f}, - radius, stroke_color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {cx - radius, cy - radius, radius * 2.f, + radius * 2.f}, + radius, stroke_color); } } } @@ -484,6 +741,16 @@ void iui_draw_arc_soft(iui_context *ctx, uint32_t color) { if (ctx->renderer.draw_arc) { + /* Conservative: full circle bounding box + arc width */ + float outer = radius + width * 0.5f; + iui_ink_bounds_extend(ctx, cx - outer, cy - outer, outer * 2.f, + outer * 2.f); + /* Route through batch to preserve draw order with boxes */ + if (ctx->batch.enabled) { + iui_batch_add_arc(ctx, cx, cy, radius, start_angle, end_angle, + width, color); + return; + } ctx->renderer.draw_arc(cx, cy, radius, start_angle, end_angle, width, color, ctx->renderer.user); } else { @@ -494,9 +761,10 @@ void iui_draw_arc_soft(iui_context *ctx, * Simplified: just draw a small box at the center to indicate something * is there */ - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {cx - radius, cy - radius, radius * 2.f, radius * 2.f}, - radius, color, ctx->renderer.user); + radius, color); } } @@ -522,6 +790,13 @@ bool iui_draw_line(iui_context *ctx, { if (!ctx || !ctx->renderer.draw_line) return false; + float hw = width * 0.5f; + iui_ink_bounds_extend(ctx, fminf(x0, x1) - hw, fminf(y0, y1) - hw, + fabsf(x1 - x0) + width, fabsf(y1 - y0) + width); + if (ctx->batch.enabled) { + iui_batch_add_line(ctx, x0, y0, x1, y1, width, color); + return true; + } ctx->renderer.draw_line(x0, y0, x1, y1, width, color, ctx->renderer.user); return true; } @@ -536,6 +811,14 @@ bool iui_draw_circle(iui_context *ctx, { if (!ctx || !ctx->renderer.draw_circle) return false; + float outer = radius + stroke_width; + iui_ink_bounds_extend(ctx, cx - outer, cy - outer, outer * 2.f, + outer * 2.f); + if (ctx->batch.enabled) { + iui_batch_add_circle(ctx, cx, cy, radius, fill_color, stroke_color, + stroke_width); + return true; + } ctx->renderer.draw_circle(cx, cy, radius, fill_color, stroke_color, stroke_width, ctx->renderer.user); return true; @@ -552,6 +835,14 @@ bool iui_draw_arc(iui_context *ctx, { if (!ctx || !ctx->renderer.draw_arc) return false; + float outer = radius + width * 0.5f; + iui_ink_bounds_extend(ctx, cx - outer, cy - outer, outer * 2.f, + outer * 2.f); + if (ctx->batch.enabled) { + iui_batch_add_arc(ctx, cx, cy, radius, start_angle, end_angle, width, + color); + return true; + } ctx->renderer.draw_arc(cx, cy, radius, start_angle, end_angle, width, color, ctx->renderer.user); return true; @@ -635,8 +926,7 @@ void iui_draw_state_layer(iui_context *ctx, uint32_t layer_color = iui_state_layer(content_color, alpha); /* Draw the state layer overlay */ - ctx->renderer.draw_box(bounds, corner_radius, layer_color, - ctx->renderer.user); + iui_emit_box(ctx, bounds, corner_radius, layer_color); } /* MD3 Elevation Shadow System @@ -728,8 +1018,7 @@ void iui_draw_shadow(iui_context *ctx, .height = bounds.height + spread * 2.f, }; - ctx->renderer.draw_box(rect, corner_radius + spread * 0.5f, color, - ctx->renderer.user); + iui_emit_box(ctx, rect, corner_radius + spread * 0.5f, color); } /* Render key shadow (directional, offset down) @@ -756,8 +1045,7 @@ void iui_draw_shadow(iui_context *ctx, .height = bounds.height + spread, }; - ctx->renderer.draw_box(rect, corner_radius + spread * 0.3f, color, - ctx->renderer.user); + iui_emit_box(ctx, rect, corner_radius + spread * 0.3f, color); } } @@ -794,7 +1082,7 @@ void iui_draw_elevated_box(iui_context *ctx, #endif /* Draw the box on top */ - ctx->renderer.draw_box(bounds, corner_radius, color, ctx->renderer.user); + iui_emit_box(ctx, bounds, corner_radius, color); } /* Clip stack functions */ @@ -807,7 +1095,8 @@ bool iui_push_clip(iui_context *ctx, iui_rect_t rect) if (ctx->clip.depth > 0) { iui_rect_t current = ctx->clip.stack[ctx->clip.depth - 1]; /* Compute right/bottom BEFORE clamping origin so the original - * rect.x + rect.width is used, not the shifted value. */ + * rect.x + rect.width is used, not the shifted value. + */ float right = fminf(rect.x + rect.width, current.x + current.width); float bottom = fminf(rect.y + rect.height, current.y + current.height); rect.x = fmaxf(rect.x, current.x); @@ -941,6 +1230,14 @@ static void batch_flush_internal(iui_context *ctx) } } ctx->batch.count = 0; + + /* Restore the logical clip state so subsequent direct draws (e.g. long-text + * fallback) use the correct clip rectangle, not whatever the last batched + * command happened to set. + */ + ctx->renderer.set_clip_rect(ctx->current_clip.minx, ctx->current_clip.miny, + ctx->current_clip.maxx, ctx->current_clip.maxy, + ctx->renderer.user); } void iui_batch_init(iui_context *ctx) diff --git a/src/fab.c b/src/fab.c index 87d062a..2af8f1c 100644 --- a/src/fab.c +++ b/src/fab.c @@ -55,8 +55,7 @@ static bool iui_fab_internal(iui_context *ctx, iui_draw_shadow(ctx, fab_rect, corner_radius, IUI_ELEVATION_3); /* Draw container background */ - ctx->renderer.draw_box(fab_rect, corner_radius, container_color, - ctx->renderer.user); + iui_emit_box(ctx, fab_rect, corner_radius, container_color); /* Draw state layer for hover/press */ iui_draw_state_layer(ctx, fab_rect, corner_radius, content_color, state); @@ -247,8 +246,7 @@ static bool iui_icon_button_internal(iui_context *ctx, /* Draw container background (if applicable) */ if (draw_container) { - ctx->renderer.draw_box(button_rect, corner_radius, container_color, - ctx->renderer.user); + iui_emit_box(ctx, button_rect, corner_radius, container_color); } /* Draw outline (for outlined variant) */ diff --git a/src/icons.c b/src/icons.c index a0b9fbd..671712b 100644 --- a/src/icons.c +++ b/src/icons.c @@ -99,9 +99,8 @@ static void iui_draw_icon_error(iui_context *ctx, float dot_r = size * 0.08f; /* Vertical bar */ - ctx->renderer.draw_box( - (iui_rect_t) {cx - 1.5f, cy - bar_h, 3.f, bar_h * 1.4f}, 1.f, color, - ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {cx - 1.5f, cy - bar_h, 3.f, bar_h * 1.4f}, + 1.f, color); /* Dot below */ iui_draw_circle_soft(ctx, cx, cy + bar_h * 0.6f, dot_r, color, 0, 0); diff --git a/src/input.c b/src/input.c index e9a61de..1e7b4f5 100644 --- a/src/input.c +++ b/src/input.c @@ -45,19 +45,18 @@ static void textfield_draw_background(iui_context *ctx, const textfield_colors_t *c) { if (style == IUI_TEXTFIELD_OUTLINED) { - ctx->renderer.draw_box(rect, ctx->corner, c->border, - ctx->renderer.user); - ctx->renderer.draw_box((iui_rect_t) {rect.x + 1, rect.y + 1, - rect.width - 2, rect.height - 2}, - ctx->corner - 1, - ctx->colors.surface_container_high, - ctx->renderer.user); + iui_emit_box(ctx, rect, ctx->corner, c->border); + iui_emit_box(ctx, + (iui_rect_t) {rect.x + 1, rect.y + 1, rect.width - 2, + rect.height - 2}, + ctx->corner - 1, ctx->colors.surface_container_high); } else { - ctx->renderer.draw_box(rect, ctx->corner, c->bg, ctx->renderer.user); - ctx->renderer.draw_box( + iui_emit_box(ctx, rect, ctx->corner, c->bg); + iui_emit_box( + ctx, (iui_rect_t) {rect.x, rect.y + rect.height - c->indicator_height, rect.width, c->indicator_height}, - 0.f, c->border, ctx->renderer.user); + 0.f, c->border); } } @@ -83,8 +82,7 @@ static void textfield_draw_icons(iui_context *ctx, /* Trailing icon hover effect */ if (!opts->disabled && in_rect(&trailing_rect, ctx->mouse_pos)) { uint32_t hover = iui_state_layer(icon_color, IUI_STATE_HOVER_ALPHA); - ctx->renderer.draw_box(trailing_rect, icon_size * 0.5f, hover, - ctx->renderer.user); + iui_emit_box(ctx, trailing_rect, icon_size * 0.5f, hover); } iui_draw_textfield_icon(ctx, opts->trailing_icon, cx, cy, icon_size * 0.8f, icon_color); @@ -257,10 +255,10 @@ iui_textfield_result iui_textfield(iui_context *ctx, pos = len; float cursor_x = text_x + textfield_get_width_to_pos( ctx, buffer, pos, opts.password_mode); - ctx->renderer.draw_box( - (iui_rect_t) {cursor_x, text_y, IUI_TEXTFIELD_CURSOR_WIDTH, - ctx->font_height}, - 0.f, ctx->colors.primary, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {cursor_x, text_y, IUI_TEXTFIELD_CURSOR_WIDTH, + ctx->font_height}, + 0.f, ctx->colors.primary); } iui_newline(ctx); @@ -810,10 +808,11 @@ bool iui_edit_with_selection(iui_context *ctx, has_focus ? IUI_SELECTION_ALPHA : (IUI_SELECTION_ALPHA / 2); uint32_t selection_color = iui_state_layer(ctx->colors.primary, sel_alpha); - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {visible_start, text_y, visible_end - visible_start, ctx->font_height}, - 0.f, selection_color, ctx->renderer.user); + 0.f, selection_color); } } @@ -827,10 +826,11 @@ bool iui_edit_with_selection(iui_context *ctx, ctx, buffer, state->cursor, false); if (cursor_x >= text_clip.x && cursor_x <= text_clip.x + text_clip.width) { - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {cursor_x, text_y, IUI_TEXTFIELD_CURSOR_WIDTH, ctx->font_height}, - 0.f, ctx->colors.primary, ctx->renderer.user); + 0.f, ctx->colors.primary); } } @@ -1080,10 +1080,11 @@ iui_textfield_result iui_textfield_with_selection( has_focus ? IUI_SELECTION_ALPHA : (IUI_SELECTION_ALPHA / 2); uint32_t selection_color = iui_state_layer(ctx->colors.primary, sel_alpha); - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {visible_start, text_y, visible_end - visible_start, ctx->font_height}, - 0.f, selection_color, ctx->renderer.user); + 0.f, selection_color); } } @@ -1110,10 +1111,11 @@ iui_textfield_result iui_textfield_with_selection( opts.password_mode); if (cursor_x >= text_x_start && cursor_x <= text_x_start + text_area_width) { - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {cursor_x, text_y, IUI_TEXTFIELD_CURSOR_WIDTH, ctx->font_height}, - 0.f, ctx->colors.primary, ctx->renderer.user); + 0.f, ctx->colors.primary); } } @@ -1154,15 +1156,14 @@ static void checkbox_draw(iui_context *ctx, iui_state_layer(ctx->colors.on_primary, IUI_STATE_FOCUS_ALPHA); bg_color = iui_blend_color(bg_color, focus_layer); } - ctx->renderer.draw_box(widget_rect, corner, bg_color, - ctx->renderer.user); + iui_emit_box(ctx, widget_rect, corner, bg_color); float mark_margin = widget_rect.width * 0.25f; - ctx->renderer.draw_box( - (iui_rect_t) {widget_rect.x + mark_margin, - widget_rect.y + mark_margin, - widget_rect.width - mark_margin * 2, - widget_rect.height - mark_margin * 2}, - corner * 0.5f, ctx->colors.on_primary, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {widget_rect.x + mark_margin, + widget_rect.y + mark_margin, + widget_rect.width - mark_margin * 2, + widget_rect.height - mark_margin * 2}, + corner * 0.5f, ctx->colors.on_primary); } else { uint32_t bg_color = ctx->colors.surface_container; if (is_focused) { @@ -1170,8 +1171,7 @@ static void checkbox_draw(iui_context *ctx, iui_state_layer(ctx->colors.primary, IUI_STATE_FOCUS_ALPHA); bg_color = iui_blend_color(bg_color, focus_layer); } - ctx->renderer.draw_box(widget_rect, corner, bg_color, - ctx->renderer.user); + iui_emit_box(ctx, widget_rect, corner, bg_color); } } @@ -1191,15 +1191,16 @@ static void radio_draw(iui_context *ctx, IUI_STATE_FOCUS_ALPHA); bg_color = iui_blend_color(bg_color, focus_layer); } - ctx->renderer.draw_box(widget_rect, corner, bg_color, ctx->renderer.user); + iui_emit_box(ctx, widget_rect, corner, bg_color); if (is_active) { float dot_size = widget_rect.width * 0.5f; float dot_margin = (widget_rect.width - dot_size) * 0.5f; - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {widget_rect.x + dot_margin, widget_rect.y + dot_margin, dot_size, dot_size}, - dot_size * 0.5f, ctx->colors.on_primary, ctx->renderer.user); + dot_size * 0.5f, ctx->colors.on_primary); } } @@ -1369,17 +1370,17 @@ bool iui_switch(iui_context *ctx, } /* Draw track (MD3: filled track when on, surface_variant when off) */ - ctx->renderer.draw_box((iui_rect_t) {track_rect.x, track_rect.y, - track_rect.width, switch_track_height}, - corner, track_color, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {track_rect.x, track_rect.y, track_rect.width, + switch_track_height}, + corner, track_color); /* Draw thumb (filled circle) */ float thumb_y = track_rect.y + thumb_margin; uint32_t thumb_color = *value ? ctx->colors.on_primary : ctx->colors.outline; - ctx->renderer.draw_box( - (iui_rect_t) {thumb_x, thumb_y, thumb_size, thumb_size}, - thumb_size * 0.5f, thumb_color, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {thumb_x, thumb_y, thumb_size, thumb_size}, + thumb_size * 0.5f, thumb_color); /* Optionally draw icons inside thumb (scale to fit within thumb) */ if ((*value && on_icon) || (!(*value) && off_icon)) { @@ -1522,7 +1523,7 @@ bool iui_dropdown(iui_context *ctx, const iui_dropdown_options *options) options->disabled ? iui_state_layer(ctx->colors.surface_container_high, IUI_STATE_DISABLE_ALPHA) : ctx->colors.surface_container_highest; - ctx->renderer.draw_box(field_rect, corner, bg_color, ctx->renderer.user); + iui_emit_box(ctx, field_rect, corner, bg_color); /* Draw underline indicator for filled style */ if (!is_open) { @@ -1532,21 +1533,19 @@ bool iui_dropdown(iui_context *ctx, const iui_dropdown_options *options) : ctx->colors.on_surface_variant; iui_rect_t underline = {field_rect.x, field_rect.y + field_h - 1.f, field_rect.width, 1.f}; - ctx->renderer.draw_box(underline, 0.f, line_color, ctx->renderer.user); + iui_emit_box(ctx, underline, 0.f, line_color); } else { /* Draw active indicator (2px primary underline) */ iui_rect_t underline = {field_rect.x, field_rect.y + field_h - 2.f, field_rect.width, 2.f}; - ctx->renderer.draw_box(underline, 0.f, ctx->colors.primary, - ctx->renderer.user); + iui_emit_box(ctx, underline, 0.f, ctx->colors.primary); } /* Draw state layer */ if (!options->disabled && iui_state_is_interactive(state)) { uint32_t layer_color = iui_state_layer(ctx->colors.on_surface, iui_state_get_alpha(state)); - ctx->renderer.draw_box(field_rect, corner, layer_color, - ctx->renderer.user); + iui_emit_box(ctx, field_rect, corner, layer_color); } /* Draw floating label */ @@ -1626,8 +1625,7 @@ bool iui_dropdown(iui_context *ctx, const iui_dropdown_options *options) /* Draw menu shadow and background */ iui_draw_shadow(ctx, menu_rect, corner, IUI_ELEVATION_3); - ctx->renderer.draw_box(menu_rect, corner, ctx->colors.surface_container, - ctx->renderer.user); + iui_emit_box(ctx, menu_rect, corner, ctx->colors.surface_container); /* Draw menu items */ for (int i = 0; i < options->option_count; i++) { @@ -1647,14 +1645,12 @@ bool iui_dropdown(iui_context *ctx, const iui_dropdown_options *options) /* Draw selection/hover background */ if (i == selected) { /* Selected item has subtle background */ - ctx->renderer.draw_box(item_rect, 0.f, - ctx->colors.secondary_container, - ctx->renderer.user); + iui_emit_box(ctx, item_rect, 0.f, + ctx->colors.secondary_container); } else if (iui_state_is_interactive(item_state)) { uint32_t layer_color = iui_state_layer( ctx->colors.on_surface, iui_state_get_alpha(item_state)); - ctx->renderer.draw_box(item_rect, 0.f, layer_color, - ctx->renderer.user); + iui_emit_box(ctx, item_rect, 0.f, layer_color); } /* Draw item text */ diff --git a/src/internal.h b/src/internal.h index 6c56fb6..b6d98da 100644 --- a/src/internal.h +++ b/src/internal.h @@ -63,6 +63,10 @@ #define IUI_TEXT_CACHE_DECAY_COUNT 4 /* entries to age per frame */ #endif +/* Text cache uses (size - 1) bitmask for indexing; must be power of two */ +_Static_assert((IUI_TEXT_CACHE_SIZE & (IUI_TEXT_CACHE_SIZE - 1)) == 0, + "IUI_TEXT_CACHE_SIZE must be a power of two"); + /* Per-frame field tracking constants */ #ifndef IUI_MAX_TRACKED_TEXTFIELDS #define IUI_MAX_TRACKED_TEXTFIELDS 32 /* max text fields per frame */ @@ -353,12 +357,24 @@ typedef struct { bool enabled; } iui_dirty_state; +/* Per-frame ink-bounds tracking: union bounding box of all draw calls. + * Backend can use this to blit only the drawn region instead of the full + * framebuffer. Reset each frame in iui_begin_frame(). + */ +typedef struct { + float min_x, min_y, max_x, max_y; + bool valid; /* false = no draw calls this frame */ + bool enabled; +} iui_ink_bounds_t; + /* Text width cache entry */ typedef struct { - uint32_t hash; /* 0 = empty slot */ - const char *text; /* pointer for collision detection */ - float width; /* cached width */ - uint8_t hits; /* usage counter for eviction */ + uint32_t hash; /* 0 = empty slot */ + const char *text; /* pointer for collision detection */ + float font_height; /* font size at cache time (prevents cross-size + poisoning) */ + float width; /* cached width */ + uint8_t hits; /* usage counter for eviction */ } iui_text_cache_entry; /* Text width cache state */ @@ -501,6 +517,7 @@ struct iui_context { /* PERFORMANCE SYSTEMS - Optimization Caches */ iui_dirty_state dirty; + iui_ink_bounds_t ink_bounds; iui_text_cache_state text_cache; iui_draw_batch batch; iui_field_tracking field_tracking; @@ -1148,6 +1165,79 @@ bool iui_batch_add_arc(iui_context *ctx, float width, uint32_t color); +/* Ink-bounds tracking - internal functions (draw.c) + * Tracks the union bounding box of all draw calls within a frame. + * Note: iui_ink_bounds_enable/get/valid are public in iui.h + */ +void iui_ink_bounds_init(iui_context *ctx); +void iui_ink_bounds_reset(iui_context *ctx); + +/* Internal draw emission: updates ink-bounds and routes through batch when + * enabled. All internal code should call iui_emit_box() instead of + * ctx->renderer.draw_box() directly. + * + * Branchless accumulation: reset initializes bounds to FLT_MAX/-FLT_MAX + * so the first extend unconditionally wins all comparisons. Eliminates the + * per-call 'valid' branch on Arm Cortex-M pipeline. + */ +static inline void iui_ink_bounds_extend(iui_context *ctx, + float x, + float y, + float w, + float h) +{ + if (!ctx->ink_bounds.enabled) + return; + + /* Reject NaN/Inf early: NaN silently fails all comparisons (leaving bounds + * untouched) and Inf corrupts the bounding box geometry. isfinite() is C99 + * , typically a compiler builtin on ARM. + */ + if (!isfinite(x) || !isfinite(y) || !isfinite(w) || !isfinite(h)) + return; + + float x1 = x + w, y1 = y + h; + + /* Normalize: handle negative width/height */ + if (w < 0) { + float t = x; + x = x1; + x1 = t; + } + if (h < 0) { + float t = y; + y = y1; + y1 = t; + } + + if (x < ctx->ink_bounds.min_x) + ctx->ink_bounds.min_x = x; + if (y < ctx->ink_bounds.min_y) + ctx->ink_bounds.min_y = y; + if (x1 > ctx->ink_bounds.max_x) + ctx->ink_bounds.max_x = x1; + if (y1 > ctx->ink_bounds.max_y) + ctx->ink_bounds.max_y = y1; + ctx->ink_bounds.valid = true; +} + +/* Universal draw emission: updates ink-bounds then routes to batch or + * direct renderer. All internal code should call iui_emit_box() instead + * of ctx->renderer.draw_box() directly. + */ +static inline void iui_emit_box(iui_context *ctx, + iui_rect_t rect, + float radius, + uint32_t color) +{ + iui_ink_bounds_extend(ctx, rect.x, rect.y, rect.width, rect.height); + if (ctx->batch.enabled) + iui_batch_add_rect(ctx, rect.x, rect.y, rect.width, rect.height, radius, + color); + else + ctx->renderer.draw_box(rect, radius, color, ctx->renderer.user); +} + /* Dirty rectangle tracking - internal functions (layout.c) * Note: iui_dirty_enable/mark/invalidate_all/check/count are public in iui.h */ diff --git a/src/layout.c b/src/layout.c index 643d534..1e7ca75 100644 --- a/src/layout.c +++ b/src/layout.c @@ -511,6 +511,9 @@ void iui_begin_frame(iui_context *ctx, float delta_time) /* Reset batch command buffer for new frame */ ctx->batch.count = 0; + + /* Reset ink-bounds for new frame */ + iui_ink_bounds_reset(ctx); } bool iui_begin_window(iui_context *ctx, @@ -619,13 +622,12 @@ bool iui_begin_window(iui_context *ctx, /* MD3 Card/Dialog: unified surface_container_high background with outline */ - ctx->renderer.draw_box( - (iui_rect_t) {w->pos.x, w->pos.y, w->width, w->height}, ctx->corner, - ctx->colors.outline_variant, ctx->renderer.user); - ctx->renderer.draw_box((iui_rect_t) {w->pos.x + 1.f, w->pos.y + 1.f, - w->width - 2.f, w->height - 2.f}, - ctx->corner, ctx->colors.surface_container_high, - ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {w->pos.x, w->pos.y, w->width, w->height}, + ctx->corner, ctx->colors.outline_variant); + iui_emit_box(ctx, + (iui_rect_t) {w->pos.x + 1.f, w->pos.y + 1.f, w->width - 2.f, + w->height - 2.f}, + ctx->corner, ctx->colors.surface_container_high); ctx->layout = (iui_rect_t) { .x = w->pos.x + ctx->padding, @@ -643,8 +645,7 @@ bool iui_begin_window(iui_context *ctx, /* draw resize handle */ if (w->options & IUI_WINDOW_RESIZABLE) - ctx->renderer.draw_box(handle_rect, 0.f, ctx->colors.outline_variant, - ctx->renderer.user); + iui_emit_box(ctx, handle_rect, 0.f, ctx->colors.outline_variant); /* Push window content clip so nested clips (scroll, banners) intersect * with window bounds instead of escaping when depth == 0. */ diff --git a/src/list.c b/src/list.c index cc59c88..1875f4d 100644 --- a/src/list.c +++ b/src/list.c @@ -113,10 +113,11 @@ static void draw_leading_element(iui_context *ctx, case IUI_LIST_LEADING_IMAGE: /* Draw square image placeholder */ - ctx->renderer.draw_box( + iui_emit_box( + ctx, (iui_rect_t) {x, cy - IUI_LIST_ONE_LINE_HEIGHT * 0.5f, IUI_LIST_ONE_LINE_HEIGHT, IUI_LIST_ONE_LINE_HEIGHT}, - 4.f, ctx->colors.surface_container_high, ctx->renderer.user); + 4.f, ctx->colors.surface_container_high); break; default: @@ -197,8 +198,7 @@ static void draw_trailing_element(iui_context *ctx, uint32_t track_color = *item->checkbox_value ? ctx->colors.primary : ctx->colors.surface_container_highest; - ctx->renderer.draw_box(switch_rect, switch_h * 0.5f, track_color, - ctx->renderer.user); + iui_emit_box(ctx, switch_rect, switch_h * 0.5f, track_color); /* Draw thumb */ float thumb_x = *item->checkbox_value @@ -360,9 +360,8 @@ bool iui_list_item_ex(iui_context *ctx, float divider_x = text_x; float divider_y = item_y + item_height - 1.f; float divider_w = item_width - (text_x - item_x) - IUI_LIST_PADDING_H; - ctx->renderer.draw_box( - (iui_rect_t) {divider_x, divider_y, divider_w, 1.f}, 0.f, - ctx->colors.outline_variant, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {divider_x, divider_y, divider_w, 1.f}, + 0.f, ctx->colors.outline_variant); } /* Advance layout */ @@ -425,9 +424,8 @@ void iui_list_divider(iui_context *ctx) float divider_y = ctx->layout.y; float divider_w = ctx->layout.width - IUI_LIST_DIVIDER_INSET; - ctx->renderer.draw_box((iui_rect_t) {divider_x, divider_y, divider_w, 1.f}, - 0.f, ctx->colors.outline_variant, - ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {divider_x, divider_y, divider_w, 1.f}, 0.f, + ctx->colors.outline_variant); /* Add small vertical spacing */ ctx->layout.y += 1.f; diff --git a/src/menu.c b/src/menu.c index 97d81b4..18be9db 100644 --- a/src/menu.c +++ b/src/menu.c @@ -82,8 +82,7 @@ bool iui_menu_begin(iui_context *ctx, iui_draw_shadow(ctx, bg_rect, corner, IUI_ELEVATION_3); /* Draw menu background */ - ctx->renderer.draw_box(bg_rect, corner, ctx->colors.surface_container, - ctx->renderer.user); + iui_emit_box(ctx, bg_rect, corner, ctx->colors.surface_container); /* Track component for MD3 validation */ IUI_MD3_TRACK_MENU(bg_rect, corner); @@ -118,10 +117,10 @@ bool iui_menu_add_item(iui_context *ctx, if (item->is_divider) { float div_y = menu->y + menu->height, div_h = IUI_MENU_DIVIDER_HEIGHT; float line_y = div_y + div_h * 0.5f; - ctx->renderer.draw_box( - (iui_rect_t) {menu->x + IUI_MENU_PADDING_H, line_y - 0.5f, - menu->width - IUI_MENU_PADDING_H * 2.f, 1.f}, - 0.f, ctx->colors.outline_variant, ctx->renderer.user); + iui_emit_box(ctx, + (iui_rect_t) {menu->x + IUI_MENU_PADDING_H, line_y - 0.5f, + menu->width - IUI_MENU_PADDING_H * 2.f, 1.f}, + 0.f, ctx->colors.outline_variant); menu->height += div_h; return false; /* Dividers are not clickable */ diff --git a/src/navigation.c b/src/navigation.c index c7b2dac..d08429b 100644 --- a/src/navigation.c +++ b/src/navigation.c @@ -27,8 +27,7 @@ void iui_nav_rail_begin(iui_context *ctx, /* Draw rail background */ iui_rect_t rail_rect = {x, y, width, height}; - ctx->renderer.draw_box(rail_rect, 0.f, ctx->colors.surface, - ctx->renderer.user); + iui_emit_box(ctx, rail_rect, 0.f, ctx->colors.surface); /* Track component for MD3 validation */ IUI_MD3_TRACK_NAV_RAIL(rail_rect, 0.f); @@ -55,8 +54,7 @@ bool iui_nav_rail_fab(iui_context *ctx, /* Draw FAB background */ uint32_t fab_bg = ctx->colors.primary_container; - ctx->renderer.draw_box(fab_rect, IUI_FAB_CORNER_RADIUS, fab_bg, - ctx->renderer.user); + iui_emit_box(ctx, fab_rect, IUI_FAB_CORNER_RADIUS, fab_bg); /* Draw state layer for hover/press */ iui_draw_state_layer(ctx, fab_rect, IUI_FAB_CORNER_RADIUS, @@ -118,18 +116,16 @@ bool iui_nav_rail_item(iui_context *ctx, float indicator_w = IUI_NAV_RAIL_WIDTH + text_width; iui_rect_t indicator_rect = {indicator_x, indicator_y, indicator_w, IUI_NAV_RAIL_INDICATOR_HEIGHT}; - ctx->renderer.draw_box(indicator_rect, IUI_NAV_RAIL_CORNER_RADIUS, - ctx->colors.secondary_container, - ctx->renderer.user); + iui_emit_box(ctx, indicator_rect, IUI_NAV_RAIL_CORNER_RADIUS, + ctx->colors.secondary_container); IUI_MD3_TRACK_NAV_RAIL_INDICATOR(indicator_rect, IUI_NAV_RAIL_CORNER_RADIUS); } else { iui_rect_t indicator_rect = {indicator_x, indicator_y, IUI_NAV_RAIL_INDICATOR_WIDTH, IUI_NAV_RAIL_INDICATOR_HEIGHT}; - ctx->renderer.draw_box(indicator_rect, IUI_NAV_RAIL_CORNER_RADIUS, - ctx->colors.secondary_container, - ctx->renderer.user); + iui_emit_box(ctx, indicator_rect, IUI_NAV_RAIL_CORNER_RADIUS, + ctx->colors.secondary_container); IUI_MD3_TRACK_NAV_RAIL_INDICATOR(indicator_rect, IUI_NAV_RAIL_CORNER_RADIUS); } @@ -144,8 +140,7 @@ bool iui_nav_rail_item(iui_context *ctx, iui_rect_t hover_rect = {indicator_x, indicator_y, IUI_NAV_RAIL_INDICATOR_WIDTH, IUI_NAV_RAIL_INDICATOR_HEIGHT}; - ctx->renderer.draw_box(hover_rect, IUI_NAV_RAIL_CORNER_RADIUS, - layer_color, ctx->renderer.user); + iui_emit_box(ctx, hover_rect, IUI_NAV_RAIL_CORNER_RADIUS, layer_color); } /* Draw icon — centered inside the indicator */ @@ -222,8 +217,7 @@ void iui_nav_bar_begin(iui_context *ctx, /* Draw bar background */ iui_rect_t bar_rect = {x, y, width, IUI_NAV_BAR_HEIGHT}; - ctx->renderer.draw_box(bar_rect, 0.f, ctx->colors.surface_container, - ctx->renderer.user); + iui_emit_box(ctx, bar_rect, 0.f, ctx->colors.surface_container); /* Track component for MD3 validation */ IUI_MD3_TRACK_NAV_BAR(bar_rect, 0.f); @@ -266,9 +260,8 @@ bool iui_nav_bar_item(iui_context *ctx, iui_rect_t indicator_rect = {indicator_x, indicator_y, IUI_NAV_BAR_INDICATOR_WIDTH, IUI_NAV_BAR_INDICATOR_HEIGHT}; - ctx->renderer.draw_box( - indicator_rect, IUI_NAV_BAR_INDICATOR_HEIGHT * 0.5f, - ctx->colors.secondary_container, ctx->renderer.user); + iui_emit_box(ctx, indicator_rect, IUI_NAV_BAR_INDICATOR_HEIGHT * 0.5f, + ctx->colors.secondary_container); } /* Draw state layer on hover/press */ @@ -280,8 +273,8 @@ bool iui_nav_bar_item(iui_context *ctx, iui_rect_t hover_rect = {indicator_x, indicator_y, IUI_NAV_BAR_INDICATOR_WIDTH, IUI_NAV_BAR_INDICATOR_HEIGHT}; - ctx->renderer.draw_box(hover_rect, IUI_NAV_BAR_INDICATOR_HEIGHT * 0.5f, - layer_color, ctx->renderer.user); + iui_emit_box(ctx, hover_rect, IUI_NAV_BAR_INDICATOR_HEIGHT * 0.5f, + layer_color); } /* Draw icon */ @@ -372,10 +365,8 @@ bool iui_nav_drawer_begin(iui_context *ctx, iui_rect_t screen_rect = {0, 0, 10000.f, height}; uint8_t scrim_alpha = (uint8_t) (IUI_SCRIM_ALPHA * state->anim_progress); - ctx->renderer.draw_box( - screen_rect, 0.f, - (scrim_alpha << 24) | (ctx->colors.scrim & 0x00FFFFFF), - ctx->renderer.user); + iui_emit_box(ctx, screen_rect, 0.f, + (scrim_alpha << 24) | (ctx->colors.scrim & 0x00FFFFFF)); /* Push modal layer for input blocking */ iui_push_layer(ctx, 100); @@ -389,8 +380,7 @@ bool iui_nav_drawer_begin(iui_context *ctx, /* Draw drawer background */ iui_rect_t drawer_rect = {animated_x, y, drawer_width, height}; - ctx->renderer.draw_box(drawer_rect, 0.f, ctx->colors.surface, - ctx->renderer.user); + iui_emit_box(ctx, drawer_rect, 0.f, ctx->colors.surface); /* Track component for MD3 validation */ IUI_MD3_TRACK_NAV_DRAWER(drawer_rect, 0.f); @@ -429,13 +419,11 @@ bool iui_nav_drawer_item(iui_context *ctx, /* Draw selection or hover background */ if (selected) { - ctx->renderer.draw_box(item_rect, 28.f, ctx->colors.secondary_container, - ctx->renderer.user); + iui_emit_box(ctx, item_rect, 28.f, ctx->colors.secondary_container); } else if (iui_state_is_interactive(comp_state)) { uint32_t layer_color = iui_state_layer(ctx->colors.on_surface, iui_state_get_alpha(comp_state)); - ctx->renderer.draw_box(item_rect, 28.f, layer_color, - ctx->renderer.user); + iui_emit_box(ctx, item_rect, 28.f, layer_color); } /* Draw icon */ @@ -480,8 +468,8 @@ void iui_nav_drawer_divider(iui_context *ctx) ctx->layout.y += 8.f; float x = ctx->layout.x + IUI_NAV_DRAWER_PADDING_H; float w = IUI_NAV_DRAWER_WIDTH - 2 * IUI_NAV_DRAWER_PADDING_H; - ctx->renderer.draw_box((iui_rect_t) {x, ctx->layout.y, w, 1.f}, 0.f, - ctx->colors.outline_variant, ctx->renderer.user); + iui_emit_box(ctx, (iui_rect_t) {x, ctx->layout.y, w, 1.f}, 0.f, + ctx->colors.outline_variant); ctx->layout.y += 1.f + 8.f; } @@ -518,8 +506,7 @@ void iui_bottom_app_bar_begin(iui_context *ctx, /* Draw bar background */ iui_rect_t bar_rect = {x, y, width, IUI_BOTTOM_APP_BAR_HEIGHT}; - ctx->renderer.draw_box(bar_rect, 0.f, ctx->colors.surface_container, - ctx->renderer.user); + iui_emit_box(ctx, bar_rect, 0.f, ctx->colors.surface_container); /* Track component for MD3 validation */ IUI_MD3_TRACK_BOTTOM_APP_BAR(bar_rect, 0.f); @@ -600,8 +587,7 @@ bool iui_bottom_app_bar_fab(iui_context *ctx, iui_state_t comp_state = iui_get_component_state(ctx, fab_rect, false); /* Draw FAB background */ - ctx->renderer.draw_box(fab_rect, corner_radius, - ctx->colors.primary_container, ctx->renderer.user); + iui_emit_box(ctx, fab_rect, corner_radius, ctx->colors.primary_container); /* Draw state layer for hover/press */ iui_draw_state_layer(ctx, fab_rect, corner_radius, @@ -702,8 +688,7 @@ bool iui_side_sheet_begin(iui_context *ctx, (uint8_t) (IUI_SCRIM_ALPHA * state->anim_progress); uint32_t scrim_color = (scrim_alpha << 24) | (ctx->colors.scrim & 0x00FFFFFF); - ctx->renderer.draw_box(screen_rect, 0.f, scrim_color, - ctx->renderer.user); + iui_emit_box(ctx, screen_rect, 0.f, scrim_color); /* Close on scrim click (protect against opening click release using * frames_since_open, similar to iui_modal_should_close) */ @@ -725,8 +710,7 @@ bool iui_side_sheet_begin(iui_context *ctx, /* Draw left border for standard sheets (outline variant) */ if (!state->modal) { iui_rect_t border_rect = {animated_x - 1.f, 0, 1.f, sheet_height}; - ctx->renderer.draw_box(border_rect, 0.f, ctx->colors.outline_variant, - ctx->renderer.user); + iui_emit_box(ctx, border_rect, 0.f, ctx->colors.outline_variant); } iui_draw_elevated_box(ctx, sheet_rect, 0.f, elevation, diff --git a/src/pickers.c b/src/pickers.c index 8756d3b..9d6a715 100644 --- a/src/pickers.c +++ b/src/pickers.c @@ -145,17 +145,14 @@ bool iui_date_picker(iui_context *ctx, /* Draw scrim */ iui_rect_t scrim_rect = {0, 0, screen_width, screen_height}; - ctx->renderer.draw_box(scrim_rect, 0, ctx->colors.scrim, - ctx->renderer.user); + iui_emit_box(ctx, scrim_rect, 0, ctx->colors.scrim); /* Draw dialog shadow */ float corner = IUI_DIALOG_CORNER_RADIUS; iui_draw_shadow(ctx, dialog_rect, corner, IUI_ELEVATION_3); /* Draw dialog background */ - ctx->renderer.draw_box(dialog_rect, corner, - ctx->colors.surface_container_high, - ctx->renderer.user); + iui_emit_box(ctx, dialog_rect, corner, ctx->colors.surface_container_high); /* Navigation: Month Year with arrows (MD3 uses nav_h for this row) */ float nav_y = dialog_y + padding; @@ -272,8 +269,7 @@ bool iui_date_picker(iui_context *ctx, /* Draw selection background OR state layer (mutually exclusive) */ if (is_selected) { /* Selected day: filled primary rounded rect */ - ctx->renderer.draw_box(vis_rect, day_corner, - ctx->colors.primary, ctx->renderer.user); + iui_emit_box(ctx, vis_rect, day_corner, ctx->colors.primary); } else if (cell_state == IUI_STATE_HOVERED || cell_state == IUI_STATE_PRESSED) { /* State layer covers full touch target for proper feedback */ @@ -282,10 +278,8 @@ bool iui_date_picker(iui_context *ctx, : IUI_STATE_HOVER_ALPHA; /* circular touch feedback */ float touch_corner = cell_w * 0.5f; - ctx->renderer.draw_box( - touch_rect, touch_corner, - iui_state_layer(ctx->colors.on_surface, alpha), - ctx->renderer.user); + iui_emit_box(ctx, touch_rect, touch_corner, + iui_state_layer(ctx->colors.on_surface, alpha)); } /* Draw day number centered in visual rect */ @@ -344,8 +338,7 @@ bool iui_date_picker(iui_context *ctx, /* OK button (filled style) */ iui_rect_t ok_rect = {ok_x, btn_y, ok_w, button_h}; iui_state_t ok_state = iui_get_component_state(ctx, ok_rect, false); - ctx->renderer.draw_box(ok_rect, button_h * 0.5f, ctx->colors.primary, - ctx->renderer.user); + iui_emit_box(ctx, ok_rect, button_h * 0.5f, ctx->colors.primary); iui_draw_state_layer(ctx, ok_rect, button_h * 0.5f, ctx->colors.on_primary, ok_state); float ok_text_x = @@ -489,17 +482,14 @@ bool iui_time_picker(iui_context *ctx, /* Draw scrim */ iui_rect_t scrim_rect = {0, 0, screen_width, screen_height}; - ctx->renderer.draw_box(scrim_rect, 0, ctx->colors.scrim, - ctx->renderer.user); + iui_emit_box(ctx, scrim_rect, 0, ctx->colors.scrim); /* Draw dialog shadow */ float corner = IUI_SHAPE_EXTRA_LARGE; iui_draw_shadow(ctx, dialog_rect, corner, IUI_ELEVATION_3); /* Draw dialog background */ - ctx->renderer.draw_box(dialog_rect, corner, - ctx->colors.surface_container_high, - ctx->renderer.user); + iui_emit_box(ctx, dialog_rect, corner, ctx->colors.surface_container_high); /* Header: Time display (HH:MM) */ float header_y = dialog_y + padding; @@ -534,7 +524,7 @@ bool iui_time_picker(iui_context *ctx, : ctx->colors.surface_container_highest; uint32_t hour_text = hour_active ? ctx->colors.on_primary_container : ctx->colors.on_surface; - ctx->renderer.draw_box(hour_rect, 8.f, hour_bg, ctx->renderer.user); + iui_emit_box(ctx, hour_rect, 8.f, hour_bg); iui_draw_state_layer(ctx, hour_rect, 8.f, hour_text, hour_state); float hour_text_w = iui_get_text_width(ctx, hour_str); float hour_text_x = hour_rect.x + (hour_rect.width - hour_text_w) * 0.5f; @@ -557,15 +547,13 @@ bool iui_time_picker(iui_context *ctx, : ctx->colors.surface_container_highest; uint32_t min_text = minute_active ? ctx->colors.on_primary_container : ctx->colors.on_surface; - ctx->renderer.draw_box(minute_rect, 8.f, min_bg, ctx->renderer.user); + iui_emit_box(ctx, minute_rect, 8.f, min_bg); if (minute_state == IUI_STATE_HOVERED || minute_state == IUI_STATE_PRESSED) { uint8_t alpha = (minute_state == IUI_STATE_PRESSED) ? IUI_STATE_PRESS_ALPHA : IUI_STATE_HOVER_ALPHA; - ctx->renderer.draw_box(minute_rect, 8.f, - iui_state_layer(min_text, alpha), - ctx->renderer.user); + iui_emit_box(ctx, minute_rect, 8.f, iui_state_layer(min_text, alpha)); } float min_text_w = iui_get_text_width(ctx, minute_str); float min_text_x = minute_rect.x + (minute_rect.width - min_text_w) * 0.5f; @@ -600,7 +588,7 @@ bool iui_time_picker(iui_context *ctx, : ctx->colors.surface_container_highest; uint32_t am_text_color = am_selected ? ctx->colors.on_tertiary_container : ctx->colors.on_surface_variant; - ctx->renderer.draw_box(am_rect, 8.f, am_bg, ctx->renderer.user); + iui_emit_box(ctx, am_rect, 8.f, am_bg); iui_draw_state_layer(ctx, am_rect, 8.f, am_text_color, am_state); float am_w = iui_get_text_width(ctx, "AM"); iui_internal_draw_text( @@ -618,7 +606,7 @@ bool iui_time_picker(iui_context *ctx, : ctx->colors.surface_container_highest; uint32_t pm_text_color = pm_selected ? ctx->colors.on_tertiary_container : ctx->colors.on_surface_variant; - ctx->renderer.draw_box(pm_rect, 8.f, pm_bg, ctx->renderer.user); + iui_emit_box(ctx, pm_rect, 8.f, pm_bg); iui_draw_state_layer(ctx, pm_rect, 8.f, pm_text_color, pm_state); float pm_w = iui_get_text_width(ctx, "PM"); iui_internal_draw_text( @@ -644,16 +632,14 @@ bool iui_time_picker(iui_context *ctx, /* Draw dial background */ iui_rect_t dial_bg_rect = {dial_x, dial_y, dial_size, dial_size}; - ctx->renderer.draw_box(dial_bg_rect, dial_r, - ctx->colors.surface_container_highest, - ctx->renderer.user); + iui_emit_box(ctx, dial_bg_rect, dial_r, + ctx->colors.surface_container_highest); /* Draw center dot */ iui_rect_t center_dot_rect = {dial_cx - center_dot * 0.5f, dial_cy - center_dot * 0.5f, center_dot, center_dot}; - ctx->renderer.draw_box(center_dot_rect, center_dot * 0.5f, - ctx->colors.primary, ctx->renderer.user); + iui_emit_box(ctx, center_dot_rect, center_dot * 0.5f, ctx->colors.primary); /* Calculate number positions and draw */ int num_count = 12; /* 12 numbers for both hour and minute */ @@ -710,17 +696,15 @@ bool iui_time_picker(iui_context *ctx, /* Draw selection circle */ if (is_selected) { - ctx->renderer.draw_box(num_rect, selector_size * 0.5f, - ctx->colors.primary, ctx->renderer.user); + iui_emit_box(ctx, num_rect, selector_size * 0.5f, + ctx->colors.primary); } else if (num_state == IUI_STATE_HOVERED || num_state == IUI_STATE_PRESSED) { uint8_t alpha = (num_state == IUI_STATE_PRESSED) ? IUI_STATE_PRESS_ALPHA : IUI_STATE_HOVER_ALPHA; - ctx->renderer.draw_box( - num_rect, selector_size * 0.5f, - iui_state_layer(ctx->colors.on_surface, alpha), - ctx->renderer.user); + iui_emit_box(ctx, num_rect, selector_size * 0.5f, + iui_state_layer(ctx->colors.on_surface, alpha)); } /* Draw number text */ @@ -743,6 +727,9 @@ bool iui_time_picker(iui_context *ctx, iui_polar_to_cart(dial_cx, dial_cy, num_radius - selector_size * 0.3f, sel_angle, &sel_x, &sel_y); + iui_ink_bounds_extend( + ctx, fminf(dial_cx, sel_x) - 1.f, fminf(dial_cy, sel_y) - 1.f, + fabsf(sel_x - dial_cx) + 2.f, fabsf(sel_y - dial_cy) + 2.f); ctx->renderer.draw_line(dial_cx, dial_cy, sel_x, sel_y, 2.f, ctx->colors.primary, ctx->renderer.user); } @@ -788,8 +775,7 @@ bool iui_time_picker(iui_context *ctx, /* OK button */ iui_rect_t ok_rect = {ok_x, btn_y, ok_w, button_h}; iui_state_t ok_state = iui_get_component_state(ctx, ok_rect, false); - ctx->renderer.draw_box(ok_rect, button_h * 0.5f, ctx->colors.primary, - ctx->renderer.user); + iui_emit_box(ctx, ok_rect, button_h * 0.5f, ctx->colors.primary); iui_draw_state_layer(ctx, ok_rect, button_h * 0.5f, ctx->colors.on_primary, ok_state); float ok_text_x = diff --git a/src/searchbar.c b/src/searchbar.c index 46c9a48..5271cc3 100644 --- a/src/searchbar.c +++ b/src/searchbar.c @@ -120,9 +120,8 @@ iui_search_bar_result iui_search_bar_ex(iui_context *ctx, /* Draw container background (surface_container_high with full round * corners) */ - ctx->renderer.draw_box(bar_rect, corner_radius, - ctx->colors.surface_container_high, - ctx->renderer.user); + iui_emit_box(ctx, bar_rect, corner_radius, + ctx->colors.surface_container_high); /* Draw state layer for hover/press */ iui_draw_state_layer(ctx, bar_rect, corner_radius, ctx->colors.on_surface, @@ -174,8 +173,7 @@ iui_search_bar_result iui_search_bar_ex(iui_context *ctx, iui_rect_t cursor_rect = { cursor_x, cursor_y, IUI_TEXTFIELD_CURSOR_WIDTH, cursor_height}; - ctx->renderer.draw_box(cursor_rect, 0.f, ctx->colors.primary, - ctx->renderer.user); + iui_emit_box(ctx, cursor_rect, 0.f, ctx->colors.primary); } } @@ -197,8 +195,7 @@ iui_search_bar_result iui_search_bar_ex(iui_context *ctx, iui_rect_t trail_layer_rect = {trailing_icon_x - icon_size * 0.5f, icon_cy - icon_size * 0.5f, icon_size, icon_size}; - ctx->renderer.draw_box(trail_layer_rect, icon_size * 0.5f, - layer_color, ctx->renderer.user); + iui_emit_box(ctx, trail_layer_rect, icon_size * 0.5f, layer_color); } iui_draw_fab_icon(ctx, trailing_icon_x, icon_cy, icon_size, trail_icon, @@ -300,8 +297,7 @@ bool iui_search_view_begin(iui_context *ctx, iui_register_blocking_region(ctx, screen_bounds); /* Draw full-screen surface background */ - ctx->renderer.draw_box(screen_bounds, 0.f, ctx->colors.surface, - ctx->renderer.user); + iui_emit_box(ctx, screen_bounds, 0.f, ctx->colors.surface); /* Header dimensions */ float header_h = IUI_SEARCH_VIEW_HEADER_HEIGHT; @@ -346,9 +342,7 @@ bool iui_search_view_begin(iui_context *ctx, /* Draw search field background */ float corner = IUI_SEARCH_BAR_CORNER_RADIUS; - ctx->renderer.draw_box(field_rect, corner, - ctx->colors.surface_container_high, - ctx->renderer.user); + iui_emit_box(ctx, field_rect, corner, ctx->colors.surface_container_high); /* Auto-focus the search field and register for tracking */ ctx->focused_edit = search->query; @@ -390,8 +384,7 @@ bool iui_search_view_begin(iui_context *ctx, float cursor_x = text_x + iui_get_text_width(ctx, temp); iui_rect_t cursor_rect = {cursor_x, text_y, IUI_TEXTFIELD_CURSOR_WIDTH, ctx->font_height}; - ctx->renderer.draw_box(cursor_rect, 0.f, ctx->colors.primary, - ctx->renderer.user); + iui_emit_box(ctx, cursor_rect, 0.f, ctx->colors.primary); } /* Draw clear button if text present */ diff --git a/src/tabs.c b/src/tabs.c index 575f039..f8754a6 100644 --- a/src/tabs.c +++ b/src/tabs.c @@ -43,8 +43,7 @@ static int iui_tabs_internal(iui_context *ctx, /* Draw container background (surface color) */ iui_rect_t container_rect = {tabs_x, tabs_y, container_width, tab_height}; - ctx->renderer.draw_box(container_rect, 0.f, ctx->colors.surface, - ctx->renderer.user); + iui_emit_box(ctx, container_rect, 0.f, ctx->colors.surface); /* Track component for MD3 validation */ IUI_MD3_TRACK_TAB(container_rect, 0.f); @@ -92,8 +91,7 @@ static int iui_tabs_internal(iui_context *ctx, if (!is_selected && iui_state_is_interactive(state)) { uint32_t layer_color = iui_state_layer(ctx->colors.on_surface, iui_state_get_alpha(state)); - ctx->renderer.draw_box(tab_rect, 0.f, layer_color, - ctx->renderer.user); + iui_emit_box(ctx, tab_rect, 0.f, layer_color); } /* Determine text/icon colors based on selection state */ @@ -155,8 +153,8 @@ static int iui_tabs_internal(iui_context *ctx, /* Primary tabs: full-width indicator with rounded corners */ iui_rect_t indicator_rect = {indicator_x, indicator_y, indicator_width, indicator_height}; - ctx->renderer.draw_box(indicator_rect, indicator_height * 0.5f, - ctx->colors.primary, ctx->renderer.user); + iui_emit_box(ctx, indicator_rect, indicator_height * 0.5f, + ctx->colors.primary); } else { /* Secondary tabs: shorter indicator centered under tab */ float short_indicator_width = indicator_width * 0.6f; @@ -165,15 +163,14 @@ static int iui_tabs_internal(iui_context *ctx, iui_rect_t short_indicator_rect = {short_indicator_x, indicator_y, short_indicator_width, indicator_height}; - ctx->renderer.draw_box(short_indicator_rect, indicator_height * 0.5f, - ctx->colors.primary, ctx->renderer.user); + iui_emit_box(ctx, short_indicator_rect, indicator_height * 0.5f, + ctx->colors.primary); } /* Draw bottom divider line (outline_variant color) */ iui_rect_t divider_rect = {tabs_x, tabs_y + tab_height - 1.f, container_width, 1.f}; - ctx->renderer.draw_box(divider_rect, 0.f, ctx->colors.outline_variant, - ctx->renderer.user); + iui_emit_box(ctx, divider_rect, 0.f, ctx->colors.outline_variant); /* Advance layout cursor */ ctx->layout.y += tab_height + ctx->padding; diff --git a/tests/bench-render.c b/tests/bench-render.c new file mode 100644 index 0000000..ce8caf7 --- /dev/null +++ b/tests/bench-render.c @@ -0,0 +1,552 @@ +/* + * Rendering Benchmark + * + * Profiles the rendering pipeline under realistic application workloads. + * Uses a simulated backend with per-call costs that model real hardware: + * + * draw_box : pixel fill proportional to area + * draw_text : glyph rasterization (area per character) + * text_width : font shaping with glyph-advance + kerning table lookups + * set_clip : GPU scissor state change + * + * Scenes mirror real application screens (480x800 mobile form factor): + * Settings : scrollable list with toggles, radios, section headers + * Dashboard: metric cards, chip filters, progress bars, action buttons + * Form : sliders, checkboxes, buttons with varied label strings + */ + +#include "common.h" + +#include + +/* high-resolution timer */ +static uint64_t now_ns(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t) ts.tv_sec * 1000000000ULL + (uint64_t) ts.tv_nsec; +} + +/* benchmark parameters */ +#define SCREEN_W 480 +#define SCREEN_H 800 +#define FRAMES 5000 +#define WARMUP 300 +#define RUNS 3 + +/* simulated backend */ +static volatile uint64_t g_pixels; +static volatile int g_draws; +static volatile int g_clips; +static volatile int g_tmeas; + +static void sim_draw_box(iui_rect_t r, float radius, uint32_t color, void *user) +{ + (void) radius; + (void) color; + (void) user; + uint64_t area = (uint64_t) (r.width > 0 ? r.width : 0) * + (uint64_t) (r.height > 0 ? r.height : 0); + g_pixels += area; + g_draws++; +} + +static void sim_set_clip(uint16_t x0, + uint16_t y0, + uint16_t x1, + uint16_t y1, + void *user) +{ + (void) x0; + (void) y0; + (void) x1; + (void) y1; + (void) user; + g_clips++; +} + +static void sim_draw_text(float x, + float y, + const char *text, + uint32_t color, + void *user) +{ + (void) x; + (void) y; + (void) color; + (void) user; + g_pixels += (uint64_t) strlen(text) * 80; + g_draws++; +} + +/* Font shaping simulation. + * + * Models per-glyph work that a real font backend performs: + * glyph-advance table lookup + kerning pair probe. The volatile tables prevent + * the compiler from constant-folding the result, making each call cost ~2 + * cache-line touches per character. + */ +static volatile float g_glyph_advances[256]; +static volatile uint8_t g_kern_table[256]; + +static void init_font_tables(void) +{ + for (int i = 0; i < 256; i++) { + g_glyph_advances[i] = 6.f + (float) (i % 5); + g_kern_table[i] = (uint8_t) (i * 31 + 17); + } +} + +static float sim_text_width(const char *text, void *user) +{ + (void) user; + g_tmeas++; + float w = 0.f; + uint8_t prev = 0; + for (const unsigned char *p = (const unsigned char *) text; *p; p++) { + w += g_glyph_advances[*p]; + uint8_t pair = (uint8_t) (prev ^ *p); + w += (float) g_kern_table[pair] * 0.01f; + prev = *p; + } + return w; +} + +/* scenes */ + +typedef void (*scene_fn)(iui_context *); + +/* Scene A: Settings - heavy on text labels and toggles */ +static void scene_settings(iui_context *ctx) +{ + static bool toggles[16]; + static int radio_val = 1; + + iui_begin_frame(ctx, 0.016f); + iui_begin_window(ctx, "settings", 0, 0, SCREEN_W, SCREEN_H, 0); + + iui_text(ctx, IUI_ALIGN_LEFT, "General Settings"); + iui_divider(ctx); + for (int i = 0; i < 8; i++) { + char label[48]; + snprintf(label, sizeof(label), "Enable feature %d (recommended)", + i + 1); + iui_checkbox(ctx, label, &toggles[i]); + } + + iui_text(ctx, IUI_ALIGN_LEFT, "Display"); + iui_divider(ctx); + for (int i = 8; i < 16; i++) { + char label[48]; + snprintf(label, sizeof(label), "Display option %d", i - 7); + iui_checkbox(ctx, label, &toggles[i]); + } + + iui_text(ctx, IUI_ALIGN_LEFT, "Theme"); + iui_divider(ctx); + static const char *themes[] = {"Light", "Dark", "System default", + "High contrast"}; + for (int i = 0; i < 4; i++) + iui_radio(ctx, themes[i], &radio_val, i); + + iui_text(ctx, IUI_ALIGN_LEFT, "About this application"); + iui_divider(ctx); + iui_text(ctx, IUI_ALIGN_LEFT, "Version 1.4.2 (build 2891)"); + iui_text(ctx, IUI_ALIGN_LEFT, + "Copyright 2024-2026 Example Corporation. All rights reserved."); + + iui_button(ctx, "Check for updates", IUI_ALIGN_CENTER); + iui_button(ctx, "Reset all settings", IUI_ALIGN_CENTER); + + iui_end_window(ctx); + iui_end_frame(ctx); +} + +/* Scene B: Dashboard - cards, chips, progress, mixed widgets */ +static void scene_dashboard(iui_context *ctx) +{ + static bool filters[6] = {true, false, true, false, true, false}; + + iui_begin_frame(ctx, 0.016f); + iui_begin_window(ctx, "dash", 0, 0, SCREEN_W, SCREEN_H, 0); + + /* Filter chip bar */ + iui_chip_filter(ctx, "All items", &filters[0]); + iui_chip_filter(ctx, "Active", &filters[1]); + iui_chip_filter(ctx, "Pending review", &filters[2]); + iui_chip_filter(ctx, "Archived", &filters[3]); + iui_chip_filter(ctx, "Starred", &filters[4]); + iui_chip_filter(ctx, "Flagged", &filters[5]); + + /* Metric cards */ + for (int i = 0; i < 6; i++) { + float cx = (float) (i % 2) * (SCREEN_W / 2); + float cy = 80.f + (float) (i / 2) * 150.f; + iui_card_begin(ctx, cx, cy, SCREEN_W / 2 - 8, 140, IUI_CARD_ELEVATED); + + char title[32]; + snprintf(title, sizeof(title), "Metric %c", 'A' + i); + iui_text(ctx, IUI_ALIGN_LEFT, title); + + float pct = 0.15f * (float) (i + 1); + iui_progress_linear(ctx, pct, 1.f, false); + + char detail[64]; + snprintf(detail, sizeof(detail), "%.0f%% complete - %d of %d items", + pct * 100, (int) (pct * 120), 120); + iui_text(ctx, IUI_ALIGN_LEFT, detail); + + iui_card_end(ctx); + } + + /* Action row */ + iui_button(ctx, "Export Report", IUI_ALIGN_CENTER); + iui_button(ctx, "Refresh Data", IUI_ALIGN_CENTER); + iui_button(ctx, "Share Dashboard", IUI_ALIGN_CENTER); + iui_button(ctx, "Print Summary", IUI_ALIGN_CENTER); + + iui_end_window(ctx); + iui_end_frame(ctx); +} + +/* Scene C: Form - sliders, checkboxes, buttons with varied labels */ +static void scene_form(iui_context *ctx) +{ + static float sliders[8] = {0.3f, 0.5f, 0.7f, 0.1f, 0.9f, 0.4f, 0.6f, 0.2f}; + static bool checks[8]; + + iui_begin_frame(ctx, 0.016f); + iui_begin_window(ctx, "form", 0, 0, SCREEN_W, SCREEN_H, 0); + + iui_text(ctx, IUI_ALIGN_LEFT, "Image Processing Configuration"); + iui_divider(ctx); + + static const char *slider_labels[] = { + "Brightness", "Contrast", "Saturation", "Sharpness", + "Exposure bias", "Gamma curve", "Highlights", "Shadows", + }; + for (int i = 0; i < 8; i++) + iui_slider(ctx, slider_labels[i], 0.f, 1.f, 0.01f, &sliders[i], NULL); + + iui_text(ctx, IUI_ALIGN_LEFT, "Processing Options"); + iui_divider(ctx); + + static const char *check_labels[] = { + "Auto white balance", "Noise reduction (luminance)", + "HDR tone mapping", "Lens distortion correction", + "Chromatic aberration fix", "Vignette removal", + "Perspective correction", "Auto-crop to content", + }; + for (int i = 0; i < 8; i++) + iui_checkbox(ctx, check_labels[i], &checks[i]); + + iui_text(ctx, IUI_ALIGN_LEFT, "Output"); + iui_divider(ctx); + iui_text(ctx, IUI_ALIGN_LEFT, + "Processed images will be saved to the output directory."); + + iui_button(ctx, "Apply Changes", IUI_ALIGN_CENTER); + iui_button(ctx, "Reset to Defaults", IUI_ALIGN_CENTER); + iui_button(ctx, "Preview Result", IUI_ALIGN_CENTER); + iui_button(ctx, "Cancel", IUI_ALIGN_CENTER); + + iui_end_window(ctx); + iui_end_frame(ctx); +} + +/* Scene D: Dialog - small overlay window (partial-screen update). + * This is the primary use-case for ink-bounds: only the dialog region (roughly + * 60% x 30% of screen) needs to be blitted to the display, saving ~80% of the + * framebuffer transfer cost. + */ +static void scene_dialog(iui_context *ctx) +{ + iui_begin_frame(ctx, 0.016f); + + /* Dialog overlay: centered, 280x240 on 480x800 screen */ + float dw = 280, dh = 240; + float dx = (SCREEN_W - dw) * 0.5f; + float dy = (SCREEN_H - dh) * 0.5f; + iui_begin_window(ctx, "dialog", dx, dy, dw, dh, 0); + + iui_text(ctx, IUI_ALIGN_LEFT, "Confirm Action"); + iui_divider(ctx); + iui_text(ctx, IUI_ALIGN_LEFT, "Are you sure you want to proceed?"); + iui_text(ctx, IUI_ALIGN_LEFT, "This action cannot be undone."); + + iui_button(ctx, "Confirm", IUI_ALIGN_CENTER); + iui_button(ctx, "Cancel", IUI_ALIGN_CENTER); + + iui_end_window(ctx); + iui_end_frame(ctx); +} + +/* bench harness */ +typedef struct { + double us_per_frame; + int draws_per_frame; + int clips_per_frame; + int tmeas_per_frame; +} bench_result_t; + +/* Run benchmark, return median of RUNS iterations */ +static bench_result_t bench_run(iui_context *ctx, scene_fn fn) +{ + double results[RUNS]; + + for (int run = 0; run < RUNS; run++) { + for (int i = 0; i < WARMUP; i++) + fn(ctx); + + g_pixels = 0; + g_draws = 0; + g_clips = 0; + g_tmeas = 0; + + uint64_t start = now_ns(); + for (int i = 0; i < FRAMES; i++) + fn(ctx); + uint64_t elapsed = now_ns() - start; + results[run] = (double) elapsed / (double) FRAMES / 1e3; + } + + /* Simple sort for median */ + for (int i = 0; i < RUNS - 1; i++) + for (int j = i + 1; j < RUNS; j++) + if (results[j] < results[i]) { + double tmp = results[i]; + results[i] = results[j]; + results[j] = tmp; + } + + bench_result_t r; + r.us_per_frame = results[RUNS / 2]; /* median */ + r.draws_per_frame = g_draws / FRAMES; + r.clips_per_frame = g_clips / FRAMES; + r.tmeas_per_frame = g_tmeas / FRAMES; + return r; +} + +static void print_header(const char *scene) +{ + printf("\n %s\n ", scene); + for (int i = 0; scene[i]; i++) + putchar('-'); + putchar('\n'); +} + +static void print_row(const char *label, bench_result_t r) +{ + printf(" %-36s %6.1f us/frame %3d draws %3d clips %3d tmeas\n", label, + r.us_per_frame, r.draws_per_frame, r.clips_per_frame, + r.tmeas_per_frame); +} + +static void print_pct(const char *label, double base_us, double test_us) +{ + double pct = (test_us - base_us) / base_us * 100.0; + printf(" %-32s %+6.1f%%\n", label, pct); +} + +/* per-scene benchmark */ +static void bench_scene(const char *title, scene_fn fn, iui_context *ctx) +{ + print_header(title); + + iui_ink_bounds_enable(ctx, false); + iui_text_cache_enable(ctx, false); + bench_result_t base = bench_run(ctx, fn); + print_row("baseline (all opts OFF)", base); + + iui_ink_bounds_enable(ctx, true); + iui_text_cache_enable(ctx, false); + bench_result_t ink = bench_run(ctx, fn); + print_row("ink-bounds ON", ink); + + iui_ink_bounds_enable(ctx, false); + iui_text_cache_enable(ctx, true); + bench_result_t tc = bench_run(ctx, fn); + print_row("text-cache ON", tc); + + iui_ink_bounds_enable(ctx, true); + iui_text_cache_enable(ctx, true); + bench_result_t both = bench_run(ctx, fn); + print_row("all opts ON", both); + + printf("\n"); + print_pct("ink-bounds overhead:", base.us_per_frame, ink.us_per_frame); + print_pct("text-cache effect:", base.us_per_frame, tc.us_per_frame); + print_pct("combined:", base.us_per_frame, both.us_per_frame); +} + +/* ink-bounds & text cache analysis */ +static void analyze_ink_bounds(iui_context *ctx, scene_fn fn, const char *name) +{ + iui_ink_bounds_enable(ctx, true); + fn(ctx); + iui_rect_t b; + float total = (float) SCREEN_W * (float) SCREEN_H; + if (iui_ink_bounds_get(ctx, &b)) { + float dirty = b.width * b.height; + float pct = dirty / total * 100.f; + printf(" %-18s (%.0f,%.0f) %.0fx%.0f = %5.1f%%", name, b.x, b.y, + b.width, b.height, pct); + if (pct < 99.f) + printf(" -> %.1f%% blit saved", 100.f - pct); + printf("\n"); + } + iui_ink_bounds_enable(ctx, false); +} + +static void analyze_text_cache(iui_context *ctx, scene_fn fn, const char *name) +{ + /* Measure WITHOUT cache: count raw text_width calls */ + iui_text_cache_enable(ctx, false); + g_tmeas = 0; + for (int i = 0; i < 100; i++) + fn(ctx); + int raw_calls = g_tmeas; + + /* Measure WITH cache: count backend calls that survive */ + iui_text_cache_enable(ctx, true); + iui_text_cache_clear(ctx); + g_tmeas = 0; + for (int i = 0; i < 100; i++) + fn(ctx); + int cached_calls = g_tmeas; + + int hits, misses; + iui_text_cache_stats(ctx, &hits, &misses); + int total = hits + misses; + float rate = total > 0 ? (float) hits / (float) total * 100.f : 0.f; + + int saved = raw_calls - cached_calls; + float save_pct = + raw_calls > 0 ? (float) saved / (float) raw_calls * 100.f : 0.f; + + printf( + " %-18s %4d calls/frame -> %d with cache (%.1f%% eliminated, " + "%.1f%% hit rate)\n", + name, raw_calls / 100, cached_calls / 100, save_pct, rate); + iui_text_cache_enable(ctx, false); +} + +int main(void) +{ + init_font_tables(); + + static union { + uint8_t buf[65536]; + void *align; + } mem; + + iui_renderer_t renderer = { + .draw_box = sim_draw_box, + .set_clip_rect = sim_set_clip, + .text_width = sim_text_width, + .draw_text = sim_draw_text, + }; + iui_config_t cfg = iui_make_config(mem.buf, renderer, 14.f, NULL); + iui_context *ctx = iui_init(&cfg); + if (!ctx) { + fprintf(stderr, "iui_init failed\n"); + return 1; + } + + printf( + "==================================================================\n"); + printf(" libiui Rendering Benchmark\n"); + printf(" Screen: %dx%d Frames: %d Runs: %d (median)\n", SCREEN_W, + SCREEN_H, FRAMES, RUNS); + printf( + "==================================================================\n"); + + bench_scene("Settings (16 toggles, 4 radios, labels)", scene_settings, ctx); + bench_scene("Dashboard (6 cards, 6 chips, 4 buttons)", scene_dashboard, + ctx); + bench_scene("Form (8 sliders, 8 checks, 4 buttons)", scene_form, ctx); + bench_scene("Dialog (confirm overlay, partial screen)", scene_dialog, ctx); + + /* Ink-bounds coverage */ + print_header("Ink-Bounds Coverage"); + analyze_ink_bounds(ctx, scene_settings, "Settings"); + analyze_ink_bounds(ctx, scene_dashboard, "Dashboard"); + analyze_ink_bounds(ctx, scene_form, "Form"); + analyze_ink_bounds(ctx, scene_dialog, "Dialog"); + + /* Text cache effectiveness */ + print_header("Text Cache Effectiveness (100 frames)"); + analyze_text_cache(ctx, scene_settings, "Settings"); + analyze_text_cache(ctx, scene_dashboard, "Dashboard"); + analyze_text_cache(ctx, scene_form, "Form"); + analyze_text_cache(ctx, scene_dialog, "Dialog"); + + /* Backend cost model: estimates real-world savings from ink-bounds + * partial blit + text cache. Based on measured pixel counts and + * text_width call counts from the scenes above. + */ + print_header("Estimated Backend Savings"); + { + const double blit_us_per_pixel = 1.0 / 2.5; /* 0.4 us per pixel */ + const double tw_us_per_call = 5.0; /* 5 us per text_width */ + double full_blit_us = + (double) SCREEN_W * (double) SCREEN_H * blit_us_per_pixel; + + scene_fn scenes[] = {scene_settings, scene_dashboard, scene_form, + scene_dialog}; + const char *names[] = {"Settings", "Dashboard", "Form", "Dialog"}; + + for (int s = 0; s < 4; s++) { + /* Measure ink-bounds region */ + iui_ink_bounds_enable(ctx, true); + iui_text_cache_enable(ctx, false); + g_tmeas = 0; + scenes[s](ctx); + int raw_tw = g_tmeas; + + iui_rect_t bounds; + double partial_blit_us = full_blit_us; + float coverage = 100.f; + if (iui_ink_bounds_get(ctx, &bounds)) { + double dirty = (double) bounds.width * (double) bounds.height; + partial_blit_us = dirty * blit_us_per_pixel; + coverage = + (float) (dirty / ((double) SCREEN_W * (double) SCREEN_H) * + 100.0); + } + + /* Measure cached text_width calls */ + iui_text_cache_enable(ctx, true); + iui_text_cache_clear(ctx); + g_tmeas = 0; + scenes[s](ctx); /* prime cache */ + g_tmeas = 0; + scenes[s](ctx); /* steady state */ + int cached_tw = g_tmeas; + iui_ink_bounds_enable(ctx, false); + iui_text_cache_enable(ctx, false); + + double blit_save_us = full_blit_us - partial_blit_us; + double tw_save_us = (double) (raw_tw - cached_tw) * tw_us_per_call; + double total_save_us = blit_save_us + tw_save_us; + double baseline_us = + full_blit_us + (double) raw_tw * tw_us_per_call; + double save_pct = + baseline_us > 0 ? total_save_us / baseline_us * 100.0 : 0.0; + + printf(" %-18s blit: %.0f -> %.0f us (%.0f%% coverage)\n", + names[s], full_blit_us, partial_blit_us, (double) coverage); + printf(" %-18s text_width: %d -> %d calls (%.0f us saved)\n", "", + raw_tw, cached_tw, tw_save_us); + printf(" %-18s total: %.0f us saved/frame (%.1f%% of backend)\n", + "", total_save_us, save_pct); + if (s < 3) + printf("\n"); + } + } + + printf( + "\n==================================================================" + "\n"); + return 0; +}