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
|
||||
|
||||
@@ -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_);
|
||||
|
||||
fullscreen_ = true;
|
||||
const RECT monitor = GetMonitorArea(hwnd_);
|
||||
SetWindowLongPtrW(hwnd_, GWL_STYLE, restore_style_ & ~(WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MINIMIZEBOX));
|
||||
SetWindowLongPtrW(hwnd_, GWL_EXSTYLE, restore_ex_style_);
|
||||
SetWindowPos(
|
||||
hwnd_,
|
||||
HWND_TOPMOST,
|
||||
monitor.left,
|
||||
monitor.top,
|
||||
monitor.right - monitor.left,
|
||||
monitor.bottom - monitor.top,
|
||||
SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
|
||||
} else {
|
||||
fullscreen_ = false;
|
||||
SetWindowLongPtrW(hwnd_, GWL_STYLE, restore_style_);
|
||||
SetWindowLongPtrW(hwnd_, GWL_EXSTYLE, restore_ex_style_);
|
||||
SetWindowPlacement(hwnd_, &restore_placement_);
|
||||
SetWindowPos(
|
||||
hwnd_,
|
||||
HWND_NOTOPMOST,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
|
||||
ApplyWindowFrameStyle(hwnd_);
|
||||
void NotifyResize() {
|
||||
if (delegate) {
|
||||
delegate->OnWindowResized(CurrentLayout(true));
|
||||
}
|
||||
}
|
||||
|
||||
NotifyResize();
|
||||
}
|
||||
BrowserLayout CurrentLayout(bool show_chrome) const {
|
||||
RECT client = {};
|
||||
if (hwnd) {
|
||||
GetClientRect(hwnd, &client);
|
||||
}
|
||||
|
||||
void NebulaWindow::Close() {
|
||||
if (hwnd_) {
|
||||
SendMessageW(hwnd_, WM_CLOSE, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaWindow::BeginDrag() {
|
||||
if (!hwnd_) {
|
||||
return;
|
||||
BrowserLayout layout;
|
||||
layout.chrome = show_chrome
|
||||
? ToPlatformRect(RECT{
|
||||
0,
|
||||
0,
|
||||
client.right,
|
||||
std::min<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;
|
||||
}
|
||||
|
||||
ReleaseCapture();
|
||||
SendMessageW(hwnd_, WM_NCLBUTTONDOWN, HTCAPTION, 0);
|
||||
}
|
||||
void EnableFrameHitTestForWindow(HWND child) const;
|
||||
LRESULT HitTest(LPARAM lparam) const;
|
||||
LRESULT HitTestPoint(POINT point) const;
|
||||
LRESULT WndProc(UINT message, WPARAM wparam, LPARAM lparam);
|
||||
void RegisterClass(HINSTANCE instance_handle);
|
||||
|
||||
void NebulaWindow::SetTitle(const std::wstring& title) {
|
||||
if (hwnd_) {
|
||||
SetWindowTextW(hwnd_, title.empty() ? kWindowTitle : title.c_str());
|
||||
}
|
||||
}
|
||||
static LRESULT CALLBACK StaticWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
|
||||
static LRESULT CALLBACK ChildFrameWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
|
||||
static BOOL CALLBACK EnableFrameHitTestForDescendant(HWND hwnd, LPARAM lparam);
|
||||
};
|
||||
|
||||
void NebulaWindow::EnableFrameHitTest(HWND child) const {
|
||||
if (!hwnd_ || !child) {
|
||||
return;
|
||||
}
|
||||
|
||||
EnableFrameHitTestForWindow(child);
|
||||
EnumChildWindows(child, &NebulaWindow::EnableFrameHitTestForDescendant, reinterpret_cast<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