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:
+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
|
||||
|
||||
Reference in New Issue
Block a user