Add platform abstraction & cross-platform ports
Introduce a cross-platform platform layer and port scaffolding for macOS and Linux. CMakeLists.txt refactored to select platform sources, set executable type per OS, and use CEF helper macros for runtime deployment. Add platform/types.h, startup/paths/browser_host APIs and implementations for Windows, macOS, and Linux (many are stubs for mac/linux). Refactor app entry and lifetime to use nebula::platform::AppStartup (app/main, run.{h,cpp}), move window/browser host logic into platform/browser_host.*, and update NebulaController to use platform APIs (native handles, sizing, visibility, cache-busting token, etc.). Add README and detailed docs/cross-platform.md describing build layout and porting status.
This commit is contained in:
+79
-43
@@ -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$<$<CONFIG:Debug>: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$<$<CONFIG:Debug>: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"
|
||||
"$<TARGET_FILE_DIR:NebulaBrowser>"
|
||||
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CEF_ROOT}/Resources"
|
||||
"$<TARGET_FILE_DIR:NebulaBrowser>"
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -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.
|
||||
+12
-3
@@ -1,6 +1,8 @@
|
||||
#include <windows.h>
|
||||
|
||||
#include "app/run.h"
|
||||
#include "platform/types.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
|
||||
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
|
||||
|
||||
@@ -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/<Config>/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/<os>/` and keep `src/window/nebula_window.h` free of `#include <windows.h>` (or Cocoa/X11 headers).
|
||||
3. **CEF child window embedding** → `src/platform/<os>/browser_host_*.cpp` (`MakeChildWindowInfo`, resize, visibility).
|
||||
4. **Startup / paths** → `startup_*.cpp` and `paths_*.cpp` in the same folder.
|
||||
5. **New OS** → add `src/platform/<os>/`, 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.
|
||||
+39
-109
@@ -1,7 +1,5 @@
|
||||
#include "app/nebula_controller.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cctype>
|
||||
@@ -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<int>(value.size()), nullptr, 0);
|
||||
std::wstring result(size, L'\0');
|
||||
MultiByteToWideChar(
|
||||
CP_UTF8, 0, value.data(), static_cast<int>(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<std::string>& 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<int>(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<LONG>(margin, client.right - width - margin);
|
||||
const LONG y = std::max<LONG>(0, layout.chrome.bottom - overlap);
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
std::min<LONG>(client.right, x + width),
|
||||
std::min<LONG>(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<LONG>(1, rect.right - rect.left) + 1,
|
||||
std::max<LONG>(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<CefBrowser> 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<nebula::window::NebulaWindow>(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<CefBrowser> 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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+8
-47
@@ -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<nebula::cef::NebulaApp> 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<CefCommandLine> 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();
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include "platform/types.h"
|
||||
|
||||
namespace nebula::app {
|
||||
|
||||
int RunNebula(HINSTANCE instance, int show_command);
|
||||
int RunNebula(const nebula::platform::AppStartup& startup);
|
||||
|
||||
} // namespace nebula::app
|
||||
|
||||
@@ -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<CefBrowser> browser,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#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<int, int> ParentClientSize(NativeWindow parent);
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -0,0 +1,76 @@
|
||||
#include "platform/browser_host.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace nebula::platform {
|
||||
|
||||
CefWindowInfo MakeChildWindowInfo(NativeWindow parent, const Rect& rect) {
|
||||
CefWindowInfo info;
|
||||
info.SetAsChild(
|
||||
reinterpret_cast<CefWindowHandle>(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<CefWindowHandle>(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<int, int> 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
|
||||
@@ -0,0 +1,45 @@
|
||||
#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,41 @@
|
||||
#include "platform/paths_platform.h"
|
||||
|
||||
#include <pwd.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "platform/startup.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
#include <sys/file.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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<CefCommandLine> 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
|
||||
@@ -0,0 +1,74 @@
|
||||
#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);
|
||||
}
|
||||
|
||||
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_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
|
||||
@@ -0,0 +1,45 @@
|
||||
#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,37 @@
|
||||
#include "platform/paths_platform.h"
|
||||
|
||||
#include <mach-o/dyld.h>
|
||||
#include <pwd.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "platform/startup.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
#include <sys/file.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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<CefCommandLine> 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
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace nebula::platform {
|
||||
|
||||
std::filesystem::path ExecutableDirectory();
|
||||
std::filesystem::path DefaultUserDataRoot();
|
||||
std::string PathToUtf8(const std::filesystem::path& path);
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -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<CefCommandLine> command_line, const AppStartup& startup);
|
||||
void ConfigureCefSettings(CefSettings& settings);
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -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
|
||||
@@ -0,0 +1,145 @@
|
||||
#include "platform/browser_host.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace nebula::platform {
|
||||
namespace {
|
||||
|
||||
HWND AsHwnd(NativeWindow window) {
|
||||
return static_cast<HWND>(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<int>(GetDpiForWindow(hwnd)), 96);
|
||||
}
|
||||
|
||||
std::pair<int, int> ParentClientSize(NativeWindow parent) {
|
||||
RECT client = {};
|
||||
const HWND hwnd = AsHwnd(parent);
|
||||
if (hwnd) {
|
||||
GetClientRect(hwnd, &client);
|
||||
}
|
||||
|
||||
return {static_cast<int>(client.right), static_cast<int>(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<LONG>(1, rect.right - rect.left);
|
||||
const int height = std::max<LONG>(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
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "window/nebula_window.h"
|
||||
|
||||
#include <dwmapi.h>
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -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<LONG>(ScaleForDpi(1400), work_area.right - work_area.left);
|
||||
const int height = std::min<LONG>(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<LONG>(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<int>(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_);
|
||||
void NotifyResize() {
|
||||
if (delegate) {
|
||||
delegate->OnWindowResized(CurrentLayout(true));
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
BrowserLayout CurrentLayout(bool show_chrome) const {
|
||||
RECT client = {};
|
||||
if (hwnd) {
|
||||
GetClientRect(hwnd, &client);
|
||||
}
|
||||
|
||||
BrowserLayout layout;
|
||||
layout.chrome = show_chrome
|
||||
? ToPlatformRect(RECT{
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
|
||||
ApplyWindowFrameStyle(hwnd_);
|
||||
client.right,
|
||||
std::min<LONG>(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;
|
||||
}
|
||||
|
||||
NotifyResize();
|
||||
}
|
||||
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::Close() {
|
||||
if (hwnd_) {
|
||||
SendMessageW(hwnd_, WM_CLOSE, 0, 0);
|
||||
}
|
||||
}
|
||||
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::BeginDrag() {
|
||||
if (!hwnd_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReleaseCapture();
|
||||
SendMessageW(hwnd_, WM_NCLBUTTONDOWN, HTCAPTION, 0);
|
||||
}
|
||||
|
||||
void NebulaWindow::SetTitle(const std::wstring& title) {
|
||||
if (hwnd_) {
|
||||
SetWindowTextW(hwnd_, title.empty() ? kWindowTitle : title.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaWindow::EnableFrameHitTest(HWND child) const {
|
||||
if (!hwnd_ || !child) {
|
||||
return;
|
||||
}
|
||||
|
||||
EnableFrameHitTestForWindow(child);
|
||||
EnumChildWindows(child, &NebulaWindow::EnableFrameHitTestForDescendant, reinterpret_cast<LPARAM>(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<CREATESTRUCTW*>(lparam);
|
||||
self = static_cast<NebulaWindow*>(create->lpCreateParams);
|
||||
self = static_cast<NebulaWindowImpl*>(create->lpCreateParams);
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(self));
|
||||
self->hwnd_ = hwnd;
|
||||
self->hwnd = hwnd;
|
||||
} else {
|
||||
self = reinterpret_cast<NebulaWindow*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||
self = reinterpret_cast<NebulaWindowImpl*>(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<WNDPROC>(GetPropW(hwnd, kChildFrameHitTestOldProcProp));
|
||||
|
||||
if (message == WM_NCHITTEST) {
|
||||
const auto parent = reinterpret_cast<HWND>(GetPropW(hwnd, kChildFrameHitTestParentProp));
|
||||
auto* self = parent ? reinterpret_cast<NebulaWindow*>(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr;
|
||||
auto* self = parent ? reinterpret_cast<NebulaWindowImpl*>(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<HWND>(GetPropW(hwnd, kChildFrameHitTestParentProp));
|
||||
auto* self = parent ? reinterpret_cast<NebulaWindow*>(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr;
|
||||
auto* self = parent ? reinterpret_cast<NebulaWindowImpl*>(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<HWND>(GetPropW(hwnd, kChildFrameHitTestParentProp));
|
||||
auto* self = parent ? reinterpret_cast<NebulaWindow*>(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr;
|
||||
auto* self = parent ? reinterpret_cast<NebulaWindowImpl*>(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<const NebulaWindow*>(lparam);
|
||||
BOOL CALLBACK nebula::window::NebulaWindowImpl::EnableFrameHitTestForDescendant(HWND hwnd, LPARAM lparam) {
|
||||
const auto* self = reinterpret_cast<const NebulaWindowImpl*>(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<WNDPROC>(
|
||||
SetWindowLongPtrW(child, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&NebulaWindowImpl::ChildFrameWndProc)));
|
||||
if (old_proc) {
|
||||
SetPropW(child, kChildFrameHitTestOldProcProp, reinterpret_cast<HANDLE>(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<RECT*>(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<MINMAXINFO*>(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<WNDPROC>(
|
||||
SetWindowLongPtrW(child, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&NebulaWindow::ChildFrameWndProc)));
|
||||
if (old_proc) {
|
||||
SetPropW(child, kChildFrameHitTestOldProcProp, reinterpret_cast<HANDLE>(old_proc));
|
||||
} else {
|
||||
RemovePropW(child, kChildFrameHitTestParentProp);
|
||||
}
|
||||
}
|
||||
|
||||
int NebulaWindow::ScaleForDpi(int value) const {
|
||||
return MulDiv(value, static_cast<int>(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<int>(value.size()), nullptr, 0);
|
||||
std::wstring result(size, L'\0');
|
||||
MultiByteToWideChar(
|
||||
CP_UTF8, 0, value.data(), static_cast<int>(value.size()), result.data(), size);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
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) {
|
||||
impl_->instance = static_cast<HINSTANCE>(startup.instance);
|
||||
impl_->RegisterClass(impl_->instance);
|
||||
|
||||
const RECT work_area = GetWorkArea();
|
||||
impl_->dpi = GetDpiForSystem();
|
||||
const int width =
|
||||
std::min<LONG>(impl_->ScaleForDpi(1400), work_area.right - work_area.left);
|
||||
const int height =
|
||||
std::min<LONG>(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<HWND>(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<HWND>(child));
|
||||
EnumChildWindows(
|
||||
static_cast<HWND>(child),
|
||||
&NebulaWindowImpl::EnableFrameHitTestForDescendant,
|
||||
reinterpret_cast<LPARAM>(impl_.get()));
|
||||
}
|
||||
|
||||
} // namespace nebula::window
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "platform/paths_platform.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
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<int>(wide.size()), nullptr, 0, nullptr, nullptr);
|
||||
std::string result(size, '\0');
|
||||
WideCharToMultiByte(
|
||||
CP_UTF8, 0, wide.data(), static_cast<int>(wide.size()), result.data(), size, nullptr, nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -0,0 +1,62 @@
|
||||
#include "platform/startup.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#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<HINSTANCE>(startup.instance));
|
||||
}
|
||||
|
||||
void InitCommandLine(CefRefPtr<CefCommandLine> 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
|
||||
+27
-59
@@ -1,6 +1,6 @@
|
||||
#include "ui/paths.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include "platform/paths_platform.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
@@ -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<int>(value.size()),
|
||||
nullptr, 0, nullptr, nullptr);
|
||||
std::string result(size, '\0');
|
||||
WideCharToMultiByte(
|
||||
CP_UTF8, 0, value.data(), static_cast<int>(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);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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();
|
||||
|
||||
+12
-34
@@ -1,15 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#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<NebulaWindowImpl> impl_;
|
||||
};
|
||||
|
||||
} // namespace nebula::window
|
||||
|
||||
Reference in New Issue
Block a user