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:
Andrew Zambazos
2026-05-18 17:25:04 +12:00
parent 18bc607d93
commit e51594a010
28 changed files with 1461 additions and 508 deletions
+23
View File
@@ -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
+76
View File
@@ -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
+41
View File
@@ -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
+53
View File
@@ -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
+74
View File
@@ -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
+45
View File
@@ -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
+37
View File
@@ -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
+53
View File
@@ -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
+12
View File
@@ -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
+14
View File
@@ -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
+31
View File
@@ -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
+145
View File
@@ -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
+626
View File
@@ -0,0 +1,626 @@
#include "window/nebula_window.h"
#include <dwmapi.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
namespace nebula::window {
namespace {
constexpr wchar_t kWindowClassName[] = L"NebulaBrowserWindow";
constexpr wchar_t kWindowTitle[] = L"Nebula Browser";
constexpr wchar_t kChildFrameHitTestOldProcProp[] = L"NebulaChildFrameHitTestOldProc";
constexpr wchar_t kChildFrameHitTestParentProp[] = L"NebulaChildFrameHitTestParent";
constexpr int kTitleRowHeightDip = 42;
constexpr int kWindowControlWidthDip = 46;
constexpr int kWindowControlCount = 3;
constexpr COLORREF kNoWindowBorderColor = 0xFFFFFFFE;
RECT GetWorkArea() {
RECT work_area = {};
SystemParametersInfoW(SPI_GETWORKAREA, 0, &work_area, 0);
return work_area;
}
RECT GetMonitorWorkArea(HWND hwnd) {
MONITORINFO monitor_info = {};
monitor_info.cbSize = sizeof(monitor_info);
const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
if (monitor && GetMonitorInfoW(monitor, &monitor_info)) {
return monitor_info.rcWork;
}
return GetWorkArea();
}
RECT GetMonitorArea(HWND hwnd) {
MONITORINFO monitor_info = {};
monitor_info.cbSize = sizeof(monitor_info);
const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
if (monitor && GetMonitorInfoW(monitor, &monitor_info)) {
return monitor_info.rcMonitor;
}
return GetWorkArea();
}
bool IsResizeHit(LRESULT hit) {
return hit == HTLEFT || hit == HTRIGHT || hit == HTTOP || hit == HTBOTTOM ||
hit == HTTOPLEFT || hit == HTTOPRIGHT || hit == HTBOTTOMLEFT || hit == HTBOTTOMRIGHT;
}
HCURSOR CursorForResizeHit(LRESULT hit) {
switch (hit) {
case HTLEFT:
case HTRIGHT:
return LoadCursor(nullptr, IDC_SIZEWE);
case HTTOP:
case HTBOTTOM:
return LoadCursor(nullptr, IDC_SIZENS);
case HTTOPLEFT:
case HTBOTTOMRIGHT:
return LoadCursor(nullptr, IDC_SIZENWSE);
case HTTOPRIGHT:
case HTBOTTOMLEFT:
return LoadCursor(nullptr, IDC_SIZENESW);
default:
return nullptr;
}
}
bool SetResizeCursor(LRESULT hit) {
HCURSOR cursor = CursorForResizeHit(hit);
if (!cursor) {
return false;
}
SetCursor(cursor);
return true;
}
void ApplyWindowFrameStyle(HWND hwnd) {
const BOOL dark_mode = TRUE;
const DWM_WINDOW_CORNER_PREFERENCE corner_preference = DWMWCP_ROUND;
DwmSetWindowAttribute(
hwnd,
DWMWA_USE_IMMERSIVE_DARK_MODE,
&dark_mode,
sizeof(dark_mode));
DwmSetWindowAttribute(
hwnd,
DWMWA_WINDOW_CORNER_PREFERENCE,
&corner_preference,
sizeof(corner_preference));
DwmSetWindowAttribute(
hwnd,
DWMWA_BORDER_COLOR,
&kNoWindowBorderColor,
sizeof(kNoWindowBorderColor));
}
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),
};
}
RECT ToNativeRect(const platform::Rect& rect) {
return {
rect.x,
rect.y,
rect.x + rect.width,
rect.y + rect.height,
};
}
} // 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);
}
void UpdateDpi() {
if (hwnd) {
dpi = GetDpiForWindow(hwnd);
}
}
void NotifyResize() {
if (delegate) {
delegate->OnWindowResized(CurrentLayout(true));
}
}
BrowserLayout CurrentLayout(bool show_chrome) const {
RECT client = {};
if (hwnd) {
GetClientRect(hwnd, &client);
}
BrowserLayout layout;
layout.chrome = show_chrome
? ToPlatformRect(RECT{
0,
0,
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;
}
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);
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 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<NebulaWindowImpl*>(create->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(self));
self->hwnd = hwnd;
} else {
self = reinterpret_cast<NebulaWindowImpl*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
}
return self ? self->WndProc(message, wparam, lparam)
: DefWindowProcW(hwnd, message, wparam, 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<NebulaWindowImpl*>(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr;
if (self) {
const LRESULT hit = self->HitTest(lparam);
if (IsResizeHit(hit)) {
return hit;
}
}
}
if (message == WM_SETCURSOR) {
const auto parent = reinterpret_cast<HWND>(GetPropW(hwnd, kChildFrameHitTestParentProp));
auto* self = parent ? reinterpret_cast<NebulaWindowImpl*>(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr;
POINT point = {};
if (self && GetCursorPos(&point) && SetResizeCursor(self->HitTestPoint(point))) {
return TRUE;
}
}
if (message == WM_MOUSEMOVE || message == WM_NCMOUSEMOVE) {
const auto parent = reinterpret_cast<HWND>(GetPropW(hwnd, kChildFrameHitTestParentProp));
auto* self = parent ? reinterpret_cast<NebulaWindowImpl*>(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr;
POINT point = {};
if (self && GetCursorPos(&point) && SetResizeCursor(self->HitTestPoint(point))) {
return 0;
}
}
if (message == WM_NCLBUTTONDOWN && IsResizeHit(static_cast<LRESULT>(wparam))) {
const auto parent = reinterpret_cast<HWND>(GetPropW(hwnd, kChildFrameHitTestParentProp));
if (parent) {
ReleaseCapture();
SendMessageW(parent, WM_NCLBUTTONDOWN, wparam, lparam);
return 0;
}
}
if (message == WM_NCDESTROY) {
RemovePropW(hwnd, kChildFrameHitTestParentProp);
RemovePropW(hwnd, kChildFrameHitTestOldProcProp);
if (old_proc) {
SetWindowLongPtrW(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(old_proc));
}
}
return old_proc ? CallWindowProcW(old_proc, hwnd, message, wparam, lparam)
: DefWindowProcW(hwnd, message, wparam, 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;
}
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();
}
return 0;
case WM_NCCALCSIZE:
if (wparam == TRUE) {
return 0;
}
break;
case WM_NCACTIVATE:
ApplyWindowFrameStyle(hwnd);
return TRUE;
case WM_ERASEBKGND:
return 1;
case WM_NCHITTEST:
return HitTest(lparam);
case WM_SETCURSOR: {
POINT point = {};
if (GetCursorPos(&point) && SetResizeCursor(HitTestPoint(point))) {
return TRUE;
}
break;
}
case WM_MOUSEMOVE:
case WM_NCMOUSEMOVE: {
POINT point = {};
if (GetCursorPos(&point) && SetResizeCursor(HitTestPoint(point))) {
return 0;
}
break;
}
case WM_SIZE:
NotifyResize();
return 0;
case WM_DPICHANGED: {
dpi = HIWORD(wparam);
const auto* suggested_rect = reinterpret_cast<RECT*>(lparam);
SetWindowPos(
hwnd,
nullptr,
suggested_rect->left,
suggested_rect->top,
suggested_rect->right - suggested_rect->left,
suggested_rect->bottom - suggested_rect->top,
SWP_NOZORDER | SWP_NOACTIVATE);
NotifyResize();
return 0;
}
case WM_GETMINMAXINFO: {
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;
minmax->ptMaxPosition.y = work_area.top - monitor_area.top;
minmax->ptMaxSize.x = work_area.right - work_area.left;
minmax->ptMaxSize.y = work_area.bottom - work_area.top;
return 0;
}
case WM_CLOSE:
if (delegate) {
delegate->OnWindowCloseRequested();
return 0;
}
break;
case WM_DESTROY:
hwnd = nullptr;
return 0;
}
return DefWindowProcW(hwnd, message, wparam, lparam);
}
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_handle;
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
window_class.lpszClassName = kWindowClassName;
RegisterClassExW(&window_class);
}
LRESULT nebula::window::NebulaWindowImpl::HitTest(LPARAM lparam) const {
if (!hwnd) {
return HTNOWHERE;
}
POINT point = {GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)};
return HitTestPoint(point);
}
LRESULT nebula::window::NebulaWindowImpl::HitTestPoint(POINT point) const {
if (!hwnd) {
return HTNOWHERE;
}
RECT window = {};
GetWindowRect(hwnd, &window);
if (fullscreen || IsZoomed(hwnd)) {
return HTCLIENT;
}
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;
const bool bottom = point.y < window.bottom && point.y >= window.bottom - resize_border;
if (top && left) {
return HTTOPLEFT;
}
if (top && right) {
return HTTOPRIGHT;
}
if (bottom && left) {
return HTBOTTOMLEFT;
}
if (bottom && right) {
return HTBOTTOMRIGHT;
}
if (left) {
return HTLEFT;
}
if (right) {
return HTRIGHT;
}
if (top) {
return HTTOP;
}
if (bottom) {
return HTBOTTOM;
}
const int controls_width = ScaleForDpi(kWindowControlWidthDip * kWindowControlCount);
const int controls_height = ScaleForDpi(kTitleRowHeightDip);
const bool window_controls = point.x >= window.right - controls_width && point.x < window.right &&
point.y >= window.top && point.y < window.top + controls_height;
if (window_controls) {
return HTCLIENT;
}
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
+41
View File
@@ -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
+62
View File
@@ -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