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:
@@ -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
@@ -17,4 +17,5 @@ CMakeFiles/
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
*.log
|
||||
/.cache
|
||||
|
||||
+89
-12
@@ -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()
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#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});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#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});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
build/compile_commands.json
|
||||
+28
-11
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user