425 lines
13 KiB
Plaintext
425 lines
13 KiB
Plaintext
// Dear ImGui: standalone macOS example using Cocoa + CPU framebuffer rendering.
|
|
|
|
// Learn about Dear ImGui:
|
|
// - FAQ https://dearimgui.com/faq
|
|
// - Getting Started https://dearimgui.com/getting-started
|
|
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
|
// - Introduction, links and more at the top of imgui.cpp
|
|
|
|
#import <Cocoa/Cocoa.h>
|
|
|
|
#include "imgui.h"
|
|
#include "imgui_impl_cpu.h"
|
|
#include "imgui_impl_osx.h"
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <string.h>
|
|
|
|
struct ExampleTimingHistory
|
|
{
|
|
double Samples[120];
|
|
int Count;
|
|
int Offset;
|
|
|
|
ExampleTimingHistory() : Count(0), Offset(0) { memset(Samples, 0, sizeof(Samples)); }
|
|
|
|
void AddSample(double value_ms)
|
|
{
|
|
Samples[Offset] = value_ms;
|
|
Offset = (Offset + 1) % IM_ARRAYSIZE(Samples);
|
|
if (Count < IM_ARRAYSIZE(Samples))
|
|
Count++;
|
|
}
|
|
|
|
double GetAverageMs() const
|
|
{
|
|
if (Count == 0)
|
|
return 0.0;
|
|
double total = 0.0;
|
|
for (int i = 0; i < Count; i++)
|
|
total += Samples[i];
|
|
return total / (double)Count;
|
|
}
|
|
};
|
|
|
|
struct ExampleFramebuffer
|
|
{
|
|
ImVector<unsigned char> Storage;
|
|
ImGui_ImplCPU_Framebuffer CPU;
|
|
CGColorSpaceRef ColorSpace;
|
|
CGDataProviderRef DataProvider;
|
|
CGImageRef Image;
|
|
|
|
ExampleFramebuffer() : ColorSpace(nullptr), DataProvider(nullptr), Image(nullptr)
|
|
{
|
|
CPU.Pixels = nullptr;
|
|
CPU.Width = 0;
|
|
CPU.Height = 0;
|
|
CPU.Pitch = 0;
|
|
CPU.Format = nullptr;
|
|
}
|
|
};
|
|
|
|
static double ExampleGetTimeMs()
|
|
{
|
|
typedef std::chrono::steady_clock Clock;
|
|
return std::chrono::duration<double, std::milli>(Clock::now().time_since_epoch()).count();
|
|
}
|
|
|
|
static void DestroyFramebuffer(ExampleFramebuffer* framebuffer)
|
|
{
|
|
if (framebuffer->Image != nullptr)
|
|
{
|
|
CGImageRelease(framebuffer->Image);
|
|
framebuffer->Image = nullptr;
|
|
}
|
|
if (framebuffer->DataProvider != nullptr)
|
|
{
|
|
CGDataProviderRelease(framebuffer->DataProvider);
|
|
framebuffer->DataProvider = nullptr;
|
|
}
|
|
if (framebuffer->ColorSpace != nullptr)
|
|
{
|
|
CGColorSpaceRelease(framebuffer->ColorSpace);
|
|
framebuffer->ColorSpace = nullptr;
|
|
}
|
|
|
|
framebuffer->CPU.Pixels = nullptr;
|
|
framebuffer->CPU.Width = 0;
|
|
framebuffer->CPU.Height = 0;
|
|
framebuffer->CPU.Pitch = 0;
|
|
framebuffer->Storage.clear();
|
|
}
|
|
|
|
static ImU32 PackBGRA(unsigned char r, unsigned char g, unsigned char b, unsigned char a)
|
|
{
|
|
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
|
|
return ((ImU32)b << 24) | ((ImU32)g << 16) | ((ImU32)r << 8) | (ImU32)a;
|
|
#else
|
|
return (ImU32)b | ((ImU32)g << 8) | ((ImU32)r << 16) | ((ImU32)a << 24);
|
|
#endif
|
|
}
|
|
|
|
static bool RecreateFramebuffer(ExampleFramebuffer* framebuffer, int width, int height)
|
|
{
|
|
DestroyFramebuffer(framebuffer);
|
|
|
|
framebuffer->Storage.resize(width * height * 4);
|
|
framebuffer->CPU.Pixels = framebuffer->Storage.Data;
|
|
framebuffer->CPU.Width = width;
|
|
framebuffer->CPU.Height = height;
|
|
framebuffer->CPU.Pitch = width * 4;
|
|
framebuffer->CPU.Format = ImGui_ImplCPU_GetBuiltinPixelFormat(ImGui_ImplCPU_BuiltinFormat_BGRA8888);
|
|
if (framebuffer->CPU.Pixels == nullptr)
|
|
return false;
|
|
|
|
framebuffer->ColorSpace = CGColorSpaceCreateDeviceRGB();
|
|
if (framebuffer->ColorSpace == nullptr)
|
|
{
|
|
DestroyFramebuffer(framebuffer);
|
|
return false;
|
|
}
|
|
|
|
framebuffer->DataProvider = CGDataProviderCreateWithData(nullptr, framebuffer->CPU.Pixels, (size_t)framebuffer->CPU.Pitch * (size_t)framebuffer->CPU.Height, nullptr);
|
|
if (framebuffer->DataProvider == nullptr)
|
|
{
|
|
DestroyFramebuffer(framebuffer);
|
|
return false;
|
|
}
|
|
|
|
framebuffer->Image = CGImageCreate((size_t)framebuffer->CPU.Width, (size_t)framebuffer->CPU.Height, 8, 32, (size_t)framebuffer->CPU.Pitch, framebuffer->ColorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst, framebuffer->DataProvider, nullptr, false, kCGRenderingIntentDefault);
|
|
if (framebuffer->Image == nullptr)
|
|
{
|
|
DestroyFramebuffer(framebuffer);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void ClearFramebuffer(ImGui_ImplCPU_Framebuffer* framebuffer, unsigned char r, unsigned char g, unsigned char b, unsigned char a)
|
|
{
|
|
const ImU32 pixel = PackBGRA(r, g, b, a);
|
|
for (int y = 0; y < framebuffer->Height; y++)
|
|
{
|
|
ImU32* dst = (ImU32*)(void*)(framebuffer->Pixels + (size_t)y * (size_t)framebuffer->Pitch);
|
|
for (int x = 0; x < framebuffer->Width; x++)
|
|
dst[x] = pixel;
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------------
|
|
// AppView
|
|
//-----------------------------------------------------------------------------------
|
|
|
|
@interface AppView : NSView
|
|
- (void)shutdownImGui;
|
|
@end
|
|
|
|
@implementation AppView
|
|
{
|
|
ExampleFramebuffer _framebuffer;
|
|
ExampleTimingHistory _rasterTimes;
|
|
ExampleTimingHistory _presentTimes;
|
|
BOOL _imguiInitialized;
|
|
BOOL _frameScheduled;
|
|
}
|
|
|
|
- (instancetype)initWithFrame:(NSRect)frame
|
|
{
|
|
self = [super initWithFrame:frame];
|
|
return self;
|
|
}
|
|
|
|
- (BOOL)isFlipped
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (void)viewDidMoveToWindow
|
|
{
|
|
[super viewDidMoveToWindow];
|
|
if (self.window != nil && !_imguiInitialized)
|
|
[self initializeImGui];
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[self shutdownImGui];
|
|
}
|
|
|
|
- (void)initializeImGui
|
|
{
|
|
IMGUI_CHECKVERSION();
|
|
ImGui::CreateContext();
|
|
ImGuiIO& io = ImGui::GetIO(); (void)io;
|
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
|
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
|
|
|
|
ImGui::StyleColorsDark();
|
|
|
|
ImGui_ImplOSX_Init(self);
|
|
ImGui_ImplCPU_Init();
|
|
|
|
_imguiInitialized = YES;
|
|
[self scheduleNextFrameAfter:0.0];
|
|
}
|
|
|
|
- (void)shutdownImGui
|
|
{
|
|
if (!_imguiInitialized)
|
|
return;
|
|
|
|
DestroyFramebuffer(&_framebuffer);
|
|
ImGui_ImplCPU_Shutdown();
|
|
ImGui_ImplOSX_Shutdown();
|
|
ImGui::DestroyContext();
|
|
_imguiInitialized = NO;
|
|
}
|
|
|
|
// Queue the next draw on the main loop instead of pacing to display refresh.
|
|
- (void)scheduleNextFrameAfter:(double)delay_seconds
|
|
{
|
|
if (_frameScheduled)
|
|
return;
|
|
_frameScheduled = YES;
|
|
dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay_seconds * (double)NSEC_PER_SEC));
|
|
dispatch_after(when, dispatch_get_main_queue(), ^{
|
|
self->_frameScheduled = NO;
|
|
[self setNeedsDisplay:YES];
|
|
});
|
|
}
|
|
|
|
- (void)renderFrame
|
|
{
|
|
if (!_imguiInitialized)
|
|
return;
|
|
|
|
if (self.window == nil || [self.window isMiniaturized])
|
|
{
|
|
[self scheduleNextFrameAfter:0.01];
|
|
return;
|
|
}
|
|
|
|
const CGFloat scale = self.window.backingScaleFactor > 0.0 ? self.window.backingScaleFactor : 1.0;
|
|
const int logical_w = (int)llround(self.bounds.size.width);
|
|
const int logical_h = (int)llround(self.bounds.size.height);
|
|
const int framebuffer_w = (int)llround(self.bounds.size.width * scale);
|
|
const int framebuffer_h = (int)llround(self.bounds.size.height * scale);
|
|
if (framebuffer_w <= 0 || framebuffer_h <= 0)
|
|
{
|
|
[self scheduleNextFrameAfter:0.01];
|
|
return;
|
|
}
|
|
|
|
if (_framebuffer.CPU.Width != framebuffer_w || _framebuffer.CPU.Height != framebuffer_h)
|
|
if (!RecreateFramebuffer(&_framebuffer, framebuffer_w, framebuffer_h))
|
|
return;
|
|
|
|
// Start the Dear ImGui frame
|
|
ImGui_ImplCPU_NewFrame();
|
|
ImGui_ImplOSX_NewFrame(self);
|
|
ImGui::NewFrame();
|
|
|
|
// Our state
|
|
static bool show_demo_window = true;
|
|
static bool show_another_window = false;
|
|
static ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
|
|
|
|
// 1. Show the big demo window
|
|
if (show_demo_window)
|
|
ImGui::ShowDemoWindow(&show_demo_window);
|
|
|
|
// 2. Show a simple window that we create ourselves.
|
|
{
|
|
static float f = 0.0f;
|
|
static int counter = 0;
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
ImGui::Begin("Hello, world!");
|
|
|
|
ImGui::Text("This is some useful text.");
|
|
ImGui::Checkbox("Demo Window", &show_demo_window);
|
|
ImGui::Checkbox("Another Window", &show_another_window);
|
|
|
|
ImGui::SliderFloat("float", &f, 0.0f, 1.0f);
|
|
ImGui::ColorEdit3("clear color", (float*)&clear_color);
|
|
|
|
if (ImGui::Button("Button"))
|
|
counter++;
|
|
ImGui::SameLine();
|
|
ImGui::Text("counter = %d", counter);
|
|
|
|
ImGui::Text("Raster %.3f ms | Present %.3f ms", _rasterTimes.GetAverageMs(), _presentTimes.GetAverageMs());
|
|
ImGui::Text("Window %d x %d | Framebuffer %d x %d (scale %.2f)", logical_w, logical_h, framebuffer_w, framebuffer_h, scale);
|
|
ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate);
|
|
ImGui::End();
|
|
}
|
|
|
|
// 3. Show another simple window.
|
|
if (show_another_window)
|
|
{
|
|
ImGui::Begin("Another Window", &show_another_window);
|
|
ImGui::Text("Hello from another window!");
|
|
if (ImGui::Button("Close Me"))
|
|
show_another_window = false;
|
|
ImGui::End();
|
|
}
|
|
|
|
// Rendering
|
|
ImGui::Render();
|
|
const double raster_start_ms = ExampleGetTimeMs();
|
|
ClearFramebuffer(&_framebuffer.CPU,
|
|
(unsigned char)(clear_color.x * 255),
|
|
(unsigned char)(clear_color.y * 255),
|
|
(unsigned char)(clear_color.z * 255),
|
|
(unsigned char)(clear_color.w * 255));
|
|
ImGui_ImplCPU_RenderDrawData(ImGui::GetDrawData(), &_framebuffer.CPU);
|
|
_rasterTimes.AddSample(ExampleGetTimeMs() - raster_start_ms);
|
|
}
|
|
|
|
- (void)drawRect:(NSRect)dirtyRect
|
|
{
|
|
IM_UNUSED(dirtyRect);
|
|
|
|
[self renderFrame];
|
|
|
|
if (_framebuffer.Image != nullptr)
|
|
{
|
|
CGContextRef context = NSGraphicsContext.currentContext.CGContext;
|
|
// CGImage draws with a bottom-up raster convention here, while the CPU framebuffer is top-down.
|
|
const double present_start_ms = ExampleGetTimeMs();
|
|
CGContextSaveGState(context);
|
|
CGContextTranslateCTM(context, 0.0, self.bounds.size.height);
|
|
CGContextScaleCTM(context, 1.0, -1.0);
|
|
CGContextDrawImage(context, CGRectMake(0.0, 0.0, self.bounds.size.width, self.bounds.size.height), _framebuffer.Image);
|
|
CGContextRestoreGState(context);
|
|
_presentTimes.AddSample(ExampleGetTimeMs() - present_start_ms);
|
|
}
|
|
|
|
[self scheduleNextFrameAfter:0.0];
|
|
}
|
|
|
|
@end
|
|
|
|
//-----------------------------------------------------------------------------------
|
|
// AppDelegate
|
|
//-----------------------------------------------------------------------------------
|
|
|
|
@interface AppDelegate : NSObject <NSApplicationDelegate>
|
|
@property (nonatomic, readonly) NSWindow* window;
|
|
@property (nonatomic, readonly) AppView* view;
|
|
@end
|
|
|
|
@implementation AppDelegate
|
|
{
|
|
NSWindow* _window;
|
|
AppView* _view;
|
|
}
|
|
|
|
@synthesize window = _window;
|
|
@synthesize view = _view;
|
|
|
|
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)application
|
|
{
|
|
IM_UNUSED(application);
|
|
return YES;
|
|
}
|
|
|
|
- (void)setupMenu
|
|
{
|
|
NSMenu* main_menu_bar = [[NSMenu alloc] init];
|
|
NSMenu* app_menu = [[NSMenu alloc] initWithTitle:@"Dear ImGui macOS+CPU Example"];
|
|
NSMenuItem* menu_item = [app_menu addItemWithTitle:@"Quit Dear ImGui macOS+CPU Example" action:@selector(terminate:) keyEquivalent:@"q"];
|
|
[menu_item setKeyEquivalentModifierMask:NSEventModifierFlagCommand];
|
|
|
|
menu_item = [[NSMenuItem alloc] init];
|
|
[menu_item setSubmenu:app_menu];
|
|
[main_menu_bar addItem:menu_item];
|
|
[NSApp setMainMenu:main_menu_bar];
|
|
}
|
|
|
|
- (void)applicationDidFinishLaunching:(NSNotification*)notification
|
|
{
|
|
IM_UNUSED(notification);
|
|
|
|
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
|
[self setupMenu];
|
|
|
|
NSRect view_rect = NSMakeRect(100.0, 100.0, 1280.0, 720.0);
|
|
_window = [[NSWindow alloc] initWithContentRect:view_rect styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable | NSWindowStyleMaskClosable backing:NSBackingStoreBuffered defer:NO];
|
|
[_window setTitle:@"Dear ImGui macOS+CPU Example"];
|
|
[_window setAcceptsMouseMovedEvents:YES];
|
|
[_window setOpaque:YES];
|
|
|
|
_view = [[AppView alloc] initWithFrame:_window.contentView.bounds];
|
|
[_view setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
|
|
[_window setContentView:_view];
|
|
[_window makeKeyAndOrderFront:NSApp];
|
|
[NSApp activateIgnoringOtherApps:YES];
|
|
}
|
|
|
|
- (void)applicationWillTerminate:(NSNotification*)notification
|
|
{
|
|
IM_UNUSED(notification);
|
|
[_view shutdownImGui];
|
|
}
|
|
|
|
@end
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
IM_UNUSED(argc);
|
|
IM_UNUSED(argv);
|
|
|
|
@autoreleasepool
|
|
{
|
|
NSApplication* application = [NSApplication sharedApplication];
|
|
AppDelegate* delegate = [AppDelegate new];
|
|
[application setDelegate:delegate];
|
|
[application run];
|
|
}
|
|
|
|
return 0;
|
|
}
|