diff --git a/CMakeLists.txt b/CMakeLists.txt index 980c86b..35dca5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,16 +16,11 @@ if(NOT EXISTS "${CEF_ROOT}/cmake/FindCEF.cmake") "CEF was not found.\n" "Expected CEF here:\n" " ${CEF_ROOT}\n\n" - "Make sure the contents of the CEF binary distribution are inside thirdparty/cef." + "Unpack the CEF binary distribution for your OS into thirdparty/cef." ) endif() -# ------------------------------------------------------------ -# CEF setup -# ------------------------------------------------------------ - list(APPEND CMAKE_MODULE_PATH "${CEF_ROOT}/cmake") - find_package(CEF REQUIRED) add_subdirectory( @@ -33,11 +28,13 @@ add_subdirectory( "${CMAKE_BINARY_DIR}/libcef_dll_wrapper" ) +SET_CEF_TARGET_OUT_DIR() + # ------------------------------------------------------------ -# Nebula source files +# Sources # ------------------------------------------------------------ -set(NEBULA_SOURCES +set(NEBULA_COMMON_SOURCES app/main.cpp src/app/nebula_controller.cpp src/app/run.cpp @@ -48,20 +45,47 @@ set(NEBULA_SOURCES src/cef/browser_client.cpp src/cef/nebula_app.cpp src/ui/paths.cpp - src/window/nebula_window.cpp ) -add_executable(NebulaBrowser WIN32 - ${NEBULA_SOURCES} -) +if(OS_WINDOWS) + set(NEBULA_PLATFORM_SOURCES + src/platform/win/paths_win.cpp + src/platform/win/startup_win.cpp + src/platform/win/browser_host_win.cpp + src/platform/win/nebula_window_win.cpp + ) + add_executable(NebulaBrowser WIN32 + ${NEBULA_COMMON_SOURCES} + ${NEBULA_PLATFORM_SOURCES} + ) +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 + ) + add_executable(NebulaBrowser MACOSX_BUNDLE + ${NEBULA_COMMON_SOURCES} + ${NEBULA_PLATFORM_SOURCES} + ) +elseif(OS_LINUX) + set(NEBULA_PLATFORM_SOURCES + src/platform/linux/paths_linux.cpp + src/platform/linux/startup_linux.cpp + src/platform/linux/browser_host_linux.cpp + src/platform/linux/nebula_window_linux.cpp + ) + add_executable(NebulaBrowser + ${NEBULA_COMMON_SOURCES} + ${NEBULA_PLATFORM_SOURCES} + ) +else() + message(FATAL_ERROR "Unsupported platform.") +endif() SET_EXECUTABLE_TARGET_PROPERTIES(NebulaBrowser) - -if(MSVC) - set_property(TARGET NebulaBrowser PROPERTY - MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" - ) -endif() +add_dependencies(NebulaBrowser libcef_dll_wrapper) target_include_directories(NebulaBrowser PRIVATE "${CMAKE_SOURCE_DIR}/src" @@ -69,42 +93,54 @@ target_include_directories(NebulaBrowser PRIVATE "${CEF_ROOT}/include" ) +ADD_LOGICAL_TARGET("libcef_lib" "${CEF_LIB_RELEASE}" "${CEF_LIB_DEBUG}") target_link_libraries(NebulaBrowser PRIVATE + libcef_lib libcef_dll_wrapper ${CEF_STANDARD_LIBS} ) -# ------------------------------------------------------------ -# Platform-specific CEF linking -# ------------------------------------------------------------ - -if(WIN32) - target_link_libraries(NebulaBrowser PRIVATE - "${CEF_ROOT}/Release/libcef.lib" - dwmapi - ) - - target_compile_definitions(NebulaBrowser PRIVATE - NOMINMAX - WIN32_LEAN_AND_MEAN +if(MSVC) + set_property(TARGET NebulaBrowser PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" ) endif() # ------------------------------------------------------------ -# Copy CEF runtime files after build +# Platform-specific CEF runtime deployment # ------------------------------------------------------------ -if(WIN32) - add_custom_command(TARGET NebulaBrowser POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory - "${CEF_ROOT}/Release" - "$" - - COMMAND ${CMAKE_COMMAND} -E copy_directory - "${CEF_ROOT}/Resources" - "$" - - COMMENT "Copying CEF runtime files..." +if(OS_WINDOWS) + target_link_libraries(NebulaBrowser PRIVATE dwmapi) + target_compile_definitions(NebulaBrowser PRIVATE + NOMINMAX + WIN32_LEAN_AND_MEAN + ) + COPY_FILES("NebulaBrowser" "${CEF_BINARY_FILES}" "${CEF_BINARY_DIR_RELEASE}" "${CEF_TARGET_OUT_DIR}") + COPY_FILES("NebulaBrowser" "${CEF_RESOURCE_FILES}" "${CEF_RESOURCE_DIR}" "${CEF_TARGET_OUT_DIR}") +elseif(OS_LINUX) + FIND_LINUX_LIBRARIES("X11") + COPY_FILES("NebulaBrowser" "${CEF_BINARY_FILES}" "${CEF_BINARY_DIR_RELEASE}" "${CEF_TARGET_OUT_DIR}") + COPY_FILES("NebulaBrowser" "${CEF_RESOURCE_FILES}" "${CEF_RESOURCE_DIR}" "${CEF_TARGET_OUT_DIR}") +elseif(OS_MACOSX) + set(NEBULA_APP "${CEF_TARGET_OUT_DIR}/NebulaBrowser.app") + COPY_MAC_FRAMEWORK( + "NebulaBrowser" + "Chromium Embedded Framework" + "${CEF_BINARY_DIR_RELEASE}" + "${NEBULA_APP}/Contents/Frameworks" + ) + COPY_FILES( + "NebulaBrowser" + "${CEF_BINARY_FILES}" + "${CEF_BINARY_DIR_RELEASE}" + "${NEBULA_APP}/Contents/Frameworks" + ) + COPY_FILES( + "NebulaBrowser" + "${CEF_RESOURCE_FILES}" + "${CEF_RESOURCE_DIR}" + "${NEBULA_APP}/Contents/Resources" ) endif() @@ -122,4 +158,4 @@ add_custom_command(TARGET NebulaBrowser POST_BUILD "$/ui/assets" COMMENT "Copying Nebula UI files and assets..." -) \ No newline at end of file +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..a71735c --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Nebula Browser + +A Chromium Embedded Framework (CEF) browser with a custom HTML chrome UI. + +## Documentation + +- **[Cross-platform build & architecture](docs/cross-platform.md)** — how to use one repo for Windows, macOS, and Linux; CEF setup; source layout; porting status. + +## Quick start (Windows) + +1. Download the [CEF standard binary distribution](https://cef-builds.spotifycdn.com/index.html) for Windows 64-bit. +2. Extract into `thirdparty/cef/` so `thirdparty/cef/cmake/FindCEF.cmake` exists. +3. Build: + + ```powershell + cmake -B build + cmake --build build --config Release + ``` + +4. Run `build\Release\NebulaBrowser.exe`. + +For macOS/Linux prerequisites, directory structure, and current port status, see [docs/cross-platform.md](docs/cross-platform.md). + +## License + +See the CEF distribution and Chromium license terms for third-party runtime components. diff --git a/app/main.cpp b/app/main.cpp index 27ae6be..05abb3d 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -1,6 +1,8 @@ -#include - #include "app/run.h" +#include "platform/types.h" + +#if defined(_WIN32) +#include int APIENTRY wWinMain(HINSTANCE instance, HINSTANCE previous_instance, @@ -9,5 +11,12 @@ int APIENTRY wWinMain(HINSTANCE instance, UNREFERENCED_PARAMETER(previous_instance); UNREFERENCED_PARAMETER(command_line); - return nebula::app::RunNebula(instance, show_command); + const nebula::platform::AppStartup startup{instance, show_command}; + return nebula::app::RunNebula(startup); } +#else +int main(int argc, char* argv[]) { + const nebula::platform::AppStartup startup{argc, argv}; + return nebula::app::RunNebula(startup); +} +#endif diff --git a/docs/cross-platform.md b/docs/cross-platform.md new file mode 100644 index 0000000..40c21f0 --- /dev/null +++ b/docs/cross-platform.md @@ -0,0 +1,198 @@ +# Cross-platform build guide + +Nebula Browser uses a **single codebase** and **one git branch** for Windows, macOS, and Linux. You do not maintain separate platform branches. Instead, each machine unpacks the correct [CEF](https://bitbucket.org/chromiumembedded/cef) binary distribution into `thirdparty/cef/` and builds with CMake. + +## Overview + +| Layer | Location | Platform-specific? | +|-------|----------|-------------------| +| CEF binaries | `thirdparty/cef/` (gitignored) | Yes — one distribution per OS | +| CMake / deploy | `CMakeLists.txt` | Yes — uses CEF’s `COPY_FILES`, `COPY_MAC_FRAMEWORK`, etc. | +| App entry & CEF init | `src/app/run.cpp`, `src/platform/*/startup_*.cpp` | Partly | +| Native window shell | `src/platform/win\|mac\|linux/` | Yes | +| Browser embedding | `src/platform/*/browser_host_*.cpp` | Partly | +| Tabs, URLs, UI routing | `src/browser/`, `src/ui/`, `src/cef/` | No (shared) | + +**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. + +## Directory layout + +``` +thirdparty/cef/ # CEF standard distribution for your OS (not in git) + +src/platform/ + types.h # Rect, BrowserLayout, NativeWindow, AppStartup + startup.h # PrepareApp, single-instance, CefMainArgs, CefSettings paths + browser_host.h # CefWindowInfo helpers, resize/show/destroy browser HWNDs + paths_platform.h # ExecutableDirectory, DefaultUserDataRoot, PathToUtf8 + + win/ # Windows implementation + mac/ # macOS stubs + partial CEF glue + linux/ # Linux stubs + partial CEF glue + +src/window/ + nebula_window.h # Platform-agnostic window API (PIMPL) + +src/app/ # Shared controller and run loop +src/browser/ # Shared tab/session logic +src/cef/ # Shared CEF app and browser client +src/ui/ # Shared nebula:// URLs and path helpers +ui/ # HTML/JS UI (copied next to the binary at build time) +``` + +## Prerequisites + +- **CMake** 3.21+ +- **C++20** compiler + - Windows: Visual Studio 2022 + - macOS: Xcode 16+ (Clang) + - Linux: GCC 10+ or Clang, plus development packages for X11 (CEF’s CMake runs `pkg-config` for X11 on Linux) +- **CEF standard binary distribution** for your OS and CPU architecture from the [CEF builds](https://cef-builds.spotifycdn.com/index.html) page (or your usual CEF source) + +## CEF setup + +1. Download the **standard distribution** for your platform (e.g. `cef_binary_*_windows64`, `*_macos*`, `*_linux64`). +2. Extract so that this path exists: + + ``` + thirdparty/cef/cmake/FindCEF.cmake + ``` + +3. The rest of the distribution (`Release/`, `Resources/`, `libcef_dll/`, etc.) should sit directly under `thirdparty/cef/` as shipped by CEF. + +`thirdparty/cef/` is listed in `.gitignore` so binaries are never committed. Each developer and CI machine supplies its own copy. + +Optional: override the location at configure time: + +```bash +cmake -B build -DCEF_ROOT=/path/to/cef_binary +``` + +## Building + +### Windows + +```powershell +cmake -B build +cmake --build build --config Release +``` + +Output: `build/Release/NebulaBrowser.exe` (plus CEF DLLs and `ui/` copied beside it). + +### macOS + +```bash +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: + +- 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. + +### Linux + +```bash +cmake -B build +cmake --build build +``` + +Output: `build/NebulaBrowser` (or `build//NebulaBrowser` depending on generator). CEF `.so` files and resources are copied next to the executable. + +You may need to set **SUID** on `chrome-sandbox` as printed by CMake post-build (CEF documents this in its Linux README). + +Until a Linux host window exists, the binary will not show a UI for the same reason as macOS. + +## How CMake picks a platform + +`find_package(CEF)` loads CEF’s `cef_variables.cmake`, which sets: + +- `OS_WINDOWS` +- `OS_MACOSX` +- `OS_LINUX` + +`CMakeLists.txt` then: + +1. Adds the matching `src/platform/{win,mac,linux}/*.cpp` sources +2. Sets the executable type (`WIN32`, `MACOSX_BUNDLE`, or plain executable) +3. Links `libcef_dll_wrapper`, `libcef_lib`, and `${CEF_STANDARD_LIBS}` +4. Runs CEF’s copy macros so runtime files land beside the app + +Shared sources are always compiled; only the `src/platform/...` tree changes per OS. + +## Application flow + +``` +main (app/main.cpp) + └─ RunNebula(AppStartup) src/app/run.cpp + ├─ platform::PrepareApp() + ├─ CefExecuteProcess() (subprocesses) + ├─ platform::TryAcquireSingleInstance() + ├─ CefInitialize() + ├─ NebulaController::Create() + │ └─ NebulaWindow::Create() platform-specific + └─ CefRunMessageLoop() +``` + +`AppStartup` carries platform entry data: + +- **Windows:** `HINSTANCE` + show command (`wWinMain`) +- **macOS / Linux:** `argc` / `argv` (`main`) + +## User data and profile paths + +Shared logic in `src/ui/paths.cpp` writes under: + +| OS | Default root | +|----|----------------| +| Windows | `%LOCALAPPDATA%\Nebula\User Data` (falls back to exe directory) | +| macOS | `~/Library/Application Support/Nebula/User Data` | +| Linux | `$XDG_DATA_HOME/Nebula/User Data` or `~/.local/share/Nebula/User Data` | + +Platform-specific discovery lives in `src/platform/*/paths_*.cpp`. + +## GPU / Chromium flags + +`src/cef/nebula_app.cpp` applies shared switches (`no-sandbox`, `ignore-gpu-blocklist`, etc.) and platform graphics backends: + +| OS | Backend | +|----|---------| +| Windows | ANGLE + D3D11 | +| macOS | ANGLE + Metal | +| Linux | EGL | + +## Adding or changing platform code + +1. **Shared behavior** (tabs, `nebula://` URLs, CEF clients) → edit under `src/browser/`, `src/ui/`, `src/cef/`, `src/app/` only if it is truly cross-platform. +2. **Native window or OS API** → edit `src/platform//` and keep `src/window/nebula_window.h` free of `#include ` (or Cocoa/X11 headers). +3. **CEF child window embedding** → `src/platform//browser_host_*.cpp` (`MakeChildWindowInfo`, resize, visibility). +4. **Startup / paths** → `startup_*.cpp` and `paths_*.cpp` in the same folder. +5. **New OS** → add `src/platform//`, extend the `if(OS_...)` blocks in `CMakeLists.txt`, and document CEF distribution layout here. + +## 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 +- [ ] 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 +- [ ] Package or script runtime layout (`ui/`, CEF frameworks/libs, ICU locales, etc.) + +## FAQ + +**Do I need a separate git branch per OS?** +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. + +**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 740d4c8..021e52f 100644 --- a/src/app/nebula_controller.cpp +++ b/src/app/nebula_controller.cpp @@ -1,7 +1,5 @@ #include "app/nebula_controller.h" -#include - #include #include #include @@ -15,6 +13,7 @@ #include "include/cef_browser.h" #include "include/cef_cookie.h" #include "include/wrapper/cef_helpers.h" +#include "platform/browser_host.h" #include "ui/paths.h" namespace nebula::app { @@ -22,19 +21,6 @@ namespace { constexpr size_t kMaxSiteHistoryEntries = 200; -std::wstring Utf8ToWide(const std::string& value) { - if (value.empty()) { - return {}; - } - - const int size = MultiByteToWideChar( - CP_UTF8, 0, value.data(), static_cast(value.size()), nullptr, 0); - std::wstring result(size, L'\0'); - MultiByteToWideChar( - CP_UTF8, 0, value.data(), static_cast(value.size()), result.data(), size); - return result; -} - std::filesystem::path GetSiteHistoryPath() { const auto user_data = nebula::ui::GetUserDataDirectory(); return user_data.empty() ? std::filesystem::path{} : user_data / L"site_history.txt"; @@ -96,22 +82,6 @@ std::string SiteHistoryJson(const std::vector& history) { return json; } -CefWindowInfo ChildWindowInfo(HWND parent, const RECT& rect) { - CefWindowInfo info; - info.SetAsChild( - parent, - CefRect( - rect.left, - rect.top, - rect.right - rect.left, - rect.bottom - rect.top)); - // CEF defaults to the Chrome runtime style, which ignores the - // SetAsChild hint and creates a top-level window per browser. Force the - // Alloy runtime style so each browser embeds inside the Nebula HWND. - info.runtime_style = CEF_RUNTIME_STYLE_ALLOY; - return info; -} - CefBrowserSettings BrowserSettings() { CefBrowserSettings settings; settings.webgl = STATE_ENABLED; @@ -124,47 +94,6 @@ int ParseTabId(const std::string& value) { return result.ec == std::errc{} && result.ptr == value.data() + value.size() ? tab_id : 0; } -int ScaleForWindow(HWND hwnd, int value) { - return MulDiv(value, static_cast(GetDpiForWindow(hwnd)), 96); -} - -RECT MenuPopupRect(HWND hwnd, const nebula::window::BrowserLayout& layout) { - RECT client = {}; - GetClientRect(hwnd, &client); - - const int width = ScaleForWindow(hwnd, 260); - const int height = ScaleForWindow(hwnd, 258); - const int margin = ScaleForWindow(hwnd, 12); - const int overlap = ScaleForWindow(hwnd, 2); - - const LONG x = std::max(margin, client.right - width - margin); - const LONG y = std::max(0, layout.chrome.bottom - overlap); - return { - x, - y, - std::min(client.right, x + width), - std::min(client.bottom, y + height), - }; -} - -void ApplyRoundedWindowRegion(HWND hwnd, int corner_radius) { - RECT rect = {}; - if (!hwnd || !GetClientRect(hwnd, &rect)) { - return; - } - - HRGN region = CreateRoundRectRgn( - 0, - 0, - std::max(1, rect.right - rect.left) + 1, - std::max(1, rect.bottom - rect.top) + 1, - corner_radius, - corner_radius); - if (region && !SetWindowRgn(hwnd, region, TRUE)) { - DeleteObject(region); - } -} - std::string WithCacheBuster(std::string url) { if (url.empty()) { return url; @@ -178,7 +107,7 @@ std::string WithCacheBuster(std::string url) { } const char separator = url.find('?') == std::string::npos ? '?' : '&'; - return url + separator + "nebula_cache_bust=" + std::to_string(GetTickCount64()) + fragment; + return url + separator + "nebula_cache_bust=" + nebula::platform::CacheBusterToken() + fragment; } std::string GetChromeDisplayUrl(const std::string& url) { @@ -190,23 +119,14 @@ void SetBrowserVisible(CefRefPtr browser, bool visible) { return; } - const HWND hwnd = browser->GetHost()->GetWindowHandle(); - if (!hwnd) { - return; - } - - ShowWindow(hwnd, visible ? SW_SHOW : SW_HIDE); - if (visible) { - SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); - } + nebula::platform::SetBrowserVisible(browser->GetHost()->GetWindowHandle(), visible); } } // namespace -NebulaController::NebulaController(HINSTANCE instance, std::string initial_url, int show_command) - : instance_(instance), +NebulaController::NebulaController(nebula::platform::AppStartup startup, std::string initial_url) + : startup_(startup), initial_url_(std::move(initial_url)), - show_command_(show_command), tabs_(this), site_history_(LoadSiteHistory()) {} @@ -214,7 +134,7 @@ NebulaController::~NebulaController() = default; bool NebulaController::Create() { window_ = std::make_unique(this); - return window_->Create(instance_, show_command_); + return window_->Create(startup_); } void NebulaController::OnWindowCreated() { @@ -240,8 +160,8 @@ void NebulaController::OnWindowCloseRequested() { // child browser finishes its JS unload + DoClose phase. Destroy the // Nebula window now so CEF can tear down the child browser HWNDs and // fire OnBeforeClose; MaybeFinishShutdown will then quit the loop. - if (window_ && window_->hwnd()) { - DestroyWindow(window_->hwnd()); + if (window_ && window_->native_handle()) { + nebula::platform::DestroyTopLevelWindow(window_->native_handle()); } MaybeFinishShutdown(); return; @@ -398,7 +318,7 @@ void NebulaController::OnContentTitleChanged(CefRefPtr browser, cons PersistSession(); const auto* active_tab = tabs_.ActiveTab(); if (window_ && active_tab && active_tab->browser && active_tab->browser->IsSame(browser)) { - window_->SetTitle(Utf8ToWide(title.empty() ? "Nebula Browser" : title + " - Nebula")); + window_->SetTitle(title.empty() ? "Nebula Browser" : title + " - Nebula"); } } @@ -514,20 +434,21 @@ void NebulaController::CloseTab(int tab_id) { } void NebulaController::CreateChromeBrowser() { - if (!window_ || !window_->hwnd()) { + if (!window_ || !window_->native_handle()) { return; } const auto layout = window_->CurrentLayout(); CefBrowserSettings browser_settings = BrowserSettings(); chrome_client_ = new nebula::cef::NebulaBrowserClient(nebula::cef::BrowserRole::Chrome, this); - CefWindowInfo window_info = ChildWindowInfo(window_->hwnd(), layout.chrome); + CefWindowInfo window_info = + nebula::platform::MakeChildWindowInfo(window_->native_handle(), layout.chrome); CefBrowserHost::CreateBrowser( window_info, chrome_client_, nebula::ui::GetChromeUrl(), browser_settings, nullptr, nullptr); } void NebulaController::CreateContentBrowser() { - if (!window_ || !window_->hwnd()) { + if (!window_ || !window_->native_handle()) { return; } @@ -536,7 +457,8 @@ void NebulaController::CreateContentBrowser() { const auto layout = window_->CurrentLayout(); CefBrowserSettings browser_settings = BrowserSettings(); content_client_ = new nebula::cef::NebulaBrowserClient(nebula::cef::BrowserRole::Content, this); - CefWindowInfo window_info = ChildWindowInfo(window_->hwnd(), layout.content); + CefWindowInfo window_info = + nebula::platform::MakeChildWindowInfo(window_->native_handle(), layout.content); CefBrowserHost::CreateBrowser( window_info, content_client_, nebula::ui::ResolveInternalUrl(url), browser_settings, nullptr, nullptr); } @@ -557,33 +479,38 @@ void NebulaController::CloseMenuPopup() { } void NebulaController::CreateMenuPopupBrowser() { - if (!window_ || !window_->hwnd()) { + if (!window_ || !window_->native_handle()) { return; } const auto layout = window_->CurrentLayout(); CefBrowserSettings browser_settings = BrowserSettings(); menu_popup_client_ = new nebula::cef::NebulaBrowserClient(nebula::cef::BrowserRole::MenuPopup, this); - CefWindowInfo window_info = ChildWindowInfo(window_->hwnd(), MenuPopupRect(window_->hwnd(), layout)); + CefWindowInfo window_info = nebula::platform::MakeChildWindowInfo( + window_->native_handle(), + nebula::platform::MenuPopupRect(window_->native_handle(), layout)); CefBrowserHost::CreateBrowser( window_info, menu_popup_client_, nebula::ui::GetMenuPopupUrl(), browser_settings, nullptr, nullptr); } void NebulaController::PositionMenuPopup() { - if (content_fullscreen_ || !window_ || !window_->hwnd() || !menu_popup_browser_) { + if (content_fullscreen_ || !window_ || !window_->native_handle() || !menu_popup_browser_) { return; } - const auto rect = MenuPopupRect(window_->hwnd(), window_->CurrentLayout()); - const HWND hwnd = menu_popup_browser_->GetHost()->GetWindowHandle(); - window_->ResizeChild(hwnd, rect); - ApplyRoundedWindowRegion(hwnd, ScaleForWindow(window_->hwnd(), 28)); - SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + const auto rect = + nebula::platform::MenuPopupRect(window_->native_handle(), window_->CurrentLayout()); + const auto browser_window = menu_popup_browser_->GetHost()->GetWindowHandle(); + window_->ResizeChild(browser_window, rect); + nebula::platform::ApplyRoundedBrowserRegion( + browser_window, + nebula::platform::ScaleForParentWindow(window_->native_handle(), 28)); + nebula::platform::RaiseBrowserWindow(browser_window); } void NebulaController::ToggleDevTools() { auto* tab = tabs_.ActiveTab(); - if (!tab || !tab->browser || !window_ || !window_->hwnd()) { + if (!tab || !tab->browser || !window_ || !window_->native_handle()) { return; } @@ -593,9 +520,8 @@ void NebulaController::ToggleDevTools() { return; } - CefWindowInfo window_info; - window_info.SetAsPopup(window_->hwnd(), "Nebula Developer Tools"); - window_info.runtime_style = CEF_RUNTIME_STYLE_ALLOY; + CefWindowInfo window_info = + nebula::platform::MakeDevToolsPopup(window_->native_handle(), "Nebula Developer Tools"); CefBrowserSettings browser_settings; host->ShowDevTools(window_info, content_client_, browser_settings, CefPoint()); } @@ -643,10 +569,14 @@ void NebulaController::ResizeBrowsers() { const auto layout = window_->CurrentLayout(!content_fullscreen_); if (chrome_browser_) { - window_->ResizeChild(chrome_browser_->GetHost()->GetWindowHandle(), layout.chrome); + window_->ResizeChild( + chrome_browser_->GetHost()->GetWindowHandle(), + layout.chrome); } if (const auto* tab = tabs_.ActiveTab(); tab && tab->browser) { - window_->ResizeChild(tab->browser->GetHost()->GetWindowHandle(), layout.content); + window_->ResizeChild( + tab->browser->GetHost()->GetWindowHandle(), + layout.content); } if (!content_fullscreen_) { PositionMenuPopup(); @@ -731,8 +661,8 @@ void NebulaController::MaybeFinishShutdown() { return; } - if (window_ && window_->hwnd()) { - DestroyWindow(window_->hwnd()); + if (window_ && window_->native_handle()) { + nebula::platform::DestroyTopLevelWindow(window_->native_handle()); } CefQuitMessageLoop(); } diff --git a/src/app/nebula_controller.h b/src/app/nebula_controller.h index dd08f69..66ba36f 100644 --- a/src/app/nebula_controller.h +++ b/src/app/nebula_controller.h @@ -7,6 +7,7 @@ #include "browser/tab_manager.h" #include "cef/browser_client.h" +#include "platform/types.h" #include "window/nebula_window.h" namespace nebula::app { @@ -15,7 +16,7 @@ class NebulaController final : public nebula::window::WindowDelegate, public nebula::browser::TabObserver, public nebula::cef::BrowserClientDelegate { public: - NebulaController(HINSTANCE instance, std::string initial_url, int show_command); + NebulaController(nebula::platform::AppStartup startup, std::string initial_url); ~NebulaController() override; bool Create(); @@ -60,9 +61,8 @@ private: void PersistSession() const; void MaybeFinishShutdown(); - HINSTANCE instance_ = nullptr; + nebula::platform::AppStartup startup_; std::string initial_url_; - int show_command_ = SW_SHOWDEFAULT; bool closing_ = false; bool chrome_ready_ = false; bool content_fullscreen_ = false; diff --git a/src/app/run.cpp b/src/app/run.cpp index a6e1816..fbb5978 100644 --- a/src/app/run.cpp +++ b/src/app/run.cpp @@ -4,41 +4,15 @@ #include "cef/nebula_app.h" #include "include/cef_app.h" #include "include/cef_command_line.h" +#include "platform/startup.h" #include "ui/paths.h" namespace nebula::app { -namespace { -constexpr wchar_t kMainInstanceMutexName[] = L"Local\\NebulaBrowserMainInstance"; +int RunNebula(const nebula::platform::AppStartup& startup) { + nebula::platform::PrepareApp(); -void EnableDpiAwareness() { - SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); -} - -class ScopedHandle { -public: - explicit ScopedHandle(HANDLE handle) : handle_(handle) {} - ~ScopedHandle() { - if (handle_) { - CloseHandle(handle_); - } - } - - ScopedHandle(const ScopedHandle&) = delete; - ScopedHandle& operator=(const ScopedHandle&) = delete; - - bool valid() const { return handle_ != nullptr; } - -private: - HANDLE handle_ = nullptr; -}; - -} // namespace - -int RunNebula(HINSTANCE instance, int show_command) { - EnableDpiAwareness(); - - CefMainArgs main_args(instance); + const CefMainArgs main_args = nebula::platform::MakeMainArgs(startup); CefRefPtr app(new nebula::cef::NebulaApp); const int subprocess_exit_code = CefExecuteProcess(main_args, app, nullptr); @@ -46,41 +20,28 @@ int RunNebula(HINSTANCE instance, int show_command) { return subprocess_exit_code; } - ScopedHandle main_instance_mutex(CreateMutexW(nullptr, TRUE, kMainInstanceMutexName)); - if (main_instance_mutex.valid() && GetLastError() == ERROR_ALREADY_EXISTS) { + if (!nebula::platform::TryAcquireSingleInstance()) { return 0; } CefSettings settings; settings.no_sandbox = true; settings.persist_session_cookies = true; - - // A persistent profile is required for the GPU shader cache and several - // hardware acceleration features. Without these Chromium silently falls - // back to software rendering, which causes choppy video and disables - // WebGL/WebGL2 in the GPU diagnostics page. - const std::wstring user_data_dir = nebula::ui::GetUserDataDirectory().wstring(); - const std::wstring cache_dir = nebula::ui::GetCacheDirectory().wstring(); - if (!user_data_dir.empty()) { - CefString(&settings.root_cache_path).FromWString(user_data_dir); - } - if (!cache_dir.empty()) { - CefString(&settings.cache_path).FromWString(cache_dir); - } + nebula::platform::ConfigureCefSettings(settings); if (!CefInitialize(main_args, settings, app, nullptr)) { return CefGetExitCode(); } CefRefPtr command_line = CefCommandLine::CreateCommandLine(); - command_line->InitFromString(GetCommandLineW()); + nebula::platform::InitCommandLine(command_line, startup); std::string initial_url = command_line->GetSwitchValue("url"); if (!initial_url.empty() && nebula::ui::IsChromiumNewTabUrl(initial_url)) { initial_url = nebula::ui::GetHomeUrl(); } - NebulaController controller(instance, initial_url, show_command); + NebulaController controller(startup, std::move(initial_url)); const bool created = controller.Create(); if (created) { CefRunMessageLoop(); diff --git a/src/app/run.h b/src/app/run.h index 16fa2e9..2132195 100644 --- a/src/app/run.h +++ b/src/app/run.h @@ -1,9 +1,9 @@ #pragma once -#include +#include "platform/types.h" namespace nebula::app { -int RunNebula(HINSTANCE instance, int show_command); +int RunNebula(const nebula::platform::AppStartup& startup); } // namespace nebula::app diff --git a/src/cef/nebula_app.cpp b/src/cef/nebula_app.cpp index 74ac0fd..13060d8 100644 --- a/src/cef/nebula_app.cpp +++ b/src/cef/nebula_app.cpp @@ -76,8 +76,15 @@ void NebulaApp::OnBeforeCommandLineProcessing(const CefString& process_type, // can prevent WebGL shared contexts from initializing on some drivers. command_line->AppendSwitch("ignore-gpu-blocklist"); command_line->AppendSwitch("enable-accelerated-video-decode"); + +#if defined(_WIN32) command_line->AppendSwitchWithValue("use-gl", "angle"); command_line->AppendSwitchWithValue("use-angle", "d3d11"); +#elif defined(__APPLE__) + command_line->AppendSwitchWithValue("use-angle", "metal"); +#else + command_line->AppendSwitchWithValue("use-gl", "egl"); +#endif } void NebulaApp::OnContextCreated(CefRefPtr browser, diff --git a/src/platform/browser_host.h b/src/platform/browser_host.h new file mode 100644 index 0000000..7900df7 --- /dev/null +++ b/src/platform/browser_host.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +#include "include/cef_browser.h" +#include "platform/types.h" + +namespace nebula::platform { + +CefWindowInfo MakeChildWindowInfo(NativeWindow parent, const Rect& rect); +CefWindowInfo MakeDevToolsPopup(NativeWindow parent, const char* title); +void ResizeBrowserWindow(NativeWindow browser_window, const Rect& rect); +void SetBrowserVisible(NativeWindow browser_window, bool visible); +void RaiseBrowserWindow(NativeWindow browser_window); +Rect MenuPopupRect(NativeWindow parent, const BrowserLayout& layout); +void ApplyRoundedBrowserRegion(NativeWindow browser_window, int corner_radius); +std::string CacheBusterToken(); +void DestroyTopLevelWindow(NativeWindow window); +int ScaleForParentWindow(NativeWindow parent, int value); +std::pair ParentClientSize(NativeWindow parent); + +} // namespace nebula::platform diff --git a/src/platform/linux/browser_host_linux.cpp b/src/platform/linux/browser_host_linux.cpp new file mode 100644 index 0000000..9be05ee --- /dev/null +++ b/src/platform/linux/browser_host_linux.cpp @@ -0,0 +1,76 @@ +#include "platform/browser_host.h" + +#include + +namespace nebula::platform { + +CefWindowInfo MakeChildWindowInfo(NativeWindow parent, const Rect& rect) { + CefWindowInfo info; + info.SetAsChild( + reinterpret_cast(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(reinterpret_cast(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); +} + +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_right, client_bottom] = 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_right - width - margin); + const int y = std::max(0, layout.chrome.y + layout.chrome.height - overlap); + return { + x, + y, + std::min(client_right, x + width) - x, + std::min(client_bottom, y + height) - y, + }; +} + +void ApplyRoundedBrowserRegion(NativeWindow browser_window, int corner_radius) { + UNREFERENCED_PARAMETER(browser_window); + UNREFERENCED_PARAMETER(corner_radius); +} + +std::string CacheBusterToken() { + return "0"; +} + +void DestroyTopLevelWindow(NativeWindow window) { + UNREFERENCED_PARAMETER(window); +} + +} // namespace nebula::platform diff --git a/src/platform/linux/nebula_window_linux.cpp b/src/platform/linux/nebula_window_linux.cpp new file mode 100644 index 0000000..6e674e8 --- /dev/null +++ b/src/platform/linux/nebula_window_linux.cpp @@ -0,0 +1,45 @@ +#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/linux/paths_linux.cpp b/src/platform/linux/paths_linux.cpp new file mode 100644 index 0000000..ab0e6e6 --- /dev/null +++ b/src/platform/linux/paths_linux.cpp @@ -0,0 +1,41 @@ +#include "platform/paths_platform.h" + +#include +#include + +#include + +namespace nebula::platform { + +std::filesystem::path ExecutableDirectory() { + char buffer[4096] = {}; + const ssize_t length = readlink("/proc/self/exe", buffer, sizeof(buffer) - 1); + if (length <= 0) { + return {}; + } + + buffer[length] = '\0'; + return std::filesystem::path(buffer).parent_path(); +} + +std::filesystem::path DefaultUserDataRoot() { + if (const char* xdg_data = std::getenv("XDG_DATA_HOME"); xdg_data && *xdg_data) { + return std::filesystem::path(xdg_data); + } + + if (const char* home = std::getenv("HOME")) { + return std::filesystem::path(home) / ".local" / "share"; + } + + if (passwd* pw = getpwuid(getuid())) { + return std::filesystem::path(pw->pw_dir) / ".local" / "share"; + } + + return ExecutableDirectory(); +} + +std::string PathToUtf8(const std::filesystem::path& path) { + return path.string(); +} + +} // namespace nebula::platform diff --git a/src/platform/linux/startup_linux.cpp b/src/platform/linux/startup_linux.cpp new file mode 100644 index 0000000..9a402be --- /dev/null +++ b/src/platform/linux/startup_linux.cpp @@ -0,0 +1,53 @@ +#include "platform/startup.h" + +#include +#include +#include +#include +#include + +#include "include/cef_command_line.h" +#include "ui/paths.h" + +namespace nebula::platform { +namespace { + +int g_single_instance_lock = -1; + +} // namespace + +void PrepareApp() {} + +bool TryAcquireSingleInstance() { + const auto lock_path = nebula::ui::GetUserDataDirectory() / ".nebula_instance.lock"; + std::error_code ec; + std::filesystem::create_directories(lock_path.parent_path(), ec); + + g_single_instance_lock = open(lock_path.c_str(), O_CREAT | O_RDWR, 0644); + if (g_single_instance_lock < 0) { + return true; + } + + return flock(g_single_instance_lock, LOCK_EX | LOCK_NB) == 0; +} + +CefMainArgs MakeMainArgs(const AppStartup& startup) { + return CefMainArgs(startup.argc, startup.argv); +} + +void InitCommandLine(CefRefPtr command_line, const AppStartup& startup) { + command_line->InitFromArgv(startup.argc, startup.argv); +} + +void ConfigureCefSettings(CefSettings& settings) { + const std::string user_data_dir = nebula::ui::GetUserDataDirectory().string(); + const std::string cache_dir = nebula::ui::GetCacheDirectory().string(); + if (!user_data_dir.empty()) { + CefString(&settings.root_cache_path).FromString(user_data_dir); + } + if (!cache_dir.empty()) { + CefString(&settings.cache_path).FromString(cache_dir); + } +} + +} // namespace nebula::platform diff --git a/src/platform/mac/browser_host_mac.cpp b/src/platform/mac/browser_host_mac.cpp new file mode 100644 index 0000000..2d0eb99 --- /dev/null +++ b/src/platform/mac/browser_host_mac.cpp @@ -0,0 +1,74 @@ +#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); +} + +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_right, client_bottom] = 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_right - width - margin); + const int y = std::max(0, layout.chrome.y + layout.chrome.height - overlap); + return { + x, + y, + std::min(client_right, x + width) - x, + std::min(client_bottom, y + height) - y, + }; +} + +void ApplyRoundedBrowserRegion(NativeWindow browser_window, int corner_radius) { + UNREFERENCED_PARAMETER(browser_window); + UNREFERENCED_PARAMETER(corner_radius); +} + +std::string CacheBusterToken() { + return "0"; +} + +void DestroyTopLevelWindow(NativeWindow window) { + UNREFERENCED_PARAMETER(window); +} + +} // namespace nebula::platform diff --git a/src/platform/mac/nebula_window_mac.cpp b/src/platform/mac/nebula_window_mac.cpp new file mode 100644 index 0000000..6e674e8 --- /dev/null +++ b/src/platform/mac/nebula_window_mac.cpp @@ -0,0 +1,45 @@ +#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/paths_mac.cpp b/src/platform/mac/paths_mac.cpp new file mode 100644 index 0000000..8a694a1 --- /dev/null +++ b/src/platform/mac/paths_mac.cpp @@ -0,0 +1,37 @@ +#include "platform/paths_platform.h" + +#include +#include +#include + +#include + +namespace nebula::platform { + +std::filesystem::path ExecutableDirectory() { + char buffer[4096] = {}; + uint32_t size = sizeof(buffer); + if (_NSGetExecutablePath(buffer, &size) != 0) { + return {}; + } + + return std::filesystem::path(buffer).parent_path(); +} + +std::filesystem::path DefaultUserDataRoot() { + if (const char* home = std::getenv("HOME")) { + return std::filesystem::path(home) / "Library" / "Application Support"; + } + + if (passwd* pw = getpwuid(getuid())) { + return std::filesystem::path(pw->pw_dir) / "Library" / "Application Support"; + } + + return ExecutableDirectory(); +} + +std::string PathToUtf8(const std::filesystem::path& path) { + return path.string(); +} + +} // namespace nebula::platform diff --git a/src/platform/mac/startup_mac.cpp b/src/platform/mac/startup_mac.cpp new file mode 100644 index 0000000..9a402be --- /dev/null +++ b/src/platform/mac/startup_mac.cpp @@ -0,0 +1,53 @@ +#include "platform/startup.h" + +#include +#include +#include +#include +#include + +#include "include/cef_command_line.h" +#include "ui/paths.h" + +namespace nebula::platform { +namespace { + +int g_single_instance_lock = -1; + +} // namespace + +void PrepareApp() {} + +bool TryAcquireSingleInstance() { + const auto lock_path = nebula::ui::GetUserDataDirectory() / ".nebula_instance.lock"; + std::error_code ec; + std::filesystem::create_directories(lock_path.parent_path(), ec); + + g_single_instance_lock = open(lock_path.c_str(), O_CREAT | O_RDWR, 0644); + if (g_single_instance_lock < 0) { + return true; + } + + return flock(g_single_instance_lock, LOCK_EX | LOCK_NB) == 0; +} + +CefMainArgs MakeMainArgs(const AppStartup& startup) { + return CefMainArgs(startup.argc, startup.argv); +} + +void InitCommandLine(CefRefPtr command_line, const AppStartup& startup) { + command_line->InitFromArgv(startup.argc, startup.argv); +} + +void ConfigureCefSettings(CefSettings& settings) { + const std::string user_data_dir = nebula::ui::GetUserDataDirectory().string(); + const std::string cache_dir = nebula::ui::GetCacheDirectory().string(); + if (!user_data_dir.empty()) { + CefString(&settings.root_cache_path).FromString(user_data_dir); + } + if (!cache_dir.empty()) { + CefString(&settings.cache_path).FromString(cache_dir); + } +} + +} // namespace nebula::platform diff --git a/src/platform/paths_platform.h b/src/platform/paths_platform.h new file mode 100644 index 0000000..7f200bb --- /dev/null +++ b/src/platform/paths_platform.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +namespace nebula::platform { + +std::filesystem::path ExecutableDirectory(); +std::filesystem::path DefaultUserDataRoot(); +std::string PathToUtf8(const std::filesystem::path& path); + +} // namespace nebula::platform diff --git a/src/platform/startup.h b/src/platform/startup.h new file mode 100644 index 0000000..2186ebd --- /dev/null +++ b/src/platform/startup.h @@ -0,0 +1,14 @@ +#pragma once + +#include "include/cef_app.h" +#include "platform/types.h" + +namespace nebula::platform { + +void PrepareApp(); +bool TryAcquireSingleInstance(); +CefMainArgs MakeMainArgs(const AppStartup& startup); +void InitCommandLine(CefRefPtr command_line, const AppStartup& startup); +void ConfigureCefSettings(CefSettings& settings); + +} // namespace nebula::platform diff --git a/src/platform/types.h b/src/platform/types.h new file mode 100644 index 0000000..2ecf450 --- /dev/null +++ b/src/platform/types.h @@ -0,0 +1,31 @@ +#pragma once + +namespace nebula::platform { + +struct Rect { + int x = 0; + int y = 0; + int width = 0; + int height = 0; +}; + +struct BrowserLayout { + Rect chrome; + Rect content; +}; + +using NativeWindow = void*; + +#if defined(_WIN32) +struct AppStartup { + void* instance = nullptr; + int show_command = 0; +}; +#else +struct AppStartup { + int argc = 0; + char** argv = nullptr; +}; +#endif + +} // namespace nebula::platform diff --git a/src/platform/win/browser_host_win.cpp b/src/platform/win/browser_host_win.cpp new file mode 100644 index 0000000..a12c5ed --- /dev/null +++ b/src/platform/win/browser_host_win.cpp @@ -0,0 +1,145 @@ +#include "platform/browser_host.h" + +#include + +#include + +namespace nebula::platform { +namespace { + +HWND AsHwnd(NativeWindow window) { + return static_cast(window); +} + +RECT ToRect(const Rect& rect) { + return { + rect.x, + rect.y, + rect.x + rect.width, + rect.y + rect.height, + }; +} + +} // namespace + +CefWindowInfo MakeChildWindowInfo(NativeWindow parent, const Rect& rect) { + CefWindowInfo info; + info.SetAsChild( + AsHwnd(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(AsHwnd(parent), title); + info.runtime_style = CEF_RUNTIME_STYLE_ALLOY; + return info; +} + +void ResizeBrowserWindow(NativeWindow browser_window, const Rect& rect) { + const HWND hwnd = AsHwnd(browser_window); + if (!hwnd) { + return; + } + + SetWindowPos( + hwnd, + nullptr, + rect.x, + rect.y, + std::max(0, rect.width), + std::max(0, rect.height), + SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOOWNERZORDER); +} + +void SetBrowserVisible(NativeWindow browser_window, bool visible) { + const HWND hwnd = AsHwnd(browser_window); + if (!hwnd) { + return; + } + + ShowWindow(hwnd, visible ? SW_SHOW : SW_HIDE); + if (visible) { + SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + } +} + +void RaiseBrowserWindow(NativeWindow browser_window) { + const HWND hwnd = AsHwnd(browser_window); + if (!hwnd) { + return; + } + + SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); +} + +int ScaleForParentWindow(NativeWindow parent, int value) { + const HWND hwnd = AsHwnd(parent); + if (!hwnd) { + return value; + } + + return MulDiv(value, static_cast(GetDpiForWindow(hwnd)), 96); +} + +std::pair ParentClientSize(NativeWindow parent) { + RECT client = {}; + const HWND hwnd = AsHwnd(parent); + if (hwnd) { + GetClientRect(hwnd, &client); + } + + return {static_cast(client.right), static_cast(client.bottom)}; +} + +Rect MenuPopupRect(NativeWindow parent, const BrowserLayout& layout) { + const auto [client_right, client_bottom] = ParentClientSize(parent); + + const int width = ScaleForParentWindow(parent, 260); + const int height = ScaleForParentWindow(parent, 258); + const int margin = ScaleForParentWindow(parent, 12); + const int overlap = ScaleForParentWindow(parent, 2); + + const int x = std::max(0, client_right - width - margin); + const int y = std::max(0, layout.chrome.y + layout.chrome.height - overlap); + return { + x, + y, + std::min(client_right, x + width) - x, + std::min(client_bottom, y + height) - y, + }; +} + +void ApplyRoundedBrowserRegion(NativeWindow browser_window, int corner_radius) { + const HWND hwnd = AsHwnd(browser_window); + if (!hwnd) { + return; + } + + RECT rect = {}; + if (!GetClientRect(hwnd, &rect)) { + return; + } + + const int width = std::max(1, rect.right - rect.left); + const int height = std::max(1, rect.bottom - rect.top); + HRGN region = CreateRoundRectRgn(0, 0, width + 1, height + 1, corner_radius, corner_radius); + if (region && !SetWindowRgn(hwnd, region, TRUE)) { + DeleteObject(region); + } +} + +std::string CacheBusterToken() { + return std::to_string(GetTickCount64()); +} + +void DestroyTopLevelWindow(NativeWindow window) { + const HWND hwnd = AsHwnd(window); + if (hwnd) { + DestroyWindow(hwnd); + } +} + +} // namespace nebula::platform diff --git a/src/window/nebula_window.cpp b/src/platform/win/nebula_window_win.cpp similarity index 57% rename from src/window/nebula_window.cpp rename to src/platform/win/nebula_window_win.cpp index f08c623..7b77d74 100644 --- a/src/window/nebula_window.cpp +++ b/src/platform/win/nebula_window_win.cpp @@ -1,6 +1,7 @@ #include "window/nebula_window.h" #include +#include #include #include @@ -103,190 +104,106 @@ void ApplyWindowFrameStyle(HWND hwnd) { sizeof(kNoWindowBorderColor)); } -} // namespace - -NebulaWindow::NebulaWindow(WindowDelegate* delegate) : delegate_(delegate) {} - -NebulaWindow::~NebulaWindow() = default; - -bool NebulaWindow::Create(HINSTANCE instance, int show_command) { - instance_ = instance; - RegisterClass(instance); - - const RECT work_area = GetWorkArea(); - dpi_ = GetDpiForSystem(); - const int width = std::min(ScaleForDpi(1400), work_area.right - work_area.left); - const int height = std::min(ScaleForDpi(900), work_area.bottom - work_area.top); - const int x = work_area.left + ((work_area.right - work_area.left) - width) / 2; - const int y = work_area.top + ((work_area.bottom - work_area.top) - height) / 2; - - hwnd_ = CreateWindowExW( - 0, - kWindowClassName, - kWindowTitle, - WS_POPUP | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_CLIPCHILDREN, - x, - y, - width, - height, - nullptr, - nullptr, - instance_, - this); - - if (!hwnd_) { - return false; - } - - UpdateDpi(); - ApplyWindowFrameStyle(hwnd_); - - const MARGINS margins = {0, 0, 0, 0}; - DwmExtendFrameIntoClientArea(hwnd_, &margins); - - ShowWindow(hwnd_, show_command); - UpdateWindow(hwnd_); - return true; -} - -BrowserLayout NebulaWindow::CurrentLayout(bool show_chrome) const { - RECT client = {}; - if (hwnd_) { - GetClientRect(hwnd_, &client); - } - - BrowserLayout layout; - layout.chrome = show_chrome - ? RECT{0, 0, client.right, std::min(ScaleForDpi(chrome_height_dip_), client.bottom)} - : RECT{0, 0, 0, 0}; - layout.content = {0, layout.chrome.bottom, client.right, client.bottom}; - return layout; -} - -void NebulaWindow::ResizeChild(HWND child, const RECT& rect) const { - if (!child) { - return; - } - - EnableFrameHitTest(child); - SetWindowPos( - child, - nullptr, +platform::Rect ToPlatformRect(const RECT& rect) { + return { rect.left, rect.top, std::max(0L, rect.right - rect.left), std::max(0L, rect.bottom - rect.top), - SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOOWNERZORDER); + }; } -void NebulaWindow::Minimize() { - if (hwnd_) { - ShowWindow(hwnd_, SW_MINIMIZE); - } +RECT ToNativeRect(const platform::Rect& rect) { + return { + rect.x, + rect.y, + rect.x + rect.width, + rect.y + rect.height, + }; } -void NebulaWindow::ToggleMaximize() { - if (!hwnd_ || fullscreen_) { - return; +} // namespace + +struct nebula::window::NebulaWindowImpl { + WindowDelegate* delegate = nullptr; + HINSTANCE instance = nullptr; + HWND hwnd = nullptr; + bool fullscreen = false; + LONG_PTR restore_style = 0; + LONG_PTR restore_ex_style = 0; + WINDOWPLACEMENT restore_placement = {sizeof(WINDOWPLACEMENT)}; + UINT dpi = 96; + int resize_border_dip = 8; + int chrome_height_dip = 104; + + int ScaleForDpi(int value) const { + return MulDiv(value, static_cast(dpi), 96); } - ShowWindow(hwnd_, IsZoomed(hwnd_) ? SW_RESTORE : SW_MAXIMIZE); -} - -void NebulaWindow::SetFullscreen(bool fullscreen) { - if (!hwnd_ || fullscreen_ == fullscreen) { - return; + void UpdateDpi() { + if (hwnd) { + dpi = GetDpiForWindow(hwnd); + } } - if (fullscreen) { - restore_style_ = GetWindowLongPtrW(hwnd_, GWL_STYLE); - restore_ex_style_ = GetWindowLongPtrW(hwnd_, GWL_EXSTYLE); - restore_placement_.length = sizeof(restore_placement_); - GetWindowPlacement(hwnd_, &restore_placement_); - - fullscreen_ = true; - const RECT monitor = GetMonitorArea(hwnd_); - SetWindowLongPtrW(hwnd_, GWL_STYLE, restore_style_ & ~(WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MINIMIZEBOX)); - SetWindowLongPtrW(hwnd_, GWL_EXSTYLE, restore_ex_style_); - SetWindowPos( - hwnd_, - HWND_TOPMOST, - monitor.left, - monitor.top, - monitor.right - monitor.left, - monitor.bottom - monitor.top, - SWP_NOOWNERZORDER | SWP_FRAMECHANGED); - } else { - fullscreen_ = false; - SetWindowLongPtrW(hwnd_, GWL_STYLE, restore_style_); - SetWindowLongPtrW(hwnd_, GWL_EXSTYLE, restore_ex_style_); - SetWindowPlacement(hwnd_, &restore_placement_); - SetWindowPos( - hwnd_, - HWND_NOTOPMOST, - 0, - 0, - 0, - 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOOWNERZORDER | SWP_FRAMECHANGED); - ApplyWindowFrameStyle(hwnd_); + void NotifyResize() { + if (delegate) { + delegate->OnWindowResized(CurrentLayout(true)); + } } - NotifyResize(); -} + BrowserLayout CurrentLayout(bool show_chrome) const { + RECT client = {}; + if (hwnd) { + GetClientRect(hwnd, &client); + } -void NebulaWindow::Close() { - if (hwnd_) { - SendMessageW(hwnd_, WM_CLOSE, 0, 0); - } -} - -void NebulaWindow::BeginDrag() { - if (!hwnd_) { - return; + BrowserLayout layout; + layout.chrome = show_chrome + ? ToPlatformRect(RECT{ + 0, + 0, + client.right, + std::min(ScaleForDpi(chrome_height_dip), client.bottom)}) + : platform::Rect{}; + layout.content = ToPlatformRect( + RECT{0, layout.chrome.y + layout.chrome.height, client.right, client.bottom}); + return layout; } - ReleaseCapture(); - SendMessageW(hwnd_, WM_NCLBUTTONDOWN, HTCAPTION, 0); -} + void EnableFrameHitTestForWindow(HWND child) const; + LRESULT HitTest(LPARAM lparam) const; + LRESULT HitTestPoint(POINT point) const; + LRESULT WndProc(UINT message, WPARAM wparam, LPARAM lparam); + void RegisterClass(HINSTANCE instance_handle); -void NebulaWindow::SetTitle(const std::wstring& title) { - if (hwnd_) { - SetWindowTextW(hwnd_, title.empty() ? kWindowTitle : title.c_str()); - } -} + static LRESULT CALLBACK StaticWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + static LRESULT CALLBACK ChildFrameWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + static BOOL CALLBACK EnableFrameHitTestForDescendant(HWND hwnd, LPARAM lparam); +}; -void NebulaWindow::EnableFrameHitTest(HWND child) const { - if (!hwnd_ || !child) { - return; - } - - EnableFrameHitTestForWindow(child); - EnumChildWindows(child, &NebulaWindow::EnableFrameHitTestForDescendant, reinterpret_cast(this)); -} - -LRESULT CALLBACK NebulaWindow::StaticWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { - NebulaWindow* self = nullptr; +LRESULT CALLBACK nebula::window::NebulaWindowImpl::StaticWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + NebulaWindowImpl* self = nullptr; if (message == WM_NCCREATE) { auto* create = reinterpret_cast(lparam); - self = static_cast(create->lpCreateParams); + self = static_cast(create->lpCreateParams); SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(self)); - self->hwnd_ = hwnd; + self->hwnd = hwnd; } else { - self = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + self = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); } return self ? self->WndProc(message, wparam, lparam) : DefWindowProcW(hwnd, message, wparam, lparam); } -LRESULT CALLBACK NebulaWindow::ChildFrameWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { +LRESULT CALLBACK nebula::window::NebulaWindowImpl::ChildFrameWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { auto old_proc = reinterpret_cast(GetPropW(hwnd, kChildFrameHitTestOldProcProp)); if (message == WM_NCHITTEST) { const auto parent = reinterpret_cast(GetPropW(hwnd, kChildFrameHitTestParentProp)); - auto* self = parent ? reinterpret_cast(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr; + auto* self = parent ? reinterpret_cast(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr; if (self) { const LRESULT hit = self->HitTest(lparam); if (IsResizeHit(hit)) { @@ -297,7 +214,7 @@ LRESULT CALLBACK NebulaWindow::ChildFrameWndProc(HWND hwnd, UINT message, WPARAM if (message == WM_SETCURSOR) { const auto parent = reinterpret_cast(GetPropW(hwnd, kChildFrameHitTestParentProp)); - auto* self = parent ? reinterpret_cast(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr; + auto* self = parent ? reinterpret_cast(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr; POINT point = {}; if (self && GetCursorPos(&point) && SetResizeCursor(self->HitTestPoint(point))) { return TRUE; @@ -306,7 +223,7 @@ LRESULT CALLBACK NebulaWindow::ChildFrameWndProc(HWND hwnd, UINT message, WPARAM if (message == WM_MOUSEMOVE || message == WM_NCMOUSEMOVE) { const auto parent = reinterpret_cast(GetPropW(hwnd, kChildFrameHitTestParentProp)); - auto* self = parent ? reinterpret_cast(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr; + auto* self = parent ? reinterpret_cast(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr; POINT point = {}; if (self && GetCursorPos(&point) && SetResizeCursor(self->HitTestPoint(point))) { return 0; @@ -334,20 +251,35 @@ LRESULT CALLBACK NebulaWindow::ChildFrameWndProc(HWND hwnd, UINT message, WPARAM : DefWindowProcW(hwnd, message, wparam, lparam); } -BOOL CALLBACK NebulaWindow::EnableFrameHitTestForDescendant(HWND hwnd, LPARAM lparam) { - const auto* self = reinterpret_cast(lparam); +BOOL CALLBACK nebula::window::NebulaWindowImpl::EnableFrameHitTestForDescendant(HWND hwnd, LPARAM lparam) { + const auto* self = reinterpret_cast(lparam); if (self) { self->EnableFrameHitTestForWindow(hwnd); } return TRUE; } -LRESULT NebulaWindow::WndProc(UINT message, WPARAM wparam, LPARAM lparam) { +void nebula::window::NebulaWindowImpl::EnableFrameHitTestForWindow(HWND child) const { + if (!child || GetPropW(child, kChildFrameHitTestOldProcProp)) { + return; + } + + SetPropW(child, kChildFrameHitTestParentProp, hwnd); + const auto old_proc = reinterpret_cast( + SetWindowLongPtrW(child, GWLP_WNDPROC, reinterpret_cast(&NebulaWindowImpl::ChildFrameWndProc))); + if (old_proc) { + SetPropW(child, kChildFrameHitTestOldProcProp, reinterpret_cast(old_proc)); + } else { + RemovePropW(child, kChildFrameHitTestParentProp); + } +} + +LRESULT nebula::window::NebulaWindowImpl::WndProc(UINT message, WPARAM wparam, LPARAM lparam) { switch (message) { case WM_CREATE: UpdateDpi(); - if (delegate_) { - delegate_->OnWindowCreated(); + if (delegate) { + delegate->OnWindowCreated(); } return 0; @@ -358,7 +290,7 @@ LRESULT NebulaWindow::WndProc(UINT message, WPARAM wparam, LPARAM lparam) { break; case WM_NCACTIVATE: - ApplyWindowFrameStyle(hwnd_); + ApplyWindowFrameStyle(hwnd); return TRUE; case WM_ERASEBKGND: @@ -389,10 +321,10 @@ LRESULT NebulaWindow::WndProc(UINT message, WPARAM wparam, LPARAM lparam) { return 0; case WM_DPICHANGED: { - dpi_ = HIWORD(wparam); + dpi = HIWORD(wparam); const auto* suggested_rect = reinterpret_cast(lparam); SetWindowPos( - hwnd_, + hwnd, nullptr, suggested_rect->left, suggested_rect->top, @@ -404,8 +336,8 @@ LRESULT NebulaWindow::WndProc(UINT message, WPARAM wparam, LPARAM lparam) { } case WM_GETMINMAXINFO: { - const RECT work_area = GetMonitorWorkArea(hwnd_); - const RECT monitor_area = GetMonitorArea(hwnd_); + const RECT work_area = GetMonitorWorkArea(hwnd); + const RECT monitor_area = GetMonitorArea(hwnd); auto* minmax = reinterpret_cast(lparam); minmax->ptMaxPosition.x = work_area.left - monitor_area.left; @@ -416,64 +348,33 @@ LRESULT NebulaWindow::WndProc(UINT message, WPARAM wparam, LPARAM lparam) { } case WM_CLOSE: - if (delegate_) { - delegate_->OnWindowCloseRequested(); + if (delegate) { + delegate->OnWindowCloseRequested(); return 0; } break; case WM_DESTROY: - hwnd_ = nullptr; + hwnd = nullptr; return 0; } - return DefWindowProcW(hwnd_, message, wparam, lparam); + return DefWindowProcW(hwnd, message, wparam, lparam); } -void NebulaWindow::RegisterClass(HINSTANCE instance) { +void nebula::window::NebulaWindowImpl::RegisterClass(HINSTANCE instance_handle) { WNDCLASSEXW window_class = {}; window_class.cbSize = sizeof(window_class); window_class.lpfnWndProc = StaticWndProc; - window_class.hInstance = instance; + window_class.hInstance = instance_handle; window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); window_class.lpszClassName = kWindowClassName; RegisterClassExW(&window_class); } -void NebulaWindow::NotifyResize() { - if (delegate_) { - delegate_->OnWindowResized(CurrentLayout()); - } -} - -void NebulaWindow::EnableFrameHitTestForWindow(HWND child) const { - if (!child || GetPropW(child, kChildFrameHitTestOldProcProp)) { - return; - } - - SetPropW(child, kChildFrameHitTestParentProp, hwnd_); - const auto old_proc = reinterpret_cast( - SetWindowLongPtrW(child, GWLP_WNDPROC, reinterpret_cast(&NebulaWindow::ChildFrameWndProc))); - if (old_proc) { - SetPropW(child, kChildFrameHitTestOldProcProp, reinterpret_cast(old_proc)); - } else { - RemovePropW(child, kChildFrameHitTestParentProp); - } -} - -int NebulaWindow::ScaleForDpi(int value) const { - return MulDiv(value, static_cast(dpi_), 96); -} - -void NebulaWindow::UpdateDpi() { - if (hwnd_) { - dpi_ = GetDpiForWindow(hwnd_); - } -} - -LRESULT NebulaWindow::HitTest(LPARAM lparam) const { - if (!hwnd_) { +LRESULT nebula::window::NebulaWindowImpl::HitTest(LPARAM lparam) const { + if (!hwnd) { return HTNOWHERE; } @@ -481,19 +382,19 @@ LRESULT NebulaWindow::HitTest(LPARAM lparam) const { return HitTestPoint(point); } -LRESULT NebulaWindow::HitTestPoint(POINT point) const { - if (!hwnd_) { +LRESULT nebula::window::NebulaWindowImpl::HitTestPoint(POINT point) const { + if (!hwnd) { return HTNOWHERE; } RECT window = {}; - GetWindowRect(hwnd_, &window); + GetWindowRect(hwnd, &window); - if (fullscreen_ || IsZoomed(hwnd_)) { + if (fullscreen || IsZoomed(hwnd)) { return HTCLIENT; } - const int resize_border = ScaleForDpi(resize_border_dip_); + const int resize_border = ScaleForDpi(resize_border_dip); const bool left = point.x >= window.left && point.x < window.left + resize_border; const bool right = point.x < window.right && point.x >= window.right - resize_border; const bool top = point.y >= window.top && point.y < window.top + resize_border; @@ -535,4 +436,191 @@ LRESULT NebulaWindow::HitTestPoint(POINT point) const { return HTCLIENT; } +std::wstring Utf8ToWide(const std::string& value) { + if (value.empty()) { + return {}; + } + + const int size = MultiByteToWideChar( + CP_UTF8, 0, value.data(), static_cast(value.size()), nullptr, 0); + std::wstring result(size, L'\0'); + MultiByteToWideChar( + CP_UTF8, 0, value.data(), static_cast(value.size()), result.data(), size); + return result; +} + +} // namespace + +namespace nebula::window { + +NebulaWindow::NebulaWindow(WindowDelegate* delegate) + : impl_(std::make_unique()) { + impl_->delegate = delegate; +} + +NebulaWindow::~NebulaWindow() = default; + +bool NebulaWindow::Create(const platform::AppStartup& startup) { + impl_->instance = static_cast(startup.instance); + impl_->RegisterClass(impl_->instance); + + const RECT work_area = GetWorkArea(); + impl_->dpi = GetDpiForSystem(); + const int width = + std::min(impl_->ScaleForDpi(1400), work_area.right - work_area.left); + const int height = + std::min(impl_->ScaleForDpi(900), work_area.bottom - work_area.top); + const int x = work_area.left + ((work_area.right - work_area.left) - width) / 2; + const int y = work_area.top + ((work_area.bottom - work_area.top) - height) / 2; + + impl_->hwnd = CreateWindowExW( + 0, + kWindowClassName, + kWindowTitle, + WS_POPUP | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_CLIPCHILDREN, + x, + y, + width, + height, + nullptr, + nullptr, + impl_->instance, + impl_.get()); + + if (!impl_->hwnd) { + return false; + } + + impl_->UpdateDpi(); + ApplyWindowFrameStyle(impl_->hwnd); + + const MARGINS margins = {0, 0, 0, 0}; + DwmExtendFrameIntoClientArea(impl_->hwnd, &margins); + + ShowWindow(impl_->hwnd, startup.show_command); + UpdateWindow(impl_->hwnd); + return true; +} + +platform::NativeWindow NebulaWindow::native_handle() const { + return impl_->hwnd; +} + +BrowserLayout NebulaWindow::CurrentLayout(bool show_chrome) const { + return impl_->CurrentLayout(show_chrome); +} + +void NebulaWindow::ResizeChild(platform::NativeWindow child, const platform::Rect& rect) const { + const HWND hwnd = static_cast(child); + if (!hwnd) { + return; + } + + EnableFrameHitTest(child); + const RECT native_rect = ToNativeRect(rect); + SetWindowPos( + hwnd, + nullptr, + native_rect.left, + native_rect.top, + std::max(0L, native_rect.right - native_rect.left), + std::max(0L, native_rect.bottom - native_rect.top), + SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOOWNERZORDER); +} + +void NebulaWindow::Minimize() { + if (impl_->hwnd) { + ShowWindow(impl_->hwnd, SW_MINIMIZE); + } +} + +void NebulaWindow::ToggleMaximize() { + if (!impl_->hwnd || impl_->fullscreen) { + return; + } + + ShowWindow(impl_->hwnd, IsZoomed(impl_->hwnd) ? SW_RESTORE : SW_MAXIMIZE); +} + +void NebulaWindow::SetFullscreen(bool fullscreen) { + if (!impl_->hwnd || impl_->fullscreen == fullscreen) { + return; + } + + if (fullscreen) { + impl_->restore_style = GetWindowLongPtrW(impl_->hwnd, GWL_STYLE); + impl_->restore_ex_style = GetWindowLongPtrW(impl_->hwnd, GWL_EXSTYLE); + impl_->restore_placement.length = sizeof(impl_->restore_placement); + GetWindowPlacement(impl_->hwnd, &impl_->restore_placement); + + impl_->fullscreen = true; + const RECT monitor = GetMonitorArea(impl_->hwnd); + SetWindowLongPtrW( + impl_->hwnd, + GWL_STYLE, + impl_->restore_style & ~(WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MINIMIZEBOX)); + SetWindowLongPtrW(impl_->hwnd, GWL_EXSTYLE, impl_->restore_ex_style); + SetWindowPos( + impl_->hwnd, + HWND_TOPMOST, + monitor.left, + monitor.top, + monitor.right - monitor.left, + monitor.bottom - monitor.top, + SWP_NOOWNERZORDER | SWP_FRAMECHANGED); + } else { + impl_->fullscreen = false; + SetWindowLongPtrW(impl_->hwnd, GWL_STYLE, impl_->restore_style); + SetWindowLongPtrW(impl_->hwnd, GWL_EXSTYLE, impl_->restore_ex_style); + SetWindowPlacement(impl_->hwnd, &impl_->restore_placement); + SetWindowPos( + impl_->hwnd, + HWND_NOTOPMOST, + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOOWNERZORDER | SWP_FRAMECHANGED); + ApplyWindowFrameStyle(impl_->hwnd); + } + + impl_->NotifyResize(); +} + +void NebulaWindow::Close() { + if (impl_->hwnd) { + SendMessageW(impl_->hwnd, WM_CLOSE, 0, 0); + } +} + +void NebulaWindow::BeginDrag() { + if (!impl_->hwnd) { + return; + } + + ReleaseCapture(); + SendMessageW(impl_->hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0); +} + +void NebulaWindow::SetTitle(const std::string& title) { + if (!impl_->hwnd) { + return; + } + + const std::wstring wide = title.empty() ? kWindowTitle : Utf8ToWide(title); + SetWindowTextW(impl_->hwnd, wide.c_str()); +} + +void NebulaWindow::EnableFrameHitTest(platform::NativeWindow child) const { + if (!impl_->hwnd || !child) { + return; + } + + impl_->EnableFrameHitTestForWindow(static_cast(child)); + EnumChildWindows( + static_cast(child), + &NebulaWindowImpl::EnableFrameHitTestForDescendant, + reinterpret_cast(impl_.get())); +} + } // namespace nebula::window diff --git a/src/platform/win/paths_win.cpp b/src/platform/win/paths_win.cpp new file mode 100644 index 0000000..ac29d81 --- /dev/null +++ b/src/platform/win/paths_win.cpp @@ -0,0 +1,41 @@ +#include "platform/paths_platform.h" + +#include + +namespace nebula::platform { + +std::filesystem::path ExecutableDirectory() { + wchar_t exe_path[MAX_PATH] = {}; + const DWORD length = GetModuleFileNameW(nullptr, exe_path, MAX_PATH); + if (length == 0 || length == MAX_PATH) { + return {}; + } + + return std::filesystem::path(exe_path).parent_path(); +} + +std::filesystem::path DefaultUserDataRoot() { + wchar_t buffer[MAX_PATH] = {}; + const DWORD length = GetEnvironmentVariableW(L"LOCALAPPDATA", buffer, MAX_PATH); + if (length > 0 && length < MAX_PATH) { + return std::filesystem::path(buffer); + } + + return ExecutableDirectory(); +} + +std::string PathToUtf8(const std::filesystem::path& path) { + const std::wstring wide = path.wstring(); + if (wide.empty()) { + return {}; + } + + const int size = WideCharToMultiByte( + CP_UTF8, 0, wide.data(), static_cast(wide.size()), nullptr, 0, nullptr, nullptr); + std::string result(size, '\0'); + WideCharToMultiByte( + CP_UTF8, 0, wide.data(), static_cast(wide.size()), result.data(), size, nullptr, nullptr); + return result; +} + +} // namespace nebula::platform diff --git a/src/platform/win/startup_win.cpp b/src/platform/win/startup_win.cpp new file mode 100644 index 0000000..9de1acc --- /dev/null +++ b/src/platform/win/startup_win.cpp @@ -0,0 +1,62 @@ +#include "platform/startup.h" + +#include + +#include "include/cef_command_line.h" +#include "ui/paths.h" + +namespace nebula::platform { +namespace { + +constexpr wchar_t kMainInstanceMutexName[] = L"Local\\NebulaBrowserMainInstance"; + +class ScopedHandle { +public: + explicit ScopedHandle(HANDLE handle) : handle_(handle) {} + ~ScopedHandle() { + if (handle_) { + CloseHandle(handle_); + } + } + + ScopedHandle(const ScopedHandle&) = delete; + ScopedHandle& operator=(const ScopedHandle&) = delete; + + bool valid() const { return handle_ != nullptr; } + +private: + HANDLE handle_ = nullptr; +}; + +} // namespace + +void PrepareApp() { + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); +} + +bool TryAcquireSingleInstance() { + static ScopedHandle mutex(CreateMutexW(nullptr, TRUE, kMainInstanceMutexName)); + return !(mutex.valid() && GetLastError() == ERROR_ALREADY_EXISTS); +} + +CefMainArgs MakeMainArgs(const AppStartup& startup) { + return CefMainArgs(static_cast(startup.instance)); +} + +void InitCommandLine(CefRefPtr command_line, const AppStartup& startup) { + UNREFERENCED_PARAMETER(startup); + command_line->InitFromString(::GetCommandLineW()); +} + +void ConfigureCefSettings(CefSettings& settings) { + const std::wstring user_data_dir = nebula::ui::GetUserDataDirectory().wstring(); + const std::wstring cache_dir = nebula::ui::GetCacheDirectory().wstring(); + if (!user_data_dir.empty()) { + CefString(&settings.root_cache_path).FromWString(user_data_dir); + } + if (!cache_dir.empty()) { + CefString(&settings.cache_path).FromWString(cache_dir); + } +} + +} // namespace nebula::platform diff --git a/src/ui/paths.cpp b/src/ui/paths.cpp index 3cbdea1..427c101 100644 --- a/src/ui/paths.cpp +++ b/src/ui/paths.cpp @@ -1,6 +1,6 @@ #include "ui/paths.h" -#include +#include "platform/paths_platform.h" #include #include @@ -10,40 +10,25 @@ namespace nebula::ui { namespace { constexpr std::string_view kNebulaScheme = "nebula://"; -constexpr std::wstring_view kInternalFallbackPage = L"404.html"; +constexpr std::string_view kInternalFallbackPage = "404.html"; struct InternalPage { std::string_view slug; - std::wstring_view file_name; + std::string_view file_name; }; constexpr InternalPage kInternalPages[] = { - {"home", L"home.html"}, - {"settings", L"settings.html"}, - {"downloads", L"downloads.html"}, - {"bigpicture", L"bigpicture.html"}, - {"big-picture", L"bigpicture.html"}, - {"gpu-diagnostics", L"gpu-diagnostics.html"}, - {"setup", L"setup.html"}, - {"404", L"404.html"}, - {"insecure", L"insecure.html"}, + {"home", "home.html"}, + {"settings", "settings.html"}, + {"downloads", "downloads.html"}, + {"bigpicture", "bigpicture.html"}, + {"big-picture", "bigpicture.html"}, + {"gpu-diagnostics", "gpu-diagnostics.html"}, + {"setup", "setup.html"}, + {"404", "404.html"}, + {"insecure", "insecure.html"}, }; -std::string WideToUtf8(const std::wstring& value) { - if (value.empty()) { - return {}; - } - - const int size = WideCharToMultiByte( - CP_UTF8, 0, value.data(), static_cast(value.size()), - nullptr, 0, nullptr, nullptr); - std::string result(size, '\0'); - WideCharToMultiByte( - CP_UTF8, 0, value.data(), static_cast(value.size()), - result.data(), size, nullptr, nullptr); - return result; -} - std::string GetUrlWithoutDecoration(std::string url) { const size_t split = url.find_first_of("?#"); if (split != std::string::npos) { @@ -64,8 +49,8 @@ std::string ToLowerAscii(std::string value) { return value; } -std::string PageFileUrl(std::wstring_view page_name) { - const auto path = GetUiPagePath(std::wstring(page_name)); +std::string PageFileUrl(std::string_view page_name) { + const auto path = GetUiPagePath(std::string(page_name)); return path.empty() ? std::string{} : FilePathToUrl(path); } @@ -127,36 +112,19 @@ const InternalPage* FindInternalPageByFileUrl(const std::string& url) { } // namespace std::filesystem::path GetExecutableDirectory() { - wchar_t exe_path[MAX_PATH] = {}; - const DWORD length = GetModuleFileNameW(nullptr, exe_path, MAX_PATH); - if (length == 0 || length == MAX_PATH) { - return {}; - } - - return std::filesystem::path(exe_path).parent_path(); + return nebula::platform::ExecutableDirectory(); } std::filesystem::path GetUserDataDirectory() { - std::filesystem::path root; - - wchar_t buffer[MAX_PATH] = {}; - // Prefer %LOCALAPPDATA% so the profile follows Chromium conventions and - // survives executable relocation. - const DWORD length = - GetEnvironmentVariableW(L"LOCALAPPDATA", buffer, MAX_PATH); - if (length > 0 && length < MAX_PATH) { - root = std::filesystem::path(buffer); - } else { - // Fall back to a directory next to the executable so a portable - // install still gets a writable profile. + auto root = nebula::platform::DefaultUserDataRoot(); + if (root.empty()) { root = GetExecutableDirectory(); } - if (root.empty()) { return {}; } - std::filesystem::path user_data = root / L"Nebula" / L"User Data"; + std::filesystem::path user_data = root / "Nebula" / "User Data"; std::error_code ec; std::filesystem::create_directories(user_data, ec); return user_data; @@ -168,7 +136,7 @@ std::filesystem::path GetCacheDirectory() { return {}; } - std::filesystem::path cache = user_data / L"Cache"; + std::filesystem::path cache = user_data / "Cache"; std::error_code ec; std::filesystem::create_directories(cache, ec); return cache; @@ -176,20 +144,20 @@ std::filesystem::path GetCacheDirectory() { std::filesystem::path GetSessionStatePath() { auto user_data = GetUserDataDirectory(); - return user_data.empty() ? std::filesystem::path{} : user_data / L"session_state.json"; + return user_data.empty() ? std::filesystem::path{} : user_data / "session_state.json"; } -std::filesystem::path GetUiPagePath(const std::wstring& page_name) { +std::filesystem::path GetUiPagePath(const std::string& page_name) { const auto exe_dir = GetExecutableDirectory(); if (exe_dir.empty()) { return {}; } - return exe_dir / L"ui" / L"pages" / page_name; + return exe_dir / "ui" / "pages" / page_name; } std::string FilePathToUrl(std::filesystem::path path) { - std::string value = WideToUtf8(path.wstring()); + std::string value = nebula::platform::PathToUtf8(path); for (char& ch : value) { if (ch == '\\') { ch = '/'; @@ -205,8 +173,8 @@ std::string FilePathToUrl(std::filesystem::path path) { } std::string GetChromeUrl() { - const auto path = GetUiPagePath(L"chrome.html"); - const std::string fallback = PageFileUrl(L"home.html"); + const auto path = GetUiPagePath("chrome.html"); + const std::string fallback = PageFileUrl("home.html"); return path.empty() ? (fallback.empty() ? "https://www.google.com" : fallback) : FilePathToUrl(path); } @@ -231,8 +199,8 @@ std::string GetGpuDiagnosticsUrl() { } std::string GetMenuPopupUrl() { - const auto path = GetUiPagePath(L"menu-popup.html"); - const std::string fallback = PageFileUrl(L"home.html"); + const auto path = GetUiPagePath("menu-popup.html"); + const std::string fallback = PageFileUrl("home.html"); return path.empty() ? (fallback.empty() ? "https://www.google.com" : fallback) : FilePathToUrl(path); } diff --git a/src/ui/paths.h b/src/ui/paths.h index 9700f3e..3c10b76 100644 --- a/src/ui/paths.h +++ b/src/ui/paths.h @@ -9,7 +9,7 @@ std::filesystem::path GetExecutableDirectory(); std::filesystem::path GetUserDataDirectory(); std::filesystem::path GetCacheDirectory(); std::filesystem::path GetSessionStatePath(); -std::filesystem::path GetUiPagePath(const std::wstring& page_name); +std::filesystem::path GetUiPagePath(const std::string& page_name); std::string FilePathToUrl(std::filesystem::path path); std::string GetChromeUrl(); std::string GetHomeUrl(); diff --git a/src/window/nebula_window.h b/src/window/nebula_window.h index c971c62..76d03ca 100644 --- a/src/window/nebula_window.h +++ b/src/window/nebula_window.h @@ -1,15 +1,15 @@ #pragma once -#include - +#include #include +#include "platform/types.h" + namespace nebula::window { -struct BrowserLayout { - RECT chrome = {}; - RECT content = {}; -}; +struct NebulaWindowImpl; + +using BrowserLayout = nebula::platform::BrowserLayout; class WindowDelegate { public: @@ -24,43 +24,21 @@ public: explicit NebulaWindow(WindowDelegate* delegate); ~NebulaWindow(); - bool Create(HINSTANCE instance, int show_command); - HWND hwnd() const { return hwnd_; } + bool Create(const nebula::platform::AppStartup& startup); + nebula::platform::NativeWindow native_handle() const; BrowserLayout CurrentLayout(bool show_chrome = true) const; - void ResizeChild(HWND child, const RECT& rect) const; + void ResizeChild(nebula::platform::NativeWindow child, const nebula::platform::Rect& rect) const; void Minimize(); void ToggleMaximize(); void SetFullscreen(bool fullscreen); void Close(); void BeginDrag(); - void SetTitle(const std::wstring& title); - void EnableFrameHitTest(HWND child) const; + void SetTitle(const std::string& title); + void EnableFrameHitTest(nebula::platform::NativeWindow child) const; private: - static LRESULT CALLBACK StaticWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); - static LRESULT CALLBACK ChildFrameWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); - static BOOL CALLBACK EnableFrameHitTestForDescendant(HWND hwnd, LPARAM lparam); - LRESULT WndProc(UINT message, WPARAM wparam, LPARAM lparam); - - void RegisterClass(HINSTANCE instance); - void NotifyResize(); - void EnableFrameHitTestForWindow(HWND child) const; - LRESULT HitTest(LPARAM lparam) const; - LRESULT HitTestPoint(POINT point) const; - int ScaleForDpi(int value) const; - void UpdateDpi(); - - WindowDelegate* delegate_ = nullptr; - HINSTANCE instance_ = nullptr; - HWND hwnd_ = nullptr; - bool fullscreen_ = false; - LONG_PTR restore_style_ = 0; - LONG_PTR restore_ex_style_ = 0; - WINDOWPLACEMENT restore_placement_ = {sizeof(WINDOWPLACEMENT)}; - UINT dpi_ = 96; - int resize_border_dip_ = 8; - int chrome_height_dip_ = 104; + std::unique_ptr impl_; }; } // namespace nebula::window