Use platform windows for windowing instead of custom draggable window system
This commit is contained in:
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
199
src/main.cpp
199
src/main.cpp
@@ -12,12 +12,16 @@
|
||||
#include "ui/ui_core.h"
|
||||
#include "ui/ui_icons.h"
|
||||
#include "ui/ui_widgets.h"
|
||||
#include "ui/ui_popups.h"
|
||||
#include "ui/ui_piano.h"
|
||||
|
||||
// [cpp]
|
||||
#include "base/base_inc.cpp"
|
||||
#include "ui/ui_core.cpp"
|
||||
#include "ui/ui_icons.cpp"
|
||||
#include "ui/ui_widgets.cpp"
|
||||
#include "ui/ui_popups.cpp"
|
||||
#include "ui/ui_piano.cpp"
|
||||
#ifdef __APPLE__
|
||||
#include "platform/platform_macos.mm"
|
||||
#include "renderer/renderer_metal.mm"
|
||||
@@ -83,7 +87,7 @@ struct AppState {
|
||||
S32 bottom_panel_tab; // 0 = Item Editor, 1 = Sample Mapper
|
||||
|
||||
// Piano state
|
||||
S32 piano_mouse_note; // MIDI note held by mouse click (-1 = none)
|
||||
UI_PianoState piano_state;
|
||||
|
||||
// Demo widget state
|
||||
B32 demo_checkbox_a;
|
||||
@@ -159,68 +163,6 @@ struct AppState {
|
||||
};
|
||||
|
||||
////////////////////////////////
|
||||
// Piano helpers
|
||||
|
||||
#define PIANO_FIRST_NOTE 21 // A0
|
||||
#define PIANO_LAST_NOTE 108 // C8
|
||||
#define PIANO_BLACK_W 11.0f
|
||||
#define PIANO_BLACK_H_PCT 0.6f
|
||||
|
||||
static B32 piano_is_black_key(S32 note) {
|
||||
S32 n = note % 12;
|
||||
return n == 1 || n == 3 || n == 6 || n == 8 || n == 10;
|
||||
}
|
||||
|
||||
// Velocity-based color: blue (vel 0) → green (mid) → red (vel 127)
|
||||
static Clay_Color velocity_color(S32 velocity) {
|
||||
F32 t = (F32)velocity / 127.0f;
|
||||
F32 r, g, b;
|
||||
if (t < 0.5f) {
|
||||
F32 s = t * 2.0f;
|
||||
r = 40.0f + s * (76.0f - 40.0f);
|
||||
g = 120.0f + s * (175.0f - 120.0f);
|
||||
b = 220.0f + s * (80.0f - 220.0f);
|
||||
} else {
|
||||
F32 s = (t - 0.5f) * 2.0f;
|
||||
r = 76.0f + s * (220.0f - 76.0f);
|
||||
g = 175.0f + s * (50.0f - 175.0f);
|
||||
b = 80.0f + s * (40.0f - 80.0f);
|
||||
}
|
||||
return Clay_Color{r, g, b, 255};
|
||||
}
|
||||
|
||||
// Piano input: handle mouse clicks on piano keys
|
||||
static void update_piano_input(AppState *app) {
|
||||
if (app->master_layout != 0) return;
|
||||
PlatformInput input = g_wstate.input;
|
||||
|
||||
if (!input.mouse_down) {
|
||||
app->piano_mouse_note = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Find hovered piano key — check black keys first (they're on top)
|
||||
S32 hovered_note = -1;
|
||||
for (S32 note = PIANO_FIRST_NOTE; note <= PIANO_LAST_NOTE; note++) {
|
||||
if (piano_is_black_key(note) && Clay_PointerOver(CLAY_IDI("PKey", note))) {
|
||||
hovered_note = note;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hovered_note == -1) {
|
||||
for (S32 note = PIANO_FIRST_NOTE; note <= PIANO_LAST_NOTE; note++) {
|
||||
if (!piano_is_black_key(note) && Clay_PointerOver(CLAY_IDI("PKey", note))) {
|
||||
hovered_note = note;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hovered_note != -1) {
|
||||
app->piano_mouse_note = hovered_note;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Panel builders
|
||||
|
||||
@@ -654,7 +596,7 @@ static void build_right_panel(AppState *app) {
|
||||
// Idle = dark gray
|
||||
Clay_Color box_color;
|
||||
if (dev->active) {
|
||||
box_color = velocity_color(dev->velocity);
|
||||
box_color = piano_velocity_color(dev->velocity);
|
||||
} else if (dev->releasing) {
|
||||
box_color = Clay_Color{255, 255, 255, 255};
|
||||
} else {
|
||||
@@ -791,83 +733,9 @@ static void build_log_panel(AppState *app) {
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM,
|
||||
}
|
||||
) {
|
||||
Clay_ElementId piano_id = CLAY_ID("PianoContainer");
|
||||
CLAY(piano_id,
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
|
||||
.layoutDirection = CLAY_LEFT_TO_RIGHT,
|
||||
}
|
||||
) {
|
||||
// Compute black key size proportional to white keys
|
||||
F32 piano_avail_h = uis(app->log_height) - TAB_HEIGHT - uip(8);
|
||||
F32 black_key_h = piano_avail_h * PIANO_BLACK_H_PCT;
|
||||
if (black_key_h < uis(20)) black_key_h = uis(20);
|
||||
|
||||
F32 white_key_w = ((F32)app->last_w - uip(8)) / 52.0f;
|
||||
F32 black_key_w = white_key_w * 0.6f;
|
||||
if (black_key_w < uis(8)) black_key_w = uis(8);
|
||||
|
||||
// White keys (grow to fill width and height)
|
||||
for (S32 note = PIANO_FIRST_NOTE; note <= PIANO_LAST_NOTE; note++) {
|
||||
if (piano_is_black_key(note)) continue;
|
||||
|
||||
B32 midi_held = midi_is_note_held(app->midi, note);
|
||||
B32 mouse_held = app->piano_mouse_note == note;
|
||||
Clay_Color bg;
|
||||
if (midi_held) {
|
||||
bg = velocity_color(midi_get_note_velocity(app->midi, note));
|
||||
} else if (mouse_held) {
|
||||
bg = g_theme.accent;
|
||||
} else {
|
||||
bg = Clay_Color{240, 240, 240, 255};
|
||||
}
|
||||
|
||||
CLAY(CLAY_IDI("PKey", note),
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
|
||||
},
|
||||
.backgroundColor = bg,
|
||||
.border = { .color = {190, 190, 190, 255}, .width = { .right = 1 } },
|
||||
) {}
|
||||
}
|
||||
|
||||
// Black keys (floating, attached to left white key)
|
||||
for (S32 note = PIANO_FIRST_NOTE; note <= PIANO_LAST_NOTE; note++) {
|
||||
if (!piano_is_black_key(note)) continue;
|
||||
|
||||
Clay_ElementId parent_wkey = CLAY_IDI("PKey", note - 1);
|
||||
B32 midi_held = midi_is_note_held(app->midi, note);
|
||||
B32 mouse_held = app->piano_mouse_note == note;
|
||||
Clay_Color bg;
|
||||
if (midi_held) {
|
||||
bg = velocity_color(midi_get_note_velocity(app->midi, note));
|
||||
} else if (mouse_held) {
|
||||
bg = g_theme.accent;
|
||||
} else {
|
||||
bg = Clay_Color{25, 25, 30, 255};
|
||||
}
|
||||
|
||||
CLAY(CLAY_IDI("PKey", note),
|
||||
.layout = {
|
||||
.sizing = {
|
||||
.width = CLAY_SIZING_FIXED(black_key_w),
|
||||
.height = CLAY_SIZING_FIXED(black_key_h),
|
||||
},
|
||||
},
|
||||
.backgroundColor = bg,
|
||||
.cornerRadius = { .topLeft = 0, .topRight = 0, .bottomLeft = uis(2), .bottomRight = uis(2) },
|
||||
.floating = {
|
||||
.parentId = parent_wkey.id,
|
||||
.zIndex = 100,
|
||||
.attachPoints = {
|
||||
.element = CLAY_ATTACH_POINT_CENTER_TOP,
|
||||
.parent = CLAY_ATTACH_POINT_RIGHT_TOP,
|
||||
},
|
||||
.attachTo = CLAY_ATTACH_TO_ELEMENT_WITH_ID,
|
||||
},
|
||||
) {}
|
||||
}
|
||||
}
|
||||
F32 piano_avail_h = uis(app->log_height) - TAB_HEIGHT - uip(8);
|
||||
F32 piano_avail_w = (F32)app->last_w - uip(8);
|
||||
ui_piano(&app->piano_state, app->midi, piano_avail_w, piano_avail_h);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1882,25 +1750,6 @@ static void build_ui(AppState *app) {
|
||||
}
|
||||
}
|
||||
|
||||
// Draggable windows (rendered as floating elements above normal UI)
|
||||
ui_window("WinSettings", "Preferences", &app->show_settings_window,
|
||||
Vec2F32{100, 100}, Vec2F32{480, 0},
|
||||
settings_window_content, app);
|
||||
|
||||
ui_window("WinAbout", "About", &app->show_about_window,
|
||||
Vec2F32{200, 150}, Vec2F32{260, 0},
|
||||
about_window_content, nullptr);
|
||||
|
||||
// Modal confirmation dialog
|
||||
if (app->show_confirm_dialog) {
|
||||
static const char *confirm_buttons[] = { "Cancel", "OK" };
|
||||
S32 modal_result = ui_modal("ModalConfirm", "Confirm Action",
|
||||
"Are you sure you want to proceed? This action cannot be undone.",
|
||||
confirm_buttons, 2);
|
||||
if (modal_result != -1) {
|
||||
app->show_confirm_dialog = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
@@ -1990,7 +1839,7 @@ static void do_frame(AppState *app) {
|
||||
}
|
||||
|
||||
ui_widgets_begin_frame(input);
|
||||
update_piano_input(app);
|
||||
if (app->master_layout == 0) ui_piano_update_input(&app->piano_state);
|
||||
update_panel_splitters(app);
|
||||
|
||||
// Build UI with Clay
|
||||
@@ -2071,7 +1920,7 @@ int main(int argc, char **argv) {
|
||||
app.show_log = 1;
|
||||
app.show_midi_devices = 1;
|
||||
app.bottom_panel_tab = 0;
|
||||
app.piano_mouse_note = -1;
|
||||
app.piano_state.mouse_note = -1;
|
||||
app.demo_knob_unsigned = 75.0f;
|
||||
app.demo_knob_signed = 0.0f;
|
||||
app.demo_slider_h = 50.0f;
|
||||
@@ -2116,9 +1965,35 @@ int main(int argc, char **argv) {
|
||||
default: break;
|
||||
}
|
||||
|
||||
// Native confirmation dialog (blocking)
|
||||
if (app.show_confirm_dialog) {
|
||||
S32 result = platform_message_box(window, "Confirm Action",
|
||||
"Are you sure you want to proceed? This action cannot be undone.",
|
||||
PLATFORM_MSGBOX_OK_CANCEL);
|
||||
app.show_confirm_dialog = 0;
|
||||
(void)result; // result: 0 = OK, 1 = Cancel
|
||||
}
|
||||
|
||||
// Open popup windows when flags transition to 1
|
||||
if (app.show_settings_window && !popup_find_by_flag(&app.show_settings_window))
|
||||
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);
|
||||
|
||||
// Check for OS close on popups
|
||||
popup_close_check();
|
||||
|
||||
if (running) do_frame(&app);
|
||||
|
||||
// Render popup windows
|
||||
// Compute dt for popups (same as main frame)
|
||||
F32 popup_dt = 1.0f / 60.0f; // approximate; popups don't need precise timing
|
||||
for (S32 i = 0; i < MAX_POPUP_WINDOWS; i++) {
|
||||
if (g_popups[i].alive) popup_do_frame(&g_popups[i], popup_dt);
|
||||
}
|
||||
}
|
||||
|
||||
popup_close_all();
|
||||
audio_destroy(audio);
|
||||
midi_destroy(midi);
|
||||
ui_destroy(ui);
|
||||
|
||||
@@ -53,10 +53,23 @@ struct PlatformInput {
|
||||
|
||||
struct PlatformWindow;
|
||||
|
||||
enum PlatformWindowStyle {
|
||||
PLATFORM_WINDOW_STYLE_NORMAL = 0,
|
||||
PLATFORM_WINDOW_STYLE_POPUP = 1, // utility panel, owned by parent
|
||||
};
|
||||
|
||||
struct PlatformWindowDesc {
|
||||
const char *title = "autosample";
|
||||
S32 width = 1280;
|
||||
S32 height = 720;
|
||||
const char *title = "autosample";
|
||||
S32 width = 1280;
|
||||
S32 height = 720;
|
||||
PlatformWindowStyle style = PLATFORM_WINDOW_STYLE_NORMAL;
|
||||
PlatformWindow *parent = nullptr;
|
||||
};
|
||||
|
||||
enum PlatformMsgBoxType {
|
||||
PLATFORM_MSGBOX_OK = 0,
|
||||
PLATFORM_MSGBOX_OK_CANCEL = 1,
|
||||
PLATFORM_MSGBOX_YES_NO = 2,
|
||||
};
|
||||
|
||||
struct PlatformMenuItem {
|
||||
@@ -88,6 +101,13 @@ S32 platform_poll_menu_command(PlatformWindow *window);
|
||||
// Returns accumulated input since last call (keyboard events + polled mouse state), then clears the buffer.
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
// Cursor shapes for resize handles
|
||||
enum PlatformCursor {
|
||||
PLATFORM_CURSOR_ARROW = 0,
|
||||
|
||||
@@ -47,7 +47,9 @@ static U8 macos_keycode_to_pkey(U16 keycode) {
|
||||
// Forward declarations
|
||||
|
||||
struct PlatformWindow;
|
||||
static PlatformWindow *g_current_window = nullptr;
|
||||
|
||||
// Main window receives menu commands
|
||||
static PlatformWindow *g_main_window = nullptr;
|
||||
|
||||
////////////////////////////////
|
||||
// Objective-C helper classes
|
||||
@@ -60,31 +62,97 @@ static PlatformWindow *g_current_window = nullptr;
|
||||
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { (void)sender; return YES; }
|
||||
@end
|
||||
|
||||
@interface ASmplWindowDelegate : NSObject <NSWindowDelegate>
|
||||
@interface ASmplWindowDelegate : NSObject <NSWindowDelegate> {
|
||||
@public
|
||||
PlatformWindow *_platformWindow;
|
||||
}
|
||||
@end
|
||||
|
||||
@interface ASmplView : NSView <NSTextInputClient> {
|
||||
@public
|
||||
PlatformWindow *_platformWindow;
|
||||
}
|
||||
@end
|
||||
|
||||
////////////////////////////////
|
||||
// PlatformWindow struct
|
||||
|
||||
struct PlatformWindow {
|
||||
NSWindow *ns_window;
|
||||
ASmplView *view;
|
||||
ASmplWindowDelegate *delegate;
|
||||
B32 should_close;
|
||||
S32 width;
|
||||
S32 height;
|
||||
S32 pending_menu_cmd;
|
||||
PlatformFrameCallback frame_callback;
|
||||
void *frame_callback_user_data;
|
||||
PlatformInput input;
|
||||
B32 prev_mouse_down;
|
||||
B32 mouse_down_state;
|
||||
F32 backing_scale;
|
||||
};
|
||||
|
||||
////////////////////////////////
|
||||
// C callback helpers (called from ObjC via PlatformWindow pointer)
|
||||
|
||||
static void platform_macos_insert_text_pw(PlatformWindow *pw, const char *utf8) {
|
||||
if (!pw || !utf8) return;
|
||||
PlatformInput *ev = &pw->input;
|
||||
while (*utf8 && ev->char_count < PLATFORM_MAX_CHARS_PER_FRAME) {
|
||||
U8 c = (U8)*utf8;
|
||||
if (c < 32) { utf8++; continue; }
|
||||
// Handle ASCII printable range (single-byte UTF-8)
|
||||
if (c < 0x80) {
|
||||
ev->chars[ev->char_count++] = (U16)c;
|
||||
utf8++;
|
||||
} else {
|
||||
// Skip multi-byte UTF-8 sequences for now (UI only handles ASCII)
|
||||
if (c < 0xE0) utf8 += 2;
|
||||
else if (c < 0xF0) utf8 += 3;
|
||||
else utf8 += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void platform_macos_key_down_pw(PlatformWindow *pw, U16 keycode, NSEventModifierFlags mods) {
|
||||
if (!pw) return;
|
||||
PlatformInput *ev = &pw->input;
|
||||
|
||||
U8 pkey = macos_keycode_to_pkey(keycode);
|
||||
if (pkey && ev->key_count < PLATFORM_MAX_KEYS_PER_FRAME)
|
||||
ev->keys[ev->key_count++] = pkey;
|
||||
|
||||
// Command = ctrl_held (macOS convention: Cmd+C, Cmd+V, etc.)
|
||||
ev->ctrl_held = (mods & NSEventModifierFlagCommand) != 0;
|
||||
ev->shift_held = (mods & NSEventModifierFlagShift) != 0;
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// ObjC class implementations
|
||||
|
||||
@implementation ASmplWindowDelegate
|
||||
- (BOOL)windowShouldClose:(id)sender {
|
||||
(void)sender;
|
||||
if (g_current_window) {
|
||||
// Set should_close flag (accessed via the struct below)
|
||||
extern void platform_macos_set_should_close();
|
||||
platform_macos_set_should_close();
|
||||
if (_platformWindow) {
|
||||
_platformWindow->should_close = true;
|
||||
}
|
||||
return NO; // We handle closing ourselves
|
||||
}
|
||||
|
||||
- (void)windowDidResize:(NSNotification *)notification {
|
||||
(void)notification;
|
||||
if (!g_current_window) return;
|
||||
extern void platform_macos_handle_resize();
|
||||
platform_macos_handle_resize();
|
||||
if (!_platformWindow) return;
|
||||
PlatformWindow *pw = _platformWindow;
|
||||
NSRect frame = [pw->view bounds];
|
||||
F32 scale = pw->backing_scale;
|
||||
pw->width = (S32)(frame.size.width * scale);
|
||||
pw->height = (S32)(frame.size.height * scale);
|
||||
if (pw->frame_callback)
|
||||
pw->frame_callback(pw->frame_callback_user_data);
|
||||
}
|
||||
@end
|
||||
|
||||
@interface ASmplView : NSView <NSTextInputClient>
|
||||
@end
|
||||
|
||||
@implementation ASmplView
|
||||
|
||||
- (BOOL)acceptsFirstResponder { return YES; }
|
||||
@@ -117,13 +185,11 @@ static PlatformWindow *g_current_window = nullptr;
|
||||
else
|
||||
str = (NSString *)string;
|
||||
|
||||
extern void platform_macos_insert_text(const char *utf8);
|
||||
platform_macos_insert_text([str UTF8String]);
|
||||
platform_macos_insert_text_pw(_platformWindow, [str UTF8String]);
|
||||
}
|
||||
|
||||
- (void)keyDown:(NSEvent *)event {
|
||||
extern void platform_macos_key_down(U16 keycode, NSEventModifierFlags mods);
|
||||
platform_macos_key_down([event keyCode], [event modifierFlags]);
|
||||
platform_macos_key_down_pw(_platformWindow, [event keyCode], [event modifierFlags]);
|
||||
|
||||
// Feed into text input system for character generation
|
||||
[self interpretKeyEvents:@[event]];
|
||||
@@ -136,112 +202,29 @@ static PlatformWindow *g_current_window = nullptr;
|
||||
|
||||
- (void)mouseDown:(NSEvent *)event {
|
||||
(void)event;
|
||||
extern void platform_macos_mouse_down();
|
||||
platform_macos_mouse_down();
|
||||
if (_platformWindow) _platformWindow->mouse_down_state = 1;
|
||||
}
|
||||
|
||||
- (void)mouseUp:(NSEvent *)event {
|
||||
(void)event;
|
||||
extern void platform_macos_mouse_up();
|
||||
platform_macos_mouse_up();
|
||||
if (_platformWindow) _platformWindow->mouse_down_state = 0;
|
||||
}
|
||||
|
||||
- (void)mouseMoved:(NSEvent *)event { (void)event; }
|
||||
- (void)mouseDragged:(NSEvent *)event { (void)event; }
|
||||
|
||||
- (void)scrollWheel:(NSEvent *)event {
|
||||
extern void platform_macos_scroll(F32 dx, F32 dy);
|
||||
if (!_platformWindow) return;
|
||||
F32 dy = (F32)[event scrollingDeltaY];
|
||||
if ([event hasPreciseScrollingDeltas])
|
||||
dy /= 40.0f; // Normalize trackpad deltas to match discrete wheel steps
|
||||
platform_macos_scroll(0, dy);
|
||||
_platformWindow->input.scroll_delta.y += dy;
|
||||
}
|
||||
|
||||
- (BOOL)acceptsFirstMouse:(NSEvent *)event { (void)event; return YES; }
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////
|
||||
// PlatformWindow struct
|
||||
|
||||
struct PlatformWindow {
|
||||
NSWindow *ns_window;
|
||||
ASmplView *view;
|
||||
ASmplWindowDelegate *delegate;
|
||||
B32 should_close;
|
||||
S32 width;
|
||||
S32 height;
|
||||
S32 pending_menu_cmd;
|
||||
PlatformFrameCallback frame_callback;
|
||||
void *frame_callback_user_data;
|
||||
PlatformInput input;
|
||||
B32 prev_mouse_down;
|
||||
B32 mouse_down_state;
|
||||
F32 backing_scale;
|
||||
};
|
||||
|
||||
////////////////////////////////
|
||||
// C callback helpers (called from ObjC)
|
||||
|
||||
void platform_macos_set_should_close() {
|
||||
if (g_current_window) g_current_window->should_close = true;
|
||||
}
|
||||
|
||||
void platform_macos_handle_resize() {
|
||||
if (!g_current_window) return;
|
||||
NSRect frame = [g_current_window->view bounds];
|
||||
F32 scale = g_current_window->backing_scale;
|
||||
g_current_window->width = (S32)(frame.size.width * scale);
|
||||
g_current_window->height = (S32)(frame.size.height * scale);
|
||||
if (g_current_window->frame_callback)
|
||||
g_current_window->frame_callback(g_current_window->frame_callback_user_data);
|
||||
}
|
||||
|
||||
void platform_macos_insert_text(const char *utf8) {
|
||||
if (!g_current_window || !utf8) return;
|
||||
PlatformInput *ev = &g_current_window->input;
|
||||
while (*utf8 && ev->char_count < PLATFORM_MAX_CHARS_PER_FRAME) {
|
||||
U8 c = (U8)*utf8;
|
||||
if (c < 32) { utf8++; continue; }
|
||||
// Handle ASCII printable range (single-byte UTF-8)
|
||||
if (c < 0x80) {
|
||||
ev->chars[ev->char_count++] = (U16)c;
|
||||
utf8++;
|
||||
} else {
|
||||
// Skip multi-byte UTF-8 sequences for now (UI only handles ASCII)
|
||||
if (c < 0xE0) utf8 += 2;
|
||||
else if (c < 0xF0) utf8 += 3;
|
||||
else utf8 += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void platform_macos_key_down(U16 keycode, NSEventModifierFlags mods) {
|
||||
if (!g_current_window) return;
|
||||
PlatformInput *ev = &g_current_window->input;
|
||||
|
||||
U8 pkey = macos_keycode_to_pkey(keycode);
|
||||
if (pkey && ev->key_count < PLATFORM_MAX_KEYS_PER_FRAME)
|
||||
ev->keys[ev->key_count++] = pkey;
|
||||
|
||||
// Command = ctrl_held (macOS convention: Cmd+C, Cmd+V, etc.)
|
||||
ev->ctrl_held = (mods & NSEventModifierFlagCommand) != 0;
|
||||
ev->shift_held = (mods & NSEventModifierFlagShift) != 0;
|
||||
}
|
||||
|
||||
void platform_macos_mouse_down() {
|
||||
if (g_current_window) g_current_window->mouse_down_state = 1;
|
||||
}
|
||||
|
||||
void platform_macos_mouse_up() {
|
||||
if (g_current_window) g_current_window->mouse_down_state = 0;
|
||||
}
|
||||
|
||||
void platform_macos_scroll(F32 dx, F32 dy) {
|
||||
(void)dx;
|
||||
if (g_current_window) g_current_window->input.scroll_delta.y += dy;
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Menu action handler
|
||||
|
||||
@@ -251,9 +234,9 @@ void platform_macos_scroll(F32 dx, F32 dy) {
|
||||
|
||||
@implementation ASmplMenuTarget
|
||||
- (void)menuAction:(id)sender {
|
||||
if (!g_current_window) return;
|
||||
if (!g_main_window) return;
|
||||
NSMenuItem *item = (NSMenuItem *)sender;
|
||||
g_current_window->pending_menu_cmd = (S32)[item tag];
|
||||
g_main_window->pending_menu_cmd = (S32)[item tag];
|
||||
}
|
||||
@end
|
||||
|
||||
@@ -276,10 +259,17 @@ PlatformWindow *platform_create_window(PlatformWindowDesc *desc) {
|
||||
|
||||
NSRect content_rect = NSMakeRect(0, 0, desc->width, desc->height);
|
||||
|
||||
NSWindowStyleMask style_mask;
|
||||
if (desc->style == PLATFORM_WINDOW_STYLE_POPUP) {
|
||||
style_mask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable;
|
||||
} else {
|
||||
style_mask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
|
||||
NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
|
||||
}
|
||||
|
||||
NSWindow *ns_window = [[NSWindow alloc]
|
||||
initWithContentRect:content_rect
|
||||
styleMask:(NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
|
||||
NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable)
|
||||
styleMask:style_mask
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
|
||||
@@ -306,7 +296,19 @@ PlatformWindow *platform_create_window(PlatformWindowDesc *desc) {
|
||||
window->width = (S32)(desc->width * window->backing_scale);
|
||||
window->height = (S32)(desc->height * window->backing_scale);
|
||||
|
||||
g_current_window = window;
|
||||
// Wire up per-window pointers
|
||||
view->_platformWindow = window;
|
||||
delegate->_platformWindow = window;
|
||||
|
||||
// Track main window for menu commands
|
||||
if (desc->style == PLATFORM_WINDOW_STYLE_NORMAL) {
|
||||
g_main_window = window;
|
||||
}
|
||||
|
||||
// If popup, add as child of parent
|
||||
if (desc->style == PLATFORM_WINDOW_STYLE_POPUP && desc->parent) {
|
||||
[desc->parent->ns_window addChildWindow:ns_window ordered:NSWindowAbove];
|
||||
}
|
||||
|
||||
[ns_window makeKeyAndOrderFront:nil];
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
@@ -317,9 +319,15 @@ PlatformWindow *platform_create_window(PlatformWindowDesc *desc) {
|
||||
void platform_destroy_window(PlatformWindow *window) {
|
||||
if (!window) return;
|
||||
|
||||
// Remove from parent if it's a child window
|
||||
NSWindow *parent = [window->ns_window parentWindow];
|
||||
if (parent) {
|
||||
[parent removeChildWindow:window->ns_window];
|
||||
}
|
||||
|
||||
[window->ns_window close];
|
||||
if (g_current_window == window)
|
||||
g_current_window = nullptr;
|
||||
if (g_main_window == window)
|
||||
g_main_window = nullptr;
|
||||
delete window;
|
||||
}
|
||||
|
||||
@@ -436,6 +444,10 @@ PlatformInput platform_get_input(PlatformWindow *window) {
|
||||
return result;
|
||||
}
|
||||
|
||||
B32 platform_window_should_close(PlatformWindow *window) {
|
||||
return window ? window->should_close : 0;
|
||||
}
|
||||
|
||||
F32 platform_get_dpi_scale(PlatformWindow *window) {
|
||||
(void)window;
|
||||
return 1.0f; // macOS handles Retina via backing scale factor, not DPI
|
||||
@@ -474,3 +486,38 @@ const char *platform_clipboard_get() {
|
||||
|
||||
return buf[0] ? buf : nullptr;
|
||||
}
|
||||
|
||||
S32 platform_message_box(PlatformWindow *parent, const char *title,
|
||||
const char *message, PlatformMsgBoxType type) {
|
||||
@autoreleasepool {
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
[alert setMessageText:[NSString stringWithUTF8String:title]];
|
||||
[alert setInformativeText:[NSString stringWithUTF8String:message]];
|
||||
|
||||
switch (type) {
|
||||
case PLATFORM_MSGBOX_OK:
|
||||
[alert addButtonWithTitle:@"OK"];
|
||||
break;
|
||||
case PLATFORM_MSGBOX_OK_CANCEL:
|
||||
[alert addButtonWithTitle:@"OK"];
|
||||
[alert addButtonWithTitle:@"Cancel"];
|
||||
break;
|
||||
case PLATFORM_MSGBOX_YES_NO:
|
||||
[alert addButtonWithTitle:@"Yes"];
|
||||
[alert addButtonWithTitle:@"No"];
|
||||
break;
|
||||
}
|
||||
|
||||
NSModalResponse response;
|
||||
if (parent && parent->ns_window) {
|
||||
response = [alert runModal];
|
||||
} else {
|
||||
response = [alert runModal];
|
||||
}
|
||||
|
||||
// NSAlertFirstButtonReturn = 1000, Second = 1001
|
||||
if (response == NSAlertFirstButtonReturn) return 0;
|
||||
if (response == NSAlertSecondButtonReturn) return 1;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,33 +18,37 @@ struct PlatformWindow {
|
||||
B32 prev_mouse_down;
|
||||
};
|
||||
|
||||
static PlatformWindow *g_current_window = nullptr;
|
||||
// Main window receives menu commands
|
||||
static PlatformWindow *g_main_window = nullptr;
|
||||
static HCURSOR g_current_cursor = nullptr;
|
||||
static B32 g_wndclass_registered = false;
|
||||
|
||||
static LRESULT CALLBACK win32_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
|
||||
PlatformWindow *pw = (PlatformWindow *)GetWindowLongPtr(hwnd, GWLP_USERDATA);
|
||||
|
||||
switch (msg) {
|
||||
case WM_SIZE:
|
||||
if (g_current_window && wparam != SIZE_MINIMIZED) {
|
||||
g_current_window->width = (S32)LOWORD(lparam);
|
||||
g_current_window->height = (S32)HIWORD(lparam);
|
||||
if (pw && wparam != SIZE_MINIMIZED) {
|
||||
pw->width = (S32)LOWORD(lparam);
|
||||
pw->height = (S32)HIWORD(lparam);
|
||||
// Render a frame during the modal resize loop so the UI
|
||||
// stays responsive instead of showing a stretched image.
|
||||
if (g_current_window->frame_callback) {
|
||||
g_current_window->frame_callback(g_current_window->frame_callback_user_data);
|
||||
if (pw->frame_callback) {
|
||||
pw->frame_callback(pw->frame_callback_user_data);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
case WM_CHAR:
|
||||
if (g_current_window && wparam >= 32 && wparam < 0xFFFF) {
|
||||
PlatformInput *ev = &g_current_window->input;
|
||||
if (pw && wparam >= 32 && wparam < 0xFFFF) {
|
||||
PlatformInput *ev = &pw->input;
|
||||
if (ev->char_count < PLATFORM_MAX_CHARS_PER_FRAME)
|
||||
ev->chars[ev->char_count++] = (U16)wparam;
|
||||
}
|
||||
return 0;
|
||||
case WM_KEYDOWN:
|
||||
case WM_SYSKEYDOWN:
|
||||
if (g_current_window) {
|
||||
PlatformInput *ev = &g_current_window->input;
|
||||
if (pw) {
|
||||
PlatformInput *ev = &pw->input;
|
||||
if (ev->key_count < PLATFORM_MAX_KEYS_PER_FRAME)
|
||||
ev->keys[ev->key_count++] = (U8)wparam;
|
||||
ev->ctrl_held = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
|
||||
@@ -52,14 +56,15 @@ static LRESULT CALLBACK win32_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM
|
||||
}
|
||||
break; // fall through to DefWindowProc for system keys
|
||||
case WM_MOUSEWHEEL:
|
||||
if (g_current_window) {
|
||||
if (pw) {
|
||||
S16 wheel_delta = (S16)HIWORD(wparam);
|
||||
g_current_window->input.scroll_delta.y += (F32)wheel_delta / (F32)WHEEL_DELTA * 6.0f;
|
||||
pw->input.scroll_delta.y += (F32)wheel_delta / (F32)WHEEL_DELTA * 6.0f;
|
||||
}
|
||||
return 0;
|
||||
case WM_COMMAND:
|
||||
if (g_current_window && HIWORD(wparam) == 0)
|
||||
g_current_window->pending_menu_cmd = (S32)LOWORD(wparam);
|
||||
// Route menu commands to main window
|
||||
if (g_main_window && HIWORD(wparam) == 0)
|
||||
g_main_window->pending_menu_cmd = (S32)LOWORD(wparam);
|
||||
return 0;
|
||||
case WM_SETCURSOR:
|
||||
// When the cursor is in our client area, use the app-set cursor.
|
||||
@@ -69,7 +74,7 @@ static LRESULT CALLBACK win32_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM
|
||||
}
|
||||
break;
|
||||
case WM_DPICHANGED:
|
||||
if (g_current_window) {
|
||||
if (pw) {
|
||||
RECT *suggested = (RECT *)lparam;
|
||||
SetWindowPos(hwnd, nullptr, suggested->left, suggested->top,
|
||||
suggested->right - suggested->left, suggested->bottom - suggested->top,
|
||||
@@ -77,8 +82,8 @@ static LRESULT CALLBACK win32_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM
|
||||
}
|
||||
return 0;
|
||||
case WM_CLOSE:
|
||||
if (g_current_window)
|
||||
g_current_window->should_close = true;
|
||||
if (pw)
|
||||
pw->should_close = true;
|
||||
return 0;
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
@@ -94,14 +99,17 @@ static LRESULT CALLBACK win32_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM
|
||||
PlatformWindow *platform_create_window(PlatformWindowDesc *desc) {
|
||||
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
|
||||
WNDCLASSEXW wc = {};
|
||||
wc.cbSize = sizeof(wc);
|
||||
wc.style = CS_CLASSDC;
|
||||
wc.lpfnWndProc = win32_wndproc;
|
||||
wc.hInstance = GetModuleHandleW(nullptr);
|
||||
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
wc.lpszClassName = L"autosample_wc";
|
||||
RegisterClassExW(&wc);
|
||||
if (!g_wndclass_registered) {
|
||||
WNDCLASSEXW wc = {};
|
||||
wc.cbSize = sizeof(wc);
|
||||
wc.style = CS_CLASSDC;
|
||||
wc.lpfnWndProc = win32_wndproc;
|
||||
wc.hInstance = GetModuleHandleW(nullptr);
|
||||
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
wc.lpszClassName = L"autosample_wc";
|
||||
RegisterClassExW(&wc);
|
||||
g_wndclass_registered = true;
|
||||
}
|
||||
|
||||
UINT dpi = GetDpiForSystem();
|
||||
int screen_w = GetSystemMetrics(SM_CXSCREEN);
|
||||
@@ -109,20 +117,29 @@ PlatformWindow *platform_create_window(PlatformWindowDesc *desc) {
|
||||
int x = (screen_w - desc->width) / 2;
|
||||
int y = (screen_h - desc->height) / 2;
|
||||
|
||||
DWORD style;
|
||||
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;
|
||||
} else {
|
||||
style = WS_OVERLAPPEDWINDOW;
|
||||
}
|
||||
|
||||
RECT rect = { 0, 0, (LONG)desc->width, (LONG)desc->height };
|
||||
AdjustWindowRectExForDpi(&rect, WS_OVERLAPPEDWINDOW, FALSE, 0, dpi);
|
||||
AdjustWindowRectExForDpi(&rect, style, FALSE, 0, dpi);
|
||||
|
||||
int wchar_count = MultiByteToWideChar(CP_UTF8, 0, desc->title, -1, nullptr, 0);
|
||||
wchar_t *wtitle = (wchar_t *)_malloca(wchar_count * sizeof(wchar_t));
|
||||
MultiByteToWideChar(CP_UTF8, 0, desc->title, -1, wtitle, wchar_count);
|
||||
|
||||
HWND hwnd = CreateWindowExW(
|
||||
0, wc.lpszClassName, wtitle,
|
||||
WS_OVERLAPPEDWINDOW,
|
||||
0, L"autosample_wc", wtitle,
|
||||
style,
|
||||
x, y,
|
||||
rect.right - rect.left,
|
||||
rect.bottom - rect.top,
|
||||
nullptr, nullptr, wc.hInstance, nullptr
|
||||
parent_hwnd, nullptr, GetModuleHandleW(nullptr), nullptr
|
||||
);
|
||||
|
||||
_freea(wtitle);
|
||||
@@ -130,16 +147,23 @@ PlatformWindow *platform_create_window(PlatformWindowDesc *desc) {
|
||||
if (!hwnd)
|
||||
return nullptr;
|
||||
|
||||
ShowWindow(hwnd, SW_SHOWDEFAULT);
|
||||
UpdateWindow(hwnd);
|
||||
|
||||
PlatformWindow *window = new PlatformWindow();
|
||||
window->hwnd = hwnd;
|
||||
window->should_close = false;
|
||||
window->width = desc->width;
|
||||
window->height = desc->height;
|
||||
|
||||
g_current_window = window;
|
||||
// Store PlatformWindow* on the HWND so WndProc can find it
|
||||
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)window);
|
||||
|
||||
// Track main window for menu commands
|
||||
if (desc->style == PLATFORM_WINDOW_STYLE_NORMAL) {
|
||||
g_main_window = window;
|
||||
}
|
||||
|
||||
ShowWindow(hwnd, SW_SHOWDEFAULT);
|
||||
UpdateWindow(hwnd);
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
@@ -148,11 +172,10 @@ void platform_destroy_window(PlatformWindow *window) {
|
||||
|
||||
if (window->hwnd) {
|
||||
DestroyWindow(window->hwnd);
|
||||
UnregisterClassW(L"autosample_wc", GetModuleHandleW(nullptr));
|
||||
}
|
||||
|
||||
if (g_current_window == window)
|
||||
g_current_window = nullptr;
|
||||
if (g_main_window == window)
|
||||
g_main_window = nullptr;
|
||||
|
||||
delete window;
|
||||
}
|
||||
@@ -237,6 +260,10 @@ PlatformInput platform_get_input(PlatformWindow *window) {
|
||||
return result;
|
||||
}
|
||||
|
||||
B32 platform_window_should_close(PlatformWindow *window) {
|
||||
return window ? window->should_close : 0;
|
||||
}
|
||||
|
||||
F32 platform_get_dpi_scale(PlatformWindow *window) {
|
||||
if (!window || !window->hwnd) return 1.0f;
|
||||
return (F32)GetDpiForWindow(window->hwnd) / 96.0f;
|
||||
@@ -267,7 +294,7 @@ void platform_clipboard_set(const char *text) {
|
||||
wbuf[wlen] = L'\0';
|
||||
GlobalUnlock(hmem);
|
||||
|
||||
HWND hwnd = g_current_window ? g_current_window->hwnd : nullptr;
|
||||
HWND hwnd = g_main_window ? g_main_window->hwnd : nullptr;
|
||||
if (OpenClipboard(hwnd)) {
|
||||
EmptyClipboard();
|
||||
SetClipboardData(CF_UNICODETEXT, hmem);
|
||||
@@ -281,7 +308,7 @@ const char *platform_clipboard_get() {
|
||||
static char buf[4096];
|
||||
buf[0] = '\0';
|
||||
|
||||
HWND hwnd = g_current_window ? g_current_window->hwnd : nullptr;
|
||||
HWND hwnd = g_main_window ? g_main_window->hwnd : nullptr;
|
||||
if (!OpenClipboard(hwnd)) return nullptr;
|
||||
|
||||
HGLOBAL hmem = GetClipboardData(CF_UNICODETEXT);
|
||||
@@ -297,3 +324,37 @@ const char *platform_clipboard_get() {
|
||||
CloseClipboard();
|
||||
return buf[0] ? buf : nullptr;
|
||||
}
|
||||
|
||||
S32 platform_message_box(PlatformWindow *parent, const char *title,
|
||||
const char *message, PlatformMsgBoxType type) {
|
||||
UINT mb_type;
|
||||
switch (type) {
|
||||
case PLATFORM_MSGBOX_OK: mb_type = MB_OK; break;
|
||||
case PLATFORM_MSGBOX_OK_CANCEL: mb_type = MB_OKCANCEL; break;
|
||||
case PLATFORM_MSGBOX_YES_NO: mb_type = MB_YESNO; break;
|
||||
default: mb_type = MB_OK; break;
|
||||
}
|
||||
|
||||
// Convert UTF-8 to wide strings
|
||||
int title_wlen = MultiByteToWideChar(CP_UTF8, 0, title, -1, nullptr, 0);
|
||||
wchar_t *wtitle = (wchar_t *)_malloca(title_wlen * sizeof(wchar_t));
|
||||
MultiByteToWideChar(CP_UTF8, 0, title, -1, wtitle, title_wlen);
|
||||
|
||||
int msg_wlen = MultiByteToWideChar(CP_UTF8, 0, message, -1, nullptr, 0);
|
||||
wchar_t *wmsg = (wchar_t *)_malloca(msg_wlen * sizeof(wchar_t));
|
||||
MultiByteToWideChar(CP_UTF8, 0, message, -1, wmsg, msg_wlen);
|
||||
|
||||
HWND hwnd = parent ? parent->hwnd : nullptr;
|
||||
int result = MessageBoxW(hwnd, wmsg, wtitle, mb_type);
|
||||
|
||||
_freea(wmsg);
|
||||
_freea(wtitle);
|
||||
|
||||
switch (result) {
|
||||
case IDOK: return 0;
|
||||
case IDYES: return 0;
|
||||
case IDCANCEL: return 1;
|
||||
case IDNO: return 1;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,13 @@ struct RendererDesc {
|
||||
};
|
||||
|
||||
Renderer *renderer_create(RendererDesc *desc);
|
||||
|
||||
// creates a lightweight renderer that shares the GPU device, pipeline, shaders, font atlas, and icon textures from a
|
||||
// parent renderer, but creates its own drawable surface (CAMetalLayer / swap chain), vertex/index buffers, and frame synchronization
|
||||
// primitives. On each renderer_begin_frame(), shared renderers sync font atlas updates from the parent so Cmd+/- zoom works across all
|
||||
// windows.
|
||||
Renderer *renderer_create_shared(Renderer *parent, RendererDesc *desc);
|
||||
|
||||
void renderer_destroy(Renderer *renderer);
|
||||
B32 renderer_begin_frame(Renderer *renderer);
|
||||
void renderer_end_frame(Renderer *renderer, Clay_RenderCommandArray render_commands);
|
||||
|
||||
@@ -178,6 +178,7 @@ struct FrameContext {
|
||||
// Renderer struct
|
||||
|
||||
struct Renderer {
|
||||
Renderer *parent; // non-null for shared renderers
|
||||
HWND hwnd;
|
||||
S32 width;
|
||||
S32 height;
|
||||
@@ -1063,23 +1064,78 @@ fail:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Renderer *renderer_create_shared(Renderer *parent, RendererDesc *desc) {
|
||||
if (!parent) return nullptr;
|
||||
|
||||
Renderer *r = new Renderer();
|
||||
memset(r, 0, sizeof(*r));
|
||||
|
||||
r->parent = parent;
|
||||
r->hwnd = (HWND)desc->window_handle;
|
||||
r->width = desc->width;
|
||||
r->height = desc->height;
|
||||
r->frame_count = desc->frame_count;
|
||||
if (r->frame_count > NUM_BACK_BUFFERS) r->frame_count = NUM_BACK_BUFFERS;
|
||||
|
||||
// Share from parent (not ref-counted — parent must outlive children)
|
||||
r->device = parent->device;
|
||||
r->command_queue = parent->command_queue;
|
||||
r->root_signature = parent->root_signature;
|
||||
r->pipeline_state = parent->pipeline_state;
|
||||
r->font_texture = parent->font_texture;
|
||||
r->icon_texture = parent->icon_texture;
|
||||
r->icon_srv_heap = parent->icon_srv_heap;
|
||||
r->srv_heap = parent->srv_heap;
|
||||
r->ft_lib = parent->ft_lib;
|
||||
r->ft_face = parent->ft_face;
|
||||
memcpy(r->glyphs, parent->glyphs, sizeof(r->glyphs));
|
||||
r->font_atlas_size = parent->font_atlas_size;
|
||||
r->font_line_height = parent->font_line_height;
|
||||
|
||||
// Create own RTV heap (we don't need own SRV heap — use parent's)
|
||||
{
|
||||
D3D12_DESCRIPTOR_HEAP_DESC rtv_desc = {};
|
||||
rtv_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
|
||||
rtv_desc.NumDescriptors = NUM_BACK_BUFFERS;
|
||||
rtv_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
|
||||
rtv_desc.NodeMask = 1;
|
||||
if (r->device->CreateDescriptorHeap(&rtv_desc, IID_PPV_ARGS(&r->rtv_heap)) != S_OK)
|
||||
goto fail;
|
||||
|
||||
SIZE_T rtv_size = r->device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
|
||||
D3D12_CPU_DESCRIPTOR_HANDLE handle = r->rtv_heap->GetCPUDescriptorHandleForHeapStart();
|
||||
for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) {
|
||||
r->rtv_descriptors[i] = handle;
|
||||
handle.ptr += rtv_size;
|
||||
}
|
||||
}
|
||||
|
||||
if (!create_frame_resources(r)) goto fail;
|
||||
if (!create_swap_chain(r)) goto fail;
|
||||
create_render_targets(r);
|
||||
if (!create_ui_buffers(r)) goto fail;
|
||||
|
||||
r->clear_r = parent->clear_r;
|
||||
r->clear_g = parent->clear_g;
|
||||
r->clear_b = parent->clear_b;
|
||||
|
||||
return r;
|
||||
|
||||
fail:
|
||||
renderer_destroy(r);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void renderer_destroy(Renderer *r) {
|
||||
if (!r) return;
|
||||
|
||||
wait_for_pending(r);
|
||||
|
||||
// Per-window resources (always owned)
|
||||
for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) {
|
||||
if (r->vertex_buffers[i]) r->vertex_buffers[i]->Release();
|
||||
if (r->index_buffers[i]) r->index_buffers[i]->Release();
|
||||
}
|
||||
if (r->font_texture) r->font_texture->Release();
|
||||
if (r->icon_texture) r->icon_texture->Release();
|
||||
if (r->icon_srv_heap) r->icon_srv_heap->Release();
|
||||
if (r->pipeline_state) r->pipeline_state->Release();
|
||||
if (r->root_signature) r->root_signature->Release();
|
||||
|
||||
if (r->ft_face) FT_Done_Face(r->ft_face);
|
||||
if (r->ft_lib) FT_Done_FreeType(r->ft_lib);
|
||||
|
||||
cleanup_render_targets(r);
|
||||
|
||||
@@ -1087,26 +1143,49 @@ void renderer_destroy(Renderer *r) {
|
||||
if (r->swap_chain_waitable) CloseHandle(r->swap_chain_waitable);
|
||||
for (S32 i = 0; i < r->frame_count; i++)
|
||||
if (r->frames[i].command_allocator) r->frames[i].command_allocator->Release();
|
||||
if (r->command_queue) r->command_queue->Release();
|
||||
if (r->command_list) r->command_list->Release();
|
||||
if (r->rtv_heap) r->rtv_heap->Release();
|
||||
if (r->srv_heap) r->srv_heap->Release();
|
||||
if (r->fence) r->fence->Release();
|
||||
if (r->fence_event) CloseHandle(r->fence_event);
|
||||
if (r->device) r->device->Release();
|
||||
|
||||
// Shared resources (only freed by root renderer)
|
||||
if (!r->parent) {
|
||||
if (r->font_texture) r->font_texture->Release();
|
||||
if (r->icon_texture) r->icon_texture->Release();
|
||||
if (r->icon_srv_heap) r->icon_srv_heap->Release();
|
||||
if (r->pipeline_state) r->pipeline_state->Release();
|
||||
if (r->root_signature) r->root_signature->Release();
|
||||
if (r->srv_heap) r->srv_heap->Release();
|
||||
if (r->command_queue) r->command_queue->Release();
|
||||
if (r->device) r->device->Release();
|
||||
|
||||
if (r->ft_face) FT_Done_Face(r->ft_face);
|
||||
if (r->ft_lib) FT_Done_FreeType(r->ft_lib);
|
||||
|
||||
#ifdef DX12_ENABLE_DEBUG_LAYER
|
||||
IDXGIDebug1 *dxgi_debug = nullptr;
|
||||
if (SUCCEEDED(DXGIGetDebugInterface1(0, IID_PPV_ARGS(&dxgi_debug)))) {
|
||||
dxgi_debug->ReportLiveObjects(DXGI_DEBUG_ALL, DXGI_DEBUG_RLO_SUMMARY);
|
||||
dxgi_debug->Release();
|
||||
}
|
||||
IDXGIDebug1 *dxgi_debug = nullptr;
|
||||
if (SUCCEEDED(DXGIGetDebugInterface1(0, IID_PPV_ARGS(&dxgi_debug)))) {
|
||||
dxgi_debug->ReportLiveObjects(DXGI_DEBUG_ALL, DXGI_DEBUG_RLO_SUMMARY);
|
||||
dxgi_debug->Release();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
delete r;
|
||||
}
|
||||
|
||||
B32 renderer_begin_frame(Renderer *r) {
|
||||
// Sync shared resources from parent (font atlas may have been rebuilt)
|
||||
if (r->parent) {
|
||||
r->font_texture = r->parent->font_texture;
|
||||
r->icon_texture = r->parent->icon_texture;
|
||||
r->icon_srv_heap = r->parent->icon_srv_heap;
|
||||
r->srv_heap = r->parent->srv_heap;
|
||||
memcpy(r->glyphs, r->parent->glyphs, sizeof(r->glyphs));
|
||||
r->font_atlas_size = r->parent->font_atlas_size;
|
||||
r->font_line_height = r->parent->font_line_height;
|
||||
}
|
||||
|
||||
if ((r->swap_chain_occluded && r->swap_chain->Present(0, DXGI_PRESENT_TEST) == DXGI_STATUS_OCCLUDED)
|
||||
|| IsIconic(r->hwnd))
|
||||
{
|
||||
|
||||
@@ -153,6 +153,7 @@ fragment float4 fragment_main(Fragment in [[stage_in]],
|
||||
// Renderer struct
|
||||
|
||||
struct Renderer {
|
||||
Renderer *parent; // non-null for shared renderers
|
||||
S32 width;
|
||||
S32 height;
|
||||
S32 frame_count;
|
||||
@@ -641,16 +642,91 @@ Renderer *renderer_create(RendererDesc *desc) {
|
||||
return r;
|
||||
}
|
||||
|
||||
Renderer *renderer_create_shared(Renderer *parent, RendererDesc *desc) {
|
||||
if (!parent) return nullptr;
|
||||
|
||||
Renderer *r = new Renderer();
|
||||
memset(r, 0, sizeof(*r));
|
||||
|
||||
r->parent = parent;
|
||||
r->width = desc->width;
|
||||
r->height = desc->height;
|
||||
r->frame_count = desc->frame_count;
|
||||
if (r->frame_count > NUM_BACK_BUFFERS) r->frame_count = NUM_BACK_BUFFERS;
|
||||
|
||||
// Share from parent
|
||||
r->device = parent->device;
|
||||
r->command_queue = parent->command_queue;
|
||||
r->pipeline_state = parent->pipeline_state;
|
||||
r->font_texture = parent->font_texture;
|
||||
r->font_sampler = parent->font_sampler;
|
||||
r->icon_texture = parent->icon_texture;
|
||||
r->ft_lib = parent->ft_lib;
|
||||
r->ft_face = parent->ft_face;
|
||||
memcpy(r->glyphs, parent->glyphs, sizeof(r->glyphs));
|
||||
r->font_atlas_size = parent->font_atlas_size;
|
||||
r->font_line_height = parent->font_line_height;
|
||||
|
||||
// Create own metal layer for the popup's NSView
|
||||
NSView *view = (__bridge NSView *)desc->window_handle;
|
||||
[view setWantsLayer:YES];
|
||||
|
||||
CAMetalLayer *layer = [CAMetalLayer layer];
|
||||
layer.device = r->device;
|
||||
layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
|
||||
layer.framebufferOnly = YES;
|
||||
layer.displaySyncEnabled = YES;
|
||||
layer.maximumDrawableCount = 3;
|
||||
|
||||
NSWindow *window = [view window];
|
||||
r->backing_scale = (F32)[window backingScaleFactor];
|
||||
layer.contentsScale = r->backing_scale;
|
||||
layer.drawableSize = CGSizeMake(r->width, r->height);
|
||||
|
||||
[view setLayer:layer];
|
||||
r->metal_layer = layer;
|
||||
|
||||
r->frame_semaphore = dispatch_semaphore_create(NUM_BACK_BUFFERS);
|
||||
|
||||
// Create own vertex/index buffers
|
||||
for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) {
|
||||
r->vertex_buffers[i] = [r->device newBufferWithLength:MAX_VERTICES * sizeof(UIVertex)
|
||||
options:MTLResourceStorageModeShared];
|
||||
r->index_buffers[i] = [r->device newBufferWithLength:MAX_INDICES * sizeof(U32)
|
||||
options:MTLResourceStorageModeShared];
|
||||
}
|
||||
|
||||
// Default clear color
|
||||
r->clear_r = parent->clear_r;
|
||||
r->clear_g = parent->clear_g;
|
||||
r->clear_b = parent->clear_b;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
void renderer_destroy(Renderer *r) {
|
||||
if (!r) return;
|
||||
if (r->ft_face) FT_Done_Face(r->ft_face);
|
||||
if (r->ft_lib) FT_Done_FreeType(r->ft_lib);
|
||||
// Only free FreeType resources if we own them (not shared)
|
||||
if (!r->parent) {
|
||||
if (r->ft_face) FT_Done_Face(r->ft_face);
|
||||
if (r->ft_lib) FT_Done_FreeType(r->ft_lib);
|
||||
}
|
||||
delete r;
|
||||
}
|
||||
|
||||
B32 renderer_begin_frame(Renderer *r) {
|
||||
if (r->width <= 0 || r->height <= 0) return false;
|
||||
|
||||
// Sync shared resources from parent (font atlas may have been rebuilt)
|
||||
if (r->parent) {
|
||||
r->font_texture = r->parent->font_texture;
|
||||
r->font_sampler = r->parent->font_sampler;
|
||||
r->icon_texture = r->parent->icon_texture;
|
||||
memcpy(r->glyphs, r->parent->glyphs, sizeof(r->glyphs));
|
||||
r->font_atlas_size = r->parent->font_atlas_size;
|
||||
r->font_line_height = r->parent->font_line_height;
|
||||
}
|
||||
|
||||
// Wait for an in-flight frame to finish, then acquire a drawable.
|
||||
// Doing this BEFORE input sampling ensures the freshest mouse position
|
||||
// is used for rendering, reducing perceived drag latency.
|
||||
|
||||
@@ -183,13 +183,6 @@ struct CustomRotatedIconData {
|
||||
|
||||
#define CORNER_RADIUS uis(g_theme.corner_radius)
|
||||
|
||||
////////////////////////////////
|
||||
// Modal / window styling
|
||||
|
||||
#define MODAL_OVERLAY_COLOR Clay_Color{ 0, 0, 0, 120}
|
||||
#define MODAL_WIDTH uis(400)
|
||||
#define WINDOW_TITLE_HEIGHT uis(32)
|
||||
|
||||
////////////////////////////////
|
||||
// Panel sizing
|
||||
|
||||
|
||||
136
src/ui/ui_piano.cpp
Normal file
136
src/ui/ui_piano.cpp
Normal file
@@ -0,0 +1,136 @@
|
||||
#include "ui/ui_piano.h"
|
||||
|
||||
#define PIANO_BLACK_W 11.0f
|
||||
#define PIANO_BLACK_H_PCT 0.6f
|
||||
|
||||
B32 piano_is_black_key(S32 note) {
|
||||
S32 n = note % 12;
|
||||
return n == 1 || n == 3 || n == 6 || n == 8 || n == 10;
|
||||
}
|
||||
|
||||
// Velocity-based color: blue (vel 0) → green (mid) → red (vel 127)
|
||||
Clay_Color piano_velocity_color(S32 velocity) {
|
||||
F32 t = (F32)velocity / 127.0f;
|
||||
F32 r, g, b;
|
||||
if (t < 0.5f) {
|
||||
F32 s = t * 2.0f;
|
||||
r = 40.0f + s * (76.0f - 40.0f);
|
||||
g = 120.0f + s * (175.0f - 120.0f);
|
||||
b = 220.0f + s * (80.0f - 220.0f);
|
||||
} else {
|
||||
F32 s = (t - 0.5f) * 2.0f;
|
||||
r = 76.0f + s * (220.0f - 76.0f);
|
||||
g = 175.0f + s * (50.0f - 175.0f);
|
||||
b = 80.0f + s * (40.0f - 80.0f);
|
||||
}
|
||||
return Clay_Color{r, g, b, 255};
|
||||
}
|
||||
|
||||
void ui_piano(UI_PianoState *state, MidiEngine *midi, F32 avail_w, F32 avail_h) {
|
||||
Clay_ElementId piano_id = CLAY_ID("PianoContainer");
|
||||
CLAY(piano_id,
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
|
||||
.layoutDirection = CLAY_LEFT_TO_RIGHT,
|
||||
}
|
||||
) {
|
||||
// Compute black key size proportional to white keys
|
||||
F32 black_key_h = avail_h * PIANO_BLACK_H_PCT;
|
||||
if (black_key_h < uis(20)) black_key_h = uis(20);
|
||||
|
||||
F32 white_key_w = avail_w / 52.0f;
|
||||
F32 black_key_w = white_key_w * 0.6f;
|
||||
if (black_key_w < uis(8)) black_key_w = uis(8);
|
||||
|
||||
// White keys (grow to fill width and height)
|
||||
for (S32 note = PIANO_FIRST_NOTE; note <= PIANO_LAST_NOTE; note++) {
|
||||
if (piano_is_black_key(note)) continue;
|
||||
|
||||
B32 midi_held = midi_is_note_held(midi, note);
|
||||
B32 mouse_held = state->mouse_note == note;
|
||||
Clay_Color bg;
|
||||
if (midi_held) {
|
||||
bg = piano_velocity_color(midi_get_note_velocity(midi, note));
|
||||
} else if (mouse_held) {
|
||||
bg = g_theme.accent;
|
||||
} else {
|
||||
bg = Clay_Color{240, 240, 240, 255};
|
||||
}
|
||||
|
||||
CLAY(CLAY_IDI("PKey", note),
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
|
||||
},
|
||||
.backgroundColor = bg,
|
||||
.border = { .color = {190, 190, 190, 255}, .width = { .right = 1 } },
|
||||
) {}
|
||||
}
|
||||
|
||||
// Black keys (floating, attached to left white key)
|
||||
for (S32 note = PIANO_FIRST_NOTE; note <= PIANO_LAST_NOTE; note++) {
|
||||
if (!piano_is_black_key(note)) continue;
|
||||
|
||||
Clay_ElementId parent_wkey = CLAY_IDI("PKey", note - 1);
|
||||
B32 midi_held = midi_is_note_held(midi, note);
|
||||
B32 mouse_held = state->mouse_note == note;
|
||||
Clay_Color bg;
|
||||
if (midi_held) {
|
||||
bg = piano_velocity_color(midi_get_note_velocity(midi, note));
|
||||
} else if (mouse_held) {
|
||||
bg = g_theme.accent;
|
||||
} else {
|
||||
bg = Clay_Color{25, 25, 30, 255};
|
||||
}
|
||||
|
||||
CLAY(CLAY_IDI("PKey", note),
|
||||
.layout = {
|
||||
.sizing = {
|
||||
.width = CLAY_SIZING_FIXED(black_key_w),
|
||||
.height = CLAY_SIZING_FIXED(black_key_h),
|
||||
},
|
||||
},
|
||||
.backgroundColor = bg,
|
||||
.cornerRadius = { .topLeft = 0, .topRight = 0, .bottomLeft = uis(2), .bottomRight = uis(2) },
|
||||
.floating = {
|
||||
.parentId = parent_wkey.id,
|
||||
.zIndex = 100,
|
||||
.attachPoints = {
|
||||
.element = CLAY_ATTACH_POINT_CENTER_TOP,
|
||||
.parent = CLAY_ATTACH_POINT_RIGHT_TOP,
|
||||
},
|
||||
.attachTo = CLAY_ATTACH_TO_ELEMENT_WITH_ID,
|
||||
},
|
||||
) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ui_piano_update_input(UI_PianoState *state) {
|
||||
PlatformInput input = g_wstate.input;
|
||||
|
||||
if (!input.mouse_down) {
|
||||
state->mouse_note = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Find hovered piano key — check black keys first (they're on top)
|
||||
S32 hovered_note = -1;
|
||||
for (S32 note = PIANO_FIRST_NOTE; note <= PIANO_LAST_NOTE; note++) {
|
||||
if (piano_is_black_key(note) && Clay_PointerOver(CLAY_IDI("PKey", note))) {
|
||||
hovered_note = note;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hovered_note == -1) {
|
||||
for (S32 note = PIANO_FIRST_NOTE; note <= PIANO_LAST_NOTE; note++) {
|
||||
if (!piano_is_black_key(note) && Clay_PointerOver(CLAY_IDI("PKey", note))) {
|
||||
hovered_note = note;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hovered_note != -1) {
|
||||
state->mouse_note = hovered_note;
|
||||
}
|
||||
}
|
||||
20
src/ui/ui_piano.h
Normal file
20
src/ui/ui_piano.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include "ui/ui_core.h"
|
||||
#include "midi/midi.h"
|
||||
|
||||
#define PIANO_FIRST_NOTE 21 // A0
|
||||
#define PIANO_LAST_NOTE 108 // C8
|
||||
|
||||
struct UI_PianoState {
|
||||
S32 mouse_note; // MIDI note held by mouse click (-1 = none)
|
||||
};
|
||||
|
||||
B32 piano_is_black_key(S32 note);
|
||||
Clay_Color piano_velocity_color(S32 velocity);
|
||||
|
||||
// Render the 88-key piano widget.
|
||||
// avail_w: total width for key layout, avail_h: total height for keys
|
||||
void ui_piano(UI_PianoState *state, MidiEngine *midi, F32 avail_w, F32 avail_h);
|
||||
|
||||
// Update piano mouse input (call after Clay layout is computed)
|
||||
void ui_piano_update_input(UI_PianoState *state);
|
||||
143
src/ui/ui_popups.cpp
Normal file
143
src/ui/ui_popups.cpp
Normal file
@@ -0,0 +1,143 @@
|
||||
#include "ui/ui_popups.h"
|
||||
|
||||
static PopupWindow g_popups[MAX_POPUP_WINDOWS];
|
||||
|
||||
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)
|
||||
return &g_popups[i];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
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) {
|
||||
// Find free slot
|
||||
PopupWindow *popup = nullptr;
|
||||
for (S32 i = 0; i < MAX_POPUP_WINDOWS; i++) {
|
||||
if (!g_popups[i].alive) { popup = &g_popups[i]; break; }
|
||||
}
|
||||
if (!popup) return nullptr;
|
||||
|
||||
memset(popup, 0, sizeof(*popup));
|
||||
|
||||
// Create native popup window
|
||||
PlatformWindowDesc desc = {};
|
||||
desc.title = title;
|
||||
desc.width = width;
|
||||
desc.height = height;
|
||||
desc.style = PLATFORM_WINDOW_STYLE_POPUP;
|
||||
desc.parent = parent_window;
|
||||
popup->platform_window = platform_create_window(&desc);
|
||||
if (!popup->platform_window) return nullptr;
|
||||
|
||||
// Create shared renderer
|
||||
S32 pw, ph;
|
||||
platform_get_size(popup->platform_window, &pw, &ph);
|
||||
|
||||
RendererDesc rdesc = {};
|
||||
rdesc.window_handle = platform_get_native_handle(popup->platform_window);
|
||||
rdesc.width = pw;
|
||||
rdesc.height = ph;
|
||||
popup->renderer = renderer_create_shared(parent_renderer, &rdesc);
|
||||
if (!popup->renderer) {
|
||||
platform_destroy_window(popup->platform_window);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Create UI context
|
||||
popup->ui_ctx = ui_create((F32)pw, (F32)ph);
|
||||
ui_set_measure_text_fn(popup->ui_ctx, renderer_measure_text, popup->renderer);
|
||||
|
||||
popup->alive = 1;
|
||||
popup->open_flag = open_flag;
|
||||
popup->content_fn = content_fn;
|
||||
popup->content_user_data = user_data;
|
||||
popup->width = width;
|
||||
popup->height = height;
|
||||
popup->last_w = pw;
|
||||
popup->last_h = ph;
|
||||
popup->title = title;
|
||||
popup->wstate = {};
|
||||
|
||||
return popup;
|
||||
}
|
||||
|
||||
void popup_close(PopupWindow *popup) {
|
||||
if (!popup || !popup->alive) return;
|
||||
|
||||
if (popup->open_flag)
|
||||
*popup->open_flag = 0;
|
||||
|
||||
ui_destroy(popup->ui_ctx);
|
||||
renderer_destroy(popup->renderer);
|
||||
platform_destroy_window(popup->platform_window);
|
||||
|
||||
popup->alive = 0;
|
||||
}
|
||||
|
||||
void popup_do_frame(PopupWindow *popup, F32 dt) {
|
||||
if (!popup->alive) return;
|
||||
|
||||
// Check window size
|
||||
S32 w, h;
|
||||
platform_get_size(popup->platform_window, &w, &h);
|
||||
if (w != popup->last_w || h != popup->last_h) {
|
||||
renderer_resize(popup->renderer, w, h);
|
||||
popup->last_w = w;
|
||||
popup->last_h = h;
|
||||
}
|
||||
|
||||
if (!renderer_begin_frame(popup->renderer))
|
||||
return;
|
||||
|
||||
// Gather input from popup window
|
||||
PlatformInput input = platform_get_input(popup->platform_window);
|
||||
|
||||
// Swap widget state
|
||||
UI_WidgetState saved_wstate = g_wstate;
|
||||
g_wstate = popup->wstate;
|
||||
|
||||
ui_widgets_begin_frame(input);
|
||||
|
||||
// Build UI
|
||||
ui_begin_frame(popup->ui_ctx, (F32)w, (F32)h, input.mouse_pos, input.mouse_down,
|
||||
input.scroll_delta, dt);
|
||||
|
||||
// Background fill
|
||||
CLAY(CLAY_ID("PopupBg"),
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
|
||||
.padding = { uip(12), uip(12), uip(10), uip(10) },
|
||||
.childGap = uip(8),
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM,
|
||||
},
|
||||
.backgroundColor = g_theme.bg_medium,
|
||||
) {
|
||||
if (popup->content_fn) {
|
||||
popup->content_fn(popup->content_user_data);
|
||||
}
|
||||
}
|
||||
|
||||
Clay_RenderCommandArray render_commands = ui_end_frame(popup->ui_ctx);
|
||||
|
||||
// Save widget state back
|
||||
popup->wstate = g_wstate;
|
||||
g_wstate = saved_wstate;
|
||||
|
||||
// Render
|
||||
renderer_end_frame(popup->renderer, render_commands);
|
||||
}
|
||||
|
||||
void popup_close_all() {
|
||||
for (S32 i = 0; i < MAX_POPUP_WINDOWS; i++)
|
||||
if (g_popups[i].alive) popup_close(&g_popups[i]);
|
||||
}
|
||||
|
||||
void popup_close_check() {
|
||||
for (S32 i = 0; i < MAX_POPUP_WINDOWS; i++) {
|
||||
if (g_popups[i].alive && platform_window_should_close(g_popups[i].platform_window))
|
||||
popup_close(&g_popups[i]);
|
||||
}
|
||||
}
|
||||
31
src/ui/ui_popups.h
Normal file
31
src/ui/ui_popups.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include "ui/ui_core.h"
|
||||
#include "ui/ui_widgets.h"
|
||||
#include "platform/platform.h"
|
||||
#include "renderer/renderer.h"
|
||||
|
||||
#define MAX_POPUP_WINDOWS 4
|
||||
|
||||
struct PopupWindow {
|
||||
B32 alive;
|
||||
B32 *open_flag; // e.g. &app->show_settings_window
|
||||
PlatformWindow *platform_window;
|
||||
Renderer *renderer;
|
||||
UI_Context *ui_ctx;
|
||||
UI_WidgetState wstate;
|
||||
UI_WindowContentFn content_fn;
|
||||
void *content_user_data;
|
||||
S32 width, height;
|
||||
S32 last_w, last_h;
|
||||
const char *title;
|
||||
};
|
||||
|
||||
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);
|
||||
void popup_close(PopupWindow *popup);
|
||||
PopupWindow *popup_find_by_flag(B32 *flag);
|
||||
void popup_do_frame(PopupWindow *popup, F32 dt);
|
||||
void popup_close_all(void);
|
||||
void popup_close_check(void);
|
||||
@@ -149,11 +149,6 @@ void ui_widgets_begin_frame(PlatformInput input) {
|
||||
}
|
||||
}
|
||||
|
||||
// Drag cleanup: if mouse was released between frames, end any active drag
|
||||
if (g_wstate.drag.dragging_id != 0 && !input.mouse_down) {
|
||||
g_wstate.drag.dragging_id = 0;
|
||||
}
|
||||
|
||||
ui_text_input_reset_display_bufs();
|
||||
}
|
||||
|
||||
@@ -971,358 +966,6 @@ B32 ui_dropdown(const char *id, const char **options, S32 count, S32 *selected)
|
||||
return changed;
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Modal dialog
|
||||
|
||||
B32 ui_modal_is_active() {
|
||||
return g_wstate.modal.active;
|
||||
}
|
||||
|
||||
S32 ui_modal(const char *id, const char *title, const char *message,
|
||||
const char **buttons, S32 button_count) {
|
||||
ensure_widget_text_configs();
|
||||
|
||||
Clay_ElementId eid = WID(id);
|
||||
|
||||
// First call activates the modal
|
||||
if (!g_wstate.modal.active) {
|
||||
g_wstate.modal.active = 1;
|
||||
g_wstate.modal.id = eid.id;
|
||||
g_wstate.modal.result = -1;
|
||||
}
|
||||
|
||||
// If a different modal is active, ignore this one
|
||||
if (g_wstate.modal.id != eid.id) return -1;
|
||||
|
||||
S32 result = -1;
|
||||
|
||||
// Check Escape key to dismiss
|
||||
for (S32 k = 0; k < g_wstate.input.key_count; k++) {
|
||||
if (g_wstate.input.keys[k] == PKEY_ESCAPE) {
|
||||
result = -2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Full-screen overlay (dims background, captures all pointer events)
|
||||
CLAY(WIDI(id, 2000),
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
|
||||
},
|
||||
.backgroundColor = Clay_Color{0, 0, 0, 120},
|
||||
.floating = {
|
||||
.zIndex = 1000,
|
||||
.attachPoints = {
|
||||
.element = CLAY_ATTACH_POINT_LEFT_TOP,
|
||||
.parent = CLAY_ATTACH_POINT_LEFT_TOP,
|
||||
},
|
||||
.pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_CAPTURE,
|
||||
.attachTo = CLAY_ATTACH_TO_ROOT,
|
||||
}
|
||||
) {}
|
||||
|
||||
// Modal drop shadow
|
||||
{
|
||||
Clay_ElementId modal_box_id = WIDI(id, 2001);
|
||||
Clay_BoundingBox modal_bb = Clay_GetElementData(modal_box_id).boundingBox;
|
||||
emit_shadow(modal_bb, uis(3), uis(4), uis(10),
|
||||
g_theme.shadow.a, 1000,
|
||||
1, 0, CLAY_ATTACH_POINT_LEFT_TOP);
|
||||
}
|
||||
|
||||
// Dialog box (centered)
|
||||
CLAY(WIDI(id, 2001),
|
||||
.layout = {
|
||||
.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(CORNER_RADIUS),
|
||||
.floating = {
|
||||
.zIndex = 1001,
|
||||
.attachPoints = {
|
||||
.element = CLAY_ATTACH_POINT_CENTER_CENTER,
|
||||
.parent = CLAY_ATTACH_POINT_CENTER_CENTER,
|
||||
},
|
||||
.pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_CAPTURE,
|
||||
.attachTo = CLAY_ATTACH_TO_ROOT,
|
||||
},
|
||||
.border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } }
|
||||
) {
|
||||
// Title bar (gradient: lighter top)
|
||||
{
|
||||
Clay_Color mtb = g_theme.title_bar;
|
||||
Clay_Color mtb_top = {(F32)Min((S32)mtb.r+12,255), (F32)Min((S32)mtb.g+12,255), (F32)Min((S32)mtb.b+12,255), mtb.a};
|
||||
CustomGradientData *mtb_grad = alloc_gradient(mtb_top, mtb);
|
||||
|
||||
CLAY(WIDI(id, 2002),
|
||||
.layout = {
|
||||
.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 = { CORNER_RADIUS, CORNER_RADIUS, 0, 0 },
|
||||
.custom = { .customData = mtb_grad },
|
||||
.border = { .color = g_theme.border, .width = { .bottom = 1 } },
|
||||
) {
|
||||
CLAY_TEXT(clay_str(title), &g_widget_text_config);
|
||||
}
|
||||
}
|
||||
|
||||
// Message body
|
||||
static Clay_TextElementConfig msg_text_config;
|
||||
msg_text_config = g_widget_text_config;
|
||||
msg_text_config.wrapMode = CLAY_TEXT_WRAP_WORDS;
|
||||
|
||||
CLAY(WIDI(id, 2003),
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
|
||||
.padding = { uip(16), uip(16), uip(16), uip(16) },
|
||||
}
|
||||
) {
|
||||
CLAY_TEXT(clay_str(message), &msg_text_config);
|
||||
}
|
||||
|
||||
// Button row (right-aligned)
|
||||
CLAY(WIDI(id, 2004),
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
|
||||
.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,
|
||||
}
|
||||
) {
|
||||
for (S32 i = 0; i < button_count; i++) {
|
||||
Clay_ElementId btn_id = WIDI(id, 2100 + i);
|
||||
B32 btn_hovered = Clay_PointerOver(btn_id);
|
||||
|
||||
Clay_Color mbtn_base = btn_hovered ? g_theme.accent_hover : g_theme.accent;
|
||||
Clay_Color mbtn_top = {(F32)Min((S32)mbtn_base.r+12,255), (F32)Min((S32)mbtn_base.g+12,255), (F32)Min((S32)mbtn_base.b+12,255), mbtn_base.a};
|
||||
Clay_Color mbtn_bot = {(F32)Max((S32)mbtn_base.r-15,0), (F32)Max((S32)mbtn_base.g-15,0), (F32)Max((S32)mbtn_base.b-15,0), mbtn_base.a};
|
||||
CustomGradientData *mbtn_grad = alloc_gradient(mbtn_top, mbtn_bot);
|
||||
|
||||
CLAY(btn_id,
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(WIDGET_BUTTON_HEIGHT) },
|
||||
.padding = { uip(16), uip(16), uip(1), 0 },
|
||||
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
|
||||
},
|
||||
.backgroundColor = mbtn_base,
|
||||
.cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS),
|
||||
.custom = { .customData = mbtn_grad },
|
||||
) {
|
||||
CLAY_TEXT(clay_str(buttons[i]), &g_widget_text_config_btn);
|
||||
}
|
||||
|
||||
if (btn_hovered && g_wstate.mouse_clicked) {
|
||||
result = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deactivate modal if a result was produced
|
||||
if (result != -1) {
|
||||
g_wstate.modal.active = 0;
|
||||
g_wstate.modal.id = 0;
|
||||
g_wstate.modal.result = result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Draggable window
|
||||
|
||||
static UI_WindowSlot *find_or_create_window_slot(U32 id, Vec2F32 initial_pos, Vec2F32 initial_size) {
|
||||
// Look for existing slot
|
||||
for (S32 i = 0; i < g_wstate.window_count; i++) {
|
||||
if (g_wstate.windows[i].id == id) {
|
||||
return &g_wstate.windows[i];
|
||||
}
|
||||
}
|
||||
// Create new slot
|
||||
if (g_wstate.window_count >= UI_WIDGET_MAX_WINDOWS) return nullptr;
|
||||
UI_WindowSlot *slot = &g_wstate.windows[g_wstate.window_count++];
|
||||
slot->id = id;
|
||||
slot->position = initial_pos;
|
||||
slot->size = initial_size;
|
||||
slot->open = 1;
|
||||
slot->z_order = g_wstate.next_z++;
|
||||
return slot;
|
||||
}
|
||||
|
||||
static void bring_window_to_front(UI_WindowSlot *slot) {
|
||||
// Renormalize if approaching modal z-range
|
||||
if (g_wstate.next_z > 800) {
|
||||
S16 sorted[UI_WIDGET_MAX_WINDOWS];
|
||||
S32 count = g_wstate.window_count;
|
||||
for (S32 i = 0; i < count; i++) sorted[i] = g_wstate.windows[i].z_order;
|
||||
// Bubble sort (tiny array)
|
||||
for (S32 i = 0; i < count - 1; i++) {
|
||||
for (S32 j = i + 1; j < count; j++) {
|
||||
if (sorted[j] < sorted[i]) {
|
||||
S16 tmp = sorted[i]; sorted[i] = sorted[j]; sorted[j] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (S32 i = 0; i < count; i++) {
|
||||
for (S32 j = 0; j < count; j++) {
|
||||
if (g_wstate.windows[j].z_order == sorted[i]) {
|
||||
g_wstate.windows[j].z_order = (S16)i;
|
||||
}
|
||||
}
|
||||
}
|
||||
g_wstate.next_z = (S16)count;
|
||||
}
|
||||
slot->z_order = g_wstate.next_z++;
|
||||
}
|
||||
|
||||
B32 ui_window(const char *id, const char *title, B32 *open,
|
||||
Vec2F32 initial_pos, Vec2F32 initial_size,
|
||||
UI_WindowContentFn content_fn, void *user_data) {
|
||||
ensure_widget_text_configs();
|
||||
|
||||
if (!*open) return 0;
|
||||
|
||||
Clay_ElementId eid = WID(id);
|
||||
UI_WindowSlot *slot = find_or_create_window_slot(eid.id, initial_pos, initial_size);
|
||||
if (!slot) return 0;
|
||||
|
||||
// Drag handling
|
||||
Clay_ElementId title_bar_id = WIDI(id, 3000);
|
||||
B32 title_hovered = Clay_PointerOver(title_bar_id);
|
||||
|
||||
// Start drag on title bar click
|
||||
if (title_hovered && g_wstate.mouse_clicked && g_wstate.drag.dragging_id == 0) {
|
||||
g_wstate.drag.dragging_id = eid.id;
|
||||
g_wstate.drag.drag_anchor = g_wstate.input.mouse_pos;
|
||||
g_wstate.drag.pos_anchor = slot->position;
|
||||
bring_window_to_front(slot);
|
||||
}
|
||||
|
||||
// Continue drag
|
||||
if (g_wstate.drag.dragging_id == eid.id && g_wstate.input.mouse_down) {
|
||||
Vec2F32 delta;
|
||||
delta.x = g_wstate.input.mouse_pos.x - g_wstate.drag.drag_anchor.x;
|
||||
delta.y = g_wstate.input.mouse_pos.y - g_wstate.drag.drag_anchor.y;
|
||||
slot->position.x = g_wstate.drag.pos_anchor.x + delta.x;
|
||||
slot->position.y = g_wstate.drag.pos_anchor.y + delta.y;
|
||||
}
|
||||
|
||||
// End drag on release
|
||||
if (g_wstate.drag.dragging_id == eid.id && !g_wstate.input.mouse_down) {
|
||||
g_wstate.drag.dragging_id = 0;
|
||||
}
|
||||
|
||||
// Click anywhere on window body brings to front
|
||||
B32 body_hovered = Clay_PointerOver(eid);
|
||||
if (body_hovered && g_wstate.mouse_clicked && g_wstate.drag.dragging_id == 0) {
|
||||
bring_window_to_front(slot);
|
||||
}
|
||||
|
||||
// Close button
|
||||
Clay_ElementId close_id = WIDI(id, 3001);
|
||||
B32 close_hovered = Clay_PointerOver(close_id);
|
||||
|
||||
// Drop shadow
|
||||
{
|
||||
Clay_BoundingBox win_bb = Clay_GetElementData(eid).boundingBox;
|
||||
// Use absolute position since window uses offset-based floating
|
||||
Clay_BoundingBox shadow_bb = { slot->position.x, slot->position.y, win_bb.width, win_bb.height };
|
||||
emit_shadow(shadow_bb, uis(3), uis(3), uis(8),
|
||||
g_theme.shadow.a, (S16)(100 + slot->z_order - 1),
|
||||
1, 0, CLAY_ATTACH_POINT_LEFT_TOP);
|
||||
}
|
||||
|
||||
// Window floating element
|
||||
CLAY(eid,
|
||||
.layout = {
|
||||
.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(CORNER_RADIUS),
|
||||
.floating = {
|
||||
.offset = { slot->position.x, slot->position.y },
|
||||
.zIndex = (S16)(100 + slot->z_order),
|
||||
.attachPoints = {
|
||||
.element = CLAY_ATTACH_POINT_LEFT_TOP,
|
||||
.parent = CLAY_ATTACH_POINT_LEFT_TOP,
|
||||
},
|
||||
.pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_CAPTURE,
|
||||
.attachTo = CLAY_ATTACH_TO_ROOT,
|
||||
},
|
||||
.border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } }
|
||||
) {
|
||||
// Title bar (gradient: lighter top)
|
||||
Clay_Color tb = g_theme.title_bar;
|
||||
Clay_Color tb_top = {(F32)Min((S32)tb.r+12,255), (F32)Min((S32)tb.g+12,255), (F32)Min((S32)tb.b+12,255), tb.a};
|
||||
CustomGradientData *tb_grad = alloc_gradient(tb_top, tb);
|
||||
|
||||
CLAY(title_bar_id,
|
||||
.layout = {
|
||||
.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 = { CORNER_RADIUS, CORNER_RADIUS, 0, 0 },
|
||||
.custom = { .customData = tb_grad },
|
||||
.border = { .color = g_theme.border, .width = { .bottom = 1 } },
|
||||
) {
|
||||
// Title text (grows to push close button right)
|
||||
CLAY(WIDI(id, 3002),
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
|
||||
}
|
||||
) {
|
||||
CLAY_TEXT(clay_str(title), &g_widget_text_config);
|
||||
}
|
||||
|
||||
// Close button
|
||||
Clay_Color close_bg = g_theme_id == 1
|
||||
? (close_hovered ? Clay_Color{220, 50, 50, 255} : Clay_Color{200, 70, 70, 255})
|
||||
: (close_hovered ? Clay_Color{200, 60, 60, 255} : Clay_Color{120, 50, 50, 255});
|
||||
CLAY(close_id,
|
||||
.layout = {
|
||||
.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(CORNER_RADIUS)
|
||||
) {
|
||||
ui_icon(UI_ICON_CLOSE, uis(12), g_theme.button_text);
|
||||
}
|
||||
}
|
||||
|
||||
// Content area
|
||||
CLAY(WIDI(id, 3003),
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
|
||||
.padding = { uip(12), uip(12), uip(10), uip(10) },
|
||||
.childGap = uip(8),
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM,
|
||||
}
|
||||
) {
|
||||
if (content_fn) {
|
||||
content_fn(user_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle close button click
|
||||
if (close_hovered && g_wstate.mouse_clicked) {
|
||||
*open = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Tab bar
|
||||
|
||||
|
||||
@@ -15,38 +15,17 @@
|
||||
|
||||
#define UI_WIDGET_MAX_DROPDOWN_ITEMS 32
|
||||
#define UI_WIDGET_MAX_TEXT_INPUTS 16
|
||||
#define UI_WIDGET_MAX_WINDOWS 16
|
||||
|
||||
struct UI_ModalState {
|
||||
B32 active;
|
||||
U32 id; // Hash of the modal's string ID
|
||||
S32 result; // Button index pressed, -1 = pending
|
||||
};
|
||||
|
||||
struct UI_WindowSlot {
|
||||
U32 id; // Hash of the window's string ID (0 = unused)
|
||||
Vec2F32 position;
|
||||
Vec2F32 size;
|
||||
B32 open;
|
||||
S16 z_order;
|
||||
};
|
||||
|
||||
struct UI_KnobDragState {
|
||||
U32 dragging_id; // Hash of the knob being dragged (0 = none)
|
||||
U32 dragging_id; // Hash of the knob being dragged (0 = none)
|
||||
F32 drag_start_y; // Mouse Y when drag started
|
||||
F32 drag_start_x; // Mouse X when drag started (for h-slider)
|
||||
F32 value_at_start; // Value when drag started
|
||||
B32 was_shift; // Shift state last frame (to re-anchor on change)
|
||||
U32 last_click_id; // Knob hash of last click (for F64-click detection)
|
||||
U32 last_click_id; // Knob hash of last click (for F64-click detection)
|
||||
S32 last_click_frame; // Frame number of last click
|
||||
};
|
||||
|
||||
struct UI_DragState {
|
||||
U32 dragging_id; // Window ID currently being dragged (0 = none)
|
||||
Vec2F32 drag_anchor; // Mouse position when drag started
|
||||
Vec2F32 pos_anchor; // Window position when drag started
|
||||
};
|
||||
|
||||
struct UI_WidgetState {
|
||||
// Text input focus
|
||||
U32 focused_id; // Clay element ID hash of the focused text input (0 = none)
|
||||
@@ -58,8 +37,8 @@ struct UI_WidgetState {
|
||||
S32 sel_end; // Selection extent (moves with cursor)
|
||||
|
||||
// Tab cycling: registered text input IDs in order of declaration
|
||||
U32 text_input_ids[UI_WIDGET_MAX_TEXT_INPUTS];
|
||||
S32 text_input_count;
|
||||
U32 text_input_ids[UI_WIDGET_MAX_TEXT_INPUTS];
|
||||
S32 text_input_count;
|
||||
B32 tab_pressed; // True on the frame Tab was pressed
|
||||
|
||||
// Dropdown
|
||||
@@ -71,15 +50,6 @@ struct UI_WidgetState {
|
||||
// Click detection
|
||||
B32 mouse_clicked; // true on the frame mouse transitions from up->down
|
||||
|
||||
// Modal state
|
||||
UI_ModalState modal;
|
||||
|
||||
// Window state
|
||||
UI_WindowSlot windows[UI_WIDGET_MAX_WINDOWS];
|
||||
S32 window_count;
|
||||
S16 next_z;
|
||||
UI_DragState drag;
|
||||
|
||||
// Knob drag state
|
||||
UI_KnobDragState knob_drag;
|
||||
|
||||
@@ -134,22 +104,12 @@ B32 ui_text_input(const char *id, char *buf, S32 buf_size);
|
||||
// options is an array of label strings, count is the number of options.
|
||||
B32 ui_dropdown(const char *id, const char **options, S32 count, S32 *selected);
|
||||
|
||||
// Modal dialog. Returns button index pressed (0-based), -1 if pending, -2 if Escape dismissed.
|
||||
// Call every frame while active — it draws the overlay and dialog box.
|
||||
S32 ui_modal(const char *id, const char *title, const char *message,
|
||||
const char **buttons, S32 button_count);
|
||||
B32 ui_modal_is_active();
|
||||
|
||||
// Tab bar. Renders a row of tabs with active/inactive states.
|
||||
// Returns the currently selected index. Clicking an inactive tab updates *selected.
|
||||
S32 ui_tab_bar(const char *id, const char **labels, S32 count, S32 *selected);
|
||||
|
||||
// Draggable floating window. content_fn is called inside the window body each frame.
|
||||
// *open is set to 0 when the close button is clicked. Returns true while window is open.
|
||||
// Content function type (used by popup windows)
|
||||
typedef void (*UI_WindowContentFn)(void *user_data);
|
||||
B32 ui_window(const char *id, const char *title, B32 *open,
|
||||
Vec2F32 initial_pos, Vec2F32 initial_size,
|
||||
UI_WindowContentFn content_fn, void *user_data);
|
||||
|
||||
// Knob / potentiometer. Vertical drag to change value.
|
||||
// unsigned (is_signed=0): value in [0, max_val]
|
||||
|
||||
Reference in New Issue
Block a user