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:
@@ -211,7 +211,7 @@ void NebulaController::OnWindowCreated() {
|
||||
}
|
||||
|
||||
void NebulaController::OnWindowResized(const nebula::window::BrowserLayout& layout) {
|
||||
UNREFERENCED_PARAMETER(layout);
|
||||
(void)layout;
|
||||
ResizeBrowsers();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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";
|
||||
@@ -1,5 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef UNREFERENCED_PARAMETER
|
||||
#define UNREFERENCED_PARAMETER(P) (void)(P)
|
||||
#endif
|
||||
|
||||
namespace nebula::platform {
|
||||
|
||||
struct Rect {
|
||||
|
||||
Reference in New Issue
Block a user