Skip to content

Commit a68565d

Browse files
committed
Add MD3 side sheet/carousel widgets
This introduce side sheet and carousel APIs with state structs, spec constants, and headless coverage, plus a middle-inset divider helper. It extends MD3 runtime tracking/validation across navigation, container, input, list, dialog, menu, and search components, including generated validator support for new DSL fields/components. Fix integration issues uncovered during review, including side-sheet modal/input-layer/layout lifecycle handling, scrim-dismiss timing, carousel viewport sizing and scroll width accounting, and correct tracking bounds for slider and bottom sheet validation.
1 parent 0a06dd9 commit a68565d

18 files changed

Lines changed: 637 additions & 15 deletions

include/iui-spec.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,25 @@
415415
#define IUI_NAV_DRAWER_ICON_GAP 12.f
416416
#endif
417417

418+
/* MD3 Side Sheet */
419+
#ifndef IUI_SIDE_SHEET_WIDTH
420+
#define IUI_SIDE_SHEET_WIDTH 400.f /* Standard side sheet width */
421+
#endif
422+
#ifndef IUI_SIDE_SHEET_PADDING
423+
#define IUI_SIDE_SHEET_PADDING 24.f
424+
#endif
425+
426+
/* MD3 Carousel */
427+
#ifndef IUI_CAROUSEL_ITEM_WIDTH
428+
#define IUI_CAROUSEL_ITEM_WIDTH 240.f
429+
#endif
430+
#ifndef IUI_CAROUSEL_ITEM_GAP
431+
#define IUI_CAROUSEL_ITEM_GAP 8.f
432+
#endif
433+
#ifndef IUI_CAROUSEL_CORNER_RADIUS
434+
#define IUI_CAROUSEL_CORNER_RADIUS 28.f
435+
#endif
436+
418437
/* List - https://m3.material.io/components/lists/specs */
419438
#ifndef IUI_LIST_ONE_LINE_HEIGHT
420439
#define IUI_LIST_ONE_LINE_HEIGHT 56.f

include/iui.h

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,27 @@ typedef struct {
448448
float anim_progress; /* 0.0 = closed, 1.0 = fully open */
449449
} iui_nav_drawer_state;
450450

