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);