Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 35 additions & 23 deletions .ci/install-deps.sh
Original file line number Diff line number Diff line change
@@ -1,14 +1,36 @@
#!/usr/bin/env bash

# Install build dependencies for CI
# Usage: .ci/install-deps.sh [sdl2|headless|format]
# Usage: .ci/install-deps.sh [sdl2|headless|format|analysis]

set -euo pipefail

source "$(dirname "$0")/common.sh"

MODE="${1:-sdl2}"

# Setup LLVM 20 APT repository (shared by format and analysis modes)
setup_llvm_repo() {
LLVM_KEYRING=/usr/share/keyrings/llvm-archive-keyring.gpg
LLVM_KEY_FP="6084F3CF814B57C1CF12EFD515CF4D18AF4F7421"
TMPKEY=$(mktemp)
download_to_file https://apt.llvm.org/llvm-snapshot.gpg.key "$TMPKEY"
ACTUAL_FP=$(gpg --with-fingerprint --with-colons "$TMPKEY" 2>/dev/null | grep fpr | head -1 | cut -d: -f10)
if [ "$ACTUAL_FP" != "$LLVM_KEY_FP" ]; then
print_error "LLVM key fingerprint mismatch!"
print_error "Expected: $LLVM_KEY_FP"
print_error "Got: $ACTUAL_FP"
rm -f "$TMPKEY"
exit 1
fi
sudo gpg --dearmor -o "$LLVM_KEYRING" <"$TMPKEY"
rm -f "$TMPKEY"

CODENAME=$(lsb_release -cs)
echo "deb [signed-by=${LLVM_KEYRING}] https://apt.llvm.org/${CODENAME}/ llvm-toolchain-${CODENAME}-20 main" | sudo tee /etc/apt/sources.list.d/llvm-20.list
sudo apt-get update -q=2
}

case "$MODE" in
sdl2)
if [ "$OS_TYPE" = "Linux" ]; then
Expand All @@ -33,35 +55,25 @@ format)
fi
sudo apt-get update -q=2
sudo apt-get install -y -q=2 --no-install-recommends shfmt python3-pip gnupg ca-certificates lsb-release

# Install clang-format-20 from LLVM repository
# LLVM signing key fingerprint: 6084F3CF814B57C1CF12EFD515CF4D18AF4F7421
LLVM_KEYRING=/usr/share/keyrings/llvm-archive-keyring.gpg
LLVM_KEY_FP="6084F3CF814B57C1CF12EFD515CF4D18AF4F7421"
TMPKEY=$(mktemp)
download_to_file https://apt.llvm.org/llvm-snapshot.gpg.key "$TMPKEY"
ACTUAL_FP=$(gpg --with-fingerprint --with-colons "$TMPKEY" 2>/dev/null | grep fpr | head -1 | cut -d: -f10)
if [ "$ACTUAL_FP" != "$LLVM_KEY_FP" ]; then
print_error "LLVM key fingerprint mismatch!"
print_error "Expected: $LLVM_KEY_FP"
print_error "Got: $ACTUAL_FP"
rm -f "$TMPKEY"
exit 1
fi
sudo gpg --dearmor -o "$LLVM_KEYRING" <"$TMPKEY"
rm -f "$TMPKEY"

CODENAME=$(lsb_release -cs)
echo "deb [signed-by=${LLVM_KEYRING}] https://apt.llvm.org/${CODENAME}/ llvm-toolchain-${CODENAME}-20 main" | sudo tee /etc/apt/sources.list.d/llvm-20.list
sudo apt-get update -q=2
setup_llvm_repo
sudo apt-get install -y -q=2 --no-install-recommends clang-format-20

# Install Python formatter (version-pinned for reproducibility)
pip3 install --break-system-packages --only-binary=:all: black==25.1.0
;;
analysis)
if [ "$OS_TYPE" != "Linux" ]; then
print_error "Static analysis only supported on Linux"
exit 1
fi
sudo apt-get update -q=2
sudo apt-get install -y -q=2 --no-install-recommends gnupg ca-certificates lsb-release libsdl2-dev
setup_llvm_repo
sudo apt-get install -y -q=2 --no-install-recommends clang-20 clang-tools-20
;;
*)
print_error "Unknown mode: $MODE"
echo "Usage: $0 [sdl2|headless|format]"
echo "Usage: $0 [sdl2|headless|format|analysis]"
exit 1
;;
esac
Expand Down
37 changes: 37 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,43 @@ jobs:
- name: Check code formatting
run: .ci/check-format.sh

