Add macOS Cocoa port and CEF helper support

Introduce a macOS Cocoa-based UI and CEF helper subprocess support. CMake: enable OBJCXX on Apple, treat mac sources as .mm, set -fobjc-arc, link Cocoa frameworks, and generate helper app targets using a mac Info.plist template; keep libcef logical target off macOS. Implementation: add Objective-C++ implementations for browser_host and nebula_window, convert startup to ObjC++ (prepare NSApplication), add process_helper_mac (CEF helper entry) and load CEF library from main/main_bigpicture on mac. Tooling/docs: add .clangd fallback flags, compile_commands symlink helper, update cross-platform docs for mac status. Misc: add menu-popup UI page, define UNREFERENCED_PARAMETER in platform/types.h, small code cleanups (use (void)layout, include platform/types.h) and update .gitignore.
This commit is contained in:
2026-05-19 12:57:26 +12:00
parent 8cf9b50690
commit 29908646ea
19 changed files with 623 additions and 146 deletions
+1 -1
View File
@@ -211,7 +211,7 @@ void NebulaController::OnWindowCreated() {
}
void NebulaController::OnWindowResized(const nebula::window::BrowserLayout& layout) {
UNREFERENCED_PARAMETER(layout);
(void)layout;
ResizeBrowsers();
}
+1
View File
@@ -1,6 +1,7 @@
#include "cef/browser_client.h"
#include "include/cef_request.h"
#include "platform/types.h"
#include "include/wrapper/cef_helpers.h"
#include "ui/paths.h"
+1
View File
@@ -1,6 +1,7 @@
#include "cef/nebula_app.h"
#include "include/cef_process_message.h"
#include "platform/types.h"
#include "include/wrapper/cef_helpers.h"
namespace nebula::cef {
-75
View File
@@ -1,75 +0,0 @@
#include "platform/browser_host.h"
#include <algorithm>
namespace nebula::platform {
CefWindowInfo MakeChildWindowInfo(NativeWindow parent, const Rect& rect) {
CefWindowInfo info;
info.SetAsChild(parent, CefRect(rect.x, rect.y, rect.width, rect.height));
info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
return info;
}
CefWindowInfo MakeDevToolsPopup(NativeWindow parent, const char* title) {
CefWindowInfo info;
info.SetAsPopup(parent, title);
info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
return info;
}
void ResizeBrowserWindow(NativeWindow browser_window, const Rect& rect) {
UNREFERENCED_PARAMETER(browser_window);
UNREFERENCED_PARAMETER(rect);
}
void SetBrowserVisible(NativeWindow browser_window, bool visible) {
UNREFERENCED_PARAMETER(browser_window);
UNREFERENCED_PARAMETER(visible);
}
void RaiseBrowserWindow(NativeWindow browser_window) {
UNREFERENCED_PARAMETER(browser_window);
}
void MoveCursorToBrowserPoint(NativeWindow browser_window, int x, int y) {
UNREFERENCED_PARAMETER(browser_window);
UNREFERENCED_PARAMETER(x);
UNREFERENCED_PARAMETER(y);
}
int ScaleForParentWindow(NativeWindow parent, int value) {
UNREFERENCED_PARAMETER(parent);
return value;
}
std::pair<int, int> ParentClientSize(NativeWindow parent) {
UNREFERENCED_PARAMETER(parent);
return {1280, 720};
}
Rect MenuPopupRect(NativeWindow parent, const BrowserLayout& layout) {
const auto client_size = ParentClientSize(parent);
const int width = 260;
const int height = 258;
const int margin = 12;
const int overlap = 2;
const int x = std::max(0, client_size.first - width - margin);
const int y = std::max(0, layout.chrome.y + layout.chrome.height - overlap);
return {
x,
y,
std::min(client_size.first, x + width) - x,
std::min(client_size.second, y + height) - y,
};
}
std::string CacheBusterToken() {
return "0";
}
void DestroyTopLevelWindow(NativeWindow window) {
UNREFERENCED_PARAMETER(window);
}
} // namespace nebula::platform
+134
View File
@@ -0,0 +1,134 @@
#include "platform/browser_host.h"
#import <ApplicationServices/ApplicationServices.h>
#import <Cocoa/Cocoa.h>
#include <algorithm>
#include <chrono>
#include <cmath>
namespace nebula::platform {
namespace {
NSView* AsView(NativeWindow window) {
return (__bridge NSView*)window;
}
NSRect ToNativeRect(const Rect& rect) {
return NSMakeRect(rect.x, rect.y, std::max(0, rect.width), std::max(0, rect.height));
}
Rect ToPlatformRect(NSRect rect) {
return {
static_cast<int>(std::round(NSMinX(rect))),
static_cast<int>(std::round(NSMinY(rect))),
std::max(0, static_cast<int>(std::round(NSWidth(rect)))),
std::max(0, static_cast<int>(std::round(NSHeight(rect)))),
};
}
} // namespace
CefWindowInfo MakeChildWindowInfo(NativeWindow parent, const Rect& rect) {
CefWindowInfo info;
info.SetAsChild((__bridge CefWindowHandle)AsView(parent),
CefRect(rect.x, rect.y, rect.width, rect.height));
info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
return info;
}
CefWindowInfo MakeDevToolsPopup(NativeWindow parent, const char* title) {
(void)title;
CefWindowInfo info;
info.SetAsChild((__bridge CefWindowHandle)AsView(parent), CefRect(0, 0, 800, 600));
info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
return info;
}
void ResizeBrowserWindow(NativeWindow browser_window, const Rect& rect) {
NSView* view = AsView(browser_window);
if (!view) {
return;
}
[view setFrame:ToNativeRect(rect)];
}
void SetBrowserVisible(NativeWindow browser_window, bool visible) {
NSView* view = AsView(browser_window);
if (!view) {
return;
}
[view setHidden:!visible];
if (visible && [view superview]) {
[[view superview] addSubview:view positioned:NSWindowAbove relativeTo:nil];
}
}
void RaiseBrowserWindow(NativeWindow browser_window) {
NSView* view = AsView(browser_window);
if (view && [view superview]) {
[[view superview] addSubview:view positioned:NSWindowAbove relativeTo:nil];
}
}
void MoveCursorToBrowserPoint(NativeWindow browser_window, int x, int y) {
NSView* view = AsView(browser_window);
if (!view || ![view window]) {
return;
}
const NSPoint window_point = [view convertPoint:NSMakePoint(x, y) toView:nil];
const NSPoint screen_point = [[view window] convertPointToScreen:window_point];
const CGFloat screen_height = NSMaxY([[NSScreen mainScreen] frame]);
CGWarpMouseCursorPosition(CGPointMake(screen_point.x, screen_height - screen_point.y));
}
int ScaleForParentWindow(NativeWindow parent, int value) {
(void)parent;
return value;
}
std::pair<int, int> ParentClientSize(NativeWindow parent) {
NSView* view = AsView(parent);
if (!view) {
return {0, 0};
}
const Rect rect = ToPlatformRect([view bounds]);
return {rect.width, rect.height};
}
Rect MenuPopupRect(NativeWindow parent, const BrowserLayout& layout) {
const auto client_size = ParentClientSize(parent);
const int width = 260;
const int height = 258;
const int margin = 12;
const int overlap = 2;
const int x = std::max(0, client_size.first - width - margin);
const int y = std::max(0, layout.chrome.y + layout.chrome.height - overlap);
return {
x,
y,
std::min(client_size.first, x + width) - x,
std::min(client_size.second, y + height) - y,
};
}
std::string CacheBusterToken() {
const auto now = std::chrono::system_clock::now().time_since_epoch();
const auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
return std::to_string(millis);
}
void DestroyTopLevelWindow(NativeWindow window) {
NSView* view = AsView(window);
NSWindow* native_window = [view window];
if (native_window) {
[native_window setDelegate:nil];
[native_window close];
}
}
} // namespace nebula::platform
-45
View File
@@ -1,45 +0,0 @@
#include "window/nebula_window.h"
#include <memory>
namespace nebula::window {
struct nebula::window::NebulaWindowImpl {
WindowDelegate* delegate = nullptr;
};
NebulaWindow::NebulaWindow(WindowDelegate* delegate)
: impl_(std::make_unique<NebulaWindowImpl>()) {
impl_->delegate = delegate;
}
NebulaWindow::~NebulaWindow() = default;
bool NebulaWindow::Create(const platform::AppStartup& startup) {
UNREFERENCED_PARAMETER(startup);
return false;
}
platform::NativeWindow NebulaWindow::native_handle() const {
return nullptr;
}
BrowserLayout NebulaWindow::CurrentLayout(bool show_chrome) const {
UNREFERENCED_PARAMETER(show_chrome);
return {};
}
void NebulaWindow::ResizeChild(platform::NativeWindow child, const platform::Rect& rect) const {
UNREFERENCED_PARAMETER(child);
UNREFERENCED_PARAMETER(rect);
}
void NebulaWindow::Minimize() {}
void NebulaWindow::ToggleMaximize() {}
void NebulaWindow::SetFullscreen(bool fullscreen) { UNREFERENCED_PARAMETER(fullscreen); }
void NebulaWindow::Close() {}
void NebulaWindow::BeginDrag() {}
void NebulaWindow::SetTitle(const std::string& title) { UNREFERENCED_PARAMETER(title); }
void NebulaWindow::EnableFrameHitTest(platform::NativeWindow child) const { UNREFERENCED_PARAMETER(child); }
} // namespace nebula::window
+222
View File
@@ -0,0 +1,222 @@
#include "window/nebula_window.h"
#import <Cocoa/Cocoa.h>
#include <algorithm>
#include <cmath>
#include <memory>
namespace nebula::window {
struct NebulaWindowImpl;
} // namespace nebula::window
@interface NebulaContentView : NSView
@end
@implementation NebulaContentView
- (BOOL)isFlipped {
return YES;
}
@end
@interface NebulaWindowDelegate : NSObject <NSWindowDelegate> {
@private
nebula::window::NebulaWindowImpl* owner_;
}
- (instancetype)initWithOwner:(nebula::window::NebulaWindowImpl*)owner;
@end
namespace nebula::window {
namespace {
constexpr CGFloat kChromeHeight = 104.0;
NSRect ToNativeRect(const platform::Rect& rect) {
return NSMakeRect(rect.x, rect.y, std::max(0, rect.width), std::max(0, rect.height));
}
} // namespace
struct NebulaWindowImpl {
WindowDelegate* delegate = nullptr;
NSWindow* window = nil;
NebulaContentView* content_view = nil;
NebulaWindowDelegate* window_delegate = nil;
bool fullscreen = false;
BrowserLayout CurrentLayout(bool show_chrome) const {
const NSRect bounds = content_view ? [content_view bounds] : NSZeroRect;
const int width = std::max(0, static_cast<int>(std::round(NSWidth(bounds))));
const int height = std::max(0, static_cast<int>(std::round(NSHeight(bounds))));
const int chrome_height = show_chrome ? std::min(height, static_cast<int>(kChromeHeight)) : 0;
BrowserLayout layout;
layout.chrome = show_chrome ? platform::Rect{0, 0, width, chrome_height} : platform::Rect{};
layout.content = {0, chrome_height, width, std::max(0, height - chrome_height)};
return layout;
}
void NotifyCreated() const {
if (delegate) {
delegate->OnWindowCreated();
}
}
void NotifyResized() const {
if (delegate) {
delegate->OnWindowResized(CurrentLayout(true));
}
}
void NotifyCloseRequested() const {
if (delegate) {
delegate->OnWindowCloseRequested();
}
}
};
} // namespace nebula::window
@implementation NebulaWindowDelegate
- (instancetype)initWithOwner:(nebula::window::NebulaWindowImpl*)owner {
self = [super init];
if (self) {
owner_ = owner;
}
return self;
}
- (void)windowDidResize:(NSNotification*)notification {
(void)notification;
if (owner_) {
owner_->NotifyResized();
}
}
- (BOOL)windowShouldClose:(id)sender {
(void)sender;
if (owner_) {
owner_->NotifyCloseRequested();
}
return NO;
}
@end
namespace nebula::window {
NebulaWindow::NebulaWindow(WindowDelegate* delegate)
: impl_(std::make_unique<NebulaWindowImpl>()) {
impl_->delegate = delegate;
}
NebulaWindow::~NebulaWindow() = default;
bool NebulaWindow::Create(const platform::AppStartup& startup) {
(void)startup;
@autoreleasepool {
[NSApplication sharedApplication];
const NSRect visible_frame = [[NSScreen mainScreen] visibleFrame];
const CGFloat width = std::min<CGFloat>(1400.0, NSWidth(visible_frame));
const CGFloat height = std::min<CGFloat>(900.0, NSHeight(visible_frame));
const CGFloat x = NSMinX(visible_frame) + (NSWidth(visible_frame) - width) / 2.0;
const CGFloat y = NSMinY(visible_frame) + (NSHeight(visible_frame) - height) / 2.0;
impl_->content_view = [[NebulaContentView alloc] initWithFrame:NSMakeRect(0, 0, width, height)];
impl_->window_delegate = [[NebulaWindowDelegate alloc] initWithOwner:impl_.get()];
impl_->window = [[NSWindow alloc] initWithContentRect:NSMakeRect(x, y, width, height)
styleMask:NSWindowStyleMaskTitled |
NSWindowStyleMaskClosable |
NSWindowStyleMaskMiniaturizable |
NSWindowStyleMaskResizable |
NSWindowStyleMaskFullSizeContentView
backing:NSBackingStoreBuffered
defer:NO];
if (!impl_->window) {
return false;
}
[impl_->window setTitle:@"Nebula Browser"];
[impl_->window setTitleVisibility:NSWindowTitleHidden];
[impl_->window setTitlebarAppearsTransparent:YES];
[impl_->window setContentView:impl_->content_view];
[impl_->window setDelegate:impl_->window_delegate];
[impl_->window center];
[impl_->window makeKeyAndOrderFront:nil];
[NSApp activateIgnoringOtherApps:YES];
impl_->NotifyCreated();
return true;
}
}
platform::NativeWindow NebulaWindow::native_handle() const {
return (__bridge void*)impl_->content_view;
}
BrowserLayout NebulaWindow::CurrentLayout(bool show_chrome) const {
return impl_->CurrentLayout(show_chrome);
}
void NebulaWindow::ResizeChild(platform::NativeWindow child, const platform::Rect& rect) const {
NSView* view = (__bridge NSView*)child;
if (!view) {
return;
}
[view setFrame:ToNativeRect(rect)];
}
void NebulaWindow::Minimize() {
[impl_->window miniaturize:nil];
}
void NebulaWindow::ToggleMaximize() {
if (impl_->window && !impl_->fullscreen) {
[impl_->window zoom:nil];
}
}
void NebulaWindow::SetFullscreen(bool fullscreen) {
if (!impl_->window || impl_->fullscreen == fullscreen) {
return;
}
impl_->fullscreen = fullscreen;
[impl_->window toggleFullScreen:nil];
}
void NebulaWindow::Close() {
if (impl_->delegate) {
impl_->delegate->OnWindowCloseRequested();
}
}
void NebulaWindow::BeginDrag() {
if (!impl_->window) {
return;
}
NSEvent* event = [NSApp currentEvent];
if (event) {
[impl_->window performWindowDragWithEvent:event];
}
}
void NebulaWindow::SetTitle(const std::string& title) {
if (!impl_->window) {
return;
}
NSString* native_title = title.empty()
? @"Nebula Browser"
: [[NSString alloc] initWithUTF8String:title.c_str()];
[impl_->window setTitle:native_title ?: @"Nebula Browser"];
}
void NebulaWindow::EnableFrameHitTest(platform::NativeWindow child) const {
(void)child;
}
} // namespace nebula::window
@@ -1,5 +1,7 @@
#include "platform/startup.h"
#import <Cocoa/Cocoa.h>
#include <fcntl.h>
#include <filesystem>
#include <system_error>
@@ -16,7 +18,13 @@ int g_single_instance_lock = -1;
} // namespace
void PrepareApp() {}
void PrepareApp() {
@autoreleasepool {
[NSApplication sharedApplication];
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp finishLaunching];
}
}
bool TryAcquireSingleInstance() {
const auto lock_path = nebula::ui::GetUserDataDirectory() / ".nebula_instance.lock";
+4
View File
@@ -1,5 +1,9 @@
#pragma once
#ifndef UNREFERENCED_PARAMETER
#define UNREFERENCED_PARAMETER(P) (void)(P)
#endif
namespace nebula::platform {
struct Rect {