diff --git a/.vscode/launch.json b/.vscode/launch.json index 8488fe3..f6a7815 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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" } ] } diff --git a/src/main.cpp b/src/main.cpp index 8b77a9c..625d963 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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(); diff --git a/src/platform/platform.h b/src/platform/platform.h index 6c15d9b..0cd8859 100644 --- a/src/platform/platform.h +++ b/src/platform/platform.h @@ -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); diff --git a/src/platform/platform_macos.mm b/src/platform/platform_macos.mm index 40017c3..fb857fe 100644 --- a/src/platform/platform_macos.mm +++ b/src/platform/platform_macos.mm @@ -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 diff --git a/src/platform/platform_win32.cpp b/src/platform/platform_win32.cpp index eae8c5c..b6775df 100644 --- a/src/platform/platform_win32.cpp +++ b/src/platform/platform_win32.cpp @@ -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; diff --git a/src/renderer/renderer.h b/src/renderer/renderer.h index 7681f97..d804fe9 100644 --- a/src/renderer/renderer.h +++ b/src/renderer/renderer.h @@ -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 diff --git a/src/renderer/renderer_dx12.cpp b/src/renderer/renderer_dx12.cpp index 76317e5..bfbecf2 100644 --- a/src/renderer/renderer_dx12.cpp +++ b/src/renderer/renderer_dx12.cpp @@ -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)); +} diff --git a/src/renderer/renderer_metal.mm b/src/renderer/renderer_metal.mm index d25836a..6033f01 100644 --- a/src/renderer/renderer_metal.mm +++ b/src/renderer/renderer_metal.mm @@ -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; diff --git a/src/ui/ui_icons.cpp b/src/ui/ui_icons.cpp index 04878c2..c1641c9 100644 --- a/src/ui/ui_icons.cpp +++ b/src/ui/ui_icons.cpp @@ -126,6 +126,20 @@ static const char *g_icon_svgs[UI_ICON_COUNT] = { R"( )", + + // UI_ICON_POP_OUT - box with arrow pointing out (top-right) + R"( + + + + )", + + // UI_ICON_POP_IN - arrow pointing into a box (bottom-left) + R"( + + + + )", }; U8 *ui_icons_rasterize_atlas(S32 *out_w, S32 *out_h, S32 icon_size) { diff --git a/src/ui/ui_icons.h b/src/ui/ui_icons.h index 304b02e..0bf8973 100644 --- a/src/ui/ui_icons.h +++ b/src/ui/ui_icons.h @@ -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 }; diff --git a/src/ui/ui_popups.cpp b/src/ui/ui_popups.cpp index 131e6aa..4c56ba7 100644 --- a/src/ui/ui_popups.cpp +++ b/src/ui/ui_popups.cpp @@ -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; diff --git a/src/ui/ui_popups.h b/src/ui/ui_popups.h index 8f96f3e..b4c8396 100644 --- a/src/ui/ui_popups.h +++ b/src/ui/ui_popups.h @@ -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);