static-analysis:
needs: [detect-code-related-file-changes]
if: needs.detect-code-related-file-changes.outputs.has_code_related_changes == 'true'
timeout-minutes: 15
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Install dependencies
run: .ci/install-deps.sh analysis
- name: scan-build
run: |
make defconfig
make clean
scan-build-20 --status-bugs \
-o /tmp/scan-build-report \
make -j$(nproc)
- name: Upload scan-build report
if: failure()
uses: actions/upload-artifact@v7
with:
name: scan-build-report
path: /tmp/scan-build-report
retention-days: 7
if-no-files-found: ignore
- name: scan-build (alpha checkers, informational)
if: always()
continue-on-error: true
run: |
make clean
scan-build-20 \
-enable-checker alpha.deadcode.UnreachableCode \
-enable-checker alpha.unix.cstring.OutOfBounds \
-o /tmp/scan-build-alpha-report \
make -j$(nproc) 2>&1 | tee /tmp/alpha-output.txt
WARNINGS=$(grep -c "warning:" /tmp/alpha-output.txt || true)
echo "::notice::Alpha checkers found $WARNINGS warnings (informational only)"

unit-tests:
needs: [detect-code-related-file-changes]
if: needs.detect-code-related-file-changes.outputs.has_code_related_changes == 'true'
Expand Down
20 changes: 11 additions & 9 deletions include/iui.h
Original file line number Diff line number Diff line change
Expand Up @@ -913,8 +913,8 @@ void iui_segmented(iui_context *ctx,
*/
void iui_slider(iui_context *ctx,
const char *label,
float min,
float max,
float min_value,
float max_value,
float step,
float *value,
const char *fmt);
Expand Down Expand Up @@ -2031,7 +2031,7 @@ void iui_table_row_end(iui_context *ctx, iui_table_state *state);
* @ctx: current UI context
* @state: table state from iui_table_begin
*/
void iui_table_end(iui_context *ctx, iui_table_state *state);
void iui_table_end(iui_context *ctx, const iui_table_state *state);

/* Scrollable Container
* Creates a scrollable viewport using the existing clip stack.
Expand Down Expand Up @@ -2189,7 +2189,7 @@ bool iui_nav_rail_item(iui_context *ctx,
int index);

/* End navigation rail rendering */
void iui_nav_rail_end(iui_context *ctx, iui_nav_rail_state *state);
void iui_nav_rail_end(iui_context *ctx, const iui_nav_rail_state *state);

/* Toggle rail expanded/collapsed state */
void iui_nav_rail_toggle(iui_nav_rail_state *state);
Expand Down Expand Up @@ -2227,7 +2227,7 @@ bool iui_nav_bar_item(iui_context *ctx,
int index);

/* End navigation bar rendering */
void iui_nav_bar_end(iui_context *ctx, iui_nav_bar_state *state);
void iui_nav_bar_end(iui_context *ctx, const iui_nav_bar_state *state);

/* Navigation Drawer Component
* Side panel navigation for larger screens
Expand Down Expand Up @@ -2267,7 +2267,7 @@ bool iui_nav_drawer_item(iui_context *ctx,
void iui_nav_drawer_divider(iui_context *ctx);

/* End navigation drawer rendering */
void iui_nav_drawer_end(iui_context *ctx, iui_nav_drawer_state *state);
void iui_nav_drawer_end(iui_context *ctx, const iui_nav_drawer_state *state);

/* Open/close drawer */
void iui_nav_drawer_open(iui_nav_drawer_state *state);
Expand Down Expand Up @@ -2339,7 +2339,8 @@ bool iui_bottom_sheet_begin(iui_context *ctx,
float screen_height);

/* End bottom sheet rendering */
void iui_bottom_sheet_end(iui_context *ctx, iui_bottom_sheet_state *state);
void iui_bottom_sheet_end(iui_context *ctx,
const iui_bottom_sheet_state *state);

/* Open/close bottom sheet */
void iui_bottom_sheet_open(iui_bottom_sheet_state *state);
Expand Down Expand Up @@ -2387,12 +2388,13 @@ bool iui_bottom_app_bar_action(iui_context *ctx,

/* Add FAB (right side). Returns true if clicked */
bool iui_bottom_app_bar_fab(iui_context *ctx,
iui_bottom_app_bar_state *state,
const iui_bottom_app_bar_state *state,
const char *icon,
iui_fab_size_t size);

/* End bottom app bar rendering */
void iui_bottom_app_bar_end(iui_context *ctx, iui_bottom_app_bar_state *state);
void iui_bottom_app_bar_end(iui_context *ctx,
const iui_bottom_app_bar_state *state);

/* Menu Component
* Vertical dropdown menu with support for icons, shortcuts, dividers, and
Expand Down
4 changes: 2 additions & 2 deletions ports/headless.c
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,7 @@ const uint32_t *iui_headless_get_framebuffer(iui_port_ctx *ctx)
/*
* Get framebuffer dimensions.
*/
void iui_headless_get_framebuffer_size(iui_port_ctx *ctx,
void iui_headless_get_framebuffer_size(const iui_port_ctx *ctx,
int *width,
int *height)
{
Expand Down Expand Up @@ -1165,7 +1165,7 @@ void iui_headless_process_shm_events(iui_port_ctx *ctx)
/* Process all pending events */
while (hdr->event_read_idx != hdr->event_write_idx) {
uint32_t idx = hdr->event_read_idx % IUI_SHM_EVENT_RING_SIZE;
iui_shm_event_t *ev = &events[idx];
const iui_shm_event_t *ev = &events[idx];

switch (ev->type) {
case IUI_SHM_EVENT_MOUSE_MOVE:
Expand Down
2 changes: 1 addition & 1 deletion ports/headless.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ void iui_headless_inject_text(iui_port_ctx *ctx, uint32_t codepoint);
const uint32_t *iui_headless_get_framebuffer(iui_port_ctx *ctx);

/* Get framebuffer dimensions */
void iui_headless_get_framebuffer_size(iui_port_ctx *ctx,
void iui_headless_get_framebuffer_size(const iui_port_ctx *ctx,
int *width,
int *height);

Expand Down
6 changes: 2 additions & 4 deletions src/basic.c
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,10 @@ void iui_segmented(iui_context *ctx,
uint32_t text_color = is_selected ? ctx->colors.on_secondary_container
: ctx->colors.on_surface;

/* Draw checkmark icon on selected segment */
float icon_size = IUI_SEGMENTED_ICON_SIZE,
text_w = iui_get_text_width(ctx, entries[i]);

if (is_selected) {
/* Calculate total content width: checkmark + gap + text */
float icon_size = IUI_SEGMENTED_ICON_SIZE,
text_w = iui_get_text_width(ctx, entries[i]);
float gap = 8.f, content_width = icon_size + gap + text_w,
content_x = seg_x + (seg_width - content_width) / 2.f,
icon_cx = content_x + icon_size / 2.f,
Expand Down
4 changes: 2 additions & 2 deletions src/container.c
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,7 @@ bool iui_bottom_sheet_begin(iui_context *ctx,
return true;
}

void iui_bottom_sheet_end(iui_context *ctx, iui_bottom_sheet_state *state)
void iui_bottom_sheet_end(iui_context *ctx, const iui_bottom_sheet_state *state)
{
if (!ctx || !state)
return;
Expand Down Expand Up @@ -1186,7 +1186,7 @@ void iui_table_row_end(iui_context *ctx, iui_table_state *state)
0.f, ctx->colors.outline_variant, ctx->renderer.user);
}

void iui_table_end(iui_context *ctx, iui_table_state *state)
void iui_table_end(iui_context *ctx, const iui_table_state *state)
{
if (!ctx || !ctx->current_window || !state)
return;
Expand Down
8 changes: 4 additions & 4 deletions src/core.c
Original file line number Diff line number Diff line change
Expand Up @@ -1515,7 +1515,7 @@ void iui_pop_layer(iui_context *ctx)
ctx->input_layer.current_z_order = 0;
} else {
/* Restore to previous stack entry */
iui_layer_entry_t *prev =
const iui_layer_entry_t *prev =
&ctx->input_layer.layer_stack[ctx->input_layer.layer_depth - 1];
ctx->input_layer.current_layer_id = prev->layer_id;
ctx->input_layer.current_z_order = prev->z_order;
Expand Down Expand Up @@ -1665,7 +1665,7 @@ void iui_register_textfield(iui_context *ctx, void *buffer)

/* Probe entire table until finding empty slot or duplicate */
for (int probe = 0; probe < IUI_MAX_TRACKED_TEXTFIELDS; probe++) {
void *cached = ctx->field_tracking.textfield_ids[idx];
const void *cached = ctx->field_tracking.textfield_ids[idx];
if (!cached) {
/* Found empty slot, insert here */
ctx->field_tracking.textfield_ids[idx] = buffer;
Expand Down Expand Up @@ -1715,7 +1715,7 @@ bool iui_textfield_is_registered(const iui_context *ctx, const void *buffer)

/* Probe entire table looking for match or empty slot */
for (int probe = 0; probe < IUI_MAX_TRACKED_TEXTFIELDS; probe++) {
void *cached = ctx->field_tracking.textfield_ids[idx];
const void *cached = ctx->field_tracking.textfield_ids[idx];
if (cached == buffer)
return true;
if (!cached)
Expand All @@ -1725,7 +1725,7 @@ bool iui_textfield_is_registered(const iui_context *ctx, const void *buffer)
return false;
}

bool iui_slider_is_registered(const iui_context *ctx, uint32_t slider_id)
static bool iui_slider_is_registered(const iui_context *ctx, uint32_t slider_id)
{
if (slider_id == 0)
return false;
Expand Down
4 changes: 2 additions & 2 deletions src/fab.c
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ static bool iui_fab_internal(iui_context *ctx,
iui_internal_draw_text(ctx, content_x, label_y, label, content_color);
} else {
/* Standard FAB: centered icon only */
float icon_cx = x + fab_w * 0.5f;
float icon_cy = center_y;
if (icon) {
float icon_cx = x + fab_w * 0.5f;
float icon_cy = center_y;
iui_draw_fab_icon(ctx, icon_cx, icon_cy, icon_size, icon,
content_color);
}
Expand Down
6 changes: 3 additions & 3 deletions src/input.c
Original file line number Diff line number Diff line change
Expand Up @@ -459,11 +459,11 @@ static void textfield_move_left(const char *buffer,
state->cursor = prev;
}
while (state->cursor > 0) {
size_t prev = iui_utf8_prev(buffer, state->cursor);
uint32_t cp = iui_utf8_decode(buffer, prev, len);
size_t prev2 = iui_utf8_prev(buffer, state->cursor);
uint32_t cp = iui_utf8_decode(buffer, prev2, len);
if (!iui_utf8_is_word_char(cp))
break;
state->cursor = prev;
state->cursor = prev2;
}
} else {
state->cursor = iui_utf8_prev(buffer, state->cursor);
Expand Down
15 changes: 14 additions & 1 deletion src/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,24 @@
#ifndef IUI_INTERNAL_H
#define IUI_INTERNAL_H

#include <assert.h>
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>

/* Assert macro that is invisible to the Clang Static Analyzer.
* Preserves debug-mode abort and release-mode recovery (defense-in-depth)
* without triggering alpha.deadcode.UnreachableCode false positives.
*/
#ifdef __clang_analyzer__
#define IUI_ASSERT(cond) \
do { \
} while (0)
#else
#define IUI_ASSERT(cond) assert(cond)
#endif

#include "font.h"
#include "iui.h"
#include "iui_config.h"
Expand Down Expand Up @@ -1212,7 +1225,7 @@ void iui_field_tracking_frame_end(iui_context *ctx);
bool iui_textfield_is_registered(const iui_context *ctx, const void *buffer);

/* Check if a slider was registered this frame */
bool iui_slider_is_registered(const iui_context *ctx, uint32_t slider_id);
/* iui_slider_is_registered is static in core.c */

/* MD3 Runtime Validation (Debug Builds)
* These macros validate that rendered components conform to MD3 specifications.
Expand Down
12 changes: 7 additions & 5 deletions src/layout.c
Original file line number Diff line number Diff line change
Expand Up @@ -810,10 +810,11 @@ void iui_end_window(iui_context *ctx)
/* Assert balanced clips and no abandoned scroll regions.
* Clear current_window BEFORE iui_pop_clip so the floor guard
* (depth >= 1 while inside a window) allows this final pop. */
assert(!ctx->active_scroll &&
"abandoned scroll region: iui_scroll_end not called inside window");
assert(ctx->clip.depth == 1 &&
"unbalanced iui_push_clip/iui_pop_clip inside window");
IUI_ASSERT(
!ctx->active_scroll &&
"abandoned scroll region: iui_scroll_end not called inside window");
IUI_ASSERT(ctx->clip.depth == 1 &&
"unbalanced iui_push_clip/iui_pop_clip inside window");
ctx->clip.depth = 1; /* safety: discard any leaked clips */
ctx->current_window = NULL; /* must precede pop for floor guard bypass */
iui_pop_clip(ctx);
Expand Down Expand Up @@ -855,7 +856,8 @@ void iui_end_frame(iui_context *ctx)
ctx->active_scroll = NULL;

/* Each begin_window/end_window pair must balance its clip push/pop. */
assert(ctx->clip.depth == 0 && "leaked clip region across frame boundary");
IUI_ASSERT(ctx->clip.depth == 0 &&
"leaked clip region across frame boundary");
if (ctx->clip.depth != 0) {
/* Release recovery: reset both logical and renderer clip state so the
* next frame starts clean rather than with stale clipping applied. */
Expand Down
Loading
Loading