451+
/* MD3 Side Sheet state */
452+
typedef struct {
453+
bool is_open;
454+
bool modal;
455+
float x, y; /* side sheet position */
456+
float width; /* current or target width */
457+
float height; /* side sheet height */
458+
float anim_progress; /* 0.0 = closed, 1.0 = fully open */
459+
float saved_layout_x, saved_layout_y;
460+
float saved_layout_w, saved_layout_h;
461+
int layer_id; /* input layer ID for standard sheets */
462+
} iui_side_sheet_state;
463+
464+
/* MD3 Carousel State */
465+
typedef struct {
466+
iui_scroll_state scroll;
467+
int item_count;
468+
float item_width;
469+
float item_height;
470+
} iui_carousel_state;
471+
451472
/* MD3 Button styles */
452473
typedef enum iui_button_style {
453474
IUI_BUTTON_TONAL, /* current default (surface_container bg) */
@@ -867,6 +888,9 @@ void iui_divider(iui_context *ctx);
867888
/* MD3 Inset Divider - divider with left padding (16dp) */
868889
void iui_divider_inset(iui_context *ctx);
869890

891+
/* MD3 Middle-Inset Divider - divider with left and right padding (16dp) */
892+
void iui_divider_middle_inset(iui_context *ctx);
893+
870894
/* Displays a segmented control with mutually exclusive options
871895
* @ctx: current UI context
872896
* @entries: array of strings, must have a size of num_entries
@@ -2249,6 +2273,26 @@ void iui_nav_drawer_end(iui_context *ctx, iui_nav_drawer_state *state);
22492273
void iui_nav_drawer_open(iui_nav_drawer_state *state);
22502274
void iui_nav_drawer_close(iui_nav_drawer_state *state);
22512275

2276+
/* MD3 Side Sheet */
2277+
void iui_side_sheet_open(iui_side_sheet_state *state);
2278+
void iui_side_sheet_close(iui_side_sheet_state *state);
2279+
bool iui_side_sheet_begin(iui_context *ctx,
2280+
iui_side_sheet_state *state,
2281+
float screen_width,
2282+
float screen_height);
2283+
void iui_side_sheet_end(iui_context *ctx, iui_side_sheet_state *state);
2284+
2285+
/* MD3 Carousel */
2286+
void iui_carousel_begin(iui_context *ctx,
2287+
iui_carousel_state *state,
2288+
float width,
2289+
float height);
2290+
bool iui_carousel_item(iui_context *ctx,
2291+
iui_carousel_state *state,
2292+
const char *image_label,
2293+
const char *title);
2294+
void iui_carousel_end(iui_context *ctx, iui_carousel_state *state);
2295+
22522296
/* Bottom Sheet
22532297
* Surfaces containing supplementary content anchored to the bottom of screen
22542298
* Reference: https://m3.material.io/components/bottom-sheets

scripts/gen-md3-validate.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ class Component:
138138
indicator_height: Optional[float] = None
139139
indicator_width: Optional[float] = None
140140
item_height: Optional[float] = None
141+
item_width: Optional[float] = None
141142
icon_container: Optional[float] = None
142143
button_height: Optional[float] = None
143144
drag_handle_width: Optional[float] = None
@@ -245,6 +246,7 @@ class Spec:
245246
(r"indicator_height\s+(\d+(?:\.\d+)?)", "indicator_height", float),
246247
(r"indicator_width\s+(\d+(?:\.\d+)?)", "indicator_width", float),
247248
(r"item_height\s+(\d+(?:\.\d+)?)", "item_height", float),
249+
(r"item_width\s+(\d+(?:\.\d+)?)", "item_width", float),
248250
(r"button_height\s+(\d+(?:\.\d+)?)", "button_height", float),
249251
(r"drag_handle_width\s+(\d+(?:\.\d+)?)", "drag_handle_width", float),
250252
(r"drag_handle_height\s+(\d+(?:\.\d+)?)", "drag_handle_height", float),
@@ -614,6 +616,16 @@ def generate_header(spec: Spec) -> str:
614616
)
615617
)
616618

619+
# Item width (for carousel/lists)
620+
if comp.item_width is not None:
621+
lines.append(
622+
emit_func(
623+
f"{fn}_item_width",
624+
"int width_px, float scale",
625+
f" return md3_check_exact_dp(width_px, {comp.item_width:.1f}f, scale, MD3_SIZE_MISMATCH);",
626+
)
627+
)
628+
617629
# Icon container (for bottom app bar)
618630
if comp.icon_container is not None:
619631
lines.append(
@@ -756,7 +768,7 @@ def generate_header(spec: Spec) -> str:
756768
emit_func(
757769
f"{fn}_expanded_width",
758770
"int width_px, float scale",
759-
f" return md3_check_exact_dp(width_px, {comp.expanded_width:.1f}f, scale, MD3_WIDTH_LOW);",
771+
f" return md3_check_exact_dp(width_px, {comp.expanded_width:.1f}f, scale, MD3_SIZE_MISMATCH);",
760772
)
761773
)
762774

@@ -1111,6 +1123,19 @@ def emit_test(comment: str, *assertions: str):
11111123
)
11121124
)
11131125

1126+
# Item width
1127+
if comp.item_width is not None:
1128+
dp = int(comp.item_width)
1129+
lines.extend(
1130+
emit_test(
1131+
f"Item width {dp}dp",
1132+
f"ASSERT_EQ({fn}_item_width({dp}, 1.f), MD3_OK);",
1133+
f"ASSERT_TRUE({fn}_item_width({dp + 1}, 1.f) & MD3_SIZE_MISMATCH);",
1134+
f"ASSERT_TRUE({fn}_item_width({dp - 1}, 1.f) & MD3_SIZE_MISMATCH);",
1135+
f"ASSERT_EQ({fn}_item_width({dp * 2}, 2.f), MD3_OK);",
1136+
)
1137+
)
1138+
11141139
# Icon container
11151140
if comp.icon_container is not None:
11161141
dp = int(comp.icon_container)
@@ -1292,8 +1317,8 @@ def emit_test(comment: str, *assertions: str):
12921317
emit_test(
12931318
f"Expanded width {dp}dp",
12941319
f"ASSERT_EQ({fn}_expanded_width({dp}, 1.f), MD3_OK);",
1295-
f"ASSERT_TRUE({fn}_expanded_width({dp + 1}, 1.f) & MD3_WIDTH_LOW);",
1296-
f"ASSERT_TRUE({fn}_expanded_width({dp - 1}, 1.f) & MD3_WIDTH_LOW);",
1320+
f"ASSERT_TRUE({fn}_expanded_width({dp + 1}, 1.f) & MD3_SIZE_MISMATCH);",
1321+
f"ASSERT_TRUE({fn}_expanded_width({dp - 1}, 1.f) & MD3_SIZE_MISMATCH);",
12971322
)
12981323
)
12991324

scripts/headless-test.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,25 @@ def run_md3_tests(verbose=False):
859859
iui_fab(ctx, 60.f, 420.f, "add");
860860
iui_fab_large(ctx, 160.f, 420.f, "edit");
861861
862+
static char search_buf[64] = "";
863+
static size_t search_cur = 0;
864+
iui_search_bar(ctx, search_buf, sizeof(search_buf), &search_cur, "Search...");
865+
866+
static iui_side_sheet_state side_sheet = { .is_open = true, .modal = false };
867+
if (iui_side_sheet_begin(ctx, &side_sheet, 580.f, 580.f)) {
868+
iui_side_sheet_end(ctx, &side_sheet);
869+
}
870+
871+
static iui_carousel_state carousel;
872+
iui_carousel_begin(ctx, &carousel, 0.f, 120.f);
873+
iui_carousel_item(ctx, &carousel, "Image", "Item 1");
874+
iui_carousel_end(ctx, &carousel);
875+
876+
static iui_nav_rail_state rail = {.expanded = false};
877+
iui_nav_rail_begin(ctx, &rail, 0, 0, 580);
878+
iui_nav_rail_item(ctx, &rail, "home", "Home", 0);
879+
iui_nav_rail_end(ctx, &rail);
880+
862881
iui_end_window(ctx);
863882
iui_end_frame(ctx);
864883
g_iui_port.end_frame(port);
@@ -873,15 +892,20 @@ def run_md3_tests(verbose=False):
873892
if (frame_violations > 0 && total_violations == frame_violations) {
874893
static const char *type_names[] = {
875894
"BUTTON", "FAB", "FAB_LARGE", "CHIP", "TEXTFIELD",
876-
"SWITCH", "SLIDER", "TAB", "CHECKBOX", "RADIO", "SEGMENTED"
895+
"SWITCH", "SLIDER", "TAB", "CHECKBOX", "RADIO", "SEGMENTED",
896+
"SEARCH_BAR", "SIDE_SHEET", "CAROUSEL", "NAV_RAIL",
897+
"NAV_RAIL_INDICATOR", "NAV_DRAWER",
898+
"NAV_BAR", "BOTTOM_APP_BAR", "MENU", "LIST_ITEM_ONE_LINE",
899+
"LIST_ITEM_TWO_LINE", "LIST_ITEM_THREE_LINE", "SNACKBAR",
900+
"CARD", "DIALOG", "BOTTOM_SHEET", "TOOLTIP", "BANNER"
877901
};
878902
static const char *viol_names[] = {
879903
"HEIGHT", "WIDTH", "TOUCH_TARGET", "CORNER_RADIUS"
880904
};
881905
for (int i = 0; i < frame_tracked; i++) {
882906
const iui_md3_tracked_t *t = iui_md3_get_tracked(i);
883907
if (t && t->violations != 0) {
884-
const char *tname = (t->type < 11) ? type_names[t->type] : "UNKNOWN";
908+
const char *tname = (t->type < 29) ? type_names[t->type] : "UNKNOWN";
885909
printf(" VIOLATION: %s size=%.0fx%.0f ", tname, t->bounds.width, t->bounds.height);
886910
for (int v = 0; v < 4; v++) {
887911
if (t->violations & (1 << v)) printf("[%s] ", viol_names[v]);
@@ -941,6 +965,7 @@ def run_md3_runtime_tests(verbose=False):
941965
out = subprocess.run([str(exe)], capture_output=True, text=True, timeout=10)
942966
except subprocess.TimeoutExpired:
943967
return False, {"error": "timeout"}
968+
return False, {"error": "timeout"}
944969

945970
tracked = 0
946971
violations = 0

src/basic.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ void iui_segmented(iui_context *ctx,
4545
(iui_rect_t) {seg_x_start, seg_y, ctx->layout.width, seg_height},
4646
pill_radius, ctx->colors.surface_container_highest, ctx->renderer.user);
4747

48+
/* Track component for MD3 validation */
49+
IUI_MD3_TRACK_SEGMENTED(
50+
((iui_rect_t) {seg_x_start, seg_y, ctx->layout.width, seg_height}),
51+
pill_radius);
52+
4853
/* Selected segment highlight with animation */
4954
if (*selected < num_entries) {
5055
float sel_x = (ctx->animation.widget == selected)
@@ -359,6 +364,11 @@ float iui_slider_ex(iui_context *ctx,
359364
/* Update thumb rect with final position */
360365
thumb_rect.x = thumb_x - half_size;
361366

367+
/* Track component for MD3 validation using final thumb/touch bounds */
368+
touch_rect = thumb_rect;
369+
iui_expand_touch_target(&touch_rect, IUI_SLIDER_TOUCH_TARGET);
370+
IUI_MD3_TRACK_SLIDER(touch_rect, touch_rect.height * .5f);
371+
362372
/* MD3: State layer on hover/press/drag */
363373
if ((thumb_hovered || is_dragging) && !disabled) {
364374
float state_size = thumb_size * 1.5f;

0 commit comments

Comments
 (0)