fix & improve platform popups

This commit is contained in:
2026-03-05 17:43:19 -05:00
parent cb7ecbd3f7
commit a0875fd1ff
12 changed files with 189 additions and 17 deletions

4
.vscode/launch.json vendored
View File

@@ -8,7 +8,7 @@
"program": "${workspaceFolder}/build_debug/autosample.app/Contents/MacOS/autosample",
"args": [],
"cwd": "${workspaceFolder}",
// "preLaunchTask": "build-debug"
"preLaunchTask": "build-debug"
},
{
"name": "Debug autosample (Windows)",
@@ -18,7 +18,7 @@
"args": [],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
// "preLaunchTask": "build-debug"
"preLaunchTask": "build-debug"
}
]
}

View File

@@ -102,6 +102,10 @@ struct AppState {
B32 show_settings_window;
B32 show_about_window;
B32 show_confirm_dialog;
// Pop-out windows
B32 show_mix_popout;
B32 show_patch_popout;
S32 settings_theme_sel;
S32 settings_theme_prev;
B32 settings_vsync;
@@ -1165,7 +1169,39 @@ static void build_header_bar(AppState *app) {
CLAY_TEXT(CLAY_STRING("Mix"), mix_active ? &header_btn_active_text : &g_text_config_normal);
}
if (mix_hovered && g_wstate.mouse_clicked) {
app->master_layout = 1;
PopupWindow *mix_pop = popup_find_by_flag(&app->show_mix_popout);
if (mix_pop) {
platform_focus_window(mix_pop->platform_window);
} else {
app->master_layout = 1;
}
}
}
// Mix pop-out / pop-in button
{
B32 mix_popped = app->show_mix_popout;
Clay_ElementId pop_eid = CLAY_ID("BtnMixPopOut");
B32 pop_hovered = Clay_PointerOver(pop_eid);
CLAY(pop_eid,
.layout = {
.sizing = { .width = CLAY_SIZING_FIXED(uis(22)), .height = CLAY_SIZING_FIXED(uis(22)) },
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
},
.backgroundColor = pop_hovered ? g_theme.accent_hover : g_theme.bg_lighter,
.cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS),
) {
ui_icon(mix_popped ? UI_ICON_POP_IN : UI_ICON_POP_OUT, uis(12), g_theme.text_dim);
}
if (pop_hovered && g_wstate.mouse_clicked) {
if (mix_popped) {
PopupWindow *p = popup_find_by_flag(&app->show_mix_popout);
if (p) popup_close(p);
app->master_layout = 1;
} else {
app->show_mix_popout = 1;
if (app->master_layout == 1) app->master_layout = 0;
}
}
}
@@ -1188,7 +1224,39 @@ static void build_header_bar(AppState *app) {
CLAY_TEXT(CLAY_STRING("Patch"), patch_active ? &header_btn_active_text : &g_text_config_normal);
}
if (patch_hovered && g_wstate.mouse_clicked) {
app->master_layout = 2;
PopupWindow *patch_pop = popup_find_by_flag(&app->show_patch_popout);
if (patch_pop) {
platform_focus_window(patch_pop->platform_window);
} else {
app->master_layout = 2;
}
}
}
// Patch pop-out / pop-in button
{
B32 patch_popped = app->show_patch_popout;
Clay_ElementId pop_eid = CLAY_ID("BtnPatchPopOut");
B32 pop_hovered = Clay_PointerOver(pop_eid);
CLAY(pop_eid,
.layout = {
.sizing = { .width = CLAY_SIZING_FIXED(uis(22)), .height = CLAY_SIZING_FIXED(uis(22)) },
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
},
.backgroundColor = pop_hovered ? g_theme.accent_hover : g_theme.bg_lighter,
.cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS),
) {
ui_icon(patch_popped ? UI_ICON_POP_IN : UI_ICON_POP_OUT, uis(12), g_theme.text_dim);
}
if (pop_hovered && g_wstate.mouse_clicked) {
if (patch_popped) {
PopupWindow *p = popup_find_by_flag(&app->show_patch_popout);
if (p) popup_close(p);
app->master_layout = 2;
} else {
app->show_patch_popout = 1;
if (app->master_layout == 2) app->master_layout = 0;
}
}
}
}
@@ -1676,6 +1744,17 @@ static void build_patch_view(AppState *app) {
}
}
////////////////////////////////
// Pop-out window content callbacks
static void mix_popout_content(void *user_data) {
build_mix_view((AppState *)user_data);
}
static void patch_popout_content(void *user_data) {
build_patch_view((AppState *)user_data);
}
////////////////////////////////
// Build the full UI layout for one frame
@@ -1736,11 +1815,15 @@ static void build_ui(AppState *app) {
build_log_panel(app);
} else if (app->master_layout == 1) {
// === MIX MODE ===
build_mix_view(app);
// === MIX MODE (skip if popped out) ===
if (!app->show_mix_popout) {
build_mix_view(app);
}
} else {
// === PATCH MODE ===
build_patch_view(app);
// === PATCH MODE (skip if popped out) ===
if (!app->show_patch_popout) {
build_patch_view(app);
}
}
}
@@ -1783,6 +1866,9 @@ static void do_frame(AppState *app) {
// Gather input
PlatformInput input = platform_get_input(app->window);
// Sync scale from popups (they may have changed g_ui_scale)
app->ui_scale = g_ui_scale;
// 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) {
@@ -1973,6 +2059,10 @@ int main(int argc, char **argv) {
popup_open(window, renderer, "Preferences", &app.show_settings_window, 480, 400, settings_window_content, &app);
if (app.show_about_window && !popup_find_by_flag(&app.show_about_window))
popup_open(window, renderer, "About", &app.show_about_window, 260, 200, about_window_content, nullptr);
if (app.show_mix_popout && !popup_find_by_flag(&app.show_mix_popout))
popup_open(window, renderer, "Mix", &app.show_mix_popout, 900, 600, mix_popout_content, &app, PLATFORM_WINDOW_STYLE_POPUP_RESIZABLE, 1);
if (app.show_patch_popout && !popup_find_by_flag(&app.show_patch_popout))
popup_open(window, renderer, "Patch", &app.show_patch_popout, 900, 600, patch_popout_content, &app, PLATFORM_WINDOW_STYLE_POPUP_RESIZABLE, 1);
// Check for OS close on popups
popup_close_check();

View File

@@ -54,8 +54,9 @@ struct PlatformInput {
struct PlatformWindow;
enum PlatformWindowStyle {
PLATFORM_WINDOW_STYLE_NORMAL = 0,
PLATFORM_WINDOW_STYLE_POPUP = 1, // utility panel, owned by parent
PLATFORM_WINDOW_STYLE_NORMAL = 0,
PLATFORM_WINDOW_STYLE_POPUP = 1, // utility panel, owned by parent, fixed size
PLATFORM_WINDOW_STYLE_POPUP_RESIZABLE = 2, // utility panel, owned by parent, resizable
};
struct PlatformWindowDesc {
@@ -64,6 +65,7 @@ struct PlatformWindowDesc {
S32 height = 720;
PlatformWindowStyle style = PLATFORM_WINDOW_STYLE_NORMAL;
PlatformWindow *parent = nullptr;
B32 independent = 0; // if true, don't attach as child (independent top-level window)
};
enum PlatformMsgBoxType {
@@ -104,6 +106,9 @@ PlatformInput platform_get_input(PlatformWindow *window);
// Returns true if the window's close button was clicked (for popup windows).
B32 platform_window_should_close(PlatformWindow *window);
// Bring a window to front and give it keyboard focus.
void platform_focus_window(PlatformWindow *window);
// Blocks until user responds. Returns 0=first button, 1=second button, -1=dismissed.
S32 platform_message_box(PlatformWindow *parent, const char *title,
const char *message, PlatformMsgBoxType type);

View File

@@ -262,6 +262,8 @@ PlatformWindow *platform_create_window(PlatformWindowDesc *desc) {
NSWindowStyleMask style_mask;
if (desc->style == PLATFORM_WINDOW_STYLE_POPUP) {
style_mask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable;
} else if (desc->style == PLATFORM_WINDOW_STYLE_POPUP_RESIZABLE) {
style_mask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable;
} else {
style_mask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
@@ -305,8 +307,8 @@ PlatformWindow *platform_create_window(PlatformWindowDesc *desc) {
g_main_window = window;
}
// If popup, add as child of parent
if (desc->style == PLATFORM_WINDOW_STYLE_POPUP && desc->parent) {
// If popup, add as child of parent (unless independent)
if (!desc->independent && (desc->style == PLATFORM_WINDOW_STYLE_POPUP || desc->style == PLATFORM_WINDOW_STYLE_POPUP_RESIZABLE) && desc->parent) {
[desc->parent->ns_window addChildWindow:ns_window ordered:NSWindowAbove];
}
@@ -448,6 +450,11 @@ B32 platform_window_should_close(PlatformWindow *window) {
return window ? window->should_close : 0;
}
void platform_focus_window(PlatformWindow *window) {
if (!window || !window->ns_window) return;
[window->ns_window makeKeyAndOrderFront:nil];
}
F32 platform_get_dpi_scale(PlatformWindow *window) {
(void)window;
return 1.0f; // macOS handles Retina via backing scale factor, not DPI

View File

@@ -121,7 +121,10 @@ PlatformWindow *platform_create_window(PlatformWindowDesc *desc) {
HWND parent_hwnd = nullptr;
if (desc->style == PLATFORM_WINDOW_STYLE_POPUP) {
style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU;
if (desc->parent) parent_hwnd = desc->parent->hwnd;
if (desc->parent && !desc->independent) parent_hwnd = desc->parent->hwnd;
} else if (desc->style == PLATFORM_WINDOW_STYLE_POPUP_RESIZABLE) {
style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MAXIMIZEBOX;
if (desc->parent && !desc->independent) parent_hwnd = desc->parent->hwnd;
} else {
style = WS_OVERLAPPEDWINDOW;
}
@@ -264,6 +267,12 @@ B32 platform_window_should_close(PlatformWindow *window) {
return window ? window->should_close : 0;
}
void platform_focus_window(PlatformWindow *window) {
if (!window || !window->hwnd) return;
SetForegroundWindow(window->hwnd);
SetFocus(window->hwnd);
}
F32 platform_get_dpi_scale(PlatformWindow *window) {
if (!window || !window->hwnd) return 1.0f;
return (F32)GetDpiForWindow(window->hwnd) / 96.0f;

View File

@@ -25,6 +25,7 @@ B32 renderer_begin_frame(Renderer *renderer);
void renderer_end_frame(Renderer *renderer, Clay_RenderCommandArray render_commands);
void renderer_resize(Renderer *renderer, S32 width, S32 height);
void renderer_set_font_scale(Renderer *renderer, F32 scale);
void renderer_sync_from_parent(Renderer *renderer); // sync shared font atlas from parent
void renderer_set_clear_color(Renderer *renderer, F32 r, F32 g, F32 b);
// Text measurement callback compatible with UI_MeasureTextFn

View File

@@ -1537,3 +1537,12 @@ void renderer_set_font_scale(Renderer *r, F32 scale) {
if (r->font_texture) { r->font_texture->Release(); r->font_texture = nullptr; }
create_font_atlas(r, target_size);
}
void renderer_sync_from_parent(Renderer *r) {
if (!r || !r->parent) return;
Renderer *p = r->parent;
r->font_texture = p->font_texture;
r->font_atlas_size = p->font_atlas_size;
r->font_line_height = p->font_line_height;
memcpy(r->glyphs, p->glyphs, sizeof(r->glyphs));
}

View File

@@ -1003,6 +1003,16 @@ void renderer_set_font_scale(Renderer *r, F32 scale) {
create_font_atlas(r, target_size);
}
void renderer_sync_from_parent(Renderer *r) {
if (!r || !r->parent) return;
Renderer *p = r->parent;
r->font_texture = p->font_texture;
r->font_sampler = p->font_sampler;
r->font_atlas_size = p->font_atlas_size;
r->font_line_height = p->font_line_height;
memcpy(r->glyphs, p->glyphs, sizeof(r->glyphs));
}
void renderer_resize(Renderer *r, S32 width, S32 height) {
if (width <= 0 || height <= 0) return;
r->width = width;

View File

@@ -126,6 +126,20 @@ static const char *g_icon_svgs[UI_ICON_COUNT] = {
R"(<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="8" fill="white"/>
</svg>)",
// UI_ICON_POP_OUT - box with arrow pointing out (top-right)
R"(<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<rect x="3" y="6" width="14" height="14" rx="2" stroke="white" stroke-width="2" fill="none"/>
<path d="M14 3 L21 3 L21 10" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<path d="M21 3 L12 12" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>)",
// UI_ICON_POP_IN - arrow pointing into a box (bottom-left)
R"(<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<rect x="3" y="6" width="14" height="14" rx="2" stroke="white" stroke-width="2" fill="none"/>
<path d="M21 3 L12 12" stroke="white" stroke-width="2" stroke-linecap="round"/>
<path d="M12 5 L12 12 L19 12" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>)",
};
U8 *ui_icons_rasterize_atlas(S32 *out_w, S32 *out_h, S32 icon_size) {

View File

@@ -14,6 +14,8 @@ enum UI_IconID {
UI_ICON_TRANSPORT_STOP,
UI_ICON_TRANSPORT_PLAY,
UI_ICON_TRANSPORT_RECORD,
UI_ICON_POP_OUT,
UI_ICON_POP_IN,
UI_ICON_COUNT
};

View File

@@ -2,6 +2,12 @@
static PopupWindow g_popups[MAX_POPUP_WINDOWS];
static void popup_frame_callback(void *user_data) {
PopupWindow *popup = (PopupWindow *)user_data;
if (popup && popup->alive)
popup_do_frame(popup, 1.0f / 60.0f);
}
PopupWindow *popup_find_by_flag(B32 *flag) {
for (S32 i = 0; i < MAX_POPUP_WINDOWS; i++)
if (g_popups[i].alive && g_popups[i].open_flag == flag)
@@ -12,7 +18,8 @@ PopupWindow *popup_find_by_flag(B32 *flag) {
PopupWindow *popup_open(PlatformWindow *parent_window, Renderer *parent_renderer,
const char *title, B32 *open_flag,
S32 width, S32 height,
UI_WindowContentFn content_fn, void *user_data) {
UI_WindowContentFn content_fn, void *user_data,
PlatformWindowStyle style, B32 independent) {
// Find free slot
PopupWindow *popup = nullptr;
for (S32 i = 0; i < MAX_POPUP_WINDOWS; i++) {
@@ -27,8 +34,9 @@ PopupWindow *popup_open(PlatformWindow *parent_window, Renderer *parent_renderer
desc.title = title;
desc.width = width;
desc.height = height;
desc.style = PLATFORM_WINDOW_STYLE_POPUP;
desc.parent = parent_window;
desc.style = style;
desc.parent = parent_window;
desc.independent = independent;
popup->platform_window = platform_create_window(&desc);
if (!popup->platform_window) return nullptr;
@@ -61,6 +69,8 @@ PopupWindow *popup_open(PlatformWindow *parent_window, Renderer *parent_renderer
popup->title = title;
popup->wstate = {};
platform_set_frame_callback(popup->platform_window, popup_frame_callback, popup);
return popup;
}
@@ -95,6 +105,19 @@ void popup_do_frame(PopupWindow *popup, F32 dt) {
// Gather input from popup window
PlatformInput input = platform_get_input(popup->platform_window);
// Handle Cmd+/- zoom (propagate to global scale)
for (S32 k = 0; k < input.key_count; k++) {
if (input.ctrl_held) {
if (input.keys[k] == PKEY_EQUAL) g_ui_scale *= 1.1f;
if (input.keys[k] == PKEY_MINUS) g_ui_scale /= 1.1f;
if (input.keys[k] == PKEY_0) g_ui_scale = 1.0f;
}
}
g_ui_scale = Clamp(0.5f, g_ui_scale, 3.0f);
// Sync shared font atlas from parent renderer (picks up zoom changes)
renderer_sync_from_parent(popup->renderer);
// Swap widget state
UI_WidgetState saved_wstate = g_wstate;
g_wstate = popup->wstate;

View File

@@ -23,7 +23,9 @@ struct PopupWindow {
PopupWindow *popup_open(PlatformWindow *parent_window, Renderer *parent_renderer,
const char *title, B32 *open_flag,
S32 width, S32 height,
UI_WindowContentFn content_fn, void *user_data);
UI_WindowContentFn content_fn, void *user_data,
PlatformWindowStyle style = PLATFORM_WINDOW_STYLE_POPUP,
B32 independent = 0);
void popup_close(PopupWindow *popup);
PopupWindow *popup_find_by_flag(B32 *flag);
void popup_do_frame(PopupWindow *popup, F32 dt);