From 29908646ea3df2e884ee49a47e55e9e6433ee565 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 19 May 2026 12:57:26 +1200 Subject: [PATCH] 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. --- .clangd | 17 ++ .gitignore | 3 +- CMakeLists.txt | 101 +++++++- app/main.cpp | 11 + app/main_bigpicture.cpp | 11 + app/process_helper_mac.cc | 25 ++ cmake/mac-helper-Info.plist.in | 39 +++ compile_commands.json | 1 + docs/cross-platform.md | 39 ++- src/app/nebula_controller.cpp | 2 +- src/cef/browser_client.cpp | 1 + src/cef/nebula_app.cpp | 1 + src/platform/mac/browser_host_mac.cpp | 75 ------ src/platform/mac/browser_host_mac.mm | 134 +++++++++++ src/platform/mac/nebula_window_mac.cpp | 45 ---- src/platform/mac/nebula_window_mac.mm | 222 ++++++++++++++++++ .../mac/{startup_mac.cpp => startup_mac.mm} | 10 +- src/platform/types.h | 4 + ui/pages/menu-popup.html | 28 +++ 19 files changed, 623 insertions(+), 146 deletions(-) create mode 100644 .clangd create mode 100644 app/process_helper_mac.cc create mode 100644 cmake/mac-helper-Info.plist.in create mode 120000 compile_commands.json delete mode 100644 src/platform/mac/browser_host_mac.cpp create mode 100644 src/platform/mac/browser_host_mac.mm delete mode 100644 src/platform/mac/nebula_window_mac.cpp create mode 100644 src/platform/mac/nebula_window_mac.mm rename src/platform/mac/{startup_mac.cpp => startup_mac.mm} (86%) create mode 100644 ui/pages/menu-popup.html diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..27ca799 --- /dev/null +++ b/.clangd @@ -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 diff --git a/.gitignore b/.gitignore index 4db4cb5..4799348 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ CMakeFiles/ .DS_Store # Logs -*.log \ No newline at end of file +*.log +/.cache diff --git a/CMakeLists.txt b/CMakeLists.txt index aee675a..b35acf8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,11 @@ 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_REQUIRED ON) @@ -56,9 +61,16 @@ if(OS_WINDOWS) elseif(OS_MACOSX) set(NEBULA_PLATFORM_SOURCES src/platform/mac/paths_mac.cpp - src/platform/mac/startup_mac.cpp - src/platform/mac/browser_host_mac.cpp - src/platform/mac/nebula_window_mac.cpp + src/platform/mac/startup_mac.mm + src/platform/mac/browser_host_mac.mm + 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) set(NEBULA_PLATFORM_SOURCES @@ -71,7 +83,11 @@ else() message(FATAL_ERROR "Unsupported platform.") 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) FIND_LINUX_LIBRARIES("X11") @@ -107,11 +123,19 @@ function(add_nebula_app_target nebula_target entry_source) "${CEF_ROOT}/include" ) - target_link_libraries(${nebula_target} PRIVATE - libcef_lib - libcef_dll_wrapper - ${CEF_STANDARD_LIBS} - ) + if(OS_MACOSX) + # On macOS, CEF is a framework; don't link libcef_lib + target_link_libraries(${nebula_target} PRIVATE + libcef_dll_wrapper + ${CEF_STANDARD_LIBS} + ) + else() + target_link_libraries(${nebula_target} PRIVATE + libcef_lib + libcef_dll_wrapper + ${CEF_STANDARD_LIBS} + ) + endif() if(MSVC) 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_RESOURCE_FILES}" "${CEF_RESOURCE_DIR}" "${CEF_TARGET_OUT_DIR}") 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_HELPER_TARGET "${nebula_target}_Helper") + set(NEBULA_HELPER_OUTPUT_NAME "${nebula_target} Helper") + string(TOLOWER "${nebula_target}" NEBULA_HELPER_BUNDLE_NAME) + COPY_MAC_FRAMEWORK( "${nebula_target}" - "Chromium Embedded Framework" "${CEF_BINARY_DIR_RELEASE}" - "${NEBULA_APP}/Contents/Frameworks" + "${NEBULA_APP}" ) COPY_FILES( "${nebula_target}" @@ -154,6 +185,52 @@ function(add_nebula_app_target nebula_target entry_source) "${CEF_RESOURCE_DIR}" "${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() # ------------------------------------------------------------ diff --git a/app/main.cpp b/app/main.cpp index c5e7e8c..1bc42b5 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -3,7 +3,11 @@ #if defined(_WIN32) #include +#elif defined(__APPLE__) +#include "include/wrapper/cef_library_loader.h" +#endif +#if defined(_WIN32) int APIENTRY wWinMain(HINSTANCE instance, HINSTANCE previous_instance, LPWSTR command_line, @@ -16,6 +20,13 @@ int APIENTRY wWinMain(HINSTANCE instance, } #else 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}; return nebula::app::RunNebula(startup, {nebula::app::AppMode::Desktop}); } diff --git a/app/main_bigpicture.cpp b/app/main_bigpicture.cpp index 400444d..86cd383 100644 --- a/app/main_bigpicture.cpp +++ b/app/main_bigpicture.cpp @@ -3,7 +3,11 @@ #if defined(_WIN32) #include +#elif defined(__APPLE__) +#include "include/wrapper/cef_library_loader.h" +#endif +#if defined(_WIN32) int APIENTRY wWinMain(HINSTANCE instance, HINSTANCE previous_instance, LPWSTR command_line, @@ -16,6 +20,13 @@ int APIENTRY wWinMain(HINSTANCE instance, } #else 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}; return nebula::app::RunNebula(startup, {nebula::app::AppMode::BigPicture}); } diff --git a/app/process_helper_mac.cc b/app/process_helper_mac.cc new file mode 100644 index 0000000..620d3d7 --- /dev/null +++ b/app/process_helper_mac.cc @@ -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 app(new nebula::cef::NebulaApp); + return CefExecuteProcess(main_args, app, nullptr); +} diff --git a/cmake/mac-helper-Info.plist.in b/cmake/mac-helper-Info.plist.in new file mode 100644 index 0000000..9660d2b --- /dev/null +++ b/cmake/mac-helper-Info.plist.in @@ -0,0 +1,39 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${EXECUTABLE_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.nebula.browser.${HELPER_BUNDLE_NAME}.helper${BUNDLE_ID_SUFFIX} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleSignature + ???? + CFBundleVersion + 1.0 + CFBundleShortVersionString + 1.0 + LSEnvironment + + MallocNanoZone + 0 + + LSFileQuarantineEnabled + + LSMinimumSystemVersion + 12.0 + LSUIElement + 1 + NSSupportsAutomaticGraphicsSwitching + + + diff --git a/compile_commands.json b/compile_commands.json new file mode 120000 index 0000000..25eb4b2 --- /dev/null +++ b/compile_commands.json @@ -0,0 +1 @@ +build/compile_commands.json \ No newline at end of file diff --git a/docs/cross-platform.md b/docs/cross-platform.md index 40c21f0..43c1a94 100644 --- a/docs/cross-platform.md +++ b/docs/cross-platform.md @@ -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). -**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 @@ -29,7 +31,7 @@ src/platform/ paths_platform.h # ExecutableDirectory, DefaultUserDataRoot, PathToUtf8 win/ # Windows implementation - mac/ # macOS stubs + partial CEF glue + mac/ # macOS Cocoa implementation linux/ # Linux stubs + partial CEF glue 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. +## 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: ```bash @@ -88,12 +106,9 @@ cmake -B build -DPROJECT_ARCH=x86_64 # or arm64 on Apple Silicon 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 (see CEF’s `cefsimple` sample for the complete Mac bundle layout) - -Until that work lands, the macOS target may build but will exit early because `Create()` fails. +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 CEF’s `cefsimple` sample as the reference for the complete Mac bundle layout. ### Linux @@ -176,8 +191,10 @@ Platform-specific discovery lives in `src/platform/*/paths_*.cpp`. ## Porting checklist (macOS / Linux) -- [ ] Implement `NebulaWindow` in `nebula_window_mac.cpp` / `nebula_window_linux.cpp` -- [ ] Wire `browser_host_*.cpp` resize/show/raise to the real toolkit +- [x] macOS: implement `NebulaWindow` in `nebula_window_mac.mm` +- [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`) - [ ] Linux: confirm X11 (or chosen toolkit) parent handle for `CefWindowInfo::SetAsChild` - [ ] 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?** Not recommended (size, licensing, per-arch binaries). Keeping `thirdparty/cef/` gitignored is intentional. -**Why does macOS/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. +**Why does Linux build but not run?** +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?** `src/platform/win/nebula_window_win.cpp` — custom chrome, hit-testing, fullscreen, and child browser placement. diff --git a/src/app/nebula_controller.cpp b/src/app/nebula_controller.cpp index 1987aae..7102293 100644 --- a/src/app/nebula_controller.cpp +++ b/src/app/nebula_controller.cpp @@ -211,7 +211,7 @@ void NebulaController::OnWindowCreated() { } void NebulaController::OnWindowResized(const nebula::window::BrowserLayout& layout) { - UNREFERENCED_PARAMETER(layout); + (void)layout; ResizeBrowsers(); } diff --git a/src/cef/browser_client.cpp b/src/cef/browser_client.cpp index 7383a7a..acefced 100644 --- a/src/cef/browser_client.cpp +++ b/src/cef/browser_client.cpp @@ -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" diff --git a/src/cef/nebula_app.cpp b/src/cef/nebula_app.cpp index 13060d8..245bd9d 100644 --- a/src/cef/nebula_app.cpp +++ b/src/cef/nebula_app.cpp @@ -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 { diff --git a/src/platform/mac/browser_host_mac.cpp b/src/platform/mac/browser_host_mac.cpp deleted file mode 100644 index b18decb..0000000 --- a/src/platform/mac/browser_host_mac.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "platform/browser_host.h" - -#include - -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 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 diff --git a/src/platform/mac/browser_host_mac.mm b/src/platform/mac/browser_host_mac.mm new file mode 100644 index 0000000..cf78fc0 --- /dev/null +++ b/src/platform/mac/browser_host_mac.mm @@ -0,0 +1,134 @@ +#include "platform/browser_host.h" + +#import +#import + +#include +#include +#include + +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(std::round(NSMinX(rect))), + static_cast(std::round(NSMinY(rect))), + std::max(0, static_cast(std::round(NSWidth(rect)))), + std::max(0, static_cast(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 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(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 diff --git a/src/platform/mac/nebula_window_mac.cpp b/src/platform/mac/nebula_window_mac.cpp deleted file mode 100644 index 6e674e8..0000000 --- a/src/platform/mac/nebula_window_mac.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "window/nebula_window.h" - -#include - -namespace nebula::window { - -struct nebula::window::NebulaWindowImpl { - WindowDelegate* delegate = nullptr; -}; - -NebulaWindow::NebulaWindow(WindowDelegate* delegate) - : impl_(std::make_unique()) { - 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 diff --git a/src/platform/mac/nebula_window_mac.mm b/src/platform/mac/nebula_window_mac.mm new file mode 100644 index 0000000..f817a35 --- /dev/null +++ b/src/platform/mac/nebula_window_mac.mm @@ -0,0 +1,222 @@ +#include "window/nebula_window.h" + +#import + +#include +#include +#include + +namespace nebula::window { +struct NebulaWindowImpl; +} // namespace nebula::window + +@interface NebulaContentView : NSView +@end + +@implementation NebulaContentView +- (BOOL)isFlipped { + return YES; +} +@end + +@interface NebulaWindowDelegate : NSObject { +@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(std::round(NSWidth(bounds)))); + const int height = std::max(0, static_cast(std::round(NSHeight(bounds)))); + const int chrome_height = show_chrome ? std::min(height, static_cast(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()) { + 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(1400.0, NSWidth(visible_frame)); + const CGFloat height = std::min(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 diff --git a/src/platform/mac/startup_mac.cpp b/src/platform/mac/startup_mac.mm similarity index 86% rename from src/platform/mac/startup_mac.cpp rename to src/platform/mac/startup_mac.mm index 9a402be..f64a0aa 100644 --- a/src/platform/mac/startup_mac.cpp +++ b/src/platform/mac/startup_mac.mm @@ -1,5 +1,7 @@ #include "platform/startup.h" +#import + #include #include #include @@ -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"; diff --git a/src/platform/types.h b/src/platform/types.h index 2ecf450..204f674 100644 --- a/src/platform/types.h +++ b/src/platform/types.h @@ -1,5 +1,9 @@ #pragma once +#ifndef UNREFERENCED_PARAMETER +#define UNREFERENCED_PARAMETER(P) (void)(P) +#endif + namespace nebula::platform { struct Rect { diff --git a/ui/pages/menu-popup.html b/ui/pages/menu-popup.html new file mode 100644 index 0000000..4b62e5f --- /dev/null +++ b/ui/pages/menu-popup.html @@ -0,0 +1,28 @@ + + + + + + Nebula Menu + + + +
+ + + + + +
+ + 100% + +
+ + + +
+ + + +