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
+17
View File
@@ -0,0 +1,17 @@
# clangd configuration for Nebula Browser
#
# Full diagnostics require:
# 1. CEF binary distribution unpacked to thirdparty/cef/
# 2. Running: cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
# 3. Symlink or copy: ln -s build/compile_commands.json .
#
# Without compile_commands.json, clangd uses fallback flags below.
CompileFlags:
Add:
- -std=c++20
- -xobjective-c++
- -Isrc
- -Ithirdparty/cef
- -Ithirdparty/cef/include
- -Wno-c++17-extensions
+2 -1
View File
@@ -17,4 +17,5 @@ CMakeFiles/
.DS_Store .DS_Store
# Logs # Logs
*.log *.log
/.cache
+89 -12
View File
@@ -1,6 +1,11 @@
cmake_minimum_required(VERSION 3.21) cmake_minimum_required(VERSION 3.21)
project(NebulaBrowser LANGUAGES CXX) # Enable OBJCXX early on macOS for Cocoa integration
if(APPLE)
project(NebulaBrowser LANGUAGES CXX OBJCXX)
else()
project(NebulaBrowser LANGUAGES CXX)
endif()
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -56,9 +61,16 @@ if(OS_WINDOWS)
elseif(OS_MACOSX) elseif(OS_MACOSX)
set(NEBULA_PLATFORM_SOURCES set(NEBULA_PLATFORM_SOURCES
src/platform/mac/paths_mac.cpp src/platform/mac/paths_mac.cpp
src/platform/mac/startup_mac.cpp src/platform/mac/startup_mac.mm
src/platform/mac/browser_host_mac.cpp src/platform/mac/browser_host_mac.mm
src/platform/mac/nebula_window_mac.cpp src/platform/mac/nebula_window_mac.mm
)
set_source_files_properties(
src/platform/mac/startup_mac.mm
src/platform/mac/browser_host_mac.mm
src/platform/mac/nebula_window_mac.mm
PROPERTIES
COMPILE_FLAGS "-fobjc-arc"
) )
elseif(OS_LINUX) elseif(OS_LINUX)
set(NEBULA_PLATFORM_SOURCES set(NEBULA_PLATFORM_SOURCES
@@ -71,7 +83,11 @@ else()
message(FATAL_ERROR "Unsupported platform.") message(FATAL_ERROR "Unsupported platform.")
endif() endif()
ADD_LOGICAL_TARGET("libcef_lib" "${CEF_LIB_RELEASE}" "${CEF_LIB_DEBUG}") # On macOS, CEF is a framework linked via CEF_STANDARD_LIBS.
# On Windows/Linux, we create a logical target for libcef.
if(NOT OS_MACOSX)
ADD_LOGICAL_TARGET("libcef_lib" "${CEF_LIB_RELEASE}" "${CEF_LIB_DEBUG}")
endif()
if(OS_LINUX) if(OS_LINUX)
FIND_LINUX_LIBRARIES("X11") FIND_LINUX_LIBRARIES("X11")
@@ -107,11 +123,19 @@ function(add_nebula_app_target nebula_target entry_source)
"${CEF_ROOT}/include" "${CEF_ROOT}/include"
) )
target_link_libraries(${nebula_target} PRIVATE if(OS_MACOSX)
libcef_lib # On macOS, CEF is a framework; don't link libcef_lib
libcef_dll_wrapper target_link_libraries(${nebula_target} PRIVATE
${CEF_STANDARD_LIBS} libcef_dll_wrapper
) ${CEF_STANDARD_LIBS}
)
else()
target_link_libraries(${nebula_target} PRIVATE
libcef_lib
libcef_dll_wrapper
${CEF_STANDARD_LIBS}
)
endif()
if(MSVC) if(MSVC)
set_property(TARGET ${nebula_target} PROPERTY set_property(TARGET ${nebula_target} PROPERTY
@@ -135,12 +159,19 @@ function(add_nebula_app_target nebula_target entry_source)
COPY_FILES("${nebula_target}" "${CEF_BINARY_FILES}" "${CEF_BINARY_DIR_RELEASE}" "${CEF_TARGET_OUT_DIR}") COPY_FILES("${nebula_target}" "${CEF_BINARY_FILES}" "${CEF_BINARY_DIR_RELEASE}" "${CEF_TARGET_OUT_DIR}")
COPY_FILES("${nebula_target}" "${CEF_RESOURCE_FILES}" "${CEF_RESOURCE_DIR}" "${CEF_TARGET_OUT_DIR}") COPY_FILES("${nebula_target}" "${CEF_RESOURCE_FILES}" "${CEF_RESOURCE_DIR}" "${CEF_TARGET_OUT_DIR}")
elseif(OS_MACOSX) elseif(OS_MACOSX)
target_link_libraries(${nebula_target} PRIVATE
"-framework Cocoa"
"-framework ApplicationServices"
)
set(NEBULA_APP "${CEF_TARGET_OUT_DIR}/${nebula_target}.app") set(NEBULA_APP "${CEF_TARGET_OUT_DIR}/${nebula_target}.app")
set(NEBULA_HELPER_TARGET "${nebula_target}_Helper")
set(NEBULA_HELPER_OUTPUT_NAME "${nebula_target} Helper")
string(TOLOWER "${nebula_target}" NEBULA_HELPER_BUNDLE_NAME)
COPY_MAC_FRAMEWORK( COPY_MAC_FRAMEWORK(
"${nebula_target}" "${nebula_target}"
"Chromium Embedded Framework"
"${CEF_BINARY_DIR_RELEASE}" "${CEF_BINARY_DIR_RELEASE}"
"${NEBULA_APP}/Contents/Frameworks" "${NEBULA_APP}"
) )
COPY_FILES( COPY_FILES(
"${nebula_target}" "${nebula_target}"
@@ -154,6 +185,52 @@ function(add_nebula_app_target nebula_target entry_source)
"${CEF_RESOURCE_DIR}" "${CEF_RESOURCE_DIR}"
"${NEBULA_APP}/Contents/Resources" "${NEBULA_APP}/Contents/Resources"
) )
foreach(_suffix_list ${CEF_HELPER_APP_SUFFIXES})
string(REPLACE ":" ";" _suffix_list ${_suffix_list})
list(GET _suffix_list 0 _name_suffix)
list(GET _suffix_list 1 _target_suffix)
list(GET _suffix_list 2 _plist_suffix)
set(_helper_target "${NEBULA_HELPER_TARGET}${_target_suffix}")
set(_helper_output_name "${NEBULA_HELPER_OUTPUT_NAME}${_name_suffix}")
set(_helper_info_plist "${CMAKE_CURRENT_BINARY_DIR}/${_helper_target}-Info.plist")
file(READ "${CMAKE_SOURCE_DIR}/cmake/mac-helper-Info.plist.in" _plist_contents)
string(REPLACE "\${EXECUTABLE_NAME}" "${_helper_output_name}" _plist_contents "${_plist_contents}")
string(REPLACE "\${PRODUCT_NAME}" "${_helper_output_name}" _plist_contents "${_plist_contents}")
string(REPLACE "\${HELPER_BUNDLE_NAME}" "${NEBULA_HELPER_BUNDLE_NAME}" _plist_contents "${_plist_contents}")
string(REPLACE "\${BUNDLE_ID_SUFFIX}" "${_plist_suffix}" _plist_contents "${_plist_contents}")
file(WRITE "${_helper_info_plist}" "${_plist_contents}")
add_executable(${_helper_target} MACOSX_BUNDLE
app/process_helper_mac.cc
src/cef/nebula_app.cpp
)
SET_EXECUTABLE_TARGET_PROPERTIES(${_helper_target})
add_dependencies(${_helper_target} libcef_dll_wrapper)
target_include_directories(${_helper_target} PRIVATE
"${CMAKE_SOURCE_DIR}/src"
"${CEF_ROOT}"
"${CEF_ROOT}/include"
)
target_link_libraries(${_helper_target} PRIVATE
libcef_dll_wrapper
${CEF_STANDARD_LIBS}
)
set_target_properties(${_helper_target} PROPERTIES
MACOSX_BUNDLE_INFO_PLIST "${_helper_info_plist}"
OUTPUT_NAME "${_helper_output_name}"
)
add_dependencies(${nebula_target} "${_helper_target}")
add_custom_command(TARGET ${nebula_target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CEF_TARGET_OUT_DIR}/${_helper_output_name}.app"
"${NEBULA_APP}/Contents/Frameworks/${_helper_output_name}.app"
VERBATIM
)
endforeach()
endif() endif()
# ------------------------------------------------------------ # ------------------------------------------------------------
+11
View File
@@ -3,7 +3,11 @@
#if defined(_WIN32) #if defined(_WIN32)
#include <windows.h> #include <windows.h>
#elif defined(__APPLE__)
#include "include/wrapper/cef_library_loader.h"
#endif
#if defined(_WIN32)
int APIENTRY wWinMain(HINSTANCE instance, int APIENTRY wWinMain(HINSTANCE instance,
HINSTANCE previous_instance, HINSTANCE previous_instance,
LPWSTR command_line, LPWSTR command_line,
@@ -16,6 +20,13 @@ int APIENTRY wWinMain(HINSTANCE instance,
} }
#else #else
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
#if defined(__APPLE__)
CefScopedLibraryLoader library_loader;
if (!library_loader.LoadInMain()) {
return 1;
}
#endif
const nebula::platform::AppStartup startup{argc, argv}; const nebula::platform::AppStartup startup{argc, argv};
return nebula::app::RunNebula(startup, {nebula::app::AppMode::Desktop}); return nebula::app::RunNebula(startup, {nebula::app::AppMode::Desktop});
} }
+11
View File
@@ -3,7 +3,11 @@
#if defined(_WIN32) #if defined(_WIN32)
#include <windows.h> #include <windows.h>
#elif defined(__APPLE__)
#include "include/wrapper/cef_library_loader.h"
#endif
#if defined(_WIN32)
int APIENTRY wWinMain(HINSTANCE instance, int APIENTRY wWinMain(HINSTANCE instance,
HINSTANCE previous_instance, HINSTANCE previous_instance,
LPWSTR command_line, LPWSTR command_line,
@@ -16,6 +20,13 @@ int APIENTRY wWinMain(HINSTANCE instance,
} }
#else #else
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
#if defined(__APPLE__)
CefScopedLibraryLoader library_loader;
if (!library_loader.LoadInMain()) {
return 1;
}
#endif
const nebula::platform::AppStartup startup{argc, argv}; const nebula::platform::AppStartup startup{argc, argv};
return nebula::app::RunNebula(startup, {nebula::app::AppMode::BigPicture}); return nebula::app::RunNebula(startup, {nebula::app::AppMode::BigPicture});
} }
+25
View File
@@ -0,0 +1,25 @@
#include "cef/nebula_app.h"
#include "include/cef_app.h"
#include "include/wrapper/cef_library_loader.h"
#if defined(CEF_USE_SANDBOX)
#include "include/cef_sandbox_mac.h"
#endif
int main(int argc, char* argv[]) {
#if defined(CEF_USE_SANDBOX)
CefScopedSandboxContext sandbox_context;
if (!sandbox_context.Initialize(argc, argv)) {
return 1;
}
#endif
CefScopedLibraryLoader library_loader;
if (!library_loader.LoadInHelper()) {
return 1;
}
const CefMainArgs main_args(argc, argv);
CefRefPtr<nebula::cef::NebulaApp> app(new nebula::cef::NebulaApp);
return CefExecuteProcess(main_args, app, nullptr);
}
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>com.nebula.browser.${HELPER_BUNDLE_NAME}.helper${BUNDLE_ID_SUFFIX}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>LSEnvironment</key>
<dict>
<key>MallocNanoZone</key>
<string>0</string>
</dict>
<key>LSFileQuarantineEnabled</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSUIElement</key>
<string>1</string>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
build/compile_commands.json
+28 -11
View File
@@ -15,7 +15,9 @@ Nebula Browser uses a **single codebase** and **one git branch** for Windows, ma
**Windows** has a full native shell (custom frame, DWM, embedded CEF views). **Windows** has a full native shell (custom frame, DWM, embedded CEF views).
**macOS and Linux** compile and link today, but `NebulaWindow::Create()` is still a stub that returns `false` until a real Cocoa / X11 (or GTK) host is implemented. **macOS** has a Cocoa native shell (`NSWindow` / `NSView`) with CEF child-view embedding.
**Linux** compiles and links today, but `NebulaWindow::Create()` is still a stub that returns `false` until a real X11 (or GTK) host is implemented.
## Directory layout ## Directory layout
@@ -29,7 +31,7 @@ src/platform/
paths_platform.h # ExecutableDirectory, DefaultUserDataRoot, PathToUtf8 paths_platform.h # ExecutableDirectory, DefaultUserDataRoot, PathToUtf8
win/ # Windows implementation win/ # Windows implementation
mac/ # macOS stubs + partial CEF glue mac/ # macOS Cocoa implementation
linux/ # Linux stubs + partial CEF glue linux/ # Linux stubs + partial CEF glue
src/window/ src/window/
@@ -64,6 +66,22 @@ ui/ # HTML/JS UI (copied next to the binary at build time)
`thirdparty/cef/` is listed in `.gitignore` so binaries are never committed. Each developer and CI machine supplies its own copy. `thirdparty/cef/` is listed in `.gitignore` so binaries are never committed. Each developer and CI machine supplies its own copy.
## IDE / clangd setup
Without CEF headers, clangd will show many errors (unknown types like `CefWindowInfo`, `CefMainArgs`, etc.). To get full IDE support:
1. Unpack CEF into `thirdparty/cef/` as described above
2. Generate `compile_commands.json`:
```bash
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
```
3. Symlink it to the project root:
```bash
ln -s build/compile_commands.json .
```
The included `.clangd` file provides fallback flags, but full diagnostics require the compile database.
Optional: override the location at configure time: Optional: override the location at configure time:
```bash ```bash
@@ -88,12 +106,9 @@ cmake -B build -DPROJECT_ARCH=x86_64 # or arm64 on Apple Silicon
cmake --build build --config Release cmake --build build --config Release
``` ```
CMake builds a `NebulaBrowser.app` bundle and copies the Chromium Embedded Framework into `Contents/Frameworks`. A full macOS port still requires: CMake builds a `NebulaBrowser.app` bundle and copies the Chromium Embedded Framework into `Contents/Frameworks`. The macOS port includes a Cocoa `NSWindow` / `NSView` host and child-view embedding for CEF browsers.
- A real `NebulaWindow` implementation (Cocoa `NSWindow` / `NSView`) CEF **helper app** subprocess targets may still need to be added if the selected CEF distribution does not provide a compatible default subprocess layout. Use CEFs `cefsimple` sample as the reference for the complete Mac bundle layout.
- CEF **helper app** subprocess targets (see CEFs `cefsimple` sample for the complete Mac bundle layout)
Until that work lands, the macOS target may build but will exit early because `Create()` fails.
### Linux ### Linux
@@ -176,8 +191,10 @@ Platform-specific discovery lives in `src/platform/*/paths_*.cpp`.
## Porting checklist (macOS / Linux) ## Porting checklist (macOS / Linux)
- [ ] Implement `NebulaWindow` in `nebula_window_mac.cpp` / `nebula_window_linux.cpp` - [x] macOS: implement `NebulaWindow` in `nebula_window_mac.mm`
- [ ] Wire `browser_host_*.cpp` resize/show/raise to the real toolkit - [x] macOS: wire `browser_host_mac.mm` resize/show/raise to Cocoa views
- [ ] Linux: implement `NebulaWindow` in `nebula_window_linux.cpp`
- [ ] Linux: wire `browser_host_linux.cpp` resize/show/raise to the real toolkit
- [ ] macOS: add CEF helper app targets and bundle layout (copy from `cefsimple`) - [ ] macOS: add CEF helper app targets and bundle layout (copy from `cefsimple`)
- [ ] Linux: confirm X11 (or chosen toolkit) parent handle for `CefWindowInfo::SetAsChild` - [ ] Linux: confirm X11 (or chosen toolkit) parent handle for `CefWindowInfo::SetAsChild`
- [ ] Test GPU diagnostics page and hardware acceleration on target hardware - [ ] Test GPU diagnostics page and hardware acceleration on target hardware
@@ -191,8 +208,8 @@ No. Use one branch; swap `thirdparty/cef` and rebuild on each machine.
**Can I commit CEF into the repo?** **Can I commit CEF into the repo?**
Not recommended (size, licensing, per-arch binaries). Keeping `thirdparty/cef/` gitignored is intentional. Not recommended (size, licensing, per-arch binaries). Keeping `thirdparty/cef/` gitignored is intentional.
**Why does macOS/Linux build but not run?** **Why does Linux build but not run?**
The window stub intentionally returns `false` from `Create()` until a native shell exists. Shared CEF and browser code are ready; the missing piece is the host window. The Linux window stub intentionally returns `false` from `Create()` until a native shell exists. Shared CEF and browser code are ready; the missing piece is the host window.
**Where is the Windows UI code?** **Where is the Windows UI code?**
`src/platform/win/nebula_window_win.cpp` — custom chrome, hit-testing, fullscreen, and child browser placement. `src/platform/win/nebula_window_win.cpp` — custom chrome, hit-testing, fullscreen, and child browser placement.
+1 -1
View File
@@ -211,7 +211,7 @@ void NebulaController::OnWindowCreated() {
} }
void NebulaController::OnWindowResized(const nebula::window::BrowserLayout& layout) { void NebulaController::OnWindowResized(const nebula::window::BrowserLayout& layout) {
UNREFERENCED_PARAMETER(layout); (void)layout;
ResizeBrowsers(); ResizeBrowsers();
} }
+1
View File
@@ -1,6 +1,7 @@
#include "cef/browser_client.h" #include "cef/browser_client.h"
#include "include/cef_request.h" #include "include/cef_request.h"
#include "platform/types.h"
#include "include/wrapper/cef_helpers.h" #include "include/wrapper/cef_helpers.h"
#include "ui/paths.h" #include "ui/paths.h"
+1
View File
@@ -1,6 +1,7 @@
#include "cef/nebula_app.h" #include "cef/nebula_app.h"
#include "include/cef_process_message.h" #include "include/cef_process_message.h"
#include "platform/types.h"
#include "include/wrapper/cef_helpers.h" #include "include/wrapper/cef_helpers.h"
namespace nebula::cef { 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" #include "platform/startup.h"
#import <Cocoa/Cocoa.h>
#include <fcntl.h> #include <fcntl.h>
#include <filesystem> #include <filesystem>
#include <system_error> #include <system_error>
@@ -16,7 +18,13 @@ int g_single_instance_lock = -1;
} // namespace } // namespace
void PrepareApp() {} void PrepareApp() {
@autoreleasepool {
[NSApplication sharedApplication];
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp finishLaunching];
}
}
bool TryAcquireSingleInstance() { bool TryAcquireSingleInstance() {
const auto lock_path = nebula::ui::GetUserDataDirectory() / ".nebula_instance.lock"; const auto lock_path = nebula::ui::GetUserDataDirectory() / ".nebula_instance.lock";
+4
View File
@@ -1,5 +1,9 @@
#pragma once #pragma once
#ifndef UNREFERENCED_PARAMETER
#define UNREFERENCED_PARAMETER(P) (void)(P)
#endif
namespace nebula::platform { namespace nebula::platform {
struct Rect { struct Rect {
+28
View File
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nebula Menu</title>
<link rel="stylesheet" href="../css/menu-popup.css">
</head>
<body>
<main id="menu-popup" role="menu" aria-label="Nebula browser menu">
<button type="button" role="menuitem" data-cmd="open-settings">Settings</button>
<button type="button" role="menuitem" data-cmd="gpu-diagnostics">GPU diagnostics</button>
<button type="button" role="menuitem" data-cmd="big-picture">Big Picture mode</button>
<button type="button" role="menuitem" data-cmd="toggle-devtools">Developer tools</button>
<div class="zoom-controls" aria-label="Page zoom">
<button type="button" data-cmd="zoom-out" aria-label="Zoom out">-</button>
<span id="zoom-percent">100%</span>
<button type="button" data-cmd="zoom-in" aria-label="Zoom in">+</button>
</div>
<button type="button" role="menuitem" data-cmd="hard-reload">Hard reload</button>
<button type="button" role="menuitem" data-cmd="fresh-reload">Fresh reload</button>
</main>
<script src="../js/menu-popup.js"></script>
</body>
</html>