From bae74d7a96f7c4b71e79e4b5bb3eb4603f832e01 Mon Sep 17 00:00:00 2001 From: Max Amundsen Date: Tue, 3 Mar 2026 11:35:29 -0500 Subject: [PATCH] fix scaling issues --- src/main.cpp | 96 ++++++++++++++--------- src/platform/platform.h | 3 + src/platform/platform_macos.mm | 5 ++ src/renderer/renderer.h | 1 + src/renderer/renderer_metal.mm | 8 ++ src/ui/ui_core.cpp | 12 +++ src/ui/ui_core.h | 3 + src/ui/ui_theme.h | 56 +++++++++----- src/ui/ui_widgets.cpp | 137 +++++++++++++++++++-------------- 9 files changed, 208 insertions(+), 113 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 65a2f31..0b21b13 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -100,6 +100,9 @@ struct AppState { // Audio device selection S32 audio_device_sel; // dropdown index: 0 = None, 1+ = device S32 audio_device_prev; // previous selection for change detection + + // UI scale (Cmd+/Cmd- to zoom) + F32 ui_scale; }; //////////////////////////////// @@ -110,7 +113,7 @@ static void build_browser_panel(B32 show) { CLAY(CLAY_ID("BrowserPanel"), .layout = { - .sizing = { .width = CLAY_SIZING_FIXED(200), .height = CLAY_SIZING_GROW() }, + .sizing = { .width = CLAY_SIZING_FIXED(uis(200)), .height = CLAY_SIZING_GROW() }, .layoutDirection = CLAY_TOP_TO_BOTTOM, }, .backgroundColor = g_theme.bg_medium, @@ -125,8 +128,8 @@ static void build_browser_panel(B32 show) { CLAY(CLAY_ID("BrowserContent"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, - .padding = { 8, 8, 6, 6 }, - .childGap = 4, + .padding = { uip(8), uip(8), uip(6), uip(6) }, + .childGap = uip(4), .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { @@ -152,8 +155,8 @@ static void build_main_panel(AppState *app) { CLAY(CLAY_ID("MainContent"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, - .padding = { 16, 16, 12, 12 }, - .childGap = 12, + .padding = { uip(16), uip(16), uip(12), uip(12) }, + .childGap = uip(12), .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { @@ -162,7 +165,7 @@ static void build_main_panel(AppState *app) { CLAY(CLAY_ID("ButtonRow"), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() }, - .childGap = 8, + .childGap = uip(8), .layoutDirection = CLAY_LEFT_TO_RIGHT, } ) { @@ -209,14 +212,14 @@ static void build_main_panel(AppState *app) { CLAY(CLAY_ID("TextRow"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, - .childGap = 8, + .childGap = uip(8), .layoutDirection = CLAY_LEFT_TO_RIGHT, } ) { CLAY(CLAY_ID("TextCol1"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, - .childGap = 4, + .childGap = uip(4), .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { @@ -226,7 +229,7 @@ static void build_main_panel(AppState *app) { CLAY(CLAY_ID("TextCol2"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, - .childGap = 4, + .childGap = uip(4), .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { @@ -244,7 +247,7 @@ static void build_main_panel(AppState *app) { ui_label("LblDropdown", "Sample Rate"); CLAY(CLAY_ID("DropdownWrapper"), .layout = { - .sizing = { .width = CLAY_SIZING_FIXED(200), .height = CLAY_SIZING_FIT() }, + .sizing = { .width = CLAY_SIZING_FIXED(uis(200)), .height = CLAY_SIZING_FIT() }, } ) { static const char *rate_options[] = { "44100 Hz", "48000 Hz", "88200 Hz", "96000 Hz", "192000 Hz" }; @@ -261,7 +264,7 @@ static void build_main_panel(AppState *app) { CLAY(CLAY_ID("WindowBtnRow"), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() }, - .childGap = 8, + .childGap = uip(8), .layoutDirection = CLAY_LEFT_TO_RIGHT, } ) { @@ -296,8 +299,8 @@ static void build_right_panel(AppState *app) { CLAY(CLAY_ID("PropertiesContent"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, - .padding = { 8, 8, 6, 6 }, - .childGap = 4, + .padding = { uip(8), uip(8), uip(6), uip(6) }, + .childGap = uip(4), .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { @@ -309,8 +312,8 @@ static void build_right_panel(AppState *app) { CLAY(CLAY_ID("MidiContent"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, - .padding = { 8, 8, 6, 6 }, - .childGap = 6, + .padding = { uip(8), uip(8), uip(6), uip(6) }, + .childGap = uip(6), .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { @@ -319,12 +322,12 @@ static void build_right_panel(AppState *app) { B32 refresh_hovered = Clay_PointerOver(refresh_eid); CLAY(refresh_eid, .layout = { - .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(28) }, - .padding = { 12, 12, 0, 0 }, + .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(uis(28)) }, + .padding = { uip(12), uip(12), 0, 0 }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = refresh_hovered ? g_theme.accent_hover : g_theme.bg_lighter, - .cornerRadius = CLAY_CORNER_RADIUS(3) + .cornerRadius = CLAY_CORNER_RADIUS(uis(3)) ) { CLAY_TEXT(CLAY_STRING("Refresh"), &g_text_config_normal); } @@ -344,7 +347,7 @@ static void build_right_panel(AppState *app) { static Clay_TextElementConfig box_text_config; box_text_config = {}; box_text_config.textColor = Clay_Color{255, 255, 255, 255}; - box_text_config.fontSize = 12; + box_text_config.fontSize = FONT_SIZE_SMALL; box_text_config.wrapMode = CLAY_TEXT_WRAP_NONE; B32 has_inputs = 0; @@ -411,8 +414,8 @@ static void build_right_panel(AppState *app) { CLAY(CLAY_IDI("MidiIn", i), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, - .padding = { 4, 4, 2, 2 }, - .childGap = 6, + .padding = { uip(4), uip(4), uip(2), uip(2) }, + .childGap = uip(6), .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, .layoutDirection = CLAY_LEFT_TO_RIGHT, } @@ -420,11 +423,11 @@ static void build_right_panel(AppState *app) { // Note name box (colored by velocity) CLAY(CLAY_IDI("MidiInNote", i), .layout = { - .sizing = { .width = CLAY_SIZING_FIXED(36), .height = CLAY_SIZING_FIXED(18) }, + .sizing = { .width = CLAY_SIZING_FIXED(uis(36)), .height = CLAY_SIZING_FIXED(uis(18)) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = box_color, - .cornerRadius = CLAY_CORNER_RADIUS(3) + .cornerRadius = CLAY_CORNER_RADIUS(uis(3)) ) { CLAY_TEXT(note_str, box_txt); } @@ -453,7 +456,7 @@ static void build_right_panel(AppState *app) { CLAY(CLAY_IDI("MidiOut", i), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, - .padding = { 4, 4, 2, 2 }, + .padding = { uip(4), uip(4), uip(2), uip(2) }, } ) { CLAY_TEXT(device_str, &g_text_config_normal); @@ -472,7 +475,7 @@ static void build_log_panel(B32 show) { CLAY(CLAY_ID("LogPanel"), .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(180) }, + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(uis(180)) }, .layoutDirection = CLAY_TOP_TO_BOTTOM, }, .backgroundColor = g_theme.bg_medium, @@ -487,8 +490,8 @@ static void build_log_panel(B32 show) { CLAY(CLAY_ID("LogContent"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, - .padding = { 8, 8, 6, 6 }, - .childGap = 4, + .padding = { uip(8), uip(8), uip(6), uip(6) }, + .childGap = uip(4), .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { @@ -506,7 +509,7 @@ static void settings_window_content(void *user_data) { ui_label("SettingsLblTheme", "Theme"); static const char *theme_options[] = { "Dark", "Light", "Solarized" }; CLAY(CLAY_ID("SettingsThemeWrap"), - .layout = { .sizing = { .width = CLAY_SIZING_FIXED(180), .height = CLAY_SIZING_FIT() } } + .layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(180)), .height = CLAY_SIZING_FIT() } } ) { ui_dropdown("SettingsTheme", theme_options, 3, &app->settings_theme_sel); } @@ -517,7 +520,7 @@ static void settings_window_content(void *user_data) { // Separator CLAY(CLAY_ID("SettingsSep1"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) }, - .padding = { 0, 0, 6, 6 } }, + .padding = { 0, 0, uip(6), uip(6) } }, .backgroundColor = g_theme.border ) {} @@ -533,7 +536,7 @@ static void settings_window_content(void *user_data) { } CLAY(CLAY_ID("SettingsAudioWrap"), - .layout = { .sizing = { .width = CLAY_SIZING_FIXED(220), .height = CLAY_SIZING_FIT() } } + .layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(220)), .height = CLAY_SIZING_FIT() } } ) { ui_dropdown("SettingsAudio", audio_options, audio_count + 1, &app->audio_device_sel); } @@ -551,7 +554,7 @@ static void settings_window_content(void *user_data) { CLAY(CLAY_ID("SettingsAudioBtnRow"), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() }, - .childGap = 8, + .childGap = uip(8), .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, .layoutDirection = CLAY_LEFT_TO_RIGHT, } @@ -568,16 +571,16 @@ static void settings_window_content(void *user_data) { Clay_ElementId btn_eid = CLAY_ID("BtnTestToneDisabled"); CLAY(btn_eid, .layout = { - .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(28) }, - .padding = { 12, 12, 0, 0 }, + .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(uis(28)) }, + .padding = { uip(12), uip(12), 0, 0 }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = Clay_Color{50, 50, 50, 255}, - .cornerRadius = CLAY_CORNER_RADIUS(3) + .cornerRadius = CLAY_CORNER_RADIUS(uis(3)) ) { static Clay_TextElementConfig disabled_text = {}; disabled_text.textColor = Clay_Color{100, 100, 100, 255}; - disabled_text.fontSize = 15; + disabled_text.fontSize = FONT_SIZE_NORMAL; disabled_text.wrapMode = CLAY_TEXT_WRAP_NONE; CLAY_TEXT(CLAY_STRING("Play Test Tone"), &disabled_text); } @@ -619,7 +622,7 @@ static void build_ui(AppState *app) { if (app->show_props || app->show_midi_devices) { CLAY(CLAY_ID("RightColumn"), .layout = { - .sizing = { .width = CLAY_SIZING_FIXED(250), .height = CLAY_SIZING_GROW() }, + .sizing = { .width = CLAY_SIZING_FIXED(uis(250)), .height = CLAY_SIZING_GROW() }, .layoutDirection = CLAY_TOP_TO_BOTTOM, }, .border = { .color = g_theme.border, .width = { .left = 1 } } @@ -689,6 +692,26 @@ static void do_frame(AppState *app) { // Gather input PlatformInput input = platform_get_input(app->window); + + // Cmd+= / Cmd+- (or Ctrl on Windows) to zoom UI, Cmd+0 to reset + for (S32 k = 0; k < input.key_count; k++) { + if (input.ctrl_held) { + if (input.keys[k] == PKEY_EQUAL) app->ui_scale *= 1.1f; + if (input.keys[k] == PKEY_MINUS) app->ui_scale /= 1.1f; + if (input.keys[k] == PKEY_0) app->ui_scale = 1.0f; + } + } + app->ui_scale = Clamp(0.5f, app->ui_scale, 3.0f); + g_ui_scale = app->ui_scale; + renderer_set_font_scale(app->renderer, app->ui_scale); + + // Update text configs when scale changes + if (g_text_config_normal.fontSize != FONT_SIZE_NORMAL) { + g_text_config_normal.fontSize = FONT_SIZE_NORMAL; + g_text_config_title.fontSize = FONT_SIZE_NORMAL; + g_text_config_dim.fontSize = FONT_SIZE_NORMAL; + } + ui_widgets_begin_frame(input); // Build UI with Clay @@ -753,6 +776,7 @@ int main(int argc, char **argv) { app.ui = ui; app.last_w = w; app.last_h = h; + app.ui_scale = 1.0f; app.show_browser = 1; app.show_props = 1; app.show_log = 1; diff --git a/src/platform/platform.h b/src/platform/platform.h index d52149e..bc9abe2 100644 --- a/src/platform/platform.h +++ b/src/platform/platform.h @@ -28,6 +28,9 @@ enum { PKEY_C = 0x43, PKEY_V = 0x56, PKEY_X = 0x58, + PKEY_0 = 0x30, + PKEY_EQUAL = 0xBB, // '='/'+' (VK_OEM_PLUS) + PKEY_MINUS = 0xBD, // '-'/'_' (VK_OEM_MINUS) }; struct PlatformInput { diff --git a/src/platform/platform_macos.mm b/src/platform/platform_macos.mm index dd257b5..374514a 100644 --- a/src/platform/platform_macos.mm +++ b/src/platform/platform_macos.mm @@ -13,6 +13,8 @@ enum { kVK_Home = 0x73, kVK_End = 0x77, kVK_Command = 0x37, kVK_Shift = 0x38, kVK_RightShift = 0x3C, kVK_RightCommand = 0x36, + kVK_ANSI_Equal = 0x18, kVK_ANSI_Minus = 0x1B, + kVK_ANSI_0 = 0x1D, }; static uint8_t macos_keycode_to_pkey(uint16_t keycode) { @@ -32,6 +34,9 @@ static uint8_t macos_keycode_to_pkey(uint16_t keycode) { case kVK_DownArrow: return PKEY_DOWN; case kVK_Home: return PKEY_HOME; case kVK_End: return PKEY_END; + case kVK_ANSI_Equal: return PKEY_EQUAL; + case kVK_ANSI_Minus: return PKEY_MINUS; + case kVK_ANSI_0: return PKEY_0; default: return 0; } } diff --git a/src/renderer/renderer.h b/src/renderer/renderer.h index 9b379a0..2acfdc2 100644 --- a/src/renderer/renderer.h +++ b/src/renderer/renderer.h @@ -18,6 +18,7 @@ void renderer_destroy(Renderer *renderer); bool renderer_begin_frame(Renderer *renderer); void renderer_end_frame(Renderer *renderer, Clay_RenderCommandArray render_commands); void renderer_resize(Renderer *renderer, int32_t width, int32_t height); +void renderer_set_font_scale(Renderer *renderer, float scale); // Text measurement callback compatible with UI_MeasureTextFn // Measures text of given length (NOT necessarily null-terminated) at font_size pixels. diff --git a/src/renderer/renderer_metal.mm b/src/renderer/renderer_metal.mm index c97fd29..49b077a 100644 --- a/src/renderer/renderer_metal.mm +++ b/src/renderer/renderer_metal.mm @@ -801,6 +801,14 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) { r->frame_index++; } +void renderer_set_font_scale(Renderer *r, float scale) { + float target_size = 15.0f * scale; + if (fabsf(target_size - r->font_atlas_size) < 0.1f) return; + r->font_texture = nil; + r->font_sampler = nil; + create_font_atlas(r, target_size); +} + void renderer_resize(Renderer *r, int32_t width, int32_t height) { if (width <= 0 || height <= 0) return; r->width = width; diff --git a/src/ui/ui_core.cpp b/src/ui/ui_core.cpp index 112aa0d..9ede171 100644 --- a/src/ui/ui_core.cpp +++ b/src/ui/ui_core.cpp @@ -10,6 +10,11 @@ #include "ui/ui_core.h" +//////////////////////////////// +// UI scale global + +F32 g_ui_scale = 1.0f; + //////////////////////////////// // Theme global @@ -89,6 +94,7 @@ void ui_begin_frame(UI_Context *ctx, F32 viewport_w, F32 viewport_h, Vec2F32 mouse_pos, B32 mouse_down, Vec2F32 scroll_delta, F32 dt) { + g_measure_ctx = ctx; Clay_SetCurrentContext(ctx->clay_ctx); Clay_SetLayoutDimensions(Clay_Dimensions{viewport_w, viewport_h}); Clay_SetPointerState(Clay_Vector2{mouse_pos.x, mouse_pos.y}, mouse_down != 0); @@ -105,3 +111,9 @@ void ui_set_measure_text_fn(UI_Context *ctx, UI_MeasureTextFn fn, void *user_dat ctx->measure_text_fn = fn; ctx->measure_text_user_data = user_data; } + +Vec2F32 ui_measure_text(const char *text, S32 length, F32 font_size) { + if (!g_measure_ctx || !g_measure_ctx->measure_text_fn || length == 0) + return v2f32(0, font_size); + return g_measure_ctx->measure_text_fn(text, length, font_size, g_measure_ctx->measure_text_user_data); +} diff --git a/src/ui/ui_core.h b/src/ui/ui_core.h index 1c4463f..b9dda2d 100644 --- a/src/ui/ui_core.h +++ b/src/ui/ui_core.h @@ -42,6 +42,9 @@ Clay_RenderCommandArray ui_end_frame(UI_Context *ctx); void ui_set_measure_text_fn(UI_Context *ctx, UI_MeasureTextFn fn, void *user_data); +// Measure text using the registered callback (available after ui_begin_frame) +Vec2F32 ui_measure_text(const char *text, S32 length, F32 font_size); + //////////////////////////////// // Theme colors (convenience - 0-255 Clay_Color) diff --git a/src/ui/ui_theme.h b/src/ui/ui_theme.h index 0313e94..184a3de 100644 --- a/src/ui/ui_theme.h +++ b/src/ui/ui_theme.h @@ -4,6 +4,20 @@ #include "clay.h" +//////////////////////////////// +// UI scale (Cmd+/Cmd- zoom) + +extern F32 g_ui_scale; + +// Scale a float value (for CLAY_SIZING_FIXED, corner radii, etc.) +static inline float uis(float x) { return x * g_ui_scale; } + +// Scale to uint16_t (for Clay_Padding, childGap, etc.) +static inline uint16_t uip(float x) { return (uint16_t)(x * g_ui_scale + 0.5f); } + +// Scale to uint16_t font size +static inline uint16_t uifs(float x) { return (uint16_t)(x * g_ui_scale + 0.5f); } + //////////////////////////////// // Tab styling @@ -11,9 +25,9 @@ #define TAB_ACTIVE_BOTTOM Clay_Color{ 30, 55, 80, 255} #define TAB_INACTIVE_BG Clay_Color{ 45, 45, 48, 255} #define TAB_INACTIVE_HOVER Clay_Color{ 55, 55, 60, 255} -#define TAB_HEIGHT 26 -#define TAB_CORNER_RADIUS 5 -#define TAB_PADDING_H 10 +#define TAB_HEIGHT uis(26) +#define TAB_CORNER_RADIUS uis(5) +#define TAB_PADDING_H uip(10) //////////////////////////////// // Custom render types (for gradient rects via CLAY_RENDER_COMMAND_TYPE_CUSTOM) @@ -31,41 +45,41 @@ struct CustomGradientData { //////////////////////////////// // Font sizes -#define FONT_SIZE_NORMAL 15 -#define FONT_SIZE_SMALL 12 +#define FONT_SIZE_NORMAL uifs(15) +#define FONT_SIZE_SMALL uifs(12) //////////////////////////////// // Widget sizing -#define WIDGET_BUTTON_HEIGHT 30 -#define WIDGET_CHECKBOX_HEIGHT 28 -#define WIDGET_CHECKBOX_SIZE 18 -#define WIDGET_RADIO_OUTER 16 -#define WIDGET_RADIO_INNER 8 -#define WIDGET_INPUT_HEIGHT 30 -#define WIDGET_DROPDOWN_HEIGHT 30 -#define WIDGET_DROPDOWN_ITEM_H 28 +#define WIDGET_BUTTON_HEIGHT uis(30) +#define WIDGET_CHECKBOX_HEIGHT uis(28) +#define WIDGET_CHECKBOX_SIZE uis(18) +#define WIDGET_RADIO_OUTER uis(16) +#define WIDGET_RADIO_INNER uis(8) +#define WIDGET_INPUT_HEIGHT uis(30) +#define WIDGET_DROPDOWN_HEIGHT uis(30) +#define WIDGET_DROPDOWN_ITEM_H uis(28) //////////////////////////////// // Corner radii -#define CORNER_RADIUS_SM 3 -#define CORNER_RADIUS_MD 6 -#define CORNER_RADIUS_ROUND 8 +#define CORNER_RADIUS_SM uis(3) +#define CORNER_RADIUS_MD uis(6) +#define CORNER_RADIUS_ROUND uis(8) //////////////////////////////// // Modal / window styling #define MODAL_OVERLAY_COLOR Clay_Color{ 0, 0, 0, 120} -#define MODAL_WIDTH 400 +#define MODAL_WIDTH uis(400) #define MODAL_CORNER_RADIUS CORNER_RADIUS_MD #define WINDOW_CORNER_RADIUS CORNER_RADIUS_MD -#define WINDOW_TITLE_HEIGHT 32 +#define WINDOW_TITLE_HEIGHT uis(32) //////////////////////////////// // Panel sizing -#define PANEL_BROWSER_WIDTH 200 -#define PANEL_RIGHT_COL_WIDTH 250 -#define PANEL_LOG_HEIGHT 180 +#define PANEL_BROWSER_WIDTH uis(200) +#define PANEL_RIGHT_COL_WIDTH uis(250) +#define PANEL_LOG_HEIGHT uis(180) diff --git a/src/ui/ui_widgets.cpp b/src/ui/ui_widgets.cpp index d166e01..408d7dd 100644 --- a/src/ui/ui_widgets.cpp +++ b/src/ui/ui_widgets.cpp @@ -43,26 +43,26 @@ void ui_widgets_begin_frame(PlatformInput input) { static Clay_TextElementConfig g_widget_text_config; static Clay_TextElementConfig g_widget_text_config_dim; static Clay_TextElementConfig g_widget_text_config_sel; -static bool g_widget_text_configs_init = false; +static F32 g_widget_text_configs_scale = 0; static void ensure_widget_text_configs() { - if (g_widget_text_configs_init) return; - g_widget_text_configs_init = true; + if (g_widget_text_configs_scale == g_ui_scale) return; + g_widget_text_configs_scale = g_ui_scale; g_widget_text_config = {}; g_widget_text_config.textColor = g_theme.text; - g_widget_text_config.fontSize = 15; + g_widget_text_config.fontSize = FONT_SIZE_NORMAL; g_widget_text_config.wrapMode = CLAY_TEXT_WRAP_NONE; g_widget_text_config_dim = {}; g_widget_text_config_dim.textColor = g_theme.text_dim; - g_widget_text_config_dim.fontSize = 15; + g_widget_text_config_dim.fontSize = FONT_SIZE_NORMAL; g_widget_text_config_dim.wrapMode = CLAY_TEXT_WRAP_NONE; // Selected text: white on accent background (background set on parent element) g_widget_text_config_sel = {}; g_widget_text_config_sel.textColor = Clay_Color{255, 255, 255, 255}; - g_widget_text_config_sel.fontSize = 15; + g_widget_text_config_sel.fontSize = FONT_SIZE_NORMAL; g_widget_text_config_sel.wrapMode = CLAY_TEXT_WRAP_NONE; } @@ -86,7 +86,7 @@ void ui_label(const char *id, const char *text) { CLAY(WID(id), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, - .padding = { 0, 0, 2, 2 }, + .padding = { 0, 0, uip(2), uip(2) }, } ) { CLAY_TEXT(clay_str(text), &g_widget_text_config); @@ -104,12 +104,12 @@ B32 ui_button(const char *id, const char *text) { CLAY(eid, .layout = { - .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(30) }, - .padding = { 12, 12, 0, 0 }, + .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(WIDGET_BUTTON_HEIGHT) }, + .padding = { uip(12), uip(12), 0, 0 }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = hovered ? g_theme.accent_hover : g_theme.accent, - .cornerRadius = CLAY_CORNER_RADIUS(3) + .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS_SM) ) { CLAY_TEXT(clay_str(text), &g_widget_text_config); } @@ -129,8 +129,8 @@ B32 ui_checkbox(const char *id, const char *label, B32 *value) { CLAY(eid, .layout = { - .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(28) }, - .childGap = 8, + .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(WIDGET_CHECKBOX_HEIGHT) }, + .childGap = uip(8), .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, .layoutDirection = CLAY_LEFT_TO_RIGHT, } @@ -142,11 +142,11 @@ B32 ui_checkbox(const char *id, const char *label, B32 *value) { } CLAY(WIDI(id, 1), .layout = { - .sizing = { .width = CLAY_SIZING_FIXED(18), .height = CLAY_SIZING_FIXED(18) }, + .sizing = { .width = CLAY_SIZING_FIXED(WIDGET_CHECKBOX_SIZE), .height = CLAY_SIZING_FIXED(WIDGET_CHECKBOX_SIZE) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = box_bg, - .cornerRadius = CLAY_CORNER_RADIUS(3), + .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS_SM), .border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } } ) { if (*value) { @@ -176,7 +176,7 @@ B32 ui_radio_group(const char *id, const char **options, S32 count, S32 *selecte CLAY(WID(id), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() }, - .childGap = 4, + .childGap = uip(4), .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { @@ -187,8 +187,8 @@ B32 ui_radio_group(const char *id, const char **options, S32 count, S32 *selecte CLAY(row_id, .layout = { - .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(26) }, - .childGap = 8, + .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(uis(26)) }, + .childGap = uip(8), .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, .layoutDirection = CLAY_LEFT_TO_RIGHT, } @@ -200,20 +200,20 @@ B32 ui_radio_group(const char *id, const char **options, S32 count, S32 *selecte } CLAY(WIDI(id, i + 200), .layout = { - .sizing = { .width = CLAY_SIZING_FIXED(16), .height = CLAY_SIZING_FIXED(16) }, + .sizing = { .width = CLAY_SIZING_FIXED(WIDGET_RADIO_OUTER), .height = CLAY_SIZING_FIXED(WIDGET_RADIO_OUTER) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = dot_bg, - .cornerRadius = CLAY_CORNER_RADIUS(8), + .cornerRadius = CLAY_CORNER_RADIUS(uis(8)), .border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } } ) { if (is_selected) { CLAY(WIDI(id, i + 300), .layout = { - .sizing = { .width = CLAY_SIZING_FIXED(8), .height = CLAY_SIZING_FIXED(8) }, + .sizing = { .width = CLAY_SIZING_FIXED(WIDGET_RADIO_INNER), .height = CLAY_SIZING_FIXED(WIDGET_RADIO_INNER) }, }, .backgroundColor = g_theme.text, - .cornerRadius = CLAY_CORNER_RADIUS(4) + .cornerRadius = CLAY_CORNER_RADIUS(uis(4)) ) {} } } @@ -556,13 +556,13 @@ B32 ui_text_input(const char *id, char *buf, S32 buf_size) { CLAY(eid, .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(30) }, - .padding = { 8, 8, 0, 0 }, + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WIDGET_INPUT_HEIGHT) }, + .padding = { uip(8), uip(8), 0, 0 }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, .layoutDirection = CLAY_LEFT_TO_RIGHT, }, .backgroundColor = bg, - .cornerRadius = CLAY_CORNER_RADIUS(3), + .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS_SM), .border = { .color = border_color, .width = { 1, 1, 1, 1 } } ) { if (len == 0 && !is_focused) { @@ -637,34 +637,59 @@ B32 ui_dropdown(const char *id, const char **options, S32 count, S32 *selected) B32 header_hovered = Clay_PointerOver(eid); B32 is_open = (g_wstate.open_dropdown_id == eid.id); - // Display current selection + // Display current selection — truncate with "..." if it overflows the header const char *current_label = (*selected >= 0 && *selected < count) ? options[*selected] : "Select..."; + static char dd_trunc_buf[256]; + const char *display_label = current_label; + Clay_ElementId text_eid = WIDI(id, 500); + float avail_w = Clay_GetElementData(text_eid).boundingBox.width; + if (avail_w > 0) { + S32 label_len = (S32)strlen(current_label); + Vec2F32 text_size = ui_measure_text(current_label, label_len, FONT_SIZE_NORMAL); + if (text_size.x > avail_w) { + Vec2F32 dots = ui_measure_text("...", 3, FONT_SIZE_NORMAL); + float target_w = avail_w - dots.x; + S32 lo = 0, hi = label_len; + while (lo < hi) { + S32 mid = (lo + hi + 1) / 2; + Vec2F32 seg = ui_measure_text(current_label, mid, FONT_SIZE_NORMAL); + if (seg.x <= target_w) lo = mid; else hi = mid - 1; + } + if (lo + 3 < (S32)sizeof(dd_trunc_buf)) { + memcpy(dd_trunc_buf, current_label, lo); + dd_trunc_buf[lo] = '.'; dd_trunc_buf[lo+1] = '.'; dd_trunc_buf[lo+2] = '.'; + dd_trunc_buf[lo+3] = '\0'; + display_label = dd_trunc_buf; + } + } + } + Clay_Color bg = is_open ? g_theme.bg_dark : g_theme.bg_medium; if (header_hovered && !is_open) bg = g_theme.bg_lighter; CLAY(eid, .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(30) }, - .padding = { 8, 8, 0, 0 }, + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WIDGET_DROPDOWN_HEIGHT) }, + .padding = { uip(8), uip(8), 0, 0 }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, .layoutDirection = CLAY_LEFT_TO_RIGHT, }, .backgroundColor = bg, - .cornerRadius = CLAY_CORNER_RADIUS(3), + .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS_SM), .border = { .color = is_open ? g_theme.accent : g_theme.border, .width = { 1, 1, 1, 1 } } ) { - CLAY(WIDI(id, 500), + CLAY(text_eid, .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, } ) { - CLAY_TEXT(clay_str(current_label), &g_widget_text_config); + CLAY_TEXT(clay_str(display_label), &g_widget_text_config); } CLAY(WIDI(id, 501), .layout = { - .sizing = { .width = CLAY_SIZING_FIXED(20), .height = CLAY_SIZING_FIT() }, + .sizing = { .width = CLAY_SIZING_FIXED(uis(20)), .height = CLAY_SIZING_FIT() }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER }, } ) { @@ -714,8 +739,8 @@ B32 ui_dropdown(const char *id, const char **options, S32 count, S32 *selected) CLAY(item_id, .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(28) }, - .padding = { 8, 8, 0, 0 }, + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WIDGET_DROPDOWN_ITEM_H) }, + .padding = { uip(8), uip(8), 0, 0 }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = item_bg @@ -800,11 +825,11 @@ S32 ui_modal(const char *id, const char *title, const char *message, // Dialog box (centered) CLAY(WIDI(id, 2001), .layout = { - .sizing = { .width = CLAY_SIZING_FIXED(400), .height = CLAY_SIZING_FIT() }, + .sizing = { .width = CLAY_SIZING_FIXED(MODAL_WIDTH), .height = CLAY_SIZING_FIT() }, .layoutDirection = CLAY_TOP_TO_BOTTOM, }, .backgroundColor = g_theme.bg_medium, - .cornerRadius = CLAY_CORNER_RADIUS(6), + .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS_MD), .floating = { .zIndex = 1001, .attachPoints = { @@ -819,12 +844,12 @@ S32 ui_modal(const char *id, const char *title, const char *message, // Title bar CLAY(WIDI(id, 2002), .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(32) }, - .padding = { 12, 12, 0, 0 }, + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WINDOW_TITLE_HEIGHT) }, + .padding = { uip(12), uip(12), 0, 0 }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = g_theme.title_bar, - .cornerRadius = { 6, 6, 0, 0 }, + .cornerRadius = { MODAL_CORNER_RADIUS, MODAL_CORNER_RADIUS, 0, 0 }, .border = { .color = g_theme.border, .width = { .bottom = 1 } } ) { CLAY_TEXT(clay_str(title), &g_widget_text_config); @@ -838,7 +863,7 @@ S32 ui_modal(const char *id, const char *title, const char *message, CLAY(WIDI(id, 2003), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, - .padding = { 16, 16, 16, 16 }, + .padding = { uip(16), uip(16), uip(16), uip(16) }, } ) { CLAY_TEXT(clay_str(message), &msg_text_config); @@ -848,8 +873,8 @@ S32 ui_modal(const char *id, const char *title, const char *message, CLAY(WIDI(id, 2004), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, - .padding = { 16, 16, 8, 16 }, - .childGap = 8, + .padding = { uip(16), uip(16), uip(8), uip(16) }, + .childGap = uip(8), .childAlignment = { .x = CLAY_ALIGN_X_RIGHT, .y = CLAY_ALIGN_Y_CENTER }, .layoutDirection = CLAY_LEFT_TO_RIGHT, } @@ -860,12 +885,12 @@ S32 ui_modal(const char *id, const char *title, const char *message, CLAY(btn_id, .layout = { - .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(30) }, - .padding = { 16, 16, 0, 0 }, + .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(WIDGET_BUTTON_HEIGHT) }, + .padding = { uip(16), uip(16), 0, 0 }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = btn_hovered ? g_theme.accent_hover : g_theme.accent, - .cornerRadius = CLAY_CORNER_RADIUS(3) + .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS_SM) ) { CLAY_TEXT(clay_str(buttons[i]), &g_widget_text_config); } @@ -984,11 +1009,11 @@ B32 ui_window(const char *id, const char *title, B32 *open, // Window floating element CLAY(eid, .layout = { - .sizing = { .width = CLAY_SIZING_FIXED(slot->size.x), .height = CLAY_SIZING_FIT() }, + .sizing = { .width = CLAY_SIZING_FIXED(slot->size.x * g_ui_scale), .height = CLAY_SIZING_FIT() }, .layoutDirection = CLAY_TOP_TO_BOTTOM, }, .backgroundColor = g_theme.bg_medium, - .cornerRadius = CLAY_CORNER_RADIUS(6), + .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS_MD), .floating = { .offset = { slot->position.x, slot->position.y }, .zIndex = (int16_t)(100 + slot->z_order), @@ -1004,13 +1029,13 @@ B32 ui_window(const char *id, const char *title, B32 *open, // Title bar CLAY(title_bar_id, .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(28) }, - .padding = { 10, 10, 0, 0 }, + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WINDOW_TITLE_HEIGHT) }, + .padding = { uip(10), uip(10), 0, 0 }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, .layoutDirection = CLAY_LEFT_TO_RIGHT, }, .backgroundColor = g_theme.title_bar, - .cornerRadius = { 6, 6, 0, 0 }, + .cornerRadius = { WINDOW_CORNER_RADIUS, WINDOW_CORNER_RADIUS, 0, 0 }, .border = { .color = g_theme.border, .width = { .bottom = 1 } } ) { // Title text (grows to push close button right) @@ -1026,11 +1051,11 @@ B32 ui_window(const char *id, const char *title, B32 *open, Clay_Color close_bg = close_hovered ? Clay_Color{200, 60, 60, 255} : Clay_Color{120, 50, 50, 255}; CLAY(close_id, .layout = { - .sizing = { .width = CLAY_SIZING_FIXED(20), .height = CLAY_SIZING_FIXED(20) }, + .sizing = { .width = CLAY_SIZING_FIXED(uis(20)), .height = CLAY_SIZING_FIXED(uis(20)) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = close_bg, - .cornerRadius = CLAY_CORNER_RADIUS(3) + .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS_SM) ) { CLAY_TEXT(CLAY_STRING("x"), &g_widget_text_config); } @@ -1040,8 +1065,8 @@ B32 ui_window(const char *id, const char *title, B32 *open, CLAY(WIDI(id, 3003), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, - .padding = { 12, 12, 10, 10 }, - .childGap = 8, + .padding = { uip(12), uip(12), uip(10), uip(10) }, + .childGap = uip(8), .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { @@ -1074,7 +1099,7 @@ S32 ui_tab_bar(const char *id, const char **labels, S32 count, S32 *selected) { CLAY(row_eid, .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, - .padding = { 0, 0, 4, 0 }, + .padding = { 0, 0, uip(4), 0 }, .layoutDirection = CLAY_LEFT_TO_RIGHT, }, .backgroundColor = g_theme.bg_medium, @@ -1091,7 +1116,7 @@ S32 ui_tab_bar(const char *id, const char **labels, S32 count, S32 *selected) { CLAY(tab_eid, .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(TAB_HEIGHT) }, - .padding = { TAB_PADDING_H, TAB_PADDING_H, 0, 6 }, + .padding = { TAB_PADDING_H, TAB_PADDING_H, 0, uip(6) }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, }, .cornerRadius = { .topLeft = TAB_CORNER_RADIUS, .topRight = TAB_CORNER_RADIUS, .bottomLeft = 0, .bottomRight = 0 }, @@ -1104,7 +1129,7 @@ S32 ui_tab_bar(const char *id, const char **labels, S32 count, S32 *selected) { CLAY(tab_eid, .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(TAB_HEIGHT) }, - .padding = { TAB_PADDING_H, TAB_PADDING_H, 0, 6 }, + .padding = { TAB_PADDING_H, TAB_PADDING_H, 0, uip(6) }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = bg,