Compare commits
9 Commits
207a849f06
...
CEF
| Author | SHA1 | Date | |
|---|---|---|---|
| 18bc607d93 | |||
| 54216aa133 | |||
| 8eb5c1a3b2 | |||
| 406d73c10f | |||
| 6fac7e320b | |||
| a32940a3f3 | |||
| 10180b7109 | |||
| dd6b3fa70d | |||
| a8786b4c1c |
@@ -39,6 +39,16 @@ add_subdirectory(
|
||||
|
||||
set(NEBULA_SOURCES
|
||||
app/main.cpp
|
||||
src/app/nebula_controller.cpp
|
||||
src/app/run.cpp
|
||||
src/browser/session_state.cpp
|
||||
src/browser/tab.cpp
|
||||
src/browser/tab_manager.cpp
|
||||
src/browser/url_utils.cpp
|
||||
src/cef/browser_client.cpp
|
||||
src/cef/nebula_app.cpp
|
||||
src/ui/paths.cpp
|
||||
src/window/nebula_window.cpp
|
||||
)
|
||||
|
||||
add_executable(NebulaBrowser WIN32
|
||||
@@ -54,6 +64,7 @@ if(MSVC)
|
||||
endif()
|
||||
|
||||
target_include_directories(NebulaBrowser PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/src"
|
||||
"${CEF_ROOT}"
|
||||
"${CEF_ROOT}/include"
|
||||
)
|
||||
@@ -70,6 +81,7 @@ target_link_libraries(NebulaBrowser PRIVATE
|
||||
if(WIN32)
|
||||
target_link_libraries(NebulaBrowser PRIVATE
|
||||
"${CEF_ROOT}/Release/libcef.lib"
|
||||
dwmapi
|
||||
)
|
||||
|
||||
target_compile_definitions(NebulaBrowser PRIVATE
|
||||
|
||||
+2
-330
@@ -1,333 +1,6 @@
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include "include/cef_app.h"
|
||||
#include "include/cef_browser.h"
|
||||
#include "include/cef_client.h"
|
||||
#include "include/cef_command_line.h"
|
||||
#include "include/cef_request.h"
|
||||
#include "include/cef_request_handler.h"
|
||||
#include "include/wrapper/cef_helpers.h"
|
||||
|
||||
namespace {
|
||||
|
||||
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 FilePathToUrl(std::filesystem::path path) {
|
||||
std::string value = WideToUtf8(path.wstring());
|
||||
for (char& ch : value) {
|
||||
if (ch == '\\') {
|
||||
ch = '/';
|
||||
}
|
||||
}
|
||||
|
||||
std::string encoded;
|
||||
encoded.reserve(value.size());
|
||||
for (char ch : value) {
|
||||
encoded += ch == ' ' ? "%20" : std::string(1, ch);
|
||||
}
|
||||
return "file:///" + encoded;
|
||||
}
|
||||
|
||||
std::filesystem::path GetHomePath() {
|
||||
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() /
|
||||
"ui" / "pages" / "home.html";
|
||||
}
|
||||
|
||||
std::string GetHomeUrl() {
|
||||
const auto home_path = GetHomePath();
|
||||
if (home_path.empty()) {
|
||||
return "https://www.google.com";
|
||||
}
|
||||
|
||||
return FilePathToUrl(home_path);
|
||||
}
|
||||
|
||||
std::string GetUrlWithoutDecoration(std::string url) {
|
||||
const size_t split = url.find_first_of("?#");
|
||||
if (split != std::string::npos) {
|
||||
url.resize(split);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
std::string ToLowerAscii(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
bool IsInternalHomeUrl(const std::string& url) {
|
||||
return GetUrlWithoutDecoration(url) == GetHomeUrl();
|
||||
}
|
||||
|
||||
bool IsChromiumNewTabUrl(const std::string& url) {
|
||||
const std::string target = ToLowerAscii(GetUrlWithoutDecoration(url));
|
||||
return target == "about:blank" ||
|
||||
target == "chrome://newtab" ||
|
||||
target == "chrome://newtab/" ||
|
||||
target == "chrome://new-tab-page" ||
|
||||
target == "chrome://new-tab-page/" ||
|
||||
target == "chrome-search://local-ntp/local-ntp.html";
|
||||
}
|
||||
|
||||
bool IsEmptyOrChromiumNewTabUrl(const std::string& url) {
|
||||
return url.empty() || IsChromiumNewTabUrl(url);
|
||||
}
|
||||
|
||||
class NebulaClient final : public CefClient,
|
||||
public CefDisplayHandler,
|
||||
public CefKeyboardHandler,
|
||||
public CefLifeSpanHandler,
|
||||
public CefPermissionHandler,
|
||||
public CefRequestHandler {
|
||||
public:
|
||||
CefRefPtr<CefDisplayHandler> GetDisplayHandler() override {
|
||||
return this;
|
||||
}
|
||||
|
||||
CefRefPtr<CefKeyboardHandler> GetKeyboardHandler() override {
|
||||
return this;
|
||||
}
|
||||
|
||||
CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override {
|
||||
return this;
|
||||
}
|
||||
|
||||
CefRefPtr<CefPermissionHandler> GetPermissionHandler() override {
|
||||
return this;
|
||||
}
|
||||
|
||||
CefRefPtr<CefRequestHandler> GetRequestHandler() override {
|
||||
return this;
|
||||
}
|
||||
|
||||
void OnAddressChange(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
const CefString& url) override {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
|
||||
if (browser && frame && frame->IsMain() &&
|
||||
IsChromiumNewTabUrl(url)) {
|
||||
browser->GetMainFrame()->LoadURL(GetHomeUrl());
|
||||
}
|
||||
}
|
||||
|
||||
void OnTitleChange(CefRefPtr<CefBrowser> browser,
|
||||
const CefString& title) override {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
|
||||
CefWindowHandle window = browser->GetHost()->GetWindowHandle();
|
||||
if (window) {
|
||||
SetWindowText(window, std::wstring(title).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
bool OnPreKeyEvent(CefRefPtr<CefBrowser> browser,
|
||||
const CefKeyEvent& event,
|
||||
CefEventHandle os_event,
|
||||
bool* is_keyboard_shortcut) override {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
UNREFERENCED_PARAMETER(os_event);
|
||||
|
||||
if (event.type == KEYEVENT_RAWKEYDOWN &&
|
||||
(event.modifiers & EVENTFLAG_CONTROL_DOWN) != 0 &&
|
||||
event.windows_key_code == 'T') {
|
||||
if (is_keyboard_shortcut) {
|
||||
*is_keyboard_shortcut = true;
|
||||
}
|
||||
browser->GetMainFrame()->LoadURL(GetHomeUrl());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OnBeforePopup(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
int popup_id,
|
||||
const CefString& target_url,
|
||||
const CefString& target_frame_name,
|
||||
CefLifeSpanHandler::WindowOpenDisposition target_disposition,
|
||||
bool user_gesture,
|
||||
const CefPopupFeatures& popupFeatures,
|
||||
CefWindowInfo& windowInfo,
|
||||
CefRefPtr<CefClient>& client,
|
||||
CefBrowserSettings& settings,
|
||||
CefRefPtr<CefDictionaryValue>& extra_info,
|
||||
bool* no_javascript_access) override {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
UNREFERENCED_PARAMETER(frame);
|
||||
UNREFERENCED_PARAMETER(popup_id);
|
||||
UNREFERENCED_PARAMETER(target_frame_name);
|
||||
UNREFERENCED_PARAMETER(user_gesture);
|
||||
UNREFERENCED_PARAMETER(popupFeatures);
|
||||
UNREFERENCED_PARAMETER(windowInfo);
|
||||
UNREFERENCED_PARAMETER(settings);
|
||||
UNREFERENCED_PARAMETER(extra_info);
|
||||
UNREFERENCED_PARAMETER(no_javascript_access);
|
||||
|
||||
if (target_disposition == CEF_WOD_NEW_WINDOW &&
|
||||
IsEmptyOrChromiumNewTabUrl(target_url)) {
|
||||
client = this;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsChromiumNewTabUrl(target_url)) {
|
||||
browser->GetMainFrame()->LoadURL(GetHomeUrl());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
CefRefPtr<CefRequest> request,
|
||||
bool user_gesture,
|
||||
bool is_redirect) override {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
UNREFERENCED_PARAMETER(browser);
|
||||
UNREFERENCED_PARAMETER(user_gesture);
|
||||
UNREFERENCED_PARAMETER(is_redirect);
|
||||
|
||||
if (frame && frame->IsMain() && request &&
|
||||
IsChromiumNewTabUrl(request->GetURL())) {
|
||||
frame->LoadURL(GetHomeUrl());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void OnAfterCreated(CefRefPtr<CefBrowser> browser) override {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
++browser_count_;
|
||||
|
||||
if (browser_count_ > 1 && browser &&
|
||||
IsEmptyOrChromiumNewTabUrl(browser->GetMainFrame()->GetURL())) {
|
||||
browser->GetMainFrame()->LoadURL(GetHomeUrl());
|
||||
}
|
||||
}
|
||||
|
||||
void OnBeforeClose(CefRefPtr<CefBrowser> browser) override {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
|
||||
--browser_count_;
|
||||
if (browser_count_ == 0) {
|
||||
CefQuitMessageLoop();
|
||||
}
|
||||
}
|
||||
|
||||
bool OnShowPermissionPrompt(
|
||||
CefRefPtr<CefBrowser> browser,
|
||||
uint64_t prompt_id,
|
||||
const CefString& requesting_origin,
|
||||
uint32_t requested_permissions,
|
||||
CefRefPtr<CefPermissionPromptCallback> callback) override {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
UNREFERENCED_PARAMETER(prompt_id);
|
||||
UNREFERENCED_PARAMETER(requesting_origin);
|
||||
|
||||
if ((requested_permissions & CEF_PERMISSION_TYPE_GEOLOCATION) != 0 &&
|
||||
browser && callback &&
|
||||
IsInternalHomeUrl(browser->GetMainFrame()->GetURL())) {
|
||||
callback->Continue(CEF_PERMISSION_RESULT_ACCEPT);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
int browser_count_ = 0;
|
||||
|
||||
IMPLEMENT_REFCOUNTING(NebulaClient);
|
||||
};
|
||||
|
||||
class NebulaApp final : public CefApp {
|
||||
public:
|
||||
void OnBeforeCommandLineProcessing(
|
||||
const CefString& process_type,
|
||||
CefRefPtr<CefCommandLine> command_line) override {
|
||||
UNREFERENCED_PARAMETER(process_type);
|
||||
|
||||
// The bundled UI is loaded from file:// and uses ES modules.
|
||||
command_line->AppendSwitch("allow-file-access-from-files");
|
||||
}
|
||||
|
||||
private:
|
||||
IMPLEMENT_REFCOUNTING(NebulaApp);
|
||||
};
|
||||
|
||||
int RunNebula(HINSTANCE instance) {
|
||||
CefMainArgs main_args(instance);
|
||||
CefRefPtr<NebulaApp> app(new NebulaApp);
|
||||
|
||||
const int subprocess_exit_code =
|
||||
CefExecuteProcess(main_args, app, nullptr);
|
||||
if (subprocess_exit_code >= 0) {
|
||||
return subprocess_exit_code;
|
||||
}
|
||||
|
||||
CefSettings settings;
|
||||
settings.no_sandbox = true;
|
||||
|
||||
if (!CefInitialize(main_args, settings, app, nullptr)) {
|
||||
return CefGetExitCode();
|
||||
}
|
||||
|
||||
CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
|
||||
command_line->InitFromString(GetCommandLineW());
|
||||
|
||||
std::string url = command_line->GetSwitchValue("url");
|
||||
if (url.empty()) {
|
||||
url = GetHomeUrl();
|
||||
}
|
||||
|
||||
CefWindowInfo window_info;
|
||||
window_info.SetAsPopup(nullptr, "Nebula Browser");
|
||||
|
||||
CefBrowserSettings browser_settings;
|
||||
CefRefPtr<NebulaClient> client(new NebulaClient);
|
||||
|
||||
if (!CefBrowserHost::CreateBrowser(
|
||||
window_info, client, url, browser_settings, nullptr, nullptr)) {
|
||||
CefShutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
CefRunMessageLoop();
|
||||
CefShutdown();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#include "app/run.h"
|
||||
|
||||
int APIENTRY wWinMain(HINSTANCE instance,
|
||||
HINSTANCE previous_instance,
|
||||
@@ -335,7 +8,6 @@ int APIENTRY wWinMain(HINSTANCE instance,
|
||||
int show_command) {
|
||||
UNREFERENCED_PARAMETER(previous_instance);
|
||||
UNREFERENCED_PARAMETER(command_line);
|
||||
UNREFERENCED_PARAMETER(show_command);
|
||||
|
||||
return RunNebula(instance);
|
||||
return nebula::app::RunNebula(instance, show_command);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,740 @@
|
||||
#include "app/nebula_controller.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <system_error>
|
||||
|
||||
#include "browser/session_state.h"
|
||||
#include "browser/url_utils.h"
|
||||
#include "include/cef_app.h"
|
||||
#include "include/cef_browser.h"
|
||||
#include "include/cef_cookie.h"
|
||||
#include "include/wrapper/cef_helpers.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::app {
|
||||
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";
|
||||
}
|
||||
|
||||
std::string ToLowerAscii(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
bool IsSiteHistoryUrl(const std::string& url) {
|
||||
const std::string lower = ToLowerAscii(url);
|
||||
return lower.starts_with("http://") || lower.starts_with("https://");
|
||||
}
|
||||
|
||||
std::vector<std::string> LoadSiteHistory() {
|
||||
std::vector<std::string> history;
|
||||
std::ifstream input(GetSiteHistoryPath(), std::ios::binary);
|
||||
if (!input) {
|
||||
return history;
|
||||
}
|
||||
|
||||
std::string url;
|
||||
while (std::getline(input, url) && history.size() < kMaxSiteHistoryEntries) {
|
||||
if (IsSiteHistoryUrl(url)) {
|
||||
history.push_back(url);
|
||||
}
|
||||
}
|
||||
return history;
|
||||
}
|
||||
|
||||
void SaveSiteHistory(const std::vector<std::string>& history) {
|
||||
const auto path = GetSiteHistoryPath();
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::ofstream output(path, std::ios::binary | std::ios::trunc);
|
||||
if (!output) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& url : history) {
|
||||
output << url << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
std::string SiteHistoryJson(const std::vector<std::string>& history) {
|
||||
std::string json = "[";
|
||||
for (size_t i = 0; i < history.size(); ++i) {
|
||||
if (i > 0) {
|
||||
json += ",";
|
||||
}
|
||||
json += "\"" + nebula::browser::JsonEscape(history[i]) + "\"";
|
||||
}
|
||||
json += "]";
|
||||
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;
|
||||
return settings;
|
||||
}
|
||||
|
||||
int ParseTabId(const std::string& value) {
|
||||
int tab_id = 0;
|
||||
const auto result = std::from_chars(value.data(), value.data() + value.size(), tab_id);
|
||||
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;
|
||||
}
|
||||
|
||||
const size_t hash = url.find('#');
|
||||
std::string fragment;
|
||||
if (hash != std::string::npos) {
|
||||
fragment = url.substr(hash);
|
||||
url.resize(hash);
|
||||
}
|
||||
|
||||
const char separator = url.find('?') == std::string::npos ? '?' : '&';
|
||||
return url + separator + "nebula_cache_bust=" + std::to_string(GetTickCount64()) + fragment;
|
||||
}
|
||||
|
||||
std::string GetChromeDisplayUrl(const std::string& url) {
|
||||
return nebula::ui::IsInternalHomeUrl(url) ? std::string{} : url;
|
||||
}
|
||||
|
||||
void SetBrowserVisible(CefRefPtr<CefBrowser> browser, bool visible) {
|
||||
if (!browser) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NebulaController::NebulaController(HINSTANCE instance, std::string initial_url, int show_command)
|
||||
: instance_(instance),
|
||||
initial_url_(std::move(initial_url)),
|
||||
show_command_(show_command),
|
||||
tabs_(this),
|
||||
site_history_(LoadSiteHistory()) {}
|
||||
|
||||
NebulaController::~NebulaController() = default;
|
||||
|
||||
bool NebulaController::Create() {
|
||||
window_ = std::make_unique<nebula::window::NebulaWindow>(this);
|
||||
return window_->Create(instance_, show_command_);
|
||||
}
|
||||
|
||||
void NebulaController::OnWindowCreated() {
|
||||
if (initial_url_.empty()) {
|
||||
tabs_.CreateInitialTab(nebula::ui::GetHomeUrl());
|
||||
} else {
|
||||
tabs_.CreateInitialTab(initial_url_);
|
||||
}
|
||||
PersistSession();
|
||||
|
||||
CreateChromeBrowser();
|
||||
CreateContentBrowser();
|
||||
}
|
||||
|
||||
void NebulaController::OnWindowResized(const nebula::window::BrowserLayout& layout) {
|
||||
UNREFERENCED_PARAMETER(layout);
|
||||
ResizeBrowsers();
|
||||
}
|
||||
|
||||
void NebulaController::OnWindowCloseRequested() {
|
||||
if (closing_) {
|
||||
// CEF re-sends WM_CLOSE to the top-level window after each Alloy
|
||||
// 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());
|
||||
}
|
||||
MaybeFinishShutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
closing_ = true;
|
||||
PersistSession();
|
||||
if (auto cookie_manager = CefCookieManager::GetGlobalManager(nullptr)) {
|
||||
cookie_manager->FlushStore(nullptr);
|
||||
}
|
||||
|
||||
if (chrome_browser_) {
|
||||
chrome_browser_->GetHost()->CloseBrowser(false);
|
||||
}
|
||||
if (menu_popup_browser_) {
|
||||
menu_popup_browser_->GetHost()->CloseBrowser(false);
|
||||
}
|
||||
for (const auto& tab : tabs_.Tabs()) {
|
||||
if (tab.browser) {
|
||||
tab.browser->GetHost()->CloseBrowser(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaController::OnActiveTabChanged(const nebula::browser::NebulaTab& tab) {
|
||||
if (chrome_ready_) {
|
||||
SendChromeState(tab);
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaController::OnBrowserCreated(nebula::cef::BrowserRole role, CefRefPtr<CefBrowser> browser) {
|
||||
if (window_ && browser) {
|
||||
window_->EnableFrameHitTest(browser->GetHost()->GetWindowHandle());
|
||||
}
|
||||
|
||||
if (role == nebula::cef::BrowserRole::Chrome) {
|
||||
chrome_browser_ = browser;
|
||||
chrome_ready_ = true;
|
||||
if (const auto* tab = tabs_.ActiveTab()) {
|
||||
SendChromeState(*tab);
|
||||
}
|
||||
} else if (role == nebula::cef::BrowserRole::MenuPopup) {
|
||||
menu_popup_browser_ = browser;
|
||||
PositionMenuPopup();
|
||||
} else {
|
||||
tabs_.SetActiveBrowser(browser);
|
||||
}
|
||||
|
||||
ResizeBrowsers();
|
||||
}
|
||||
|
||||
void NebulaController::OnBrowserClosing(nebula::cef::BrowserRole role, CefRefPtr<CefBrowser> browser) {
|
||||
if (role == nebula::cef::BrowserRole::Chrome) {
|
||||
chrome_browser_ = nullptr;
|
||||
chrome_ready_ = false;
|
||||
} else if (role == nebula::cef::BrowserRole::MenuPopup) {
|
||||
menu_popup_browser_ = nullptr;
|
||||
menu_popup_client_ = nullptr;
|
||||
} else {
|
||||
if (content_fullscreen_) {
|
||||
const auto* active_tab = tabs_.ActiveTab();
|
||||
if (active_tab && active_tab->browser && active_tab->browser->IsSame(browser)) {
|
||||
SetContentFullscreen(false);
|
||||
}
|
||||
}
|
||||
tabs_.ClearBrowser(browser);
|
||||
}
|
||||
MaybeFinishShutdown();
|
||||
}
|
||||
|
||||
void NebulaController::OnChromeCommand(const std::string& command, const std::string& payload) {
|
||||
if (command == "navigate") {
|
||||
tabs_.LoadURL(payload);
|
||||
} else if (command == "navigate-insecure") {
|
||||
const std::string target = nebula::browser::NormalizeNavigationInput(payload);
|
||||
if (nebula::ui::IsHttpUrl(target)) {
|
||||
insecure_warning_bypasses_.insert(target);
|
||||
tabs_.LoadURL(target);
|
||||
}
|
||||
} else if (command == "new-tab") {
|
||||
CreateNewTab();
|
||||
} else if (command == "activate-tab") {
|
||||
ActivateTab(ParseTabId(payload));
|
||||
} else if (command == "close-tab") {
|
||||
CloseTab(ParseTabId(payload));
|
||||
} else if (command == "back") {
|
||||
tabs_.GoBack();
|
||||
} else if (command == "forward") {
|
||||
tabs_.GoForward();
|
||||
} else if (command == "reload") {
|
||||
tabs_.Reload();
|
||||
} else if (command == "stop") {
|
||||
tabs_.StopLoad();
|
||||
} else if (command == "settings") {
|
||||
tabs_.LoadURL(nebula::ui::GetSettingsUrl());
|
||||
} else if (command == "menu-popup") {
|
||||
ToggleMenuPopup();
|
||||
} else if (command == "open-settings") {
|
||||
CloseMenuPopup();
|
||||
tabs_.LoadURL(nebula::ui::GetSettingsUrl());
|
||||
} else if (command == "big-picture") {
|
||||
CloseMenuPopup();
|
||||
tabs_.LoadURL(nebula::ui::GetBigPictureUrl());
|
||||
} else if (command == "gpu-diagnostics") {
|
||||
CloseMenuPopup();
|
||||
tabs_.LoadURL(nebula::ui::GetGpuDiagnosticsUrl());
|
||||
} else if (command == "toggle-devtools") {
|
||||
ToggleDevTools();
|
||||
} else if (command == "zoom-out") {
|
||||
AdjustZoom(-0.5);
|
||||
} else if (command == "zoom-in") {
|
||||
AdjustZoom(0.5);
|
||||
} else if (command == "hard-reload") {
|
||||
CloseMenuPopup();
|
||||
if (auto* tab = tabs_.ActiveTab(); tab && tab->browser) {
|
||||
tab->browser->ReloadIgnoreCache();
|
||||
}
|
||||
} else if (command == "fresh-reload") {
|
||||
CloseMenuPopup();
|
||||
FreshReload();
|
||||
} else if (command == "close-menu-popup") {
|
||||
CloseMenuPopup();
|
||||
} else if (command == "home") {
|
||||
tabs_.LoadURL(nebula::ui::GetHomeUrl());
|
||||
} else if (command == "clear-site-history") {
|
||||
site_history_.clear();
|
||||
SaveSiteHistory(site_history_);
|
||||
if (auto* tab = tabs_.ActiveTab(); tab && tab->browser) {
|
||||
InjectSettingsHistory(tab->browser);
|
||||
}
|
||||
} else if (command == "minimize" && window_) {
|
||||
window_->Minimize();
|
||||
} else if (command == "maximize" && window_) {
|
||||
window_->ToggleMaximize();
|
||||
} else if (command == "close" && window_) {
|
||||
window_->Close();
|
||||
} else if (command == "drag" && window_) {
|
||||
window_->BeginDrag();
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaController::OnContentAddressChanged(CefRefPtr<CefBrowser> browser, const std::string& url) {
|
||||
const std::string internal_url = nebula::ui::ToInternalUrl(url);
|
||||
tabs_.UpdateURL(browser,
|
||||
nebula::ui::IsChromiumNewTabUrl(url)
|
||||
? nebula::ui::GetHomeUrl()
|
||||
: internal_url);
|
||||
RecordSiteHistory(internal_url);
|
||||
PersistSession();
|
||||
}
|
||||
|
||||
void NebulaController::OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) {
|
||||
tabs_.UpdateTitle(browser, title);
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaController::OnContentLoadingStateChanged(CefRefPtr<CefBrowser> browser, bool is_loading) {
|
||||
tabs_.UpdateLoadingState(browser, is_loading);
|
||||
}
|
||||
|
||||
void NebulaController::OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browser, double progress) {
|
||||
tabs_.UpdateLoadProgress(browser, progress);
|
||||
}
|
||||
|
||||
void NebulaController::OnContentLoadFinished(CefRefPtr<CefBrowser> browser, const std::string& url) {
|
||||
if (nebula::ui::ToInternalUrl(url).starts_with(nebula::ui::GetSettingsUrl())) {
|
||||
InjectSettingsHistory(browser);
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaController::OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) {
|
||||
tabs_.UpdateFavicon(browser, urls);
|
||||
}
|
||||
|
||||
void NebulaController::OnContentFullscreenChanged(CefRefPtr<CefBrowser> browser, bool fullscreen) {
|
||||
const auto* active_tab = tabs_.ActiveTab();
|
||||
if (!active_tab || !active_tab->browser || !active_tab->browser->IsSame(browser)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SetContentFullscreen(fullscreen);
|
||||
}
|
||||
|
||||
void NebulaController::OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) {
|
||||
if (!tabs_.OwnsBrowser(browser)) {
|
||||
return;
|
||||
}
|
||||
|
||||
tabs_.LoadURL(nebula::ui::IsEmptyOrChromiumNewTabUrl(target_url)
|
||||
? nebula::ui::GetHomeUrl()
|
||||
: target_url);
|
||||
}
|
||||
|
||||
bool NebulaController::ShouldBypassInsecureWarning(CefRefPtr<CefBrowser> browser, const std::string& target_url) {
|
||||
if (!tabs_.OwnsBrowser(browser)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto bypass = insecure_warning_bypasses_.find(target_url);
|
||||
if (bypass == insecure_warning_bypasses_.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
insecure_warning_bypasses_.erase(bypass);
|
||||
return true;
|
||||
}
|
||||
|
||||
void NebulaController::CreateNewTab() {
|
||||
if (auto* tab = tabs_.ActiveTab()) {
|
||||
SetBrowserVisible(tab->browser, false);
|
||||
}
|
||||
|
||||
tabs_.CreateTab(nebula::ui::GetHomeUrl());
|
||||
PersistSession();
|
||||
CreateContentBrowser();
|
||||
}
|
||||
|
||||
void NebulaController::ActivateTab(int tab_id) {
|
||||
auto* current_tab = tabs_.ActiveTab();
|
||||
if (current_tab && current_tab->id == tab_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
CefRefPtr<CefBrowser> previous_browser = current_tab ? current_tab->browser : nullptr;
|
||||
if (!tabs_.ActivateTab(tab_id)) {
|
||||
return;
|
||||
}
|
||||
PersistSession();
|
||||
|
||||
SetBrowserVisible(previous_browser, false);
|
||||
if (auto* active_tab = tabs_.ActiveTab()) {
|
||||
if (active_tab->browser) {
|
||||
SetBrowserVisible(active_tab->browser, true);
|
||||
} else {
|
||||
CreateContentBrowser();
|
||||
}
|
||||
}
|
||||
ResizeBrowsers();
|
||||
}
|
||||
|
||||
void NebulaController::CloseTab(int tab_id) {
|
||||
const bool was_active = [this, tab_id] {
|
||||
const auto* tab = tabs_.ActiveTab();
|
||||
return tab && tab->id == tab_id;
|
||||
}();
|
||||
|
||||
CefRefPtr<CefBrowser> closing_browser = tabs_.CloseTab(tab_id);
|
||||
PersistSession();
|
||||
if (closing_browser) {
|
||||
closing_browser->GetHost()->CloseBrowser(false);
|
||||
}
|
||||
|
||||
if (!tabs_.ActiveTab()) {
|
||||
tabs_.CreateTab(nebula::ui::GetHomeUrl());
|
||||
PersistSession();
|
||||
CreateContentBrowser();
|
||||
return;
|
||||
}
|
||||
|
||||
if (was_active) {
|
||||
if (auto* active_tab = tabs_.ActiveTab()) {
|
||||
SetBrowserVisible(active_tab->browser, true);
|
||||
}
|
||||
ResizeBrowsers();
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaController::CreateChromeBrowser() {
|
||||
if (!window_ || !window_->hwnd()) {
|
||||
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);
|
||||
CefBrowserHost::CreateBrowser(
|
||||
window_info, chrome_client_, nebula::ui::GetChromeUrl(), browser_settings, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void NebulaController::CreateContentBrowser() {
|
||||
if (!window_ || !window_->hwnd()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* tab = tabs_.ActiveTab();
|
||||
const std::string url = tab && !tab->url.empty() ? tab->url : nebula::ui::GetHomeUrl();
|
||||
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);
|
||||
CefBrowserHost::CreateBrowser(
|
||||
window_info, content_client_, nebula::ui::ResolveInternalUrl(url), browser_settings, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void NebulaController::ToggleMenuPopup() {
|
||||
if (menu_popup_browser_) {
|
||||
CloseMenuPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
CreateMenuPopupBrowser();
|
||||
}
|
||||
|
||||
void NebulaController::CloseMenuPopup() {
|
||||
if (menu_popup_browser_) {
|
||||
menu_popup_browser_->GetHost()->CloseBrowser(false);
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaController::CreateMenuPopupBrowser() {
|
||||
if (!window_ || !window_->hwnd()) {
|
||||
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));
|
||||
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_) {
|
||||
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);
|
||||
}
|
||||
|
||||
void NebulaController::ToggleDevTools() {
|
||||
auto* tab = tabs_.ActiveTab();
|
||||
if (!tab || !tab->browser || !window_ || !window_->hwnd()) {
|
||||
return;
|
||||
}
|
||||
|
||||
CefRefPtr<CefBrowserHost> host = tab->browser->GetHost();
|
||||
if (host->HasDevTools()) {
|
||||
host->CloseDevTools();
|
||||
return;
|
||||
}
|
||||
|
||||
CefWindowInfo window_info;
|
||||
window_info.SetAsPopup(window_->hwnd(), "Nebula Developer Tools");
|
||||
window_info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
|
||||
CefBrowserSettings browser_settings;
|
||||
host->ShowDevTools(window_info, content_client_, browser_settings, CefPoint());
|
||||
}
|
||||
|
||||
void NebulaController::AdjustZoom(double delta) {
|
||||
auto* tab = tabs_.ActiveTab();
|
||||
if (!tab || !tab->browser) {
|
||||
return;
|
||||
}
|
||||
|
||||
CefRefPtr<CefBrowserHost> host = tab->browser->GetHost();
|
||||
host->SetZoomLevel(host->GetZoomLevel() + delta);
|
||||
}
|
||||
|
||||
void NebulaController::FreshReload() {
|
||||
auto* tab = tabs_.ActiveTab();
|
||||
if (!tab || tab->url.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
tabs_.LoadURL(WithCacheBuster(tab->url));
|
||||
}
|
||||
|
||||
void NebulaController::SetContentFullscreen(bool fullscreen) {
|
||||
if (content_fullscreen_ == fullscreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
content_fullscreen_ = fullscreen;
|
||||
if (fullscreen) {
|
||||
CloseMenuPopup();
|
||||
}
|
||||
|
||||
SetBrowserVisible(chrome_browser_, !fullscreen);
|
||||
if (window_) {
|
||||
window_->SetFullscreen(fullscreen);
|
||||
}
|
||||
ResizeBrowsers();
|
||||
}
|
||||
|
||||
void NebulaController::ResizeBrowsers() {
|
||||
if (!window_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto layout = window_->CurrentLayout(!content_fullscreen_);
|
||||
if (chrome_browser_) {
|
||||
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);
|
||||
}
|
||||
if (!content_fullscreen_) {
|
||||
PositionMenuPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaController::SendChromeState(const nebula::browser::NebulaTab& tab) {
|
||||
if (!chrome_browser_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string display_url = GetChromeDisplayUrl(tab.url);
|
||||
std::string tabs_json = "[";
|
||||
const auto& tabs = tabs_.Tabs();
|
||||
for (size_t i = 0; i < tabs.size(); ++i) {
|
||||
const auto& item = tabs[i];
|
||||
if (i > 0) {
|
||||
tabs_json += ",";
|
||||
}
|
||||
tabs_json +=
|
||||
"{\"id\":" + std::to_string(item.id) +
|
||||
",\"title\":\"" + nebula::browser::JsonEscape(item.title) + "\"" +
|
||||
",\"isLoading\":" + std::string(item.is_loading ? "true" : "false") +
|
||||
",\"favicon\":\"" + nebula::browser::JsonEscape(item.favicon_url) + "\"" +
|
||||
"}";
|
||||
}
|
||||
tabs_json += "]";
|
||||
|
||||
const std::string script =
|
||||
"window.NebulaChrome && window.NebulaChrome.applyState({"
|
||||
"\"id\":" + std::to_string(tab.id) +
|
||||
",\"url\":\"" + nebula::browser::JsonEscape(display_url) + "\""
|
||||
",\"title\":\"" + nebula::browser::JsonEscape(tab.title) + "\""
|
||||
",\"isLoading\":" + std::string(tab.is_loading ? "true" : "false") +
|
||||
",\"progress\":" + std::to_string(tab.load_progress) +
|
||||
",\"canGoBack\":" + std::string(tab.CanGoBack() ? "true" : "false") +
|
||||
",\"canGoForward\":" + std::string(tab.CanGoForward() ? "true" : "false") +
|
||||
",\"favicon\":\"" + nebula::browser::JsonEscape(tab.favicon_url) + "\"" +
|
||||
",\"tabs\":" + tabs_json +
|
||||
"});";
|
||||
|
||||
chrome_browser_->GetMainFrame()->ExecuteJavaScript(script, nebula::ui::GetChromeUrl(), 0);
|
||||
}
|
||||
|
||||
void NebulaController::RecordSiteHistory(const std::string& url) {
|
||||
if (!IsSiteHistoryUrl(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
site_history_.erase(
|
||||
std::remove(site_history_.begin(), site_history_.end(), url),
|
||||
site_history_.end());
|
||||
site_history_.insert(site_history_.begin(), url);
|
||||
if (site_history_.size() > kMaxSiteHistoryEntries) {
|
||||
site_history_.resize(kMaxSiteHistoryEntries);
|
||||
}
|
||||
SaveSiteHistory(site_history_);
|
||||
}
|
||||
|
||||
void NebulaController::InjectSettingsHistory(CefRefPtr<CefBrowser> browser) {
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string history_json = SiteHistoryJson(site_history_);
|
||||
const std::string script =
|
||||
"localStorage.setItem('siteHistory', \"" + nebula::browser::JsonEscape(history_json) + "\");"
|
||||
"if (typeof loadHistories === 'function') { loadHistories(); }";
|
||||
browser->GetMainFrame()->ExecuteJavaScript(script, nebula::ui::GetSettingsUrl(), 0);
|
||||
}
|
||||
|
||||
void NebulaController::PersistSession() const {
|
||||
nebula::browser::SaveSessionState(tabs_.Tabs(), tabs_.ActiveTabIndex());
|
||||
}
|
||||
|
||||
void NebulaController::MaybeFinishShutdown() {
|
||||
if (!closing_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (chrome_browser_ || menu_popup_browser_ || tabs_.HasOpenBrowsers()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window_ && window_->hwnd()) {
|
||||
DestroyWindow(window_->hwnd());
|
||||
}
|
||||
CefQuitMessageLoop();
|
||||
}
|
||||
|
||||
} // namespace nebula::app
|
||||
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "browser/tab_manager.h"
|
||||
#include "cef/browser_client.h"
|
||||
#include "window/nebula_window.h"
|
||||
|
||||
namespace nebula::app {
|
||||
|
||||
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() override;
|
||||
|
||||
bool Create();
|
||||
|
||||
void OnWindowCreated() override;
|
||||
void OnWindowResized(const nebula::window::BrowserLayout& layout) override;
|
||||
void OnWindowCloseRequested() override;
|
||||
|
||||
void OnActiveTabChanged(const nebula::browser::NebulaTab& tab) override;
|
||||
|
||||
void OnBrowserCreated(nebula::cef::BrowserRole role, CefRefPtr<CefBrowser> browser) override;
|
||||
void OnBrowserClosing(nebula::cef::BrowserRole role, CefRefPtr<CefBrowser> browser) override;
|
||||
void OnChromeCommand(const std::string& command, const std::string& payload) override;
|
||||
void OnContentAddressChanged(CefRefPtr<CefBrowser> browser, const std::string& url) override;
|
||||
void OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) override;
|
||||
void OnContentLoadingStateChanged(CefRefPtr<CefBrowser> browser, bool is_loading) override;
|
||||
void OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browser, double progress) override;
|
||||
void OnContentLoadFinished(CefRefPtr<CefBrowser> browser, const std::string& url) override;
|
||||
void OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) override;
|
||||
void OnContentFullscreenChanged(CefRefPtr<CefBrowser> browser, bool fullscreen) override;
|
||||
void OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) override;
|
||||
bool ShouldBypassInsecureWarning(CefRefPtr<CefBrowser> browser, const std::string& target_url) override;
|
||||
|
||||
private:
|
||||
void CreateNewTab();
|
||||
void ActivateTab(int tab_id);
|
||||
void CloseTab(int tab_id);
|
||||
void CreateChromeBrowser();
|
||||
void CreateContentBrowser();
|
||||
void ToggleMenuPopup();
|
||||
void CloseMenuPopup();
|
||||
void CreateMenuPopupBrowser();
|
||||
void PositionMenuPopup();
|
||||
void ToggleDevTools();
|
||||
void AdjustZoom(double delta);
|
||||
void FreshReload();
|
||||
void SetContentFullscreen(bool fullscreen);
|
||||
void ResizeBrowsers();
|
||||
void SendChromeState(const nebula::browser::NebulaTab& tab);
|
||||
void RecordSiteHistory(const std::string& url);
|
||||
void InjectSettingsHistory(CefRefPtr<CefBrowser> browser);
|
||||
void PersistSession() const;
|
||||
void MaybeFinishShutdown();
|
||||
|
||||
HINSTANCE instance_ = nullptr;
|
||||
std::string initial_url_;
|
||||
int show_command_ = SW_SHOWDEFAULT;
|
||||
bool closing_ = false;
|
||||
bool chrome_ready_ = false;
|
||||
bool content_fullscreen_ = false;
|
||||
|
||||
std::unique_ptr<nebula::window::NebulaWindow> window_;
|
||||
nebula::browser::TabManager tabs_;
|
||||
CefRefPtr<CefBrowser> chrome_browser_;
|
||||
CefRefPtr<CefBrowser> menu_popup_browser_;
|
||||
CefRefPtr<nebula::cef::NebulaBrowserClient> chrome_client_;
|
||||
CefRefPtr<nebula::cef::NebulaBrowserClient> content_client_;
|
||||
CefRefPtr<nebula::cef::NebulaBrowserClient> menu_popup_client_;
|
||||
std::unordered_set<std::string> insecure_warning_bypasses_;
|
||||
std::vector<std::string> site_history_;
|
||||
};
|
||||
|
||||
} // namespace nebula::app
|
||||
@@ -0,0 +1,93 @@
|
||||
#include "app/run.h"
|
||||
|
||||
#include "app/nebula_controller.h"
|
||||
#include "cef/nebula_app.h"
|
||||
#include "include/cef_app.h"
|
||||
#include "include/cef_command_line.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::app {
|
||||
namespace {
|
||||
|
||||
constexpr wchar_t kMainInstanceMutexName[] = L"Local\\NebulaBrowserMainInstance";
|
||||
|
||||
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);
|
||||
CefRefPtr<nebula::cef::NebulaApp> app(new nebula::cef::NebulaApp);
|
||||
|
||||
const int subprocess_exit_code = CefExecuteProcess(main_args, app, nullptr);
|
||||
if (subprocess_exit_code >= 0) {
|
||||
return subprocess_exit_code;
|
||||
}
|
||||
|
||||
ScopedHandle main_instance_mutex(CreateMutexW(nullptr, TRUE, kMainInstanceMutexName));
|
||||
if (main_instance_mutex.valid() && GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
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);
|
||||
}
|
||||
|
||||
if (!CefInitialize(main_args, settings, app, nullptr)) {
|
||||
return CefGetExitCode();
|
||||
}
|
||||
|
||||
CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
|
||||
command_line->InitFromString(GetCommandLineW());
|
||||
|
||||
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);
|
||||
const bool created = controller.Create();
|
||||
if (created) {
|
||||
CefRunMessageLoop();
|
||||
}
|
||||
|
||||
CefShutdown();
|
||||
return created ? 0 : 1;
|
||||
}
|
||||
|
||||
} // namespace nebula::app
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace nebula::app {
|
||||
|
||||
int RunNebula(HINSTANCE instance, int show_command);
|
||||
|
||||
} // namespace nebula::app
|
||||
@@ -0,0 +1,229 @@
|
||||
#include "browser/session_state.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <system_error>
|
||||
|
||||
#include "browser/url_utils.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::browser {
|
||||
namespace {
|
||||
|
||||
constexpr size_t kMaxRestoredTabs = 50;
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
std::optional<size_t> ReadUnsignedValue(const std::string& json, std::string_view key) {
|
||||
const size_t key_pos = json.find(key);
|
||||
if (key_pos == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
size_t colon = json.find(':', key_pos + key.size());
|
||||
if (colon == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
++colon;
|
||||
while (colon < json.size() && std::isspace(static_cast<unsigned char>(json[colon]))) {
|
||||
++colon;
|
||||
}
|
||||
|
||||
size_t end = colon;
|
||||
while (end < json.size() && std::isdigit(static_cast<unsigned char>(json[end]))) {
|
||||
++end;
|
||||
}
|
||||
|
||||
size_t value = 0;
|
||||
const auto result = std::from_chars(json.data() + colon, json.data() + end, value);
|
||||
if (result.ec != std::errc{} || result.ptr != json.data() + end) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
std::optional<std::string> ReadStringValue(const std::string& object, std::string_view key) {
|
||||
const size_t key_pos = object.find(key);
|
||||
if (key_pos == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
size_t colon = object.find(':', key_pos + key.size());
|
||||
if (colon == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
size_t quote = object.find('"', colon + 1);
|
||||
if (quote == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string value;
|
||||
for (size_t i = quote + 1; i < object.size(); ++i) {
|
||||
const char ch = object[i];
|
||||
if (ch == '"') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (ch != '\\') {
|
||||
value += ch;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (++i >= object.size()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
switch (object[i]) {
|
||||
case '"':
|
||||
case '\\':
|
||||
case '/':
|
||||
value += object[i];
|
||||
break;
|
||||
case 'b':
|
||||
value += '\b';
|
||||
break;
|
||||
case 'f':
|
||||
value += '\f';
|
||||
break;
|
||||
case 'n':
|
||||
value += '\n';
|
||||
break;
|
||||
case 'r':
|
||||
value += '\r';
|
||||
break;
|
||||
case 't':
|
||||
value += '\t';
|
||||
break;
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<PersistedTab> ReadTabs(const std::string& json) {
|
||||
std::vector<PersistedTab> tabs;
|
||||
const size_t tabs_pos = json.find("\"tabs\"");
|
||||
if (tabs_pos == std::string::npos) {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
const size_t array_start = json.find('[', tabs_pos);
|
||||
const size_t array_end = json.find(']', array_start == std::string::npos ? tabs_pos : array_start);
|
||||
if (array_start == std::string::npos || array_end == std::string::npos) {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
size_t cursor = array_start + 1;
|
||||
while (cursor < array_end && tabs.size() < kMaxRestoredTabs) {
|
||||
const size_t object_start = json.find('{', cursor);
|
||||
if (object_start == std::string::npos || object_start >= array_end) {
|
||||
break;
|
||||
}
|
||||
|
||||
const size_t object_end = json.find('}', object_start + 1);
|
||||
if (object_end == std::string::npos || object_end > array_end) {
|
||||
break;
|
||||
}
|
||||
|
||||
const std::string object = json.substr(object_start, object_end - object_start + 1);
|
||||
const auto url = ReadStringValue(object, "\"url\"");
|
||||
if (url && !url->empty()) {
|
||||
PersistedTab tab;
|
||||
tab.url = *url;
|
||||
if (const auto title = ReadStringValue(object, "\"title\""); title && !title->empty()) {
|
||||
tab.title = *title;
|
||||
}
|
||||
tabs.push_back(std::move(tab));
|
||||
}
|
||||
|
||||
cursor = object_end + 1;
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SessionState LoadSessionState() {
|
||||
SessionState state;
|
||||
const std::string json = ReadFile(nebula::ui::GetSessionStatePath());
|
||||
if (json.empty()) {
|
||||
return state;
|
||||
}
|
||||
|
||||
state.tabs = ReadTabs(json);
|
||||
if (const auto active_index = ReadUnsignedValue(json, "\"activeTabIndex\"")) {
|
||||
state.active_tab_index = *active_index;
|
||||
}
|
||||
|
||||
if (!state.tabs.empty()) {
|
||||
state.active_tab_index = std::min(state.active_tab_index, state.tabs.size() - 1);
|
||||
} else {
|
||||
state.active_tab_index = 0;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
void SaveSessionState(const std::vector<NebulaTab>& tabs, size_t active_tab_index) {
|
||||
const auto path = nebula::ui::GetSessionStatePath();
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::ostringstream json;
|
||||
json << "{\n \"activeTabIndex\": " << active_tab_index << ",\n \"tabs\": [\n";
|
||||
|
||||
bool wrote_tab = false;
|
||||
for (const auto& tab : tabs) {
|
||||
if (tab.url.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wrote_tab) {
|
||||
json << ",\n";
|
||||
}
|
||||
|
||||
json << " {\"url\": \"" << JsonEscape(tab.url)
|
||||
<< "\", \"title\": \"" << JsonEscape(tab.title) << "\"}";
|
||||
wrote_tab = true;
|
||||
}
|
||||
|
||||
json << "\n ]\n}\n";
|
||||
|
||||
std::filesystem::path temp_path = path;
|
||||
temp_path += L".tmp";
|
||||
{
|
||||
std::ofstream output(temp_path, std::ios::binary | std::ios::trunc);
|
||||
if (!output) {
|
||||
return;
|
||||
}
|
||||
output << json.str();
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(path, ec);
|
||||
ec.clear();
|
||||
std::filesystem::rename(temp_path, path, ec);
|
||||
}
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "browser/tab.h"
|
||||
|
||||
namespace nebula::browser {
|
||||
|
||||
struct PersistedTab {
|
||||
std::string url;
|
||||
std::string title = "New Tab";
|
||||
};
|
||||
|
||||
struct SessionState {
|
||||
std::vector<PersistedTab> tabs;
|
||||
size_t active_tab_index = 0;
|
||||
};
|
||||
|
||||
SessionState LoadSessionState();
|
||||
void SaveSessionState(const std::vector<NebulaTab>& tabs, size_t active_tab_index);
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,13 @@
|
||||
#include "browser/tab.h"
|
||||
|
||||
namespace nebula::browser {
|
||||
|
||||
bool NebulaTab::CanGoBack() const {
|
||||
return browser && browser->CanGoBack();
|
||||
}
|
||||
|
||||
bool NebulaTab::CanGoForward() const {
|
||||
return browser && browser->CanGoForward();
|
||||
}
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "include/cef_browser.h"
|
||||
|
||||
namespace nebula::browser {
|
||||
|
||||
struct NebulaTab {
|
||||
int id = 1;
|
||||
std::string url;
|
||||
std::string title = "New Tab";
|
||||
bool is_loading = false;
|
||||
double load_progress = 0.0;
|
||||
std::string favicon_url;
|
||||
CefRefPtr<CefBrowser> browser;
|
||||
|
||||
bool CanGoBack() const;
|
||||
bool CanGoForward() const;
|
||||
};
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,273 @@
|
||||
#include "browser/tab_manager.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "browser/url_utils.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::browser {
|
||||
|
||||
TabManager::TabManager(TabObserver* observer) : observer_(observer) {}
|
||||
|
||||
NebulaTab& TabManager::CreateInitialTab(std::string initial_url) {
|
||||
tabs_.clear();
|
||||
NebulaTab tab;
|
||||
tab.id = next_tab_id_++;
|
||||
tab.url = std::move(initial_url);
|
||||
tabs_.push_back(std::move(tab));
|
||||
active_tab_id_ = tabs_.front().id;
|
||||
Notify();
|
||||
return tabs_.front();
|
||||
}
|
||||
|
||||
NebulaTab& TabManager::CreateTab(std::string url) {
|
||||
NebulaTab tab;
|
||||
tab.id = next_tab_id_++;
|
||||
tab.url = std::move(url);
|
||||
tabs_.push_back(std::move(tab));
|
||||
active_tab_id_ = tabs_.back().id;
|
||||
Notify();
|
||||
return tabs_.back();
|
||||
}
|
||||
|
||||
void TabManager::RestoreTabs(const std::vector<PersistedTab>& tabs, size_t active_tab_index) {
|
||||
tabs_.clear();
|
||||
active_tab_id_ = 0;
|
||||
|
||||
for (const auto& restored_tab : tabs) {
|
||||
NebulaTab tab;
|
||||
tab.id = next_tab_id_++;
|
||||
tab.url = restored_tab.url.empty() ? nebula::ui::GetHomeUrl() : restored_tab.url;
|
||||
tab.title = restored_tab.title.empty() ? "New Tab" : restored_tab.title;
|
||||
tabs_.push_back(std::move(tab));
|
||||
}
|
||||
|
||||
if (tabs_.empty()) {
|
||||
CreateInitialTab(nebula::ui::GetHomeUrl());
|
||||
return;
|
||||
}
|
||||
|
||||
active_tab_index = std::min(active_tab_index, tabs_.size() - 1);
|
||||
active_tab_id_ = tabs_[active_tab_index].id;
|
||||
Notify();
|
||||
}
|
||||
|
||||
NebulaTab* TabManager::ActiveTab() {
|
||||
for (auto& tab : tabs_) {
|
||||
if (tab.id == active_tab_id_) {
|
||||
return &tab;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const NebulaTab* TabManager::ActiveTab() const {
|
||||
for (const auto& tab : tabs_) {
|
||||
if (tab.id == active_tab_id_) {
|
||||
return &tab;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::vector<NebulaTab>& TabManager::Tabs() const {
|
||||
return tabs_;
|
||||
}
|
||||
|
||||
size_t TabManager::ActiveTabIndex() const {
|
||||
for (size_t i = 0; i < tabs_.size(); ++i) {
|
||||
if (tabs_[i].id == active_tab_id_) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool TabManager::ActivateTab(int tab_id) {
|
||||
if (!FindTab(tab_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
active_tab_id_ = tab_id;
|
||||
Notify();
|
||||
return true;
|
||||
}
|
||||
|
||||
CefRefPtr<CefBrowser> TabManager::CloseTab(int tab_id) {
|
||||
for (auto it = tabs_.begin(); it != tabs_.end(); ++it) {
|
||||
if (it->id != tab_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CefRefPtr<CefBrowser> browser = it->browser;
|
||||
const bool was_active = it->id == active_tab_id_;
|
||||
const auto next_it = tabs_.erase(it);
|
||||
|
||||
if (tabs_.empty()) {
|
||||
active_tab_id_ = 0;
|
||||
} else if (was_active) {
|
||||
active_tab_id_ = next_it != tabs_.end() ? next_it->id : tabs_.back().id;
|
||||
}
|
||||
|
||||
Notify();
|
||||
return browser;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TabManager::SetActiveBrowser(CefRefPtr<CefBrowser> browser) {
|
||||
if (NebulaTab* tab = ActiveTab()) {
|
||||
tab->browser = browser;
|
||||
if (browser && tab->url.empty()) {
|
||||
tab->url = browser->GetMainFrame()->GetURL();
|
||||
}
|
||||
Notify();
|
||||
}
|
||||
}
|
||||
|
||||
bool TabManager::OwnsBrowser(CefRefPtr<CefBrowser> browser) const {
|
||||
if (!browser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& tab : tabs_) {
|
||||
if (tab.browser && tab.browser->IsSame(browser)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TabManager::HasOpenBrowsers() const {
|
||||
for (const auto& tab : tabs_) {
|
||||
if (tab.browser) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void TabManager::ClearBrowser(CefRefPtr<CefBrowser> browser) {
|
||||
if (NebulaTab* tab = FindTab(browser)) {
|
||||
tab->browser = nullptr;
|
||||
Notify();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::LoadURL(const std::string& input) {
|
||||
NebulaTab* tab = ActiveTab();
|
||||
if (!tab || !tab->browser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string target = NormalizeNavigationInput(input);
|
||||
if (target.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
tab->url = target;
|
||||
tab->favicon_url.clear();
|
||||
tab->browser->GetMainFrame()->LoadURL(nebula::ui::ResolveInternalUrl(target));
|
||||
Notify();
|
||||
}
|
||||
|
||||
void TabManager::GoBack() {
|
||||
NebulaTab* tab = ActiveTab();
|
||||
if (tab && tab->browser && tab->browser->CanGoBack()) {
|
||||
tab->browser->GoBack();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::GoForward() {
|
||||
NebulaTab* tab = ActiveTab();
|
||||
if (tab && tab->browser && tab->browser->CanGoForward()) {
|
||||
tab->browser->GoForward();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::Reload() {
|
||||
NebulaTab* tab = ActiveTab();
|
||||
if (tab && tab->browser) {
|
||||
tab->browser->Reload();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::StopLoad() {
|
||||
NebulaTab* tab = ActiveTab();
|
||||
if (tab && tab->browser) {
|
||||
tab->browser->StopLoad();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::UpdateURL(CefRefPtr<CefBrowser> browser, std::string url) {
|
||||
if (NebulaTab* tab = FindTab(browser)) {
|
||||
tab->url = std::move(url);
|
||||
Notify();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::UpdateTitle(CefRefPtr<CefBrowser> browser, std::string title) {
|
||||
if (NebulaTab* tab = FindTab(browser)) {
|
||||
tab->title = title.empty() ? "New Tab" : std::move(title);
|
||||
Notify();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::UpdateLoadingState(CefRefPtr<CefBrowser> browser, bool is_loading) {
|
||||
if (NebulaTab* tab = FindTab(browser)) {
|
||||
tab->is_loading = is_loading;
|
||||
if (is_loading) {
|
||||
tab->favicon_url.clear();
|
||||
}
|
||||
if (!is_loading) {
|
||||
tab->load_progress = 1.0;
|
||||
}
|
||||
Notify();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::UpdateLoadProgress(CefRefPtr<CefBrowser> browser, double progress) {
|
||||
if (NebulaTab* tab = FindTab(browser)) {
|
||||
tab->load_progress = progress;
|
||||
Notify();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::UpdateFavicon(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) {
|
||||
if (NebulaTab* tab = FindTab(browser)) {
|
||||
tab->favicon_url = urls.empty() ? std::string{} : urls.front();
|
||||
Notify();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::Notify() {
|
||||
const NebulaTab* tab = ActiveTab();
|
||||
if (observer_ && tab) {
|
||||
observer_->OnActiveTabChanged(*tab);
|
||||
}
|
||||
}
|
||||
|
||||
NebulaTab* TabManager::FindTab(int tab_id) {
|
||||
for (auto& tab : tabs_) {
|
||||
if (tab.id == tab_id) {
|
||||
return &tab;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
NebulaTab* TabManager::FindTab(CefRefPtr<CefBrowser> browser) {
|
||||
if (!browser) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (auto& tab : tabs_) {
|
||||
if (tab.browser && tab.browser->IsSame(browser)) {
|
||||
return &tab;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "browser/tab.h"
|
||||
#include "browser/session_state.h"
|
||||
|
||||
namespace nebula::browser {
|
||||
|
||||
class TabObserver {
|
||||
public:
|
||||
virtual ~TabObserver() = default;
|
||||
virtual void OnActiveTabChanged(const NebulaTab& tab) = 0;
|
||||
};
|
||||
|
||||
class TabManager {
|
||||
public:
|
||||
explicit TabManager(TabObserver* observer);
|
||||
|
||||
NebulaTab& CreateInitialTab(std::string initial_url);
|
||||
NebulaTab& CreateTab(std::string url);
|
||||
void RestoreTabs(const std::vector<PersistedTab>& tabs, size_t active_tab_index);
|
||||
NebulaTab* ActiveTab();
|
||||
const NebulaTab* ActiveTab() const;
|
||||
const std::vector<NebulaTab>& Tabs() const;
|
||||
size_t ActiveTabIndex() const;
|
||||
|
||||
bool ActivateTab(int tab_id);
|
||||
CefRefPtr<CefBrowser> CloseTab(int tab_id);
|
||||
void SetActiveBrowser(CefRefPtr<CefBrowser> browser);
|
||||
bool OwnsBrowser(CefRefPtr<CefBrowser> browser) const;
|
||||
void ClearBrowser(CefRefPtr<CefBrowser> browser);
|
||||
bool HasOpenBrowsers() const;
|
||||
|
||||
void LoadURL(const std::string& input);
|
||||
void GoBack();
|
||||
void GoForward();
|
||||
void Reload();
|
||||
void StopLoad();
|
||||
|
||||
void UpdateURL(CefRefPtr<CefBrowser> browser, std::string url);
|
||||
void UpdateTitle(CefRefPtr<CefBrowser> browser, std::string title);
|
||||
void UpdateLoadingState(CefRefPtr<CefBrowser> browser, bool is_loading);
|
||||
void UpdateLoadProgress(CefRefPtr<CefBrowser> browser, double progress);
|
||||
void UpdateFavicon(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls);
|
||||
|
||||
private:
|
||||
void Notify();
|
||||
NebulaTab* FindTab(int tab_id);
|
||||
NebulaTab* FindTab(CefRefPtr<CefBrowser> browser);
|
||||
|
||||
TabObserver* observer_ = nullptr;
|
||||
std::vector<NebulaTab> tabs_;
|
||||
int active_tab_id_ = 0;
|
||||
int next_tab_id_ = 1;
|
||||
};
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,115 @@
|
||||
#include "browser/url_utils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
namespace nebula::browser {
|
||||
namespace {
|
||||
|
||||
constexpr char kSearchUrl[] = "https://www.google.com/search?q=";
|
||||
|
||||
std::string Trim(std::string value) {
|
||||
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.front()))) {
|
||||
value.erase(value.begin());
|
||||
}
|
||||
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.back()))) {
|
||||
value.pop_back();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
bool StartsWithScheme(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value.starts_with("http://") ||
|
||||
value.starts_with("https://") ||
|
||||
value.starts_with("file:") ||
|
||||
value.starts_with("data:") ||
|
||||
value.starts_with("blob:") ||
|
||||
value.starts_with("chrome:") ||
|
||||
value.starts_with("nebula://");
|
||||
}
|
||||
|
||||
bool LooksLikeHostName(const std::string& value) {
|
||||
return value.find('.') != std::string::npos &&
|
||||
value.find_first_of(" \t\r\n") == std::string::npos;
|
||||
}
|
||||
|
||||
std::string UrlEncodeSearch(const std::string& value) {
|
||||
std::ostringstream encoded;
|
||||
encoded << std::hex << std::uppercase;
|
||||
|
||||
for (unsigned char ch : value) {
|
||||
if (std::isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '~') {
|
||||
encoded << static_cast<char>(ch);
|
||||
} else if (ch == ' ') {
|
||||
encoded << '+';
|
||||
} else {
|
||||
encoded << '%' << std::setw(2) << std::setfill('0') << static_cast<int>(ch);
|
||||
}
|
||||
}
|
||||
|
||||
return encoded.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string NormalizeNavigationInput(const std::string& input) {
|
||||
const std::string value = Trim(input);
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (StartsWithScheme(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (LooksLikeHostName(value)) {
|
||||
return "https://" + value;
|
||||
}
|
||||
|
||||
return std::string(kSearchUrl) + UrlEncodeSearch(value);
|
||||
}
|
||||
|
||||
std::string JsonEscape(const std::string& value) {
|
||||
std::ostringstream escaped;
|
||||
for (unsigned char ch : value) {
|
||||
switch (ch) {
|
||||
case '\\':
|
||||
escaped << "\\\\";
|
||||
break;
|
||||
case '"':
|
||||
escaped << "\\\"";
|
||||
break;
|
||||
case '\b':
|
||||
escaped << "\\b";
|
||||
break;
|
||||
case '\f':
|
||||
escaped << "\\f";
|
||||
break;
|
||||
case '\n':
|
||||
escaped << "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
escaped << "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
escaped << "\\t";
|
||||
break;
|
||||
default:
|
||||
if (ch < 0x20) {
|
||||
escaped << "\\u" << std::hex << std::uppercase << std::setw(4)
|
||||
<< std::setfill('0') << static_cast<int>(ch);
|
||||
} else {
|
||||
escaped << static_cast<char>(ch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return escaped.str();
|
||||
}
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace nebula::browser {
|
||||
|
||||
std::string NormalizeNavigationInput(const std::string& input);
|
||||
std::string JsonEscape(const std::string& value);
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,280 @@
|
||||
#include "cef/browser_client.h"
|
||||
|
||||
#include "include/cef_request.h"
|
||||
#include "include/wrapper/cef_helpers.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::cef {
|
||||
namespace {
|
||||
|
||||
constexpr char kChromeCommandMessage[] = "NebulaChromeCommand";
|
||||
|
||||
bool IsInsecureInterstitialFrame(CefRefPtr<CefFrame> frame) {
|
||||
if (!frame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://insecure");
|
||||
}
|
||||
|
||||
bool IsSettingsFrame(CefRefPtr<CefFrame> frame) {
|
||||
if (!frame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://settings");
|
||||
}
|
||||
|
||||
std::vector<std::string> ToStringVector(const std::vector<CefString>& values) {
|
||||
std::vector<std::string> result;
|
||||
result.reserve(values.size());
|
||||
for (const auto& value : values) {
|
||||
result.push_back(value.ToString());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NebulaBrowserClient::NebulaBrowserClient(BrowserRole role, BrowserClientDelegate* delegate)
|
||||
: role_(role), delegate_(delegate) {}
|
||||
|
||||
bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
CefProcessId source_process,
|
||||
CefRefPtr<CefProcessMessage> message) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
UNREFERENCED_PARAMETER(browser);
|
||||
UNREFERENCED_PARAMETER(source_process);
|
||||
|
||||
if (!message || message->GetName().ToString() != kChromeCommandMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (role_ != BrowserRole::Chrome && role_ != BrowserRole::MenuPopup &&
|
||||
role_ != BrowserRole::Content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CefRefPtr<CefListValue> args = message->GetArgumentList();
|
||||
const std::string command = args && args->GetSize() > 0 ? args->GetString(0).ToString() : "";
|
||||
const std::string payload = args && args->GetSize() > 1 ? args->GetString(1).ToString() : "";
|
||||
if (role_ == BrowserRole::Content) {
|
||||
const bool allowed_insecure_command =
|
||||
command == "navigate-insecure" && IsInsecureInterstitialFrame(frame);
|
||||
const bool allowed_settings_command =
|
||||
IsSettingsFrame(frame) && (command == "navigate" ||
|
||||
command == "clear-site-history" ||
|
||||
command == "clear-search-history");
|
||||
if (!allowed_insecure_command && !allowed_settings_command) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (delegate_ && !command.empty()) {
|
||||
delegate_->OnChromeCommand(command, payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnAddressChange(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
const CefString& url) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
if (role_ == BrowserRole::Content && delegate_ && frame && frame->IsMain()) {
|
||||
delegate_->OnContentAddressChanged(browser, url.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnTitleChange(CefRefPtr<CefBrowser> browser,
|
||||
const CefString& title) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
if (role_ == BrowserRole::Content && delegate_) {
|
||||
delegate_->OnContentTitleChanged(browser, title.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnFaviconURLChange(CefRefPtr<CefBrowser> browser,
|
||||
const std::vector<CefString>& icon_urls) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
if (role_ == BrowserRole::Content && delegate_) {
|
||||
delegate_->OnContentFaviconChanged(browser, ToStringVector(icon_urls));
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnFullscreenModeChange(CefRefPtr<CefBrowser> browser, bool fullscreen) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
if (role_ == BrowserRole::Content && delegate_) {
|
||||
delegate_->OnContentFullscreenChanged(browser, fullscreen);
|
||||
}
|
||||
}
|
||||
|
||||
bool NebulaBrowserClient::OnPreKeyEvent(CefRefPtr<CefBrowser> browser,
|
||||
const CefKeyEvent& event,
|
||||
CefEventHandle os_event,
|
||||
bool* is_keyboard_shortcut) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
UNREFERENCED_PARAMETER(os_event);
|
||||
|
||||
if (role_ == BrowserRole::Content &&
|
||||
event.type == KEYEVENT_RAWKEYDOWN &&
|
||||
(event.modifiers & EVENTFLAG_CONTROL_DOWN) != 0 &&
|
||||
event.windows_key_code == 'T') {
|
||||
if (is_keyboard_shortcut) {
|
||||
*is_keyboard_shortcut = true;
|
||||
}
|
||||
if (delegate_) {
|
||||
delegate_->OnPopupRequested(browser, nebula::ui::GetHomeUrl());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool NebulaBrowserClient::OnBeforePopup(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
int popup_id,
|
||||
const CefString& target_url,
|
||||
const CefString& target_frame_name,
|
||||
CefLifeSpanHandler::WindowOpenDisposition target_disposition,
|
||||
bool user_gesture,
|
||||
const CefPopupFeatures& popupFeatures,
|
||||
CefWindowInfo& windowInfo,
|
||||
CefRefPtr<CefClient>& client,
|
||||
CefBrowserSettings& settings,
|
||||
CefRefPtr<CefDictionaryValue>& extra_info,
|
||||
bool* no_javascript_access) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
UNREFERENCED_PARAMETER(frame);
|
||||
UNREFERENCED_PARAMETER(popup_id);
|
||||
UNREFERENCED_PARAMETER(target_frame_name);
|
||||
UNREFERENCED_PARAMETER(target_disposition);
|
||||
UNREFERENCED_PARAMETER(user_gesture);
|
||||
UNREFERENCED_PARAMETER(popupFeatures);
|
||||
UNREFERENCED_PARAMETER(windowInfo);
|
||||
UNREFERENCED_PARAMETER(client);
|
||||
UNREFERENCED_PARAMETER(settings);
|
||||
UNREFERENCED_PARAMETER(extra_info);
|
||||
UNREFERENCED_PARAMETER(no_javascript_access);
|
||||
|
||||
if (role_ == BrowserRole::Content && delegate_) {
|
||||
delegate_->OnPopupRequested(browser, target_url.ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnAfterCreated(CefRefPtr<CefBrowser> browser) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
if (delegate_) {
|
||||
delegate_->OnBrowserCreated(role_, browser);
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnBeforeClose(CefRefPtr<CefBrowser> browser) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
if (delegate_) {
|
||||
delegate_->OnBrowserClosing(role_, browser);
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnLoadingStateChange(CefRefPtr<CefBrowser> browser,
|
||||
bool isLoading,
|
||||
bool canGoBack,
|
||||
bool canGoForward) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
UNREFERENCED_PARAMETER(canGoBack);
|
||||
UNREFERENCED_PARAMETER(canGoForward);
|
||||
|
||||
if (role_ == BrowserRole::Content && delegate_) {
|
||||
delegate_->OnContentLoadingStateChanged(browser, isLoading);
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnLoadStart(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
TransitionType transition_type) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
UNREFERENCED_PARAMETER(transition_type);
|
||||
|
||||
if (role_ == BrowserRole::Content && delegate_ && frame && frame->IsMain()) {
|
||||
delegate_->OnContentLoadProgressChanged(browser, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnLoadEnd(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
int httpStatusCode) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
|
||||
if (role_ == BrowserRole::Content && delegate_ && frame && frame->IsMain()) {
|
||||
if (httpStatusCode == 404) {
|
||||
frame->LoadURL(nebula::ui::ResolveInternalUrl(
|
||||
nebula::ui::GetNotFoundUrl(frame->GetURL().ToString())));
|
||||
return;
|
||||
}
|
||||
|
||||
delegate_->OnContentLoadProgressChanged(browser, 1.0);
|
||||
delegate_->OnContentLoadFinished(browser, frame->GetURL().ToString());
|
||||
}
|
||||
}
|
||||
|
||||
bool NebulaBrowserClient::OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
CefRefPtr<CefRequest> request,
|
||||
bool user_gesture,
|
||||
bool is_redirect) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
UNREFERENCED_PARAMETER(browser);
|
||||
UNREFERENCED_PARAMETER(user_gesture);
|
||||
UNREFERENCED_PARAMETER(is_redirect);
|
||||
|
||||
if (role_ == BrowserRole::Content && frame && frame->IsMain() && request) {
|
||||
const std::string url = request->GetURL().ToString();
|
||||
if (nebula::ui::IsChromiumNewTabUrl(url)) {
|
||||
frame->LoadURL(nebula::ui::ResolveInternalUrl(nebula::ui::GetHomeUrl()));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nebula::ui::IsNebulaInternalUrl(url)) {
|
||||
frame->LoadURL(nebula::ui::ResolveInternalUrl(url));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nebula::ui::IsHttpUrl(url) &&
|
||||
(!delegate_ || !delegate_->ShouldBypassInsecureWarning(browser, url))) {
|
||||
frame->LoadURL(nebula::ui::ResolveInternalUrl(
|
||||
nebula::ui::GetInsecureWarningUrl(url)));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool NebulaBrowserClient::OnShowPermissionPrompt(
|
||||
CefRefPtr<CefBrowser> browser,
|
||||
uint64_t prompt_id,
|
||||
const CefString& requesting_origin,
|
||||
uint32_t requested_permissions,
|
||||
CefRefPtr<CefPermissionPromptCallback> callback) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
UNREFERENCED_PARAMETER(prompt_id);
|
||||
UNREFERENCED_PARAMETER(requesting_origin);
|
||||
|
||||
if (role_ == BrowserRole::Content &&
|
||||
(requested_permissions & CEF_PERMISSION_TYPE_GEOLOCATION) != 0 &&
|
||||
browser && callback &&
|
||||
nebula::ui::IsInternalHomeUrl(browser->GetMainFrame()->GetURL().ToString())) {
|
||||
callback->Continue(CEF_PERMISSION_RESULT_ACCEPT);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace nebula::cef
|
||||
@@ -0,0 +1,122 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "include/cef_client.h"
|
||||
#include "include/cef_display_handler.h"
|
||||
#include "include/cef_keyboard_handler.h"
|
||||
#include "include/cef_life_span_handler.h"
|
||||
#include "include/cef_load_handler.h"
|
||||
#include "include/cef_permission_handler.h"
|
||||
#include "include/cef_request_handler.h"
|
||||
|
||||
namespace nebula::cef {
|
||||
|
||||
enum class BrowserRole {
|
||||
Chrome,
|
||||
Content,
|
||||
MenuPopup,
|
||||
};
|
||||
|
||||
class BrowserClientDelegate {
|
||||
public:
|
||||
virtual ~BrowserClientDelegate() = default;
|
||||
virtual void OnBrowserCreated(BrowserRole role, CefRefPtr<CefBrowser> browser) = 0;
|
||||
virtual void OnBrowserClosing(BrowserRole role, CefRefPtr<CefBrowser> browser) = 0;
|
||||
virtual void OnChromeCommand(const std::string& command, const std::string& payload) = 0;
|
||||
virtual void OnContentAddressChanged(CefRefPtr<CefBrowser> browser, const std::string& url) = 0;
|
||||
virtual void OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) = 0;
|
||||
virtual void OnContentLoadingStateChanged(CefRefPtr<CefBrowser> browser, bool is_loading) = 0;
|
||||
virtual void OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browser, double progress) = 0;
|
||||
virtual void OnContentLoadFinished(CefRefPtr<CefBrowser> browser, const std::string& url) = 0;
|
||||
virtual void OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) = 0;
|
||||
virtual void OnContentFullscreenChanged(CefRefPtr<CefBrowser> browser, bool fullscreen) = 0;
|
||||
virtual void OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) = 0;
|
||||
virtual bool ShouldBypassInsecureWarning(CefRefPtr<CefBrowser> browser, const std::string& target_url) = 0;
|
||||
};
|
||||
|
||||
class NebulaBrowserClient final : public CefClient,
|
||||
public CefDisplayHandler,
|
||||
public CefKeyboardHandler,
|
||||
public CefLifeSpanHandler,
|
||||
public CefLoadHandler,
|
||||
public CefPermissionHandler,
|
||||
public CefRequestHandler {
|
||||
public:
|
||||
NebulaBrowserClient(BrowserRole role, BrowserClientDelegate* delegate);
|
||||
|
||||
CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
|
||||
CefRefPtr<CefKeyboardHandler> GetKeyboardHandler() override { return this; }
|
||||
CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
|
||||
CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
|
||||
CefRefPtr<CefPermissionHandler> GetPermissionHandler() override { return this; }
|
||||
CefRefPtr<CefRequestHandler> GetRequestHandler() override { return this; }
|
||||
|
||||
bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
CefProcessId source_process,
|
||||
CefRefPtr<CefProcessMessage> message) override;
|
||||
|
||||
void OnAddressChange(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
const CefString& url) override;
|
||||
void OnTitleChange(CefRefPtr<CefBrowser> browser,
|
||||
const CefString& title) override;
|
||||
void OnFaviconURLChange(CefRefPtr<CefBrowser> browser,
|
||||
const std::vector<CefString>& icon_urls) override;
|
||||
void OnFullscreenModeChange(CefRefPtr<CefBrowser> browser, bool fullscreen) override;
|
||||
|
||||
bool OnPreKeyEvent(CefRefPtr<CefBrowser> browser,
|
||||
const CefKeyEvent& event,
|
||||
CefEventHandle os_event,
|
||||
bool* is_keyboard_shortcut) override;
|
||||
|
||||
bool OnBeforePopup(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
int popup_id,
|
||||
const CefString& target_url,
|
||||
const CefString& target_frame_name,
|
||||
CefLifeSpanHandler::WindowOpenDisposition target_disposition,
|
||||
bool user_gesture,
|
||||
const CefPopupFeatures& popupFeatures,
|
||||
CefWindowInfo& windowInfo,
|
||||
CefRefPtr<CefClient>& client,
|
||||
CefBrowserSettings& settings,
|
||||
CefRefPtr<CefDictionaryValue>& extra_info,
|
||||
bool* no_javascript_access) override;
|
||||
|
||||
void OnAfterCreated(CefRefPtr<CefBrowser> browser) override;
|
||||
void OnBeforeClose(CefRefPtr<CefBrowser> browser) override;
|
||||
|
||||
void OnLoadingStateChange(CefRefPtr<CefBrowser> browser,
|
||||
bool isLoading,
|
||||
bool canGoBack,
|
||||
bool canGoForward) override;
|
||||
void OnLoadStart(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
TransitionType transition_type) override;
|
||||
void OnLoadEnd(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
int httpStatusCode) override;
|
||||
|
||||
bool OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
CefRefPtr<CefRequest> request,
|
||||
bool user_gesture,
|
||||
bool is_redirect) override;
|
||||
|
||||
bool OnShowPermissionPrompt(CefRefPtr<CefBrowser> browser,
|
||||
uint64_t prompt_id,
|
||||
const CefString& requesting_origin,
|
||||
uint32_t requested_permissions,
|
||||
CefRefPtr<CefPermissionPromptCallback> callback) override;
|
||||
|
||||
private:
|
||||
BrowserRole role_;
|
||||
BrowserClientDelegate* delegate_ = nullptr;
|
||||
|
||||
IMPLEMENT_REFCOUNTING(NebulaBrowserClient);
|
||||
};
|
||||
|
||||
} // namespace nebula::cef
|
||||
@@ -0,0 +1,111 @@
|
||||
#include "cef/nebula_app.h"
|
||||
|
||||
#include "include/cef_process_message.h"
|
||||
#include "include/wrapper/cef_helpers.h"
|
||||
|
||||
namespace nebula::cef {
|
||||
namespace {
|
||||
|
||||
constexpr char kChromeCommandMessage[] = "NebulaChromeCommand";
|
||||
|
||||
class NativeBridgeHandler final : public CefV8Handler {
|
||||
public:
|
||||
bool Execute(const CefString& name,
|
||||
CefRefPtr<CefV8Value> object,
|
||||
const CefV8ValueList& arguments,
|
||||
CefRefPtr<CefV8Value>& retval,
|
||||
CefString& exception) override {
|
||||
UNREFERENCED_PARAMETER(object);
|
||||
UNREFERENCED_PARAMETER(retval);
|
||||
|
||||
if (name != "postMessage" && name != "sendToHost" && name != "send") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (arguments.empty() || !arguments[0]->IsString()) {
|
||||
exception = "nebulaNative.postMessage requires a command string.";
|
||||
return true;
|
||||
}
|
||||
|
||||
CefRefPtr<CefV8Context> context = CefV8Context::GetCurrentContext();
|
||||
CefRefPtr<CefBrowser> browser = context ? context->GetBrowser() : nullptr;
|
||||
CefRefPtr<CefFrame> frame = context ? context->GetFrame() : nullptr;
|
||||
if (!browser || !frame) {
|
||||
exception = "No CEF frame is available for native messaging.";
|
||||
return true;
|
||||
}
|
||||
|
||||
CefRefPtr<CefProcessMessage> message = CefProcessMessage::Create(kChromeCommandMessage);
|
||||
CefRefPtr<CefListValue> args = message->GetArgumentList();
|
||||
args->SetString(0, arguments[0]->GetStringValue());
|
||||
args->SetString(1, arguments.size() > 1 && arguments[1]->IsString()
|
||||
? arguments[1]->GetStringValue()
|
||||
: CefString());
|
||||
|
||||
frame->SendProcessMessage(PID_BROWSER, message);
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
IMPLEMENT_REFCOUNTING(NativeBridgeHandler);
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void NebulaApp::OnBeforeCommandLineProcessing(const CefString& process_type,
|
||||
CefRefPtr<CefCommandLine> command_line) {
|
||||
UNREFERENCED_PARAMETER(process_type);
|
||||
|
||||
// The bundled UI is loaded from file:// and uses ES modules.
|
||||
command_line->AppendSwitch("allow-file-access-from-files");
|
||||
|
||||
// CefSettings.no_sandbox disables the browser-level sandbox, but Chromium
|
||||
// still attempts to bring up a separate GPU sandbox inside the GPU process.
|
||||
// Without the host-side sandbox plumbing this fails with STATUS_BREAKPOINT
|
||||
// (-2147483645) immediately on startup, which is exactly what the GPU
|
||||
// diagnostics page was showing - the GPU process crashed three times and
|
||||
// Chromium then fell back to software rendering. Disabling the GPU sandbox
|
||||
// matches the rest of our no_sandbox configuration and lets the GPU
|
||||
// process initialize.
|
||||
command_line->AppendSwitch("no-sandbox");
|
||||
command_line->AppendSwitch("disable-gpu-sandbox");
|
||||
command_line->AppendSwitch("in-process-gpu");
|
||||
|
||||
// Avoid Chromium's conservative GPU blocklist, but let Chromium choose the
|
||||
// safest graphics backend for this machine. Forcing raster/zero-copy paths
|
||||
// can prevent WebGL shared contexts from initializing on some drivers.
|
||||
command_line->AppendSwitch("ignore-gpu-blocklist");
|
||||
command_line->AppendSwitch("enable-accelerated-video-decode");
|
||||
command_line->AppendSwitchWithValue("use-gl", "angle");
|
||||
command_line->AppendSwitchWithValue("use-angle", "d3d11");
|
||||
}
|
||||
|
||||
void NebulaApp::OnContextCreated(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
CefRefPtr<CefV8Context> context) {
|
||||
CEF_REQUIRE_RENDERER_THREAD();
|
||||
UNREFERENCED_PARAMETER(browser);
|
||||
UNREFERENCED_PARAMETER(frame);
|
||||
|
||||
CefRefPtr<CefV8Value> global = context->GetGlobal();
|
||||
CefRefPtr<NativeBridgeHandler> handler = new NativeBridgeHandler();
|
||||
CefRefPtr<CefV8Value> native = CefV8Value::CreateObject(nullptr, nullptr);
|
||||
native->SetValue(
|
||||
"postMessage",
|
||||
CefV8Value::CreateFunction("postMessage", handler),
|
||||
V8_PROPERTY_ATTRIBUTE_NONE);
|
||||
global->SetValue("nebulaNative", native, V8_PROPERTY_ATTRIBUTE_READONLY);
|
||||
|
||||
CefRefPtr<CefV8Value> electron_api = CefV8Value::CreateObject(nullptr, nullptr);
|
||||
electron_api->SetValue(
|
||||
"sendToHost",
|
||||
CefV8Value::CreateFunction("sendToHost", handler),
|
||||
V8_PROPERTY_ATTRIBUTE_NONE);
|
||||
electron_api->SetValue(
|
||||
"send",
|
||||
CefV8Value::CreateFunction("send", handler),
|
||||
V8_PROPERTY_ATTRIBUTE_NONE);
|
||||
global->SetValue("electronAPI", electron_api, V8_PROPERTY_ATTRIBUTE_READONLY);
|
||||
}
|
||||
|
||||
} // namespace nebula::cef
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "include/cef_app.h"
|
||||
#include "include/cef_render_process_handler.h"
|
||||
#include "include/cef_v8.h"
|
||||
|
||||
namespace nebula::cef {
|
||||
|
||||
class NebulaApp final : public CefApp,
|
||||
public CefRenderProcessHandler {
|
||||
public:
|
||||
CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler() override { return this; }
|
||||
|
||||
void OnBeforeCommandLineProcessing(const CefString& process_type,
|
||||
CefRefPtr<CefCommandLine> command_line) override;
|
||||
|
||||
void OnContextCreated(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
CefRefPtr<CefV8Context> context) override;
|
||||
|
||||
private:
|
||||
IMPLEMENT_REFCOUNTING(NebulaApp);
|
||||
};
|
||||
|
||||
} // namespace nebula::cef
|
||||
@@ -0,0 +1,304 @@
|
||||
#include "ui/paths.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <string_view>
|
||||
|
||||
namespace nebula::ui {
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view kNebulaScheme = "nebula://";
|
||||
constexpr std::wstring_view kInternalFallbackPage = L"404.html";
|
||||
|
||||
struct InternalPage {
|
||||
std::string_view slug;
|
||||
std::wstring_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"},
|
||||
};
|
||||
|
||||
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) {
|
||||
url.resize(split);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
std::string GetUrlDecoration(const std::string& url) {
|
||||
const size_t split = url.find_first_of("?#");
|
||||
return split == std::string::npos ? std::string{} : url.substr(split);
|
||||
}
|
||||
|
||||
std::string ToLowerAscii(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string PageFileUrl(std::wstring_view page_name) {
|
||||
const auto path = GetUiPagePath(std::wstring(page_name));
|
||||
return path.empty() ? std::string{} : FilePathToUrl(path);
|
||||
}
|
||||
|
||||
std::string PercentEncode(const std::string& value) {
|
||||
constexpr char kHex[] = "0123456789ABCDEF";
|
||||
std::string encoded;
|
||||
encoded.reserve(value.size());
|
||||
for (unsigned char ch : value) {
|
||||
if (std::isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '~') {
|
||||
encoded += static_cast<char>(ch);
|
||||
} else {
|
||||
encoded += '%';
|
||||
encoded += kHex[ch >> 4];
|
||||
encoded += kHex[ch & 0x0F];
|
||||
}
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
std::string InternalPageName(const std::string& url) {
|
||||
std::string target = ToLowerAscii(GetUrlWithoutDecoration(url));
|
||||
if (!target.starts_with(kNebulaScheme)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
target.erase(0, kNebulaScheme.size());
|
||||
while (!target.empty() && target.front() == '/') {
|
||||
target.erase(target.begin());
|
||||
}
|
||||
while (!target.empty() && target.back() == '/') {
|
||||
target.pop_back();
|
||||
}
|
||||
return target.empty() ? "home" : target;
|
||||
}
|
||||
|
||||
std::string InternalUrlForSlug(std::string_view slug) {
|
||||
return std::string(kNebulaScheme) + std::string(slug);
|
||||
}
|
||||
|
||||
const InternalPage* FindInternalPageBySlug(std::string_view slug) {
|
||||
for (const auto& page : kInternalPages) {
|
||||
if (page.slug == slug) {
|
||||
return &page;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const InternalPage* FindInternalPageByFileUrl(const std::string& url) {
|
||||
const std::string base_url = GetUrlWithoutDecoration(url);
|
||||
for (const auto& page : kInternalPages) {
|
||||
if (PageFileUrl(page.file_name) == base_url) {
|
||||
return &page;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // 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();
|
||||
}
|
||||
|
||||
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.
|
||||
root = GetExecutableDirectory();
|
||||
}
|
||||
|
||||
if (root.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::filesystem::path user_data = root / L"Nebula" / L"User Data";
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(user_data, ec);
|
||||
return user_data;
|
||||
}
|
||||
|
||||
std::filesystem::path GetCacheDirectory() {
|
||||
auto user_data = GetUserDataDirectory();
|
||||
if (user_data.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::filesystem::path cache = user_data / L"Cache";
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(cache, ec);
|
||||
return cache;
|
||||
}
|
||||
|
||||
std::filesystem::path GetSessionStatePath() {
|
||||
auto user_data = GetUserDataDirectory();
|
||||
return user_data.empty() ? std::filesystem::path{} : user_data / L"session_state.json";
|
||||
}
|
||||
|
||||
std::filesystem::path GetUiPagePath(const std::wstring& page_name) {
|
||||
const auto exe_dir = GetExecutableDirectory();
|
||||
if (exe_dir.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return exe_dir / L"ui" / L"pages" / page_name;
|
||||
}
|
||||
|
||||
std::string FilePathToUrl(std::filesystem::path path) {
|
||||
std::string value = WideToUtf8(path.wstring());
|
||||
for (char& ch : value) {
|
||||
if (ch == '\\') {
|
||||
ch = '/';
|
||||
}
|
||||
}
|
||||
|
||||
std::string encoded;
|
||||
encoded.reserve(value.size());
|
||||
for (char ch : value) {
|
||||
encoded += ch == ' ' ? "%20" : std::string(1, ch);
|
||||
}
|
||||
return "file:///" + encoded;
|
||||
}
|
||||
|
||||
std::string GetChromeUrl() {
|
||||
const auto path = GetUiPagePath(L"chrome.html");
|
||||
const std::string fallback = PageFileUrl(L"home.html");
|
||||
return path.empty() ? (fallback.empty() ? "https://www.google.com" : fallback) : FilePathToUrl(path);
|
||||
}
|
||||
|
||||
std::string GetHomeUrl() {
|
||||
return InternalUrlForSlug("home");
|
||||
}
|
||||
|
||||
std::string GetSettingsUrl() {
|
||||
return InternalUrlForSlug("settings");
|
||||
}
|
||||
|
||||
std::string GetDownloadsUrl() {
|
||||
return InternalUrlForSlug("downloads");
|
||||
}
|
||||
|
||||
std::string GetBigPictureUrl() {
|
||||
return InternalUrlForSlug("bigpicture");
|
||||
}
|
||||
|
||||
std::string GetGpuDiagnosticsUrl() {
|
||||
return InternalUrlForSlug("gpu-diagnostics");
|
||||
}
|
||||
|
||||
std::string GetMenuPopupUrl() {
|
||||
const auto path = GetUiPagePath(L"menu-popup.html");
|
||||
const std::string fallback = PageFileUrl(L"home.html");
|
||||
return path.empty() ? (fallback.empty() ? "https://www.google.com" : fallback) : FilePathToUrl(path);
|
||||
}
|
||||
|
||||
std::string GetInsecureWarningUrl(const std::string& target_url) {
|
||||
return InternalUrlForSlug("insecure") + "?target=" + PercentEncode(target_url);
|
||||
}
|
||||
|
||||
std::string GetNotFoundUrl(const std::string& target_url) {
|
||||
return InternalUrlForSlug("404") + "?url=" + PercentEncode(target_url);
|
||||
}
|
||||
|
||||
std::string ResolveInternalUrl(const std::string& url) {
|
||||
const std::string page_name = InternalPageName(url);
|
||||
if (page_name.empty()) {
|
||||
return url;
|
||||
}
|
||||
|
||||
if (const auto* page = FindInternalPageBySlug(page_name)) {
|
||||
const std::string file_url = PageFileUrl(page->file_name);
|
||||
return file_url.empty() ? url : file_url + GetUrlDecoration(url);
|
||||
}
|
||||
|
||||
const std::string fallback_url = PageFileUrl(kInternalFallbackPage);
|
||||
return fallback_url.empty() ? url : fallback_url + "?url=" + PercentEncode(url);
|
||||
}
|
||||
|
||||
std::string ToInternalUrl(const std::string& url) {
|
||||
const std::string page_name = InternalPageName(url);
|
||||
if (!page_name.empty()) {
|
||||
if (const auto* page = FindInternalPageBySlug(page_name)) {
|
||||
return InternalUrlForSlug(page->slug) + GetUrlDecoration(url);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
if (const auto* page = FindInternalPageByFileUrl(url)) {
|
||||
return InternalUrlForSlug(page->slug) + GetUrlDecoration(url);
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
bool IsInternalHomeUrl(const std::string& url) {
|
||||
return GetUrlWithoutDecoration(ToInternalUrl(url)) == GetHomeUrl();
|
||||
}
|
||||
|
||||
bool IsNebulaInternalUrl(const std::string& url) {
|
||||
return ToLowerAscii(url).starts_with(kNebulaScheme);
|
||||
}
|
||||
|
||||
bool IsHttpUrl(const std::string& url) {
|
||||
return ToLowerAscii(url).starts_with("http://");
|
||||
}
|
||||
|
||||
bool IsChromiumNewTabUrl(const std::string& url) {
|
||||
const std::string target = ToLowerAscii(GetUrlWithoutDecoration(url));
|
||||
return target == "about:blank" ||
|
||||
target == "chrome://newtab" ||
|
||||
target == "chrome://newtab/" ||
|
||||
target == "chrome://new-tab-page" ||
|
||||
target == "chrome://new-tab-page/" ||
|
||||
target == "chrome-search://local-ntp/local-ntp.html";
|
||||
}
|
||||
|
||||
bool IsEmptyOrChromiumNewTabUrl(const std::string& url) {
|
||||
return url.empty() || IsChromiumNewTabUrl(url);
|
||||
}
|
||||
|
||||
} // namespace nebula::ui
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace nebula::ui {
|
||||
|
||||
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::string FilePathToUrl(std::filesystem::path path);
|
||||
std::string GetChromeUrl();
|
||||
std::string GetHomeUrl();
|
||||
std::string GetSettingsUrl();
|
||||
std::string GetDownloadsUrl();
|
||||
std::string GetBigPictureUrl();
|
||||
std::string GetGpuDiagnosticsUrl();
|
||||
std::string GetMenuPopupUrl();
|
||||
std::string GetInsecureWarningUrl(const std::string& target_url);
|
||||
std::string GetNotFoundUrl(const std::string& target_url);
|
||||
std::string ResolveInternalUrl(const std::string& url);
|
||||
std::string ToInternalUrl(const std::string& url);
|
||||
|
||||
bool IsInternalHomeUrl(const std::string& url);
|
||||
bool IsNebulaInternalUrl(const std::string& url);
|
||||
bool IsHttpUrl(const std::string& url);
|
||||
bool IsChromiumNewTabUrl(const std::string& url);
|
||||
bool IsEmptyOrChromiumNewTabUrl(const std::string& url);
|
||||
|
||||
} // namespace nebula::ui
|
||||
@@ -0,0 +1,538 @@
|
||||
#include "window/nebula_window.h"
|
||||
|
||||
#include <dwmapi.h>
|
||||
#include <windowsx.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace nebula::window {
|
||||
namespace {
|
||||
|
||||
constexpr wchar_t kWindowClassName[] = L"NebulaBrowserWindow";
|
||||
constexpr wchar_t kWindowTitle[] = L"Nebula Browser";
|
||||
constexpr wchar_t kChildFrameHitTestOldProcProp[] = L"NebulaChildFrameHitTestOldProc";
|
||||
constexpr wchar_t kChildFrameHitTestParentProp[] = L"NebulaChildFrameHitTestParent";
|
||||
constexpr int kTitleRowHeightDip = 42;
|
||||
constexpr int kWindowControlWidthDip = 46;
|
||||
constexpr int kWindowControlCount = 3;
|
||||
constexpr COLORREF kNoWindowBorderColor = 0xFFFFFFFE;
|
||||
|
||||
RECT GetWorkArea() {
|
||||
RECT work_area = {};
|
||||
SystemParametersInfoW(SPI_GETWORKAREA, 0, &work_area, 0);
|
||||
return work_area;
|
||||
}
|
||||
|
||||
RECT GetMonitorWorkArea(HWND hwnd) {
|
||||
MONITORINFO monitor_info = {};
|
||||
monitor_info.cbSize = sizeof(monitor_info);
|
||||
|
||||
const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
|
||||
if (monitor && GetMonitorInfoW(monitor, &monitor_info)) {
|
||||
return monitor_info.rcWork;
|
||||
}
|
||||
|
||||
return GetWorkArea();
|
||||
}
|
||||
|
||||
RECT GetMonitorArea(HWND hwnd) {
|
||||
MONITORINFO monitor_info = {};
|
||||
monitor_info.cbSize = sizeof(monitor_info);
|
||||
|
||||
const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
|
||||
if (monitor && GetMonitorInfoW(monitor, &monitor_info)) {
|
||||
return monitor_info.rcMonitor;
|
||||
}
|
||||
|
||||
return GetWorkArea();
|
||||
}
|
||||
|
||||
bool IsResizeHit(LRESULT hit) {
|
||||
return hit == HTLEFT || hit == HTRIGHT || hit == HTTOP || hit == HTBOTTOM ||
|
||||
hit == HTTOPLEFT || hit == HTTOPRIGHT || hit == HTBOTTOMLEFT || hit == HTBOTTOMRIGHT;
|
||||
}
|
||||
|
||||
HCURSOR CursorForResizeHit(LRESULT hit) {
|
||||
switch (hit) {
|
||||
case HTLEFT:
|
||||
case HTRIGHT:
|
||||
return LoadCursor(nullptr, IDC_SIZEWE);
|
||||
case HTTOP:
|
||||
case HTBOTTOM:
|
||||
return LoadCursor(nullptr, IDC_SIZENS);
|
||||
case HTTOPLEFT:
|
||||
case HTBOTTOMRIGHT:
|
||||
return LoadCursor(nullptr, IDC_SIZENWSE);
|
||||
case HTTOPRIGHT:
|
||||
case HTBOTTOMLEFT:
|
||||
return LoadCursor(nullptr, IDC_SIZENESW);
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool SetResizeCursor(LRESULT hit) {
|
||||
HCURSOR cursor = CursorForResizeHit(hit);
|
||||
if (!cursor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SetCursor(cursor);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ApplyWindowFrameStyle(HWND hwnd) {
|
||||
const BOOL dark_mode = TRUE;
|
||||
const DWM_WINDOW_CORNER_PREFERENCE corner_preference = DWMWCP_ROUND;
|
||||
DwmSetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
&dark_mode,
|
||||
sizeof(dark_mode));
|
||||
|
||||
DwmSetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_WINDOW_CORNER_PREFERENCE,
|
||||
&corner_preference,
|
||||
sizeof(corner_preference));
|
||||
|
||||
DwmSetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_BORDER_COLOR,
|
||||
&kNoWindowBorderColor,
|
||||
sizeof(kNoWindowBorderColor));
|
||||
}
|
||||
|
||||
} // 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,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaWindow::ToggleMaximize() {
|
||||
if (!hwnd_ || fullscreen_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ShowWindow(hwnd_, IsZoomed(hwnd_) ? SW_RESTORE : SW_MAXIMIZE);
|
||||
}
|
||||
|
||||
void NebulaWindow::SetFullscreen(bool fullscreen) {
|
||||
if (!hwnd_ || fullscreen_ == fullscreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
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_);
|
||||
}
|
||||
|
||||
NotifyResize();
|
||||
}
|
||||
|
||||
void NebulaWindow::Close() {
|
||||
if (hwnd_) {
|
||||
SendMessageW(hwnd_, WM_CLOSE, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaWindow::BeginDrag() {
|
||||
if (!hwnd_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReleaseCapture();
|
||||
SendMessageW(hwnd_, WM_NCLBUTTONDOWN, HTCAPTION, 0);
|
||||
}
|
||||
|
||||
void NebulaWindow::SetTitle(const std::wstring& title) {
|
||||
if (hwnd_) {
|
||||
SetWindowTextW(hwnd_, title.empty() ? kWindowTitle : title.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaWindow::EnableFrameHitTest(HWND child) const {
|
||||
if (!hwnd_ || !child) {
|
||||
return;
|
||||
}
|
||||
|
||||
EnableFrameHitTestForWindow(child);
|
||||
EnumChildWindows(child, &NebulaWindow::EnableFrameHitTestForDescendant, reinterpret_cast<LPARAM>(this));
|
||||
}
|
||||
|
||||
LRESULT CALLBACK NebulaWindow::StaticWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
|
||||
NebulaWindow* self = nullptr;
|
||||
|
||||
if (message == WM_NCCREATE) {
|
||||
auto* create = reinterpret_cast<CREATESTRUCTW*>(lparam);
|
||||
self = static_cast<NebulaWindow*>(create->lpCreateParams);
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(self));
|
||||
self->hwnd_ = hwnd;
|
||||
} else {
|
||||
self = reinterpret_cast<NebulaWindow*>(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) {
|
||||
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;
|
||||
if (self) {
|
||||
const LRESULT hit = self->HitTest(lparam);
|
||||
if (IsResizeHit(hit)) {
|
||||
return hit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message == WM_SETCURSOR) {
|
||||
const auto parent = reinterpret_cast<HWND>(GetPropW(hwnd, kChildFrameHitTestParentProp));
|
||||
auto* self = parent ? reinterpret_cast<NebulaWindow*>(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr;
|
||||
POINT point = {};
|
||||
if (self && GetCursorPos(&point) && SetResizeCursor(self->HitTestPoint(point))) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
if (message == WM_MOUSEMOVE || message == WM_NCMOUSEMOVE) {
|
||||
const auto parent = reinterpret_cast<HWND>(GetPropW(hwnd, kChildFrameHitTestParentProp));
|
||||
auto* self = parent ? reinterpret_cast<NebulaWindow*>(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr;
|
||||
POINT point = {};
|
||||
if (self && GetCursorPos(&point) && SetResizeCursor(self->HitTestPoint(point))) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (message == WM_NCLBUTTONDOWN && IsResizeHit(static_cast<LRESULT>(wparam))) {
|
||||
const auto parent = reinterpret_cast<HWND>(GetPropW(hwnd, kChildFrameHitTestParentProp));
|
||||
if (parent) {
|
||||
ReleaseCapture();
|
||||
SendMessageW(parent, WM_NCLBUTTONDOWN, wparam, lparam);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (message == WM_NCDESTROY) {
|
||||
RemovePropW(hwnd, kChildFrameHitTestParentProp);
|
||||
RemovePropW(hwnd, kChildFrameHitTestOldProcProp);
|
||||
if (old_proc) {
|
||||
SetWindowLongPtrW(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(old_proc));
|
||||
}
|
||||
}
|
||||
|
||||
return old_proc ? CallWindowProcW(old_proc, hwnd, message, wparam, lparam)
|
||||
: DefWindowProcW(hwnd, message, wparam, lparam);
|
||||
}
|
||||
|
||||
BOOL CALLBACK NebulaWindow::EnableFrameHitTestForDescendant(HWND hwnd, LPARAM lparam) {
|
||||
const auto* self = reinterpret_cast<const NebulaWindow*>(lparam);
|
||||
if (self) {
|
||||
self->EnableFrameHitTestForWindow(hwnd);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
LRESULT NebulaWindow::WndProc(UINT message, WPARAM wparam, LPARAM lparam) {
|
||||
switch (message) {
|
||||
case WM_CREATE:
|
||||
UpdateDpi();
|
||||
if (delegate_) {
|
||||
delegate_->OnWindowCreated();
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_NCCALCSIZE:
|
||||
if (wparam == TRUE) {
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_NCACTIVATE:
|
||||
ApplyWindowFrameStyle(hwnd_);
|
||||
return TRUE;
|
||||
|
||||
case WM_ERASEBKGND:
|
||||
return 1;
|
||||
|
||||
case WM_NCHITTEST:
|
||||
return HitTest(lparam);
|
||||
|
||||
case WM_SETCURSOR: {
|
||||
POINT point = {};
|
||||
if (GetCursorPos(&point) && SetResizeCursor(HitTestPoint(point))) {
|
||||
return TRUE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case WM_MOUSEMOVE:
|
||||
case WM_NCMOUSEMOVE: {
|
||||
POINT point = {};
|
||||
if (GetCursorPos(&point) && SetResizeCursor(HitTestPoint(point))) {
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case WM_SIZE:
|
||||
NotifyResize();
|
||||
return 0;
|
||||
|
||||
case WM_DPICHANGED: {
|
||||
dpi_ = HIWORD(wparam);
|
||||
const auto* suggested_rect = reinterpret_cast<RECT*>(lparam);
|
||||
SetWindowPos(
|
||||
hwnd_,
|
||||
nullptr,
|
||||
suggested_rect->left,
|
||||
suggested_rect->top,
|
||||
suggested_rect->right - suggested_rect->left,
|
||||
suggested_rect->bottom - suggested_rect->top,
|
||||
SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
NotifyResize();
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_GETMINMAXINFO: {
|
||||
const RECT work_area = GetMonitorWorkArea(hwnd_);
|
||||
const RECT monitor_area = GetMonitorArea(hwnd_);
|
||||
|
||||
auto* minmax = reinterpret_cast<MINMAXINFO*>(lparam);
|
||||
minmax->ptMaxPosition.x = work_area.left - monitor_area.left;
|
||||
minmax->ptMaxPosition.y = work_area.top - monitor_area.top;
|
||||
minmax->ptMaxSize.x = work_area.right - work_area.left;
|
||||
minmax->ptMaxSize.y = work_area.bottom - work_area.top;
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_CLOSE:
|
||||
if (delegate_) {
|
||||
delegate_->OnWindowCloseRequested();
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_DESTROY:
|
||||
hwnd_ = nullptr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return DefWindowProcW(hwnd_, message, wparam, lparam);
|
||||
}
|
||||
|
||||
void NebulaWindow::RegisterClass(HINSTANCE instance) {
|
||||
WNDCLASSEXW window_class = {};
|
||||
window_class.cbSize = sizeof(window_class);
|
||||
window_class.lpfnWndProc = StaticWndProc;
|
||||
window_class.hInstance = instance;
|
||||
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_) {
|
||||
return HTNOWHERE;
|
||||
}
|
||||
|
||||
POINT point = {GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)};
|
||||
return HitTestPoint(point);
|
||||
}
|
||||
|
||||
LRESULT NebulaWindow::HitTestPoint(POINT point) const {
|
||||
if (!hwnd_) {
|
||||
return HTNOWHERE;
|
||||
}
|
||||
|
||||
RECT window = {};
|
||||
GetWindowRect(hwnd_, &window);
|
||||
|
||||
if (fullscreen_ || IsZoomed(hwnd_)) {
|
||||
return HTCLIENT;
|
||||
}
|
||||
|
||||
const int resize_border = ScaleForDpi(resize_border_dip_);
|
||||
const bool left = point.x >= window.left && point.x < window.left + resize_border;
|
||||
const bool right = point.x < window.right && point.x >= window.right - resize_border;
|
||||
const bool top = point.y >= window.top && point.y < window.top + resize_border;
|
||||
const bool bottom = point.y < window.bottom && point.y >= window.bottom - resize_border;
|
||||
|
||||
if (top && left) {
|
||||
return HTTOPLEFT;
|
||||
}
|
||||
if (top && right) {
|
||||
return HTTOPRIGHT;
|
||||
}
|
||||
if (bottom && left) {
|
||||
return HTBOTTOMLEFT;
|
||||
}
|
||||
if (bottom && right) {
|
||||
return HTBOTTOMRIGHT;
|
||||
}
|
||||
if (left) {
|
||||
return HTLEFT;
|
||||
}
|
||||
if (right) {
|
||||
return HTRIGHT;
|
||||
}
|
||||
if (top) {
|
||||
return HTTOP;
|
||||
}
|
||||
if (bottom) {
|
||||
return HTBOTTOM;
|
||||
}
|
||||
|
||||
const int controls_width = ScaleForDpi(kWindowControlWidthDip * kWindowControlCount);
|
||||
const int controls_height = ScaleForDpi(kTitleRowHeightDip);
|
||||
const bool window_controls = point.x >= window.right - controls_width && point.x < window.right &&
|
||||
point.y >= window.top && point.y < window.top + controls_height;
|
||||
if (window_controls) {
|
||||
return HTCLIENT;
|
||||
}
|
||||
|
||||
return HTCLIENT;
|
||||
}
|
||||
|
||||
} // namespace nebula::window
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace nebula::window {
|
||||
|
||||
struct BrowserLayout {
|
||||
RECT chrome = {};
|
||||
RECT content = {};
|
||||
};
|
||||
|
||||
class WindowDelegate {
|
||||
public:
|
||||
virtual ~WindowDelegate() = default;
|
||||
virtual void OnWindowCreated() = 0;
|
||||
virtual void OnWindowResized(const BrowserLayout& layout) = 0;
|
||||
virtual void OnWindowCloseRequested() = 0;
|
||||
};
|
||||
|
||||
class NebulaWindow {
|
||||
public:
|
||||
explicit NebulaWindow(WindowDelegate* delegate);
|
||||
~NebulaWindow();
|
||||
|
||||
bool Create(HINSTANCE instance, int show_command);
|
||||
HWND hwnd() const { return hwnd_; }
|
||||
BrowserLayout CurrentLayout(bool show_chrome = true) const;
|
||||
|
||||
void ResizeChild(HWND child, const 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;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
} // namespace nebula::window
|
||||
@@ -0,0 +1,363 @@
|
||||
:root {
|
||||
--bg: #080a0f;
|
||||
--surface: #0e1119;
|
||||
--surface-raised: #141824;
|
||||
--surface-hover: rgba(255, 255, 255, 0.06);
|
||||
--text: #e8e8f0;
|
||||
--muted: #7a7e90;
|
||||
--accent: #7b2eff;
|
||||
--accent-2: #00c6ff;
|
||||
--outline: #1f2533;
|
||||
--outline-soft: rgba(255, 255, 255, 0.06);
|
||||
--danger: #e0445c;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "InterVariable";
|
||||
src: url("../assets/fonts/InterVariable.ttf") format("truetype");
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: "InterVariable", "Segoe UI", system-ui, sans-serif;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* ── Chrome shell ───────────────────────────────────────────── */
|
||||
|
||||
.nebula-chrome {
|
||||
display: grid;
|
||||
grid-template-rows: 42px 52px;
|
||||
height: 100%;
|
||||
border-bottom: 1px solid var(--outline);
|
||||
}
|
||||
|
||||
/* ── Title row ──────────────────────────────────────────────── */
|
||||
|
||||
.title-row,
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
gap: 10px;
|
||||
padding: 0 0 0 12px;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
/* ── Tabs ───────────────────────────────────────────────────── */
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
width: min(260px, 38vw);
|
||||
height: 33px;
|
||||
padding: 0 14px;
|
||||
border-radius: 10px 10px 0 0;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem;
|
||||
transition: background 120ms, color 120ms;
|
||||
}
|
||||
|
||||
.tab:hover:not(.active) {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: var(--surface-raised);
|
||||
border-color: var(--outline);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tab-favicon,
|
||||
.tab-loading {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.tab-favicon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--accent);
|
||||
opacity: 0.85;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-favicon.has-favicon {
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tab-favicon.empty {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.tab-favicon img {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.tab-loading {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border: 2px solid rgba(0, 198, 255, 0.2);
|
||||
border-top-color: var(--accent-2);
|
||||
border-radius: 999px;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
opacity: 0;
|
||||
transition: background 120ms, color 120ms, opacity 120ms;
|
||||
}
|
||||
|
||||
.tab:hover .tab-close,
|
||||
.tab.active .tab-close,
|
||||
.tab-close:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tab-close:hover {
|
||||
background: var(--surface-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.tab-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-bottom: 2px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
transition: background 120ms, color 120ms, border-color 120ms;
|
||||
}
|
||||
|
||||
.tab-add:hover {
|
||||
background: var(--surface-hover);
|
||||
border-color: var(--outline);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ── Window controls ────────────────────────────────────────── */
|
||||
|
||||
.window-controls {
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.window-controls button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 46px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
transition: background 100ms, color 100ms;
|
||||
}
|
||||
|
||||
.window-controls button:hover {
|
||||
background: var(--surface-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.window-controls .close:hover {
|
||||
background: var(--danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ── Toolbar ────────────────────────────────────────────────── */
|
||||
|
||||
.toolbar {
|
||||
gap: 4px;
|
||||
padding: 0 12px;
|
||||
background: var(--surface-raised);
|
||||
border-top: 1px solid var(--outline);
|
||||
}
|
||||
|
||||
/* ── Lucide icon sizing ─────────────────────────────────────── */
|
||||
|
||||
/* Lucide replaces <i data-lucide> with <svg>; enforce consistent size */
|
||||
.icon-button svg,
|
||||
.tab-close svg,
|
||||
.tab-add svg,
|
||||
.window-controls svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
stroke-width: 1.75;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ── Icon buttons ───────────────────────────────────────────── */
|
||||
|
||||
.icon-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
color: var(--muted);
|
||||
flex-shrink: 0;
|
||||
transition: background 120ms, color 120ms, border-color 120ms;
|
||||
}
|
||||
|
||||
.icon-button:hover:not(:disabled) {
|
||||
background: var(--surface-hover);
|
||||
border-color: var(--outline);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ── Address bar ────────────────────────────────────────────── */
|
||||
|
||||
.address-shell {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 160px;
|
||||
height: 36px;
|
||||
flex: 1;
|
||||
margin: 0 4px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--outline);
|
||||
border-radius: 10px;
|
||||
background: var(--surface);
|
||||
transition: border-color 140ms, box-shadow 140ms;
|
||||
}
|
||||
|
||||
.address-shell:focus-within {
|
||||
border-color: rgba(123, 46, 255, 0.55);
|
||||
box-shadow: 0 0 0 3px rgba(123, 46, 255, 0.12);
|
||||
}
|
||||
|
||||
.address-shell input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
padding: 0 16px;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.address-shell input::placeholder {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
width: 0%;
|
||||
pointer-events: none;
|
||||
background: var(--accent);
|
||||
border-radius: 0 2px 2px 0;
|
||||
opacity: 0.7;
|
||||
transition: width 120ms ease, opacity 160ms ease;
|
||||
}
|
||||
|
||||
/* ── Utilities ──────────────────────────────────────────────── */
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -338,20 +338,6 @@ function initNavigation() {
|
||||
launchNebot.addEventListener('click', () => navigateTo('nebula://nebot'));
|
||||
}
|
||||
|
||||
// History section buttons
|
||||
const clearHistoryBtn = document.getElementById('clearHistoryBtn');
|
||||
if (clearHistoryBtn) {
|
||||
clearHistoryBtn.addEventListener('click', clearHistory);
|
||||
}
|
||||
|
||||
const refreshHistoryBtn = document.getElementById('refreshHistoryBtn');
|
||||
if (refreshHistoryBtn) {
|
||||
refreshHistoryBtn.addEventListener('click', async () => {
|
||||
await loadHistory();
|
||||
showToast('History refreshed');
|
||||
});
|
||||
}
|
||||
|
||||
// Bookmarks actions
|
||||
const addBookmarkBtn = document.getElementById('addBookmarkBtn');
|
||||
if (addBookmarkBtn) {
|
||||
@@ -1481,8 +1467,6 @@ async function loadHistory() {
|
||||
const stored = localStorage.getItem('siteHistory');
|
||||
state.history = stored ? JSON.parse(stored) : [];
|
||||
}
|
||||
renderHistory();
|
||||
renderRecentSites();
|
||||
} catch (err) {
|
||||
console.error('[BigPicture] Failed to load history:', err);
|
||||
state.history = [];
|
||||
@@ -1505,8 +1489,6 @@ async function saveToHistory(url) {
|
||||
if (history.length > 100) history = history.slice(0, 100);
|
||||
localStorage.setItem('siteHistory', JSON.stringify(history));
|
||||
state.history = history;
|
||||
renderHistory();
|
||||
renderRecentSites();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[BigPicture] Failed to save history:', err);
|
||||
@@ -1522,8 +1504,6 @@ async function clearHistory() {
|
||||
localStorage.removeItem('siteHistory');
|
||||
}
|
||||
state.history = [];
|
||||
renderHistory();
|
||||
renderRecentSites();
|
||||
showToast('History cleared');
|
||||
} catch (err) {
|
||||
console.error('[BigPicture] Failed to clear history:', err);
|
||||
@@ -2484,8 +2464,6 @@ async function clearAllBrowsingData() {
|
||||
// Also clear localStorage
|
||||
localStorage.removeItem('siteHistory');
|
||||
state.history = [];
|
||||
renderHistory();
|
||||
renderRecentSites();
|
||||
|
||||
showToast('All browsing data cleared');
|
||||
playSelectSound();
|
||||
@@ -2503,8 +2481,6 @@ async function clearBrowsingHistory() {
|
||||
|
||||
localStorage.removeItem('siteHistory');
|
||||
state.history = [];
|
||||
renderHistory();
|
||||
renderRecentSites();
|
||||
|
||||
showToast('Browsing history cleared');
|
||||
playSelectSound();
|
||||
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
const SEARCH_URL = 'https://www.google.com/search?q=';
|
||||
|
||||
const state = {
|
||||
id: 1,
|
||||
url: '',
|
||||
title: 'New Tab',
|
||||
isLoading: false,
|
||||
progress: 0,
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
favicon: '',
|
||||
tabs: []
|
||||
};
|
||||
|
||||
function toNavigationUrl(input) {
|
||||
const value = (input || '').trim();
|
||||
if (!value) return null;
|
||||
if (/^(https?:|file:|data:|blob:|chrome:|nebula:\/\/)/i.test(value)) return value;
|
||||
if (value.includes('.') && !/\s/.test(value)) return `https://${value}`;
|
||||
return `${SEARCH_URL}${encodeURIComponent(value)}`;
|
||||
}
|
||||
|
||||
function postCommand(command, payload = '') {
|
||||
if (window.nebulaNative && typeof window.nebulaNative.postMessage === 'function') {
|
||||
window.nebulaNative.postMessage(command, String(payload));
|
||||
}
|
||||
}
|
||||
|
||||
function renderFavicon(favicon, tab) {
|
||||
const url = (tab.favicon || '').trim();
|
||||
favicon.className = 'tab-favicon';
|
||||
favicon.textContent = '';
|
||||
|
||||
if (!url) {
|
||||
favicon.classList.add('empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const image = document.createElement('img');
|
||||
image.alt = '';
|
||||
image.decoding = 'async';
|
||||
image.draggable = false;
|
||||
image.addEventListener('load', () => {
|
||||
favicon.classList.add('has-favicon');
|
||||
});
|
||||
image.addEventListener('error', () => {
|
||||
image.remove();
|
||||
favicon.classList.remove('has-favicon');
|
||||
favicon.classList.add('empty');
|
||||
});
|
||||
|
||||
favicon.append(image);
|
||||
image.src = url;
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
const tabsElement = document.querySelector('.tabs');
|
||||
const addButton = tabsElement.querySelector('.tab-add');
|
||||
const tabs = state.tabs.length
|
||||
? state.tabs
|
||||
: [{ id: state.id, title: state.title, isLoading: state.isLoading, favicon: state.favicon }];
|
||||
|
||||
tabsElement.querySelectorAll('.tab').forEach(tab => tab.remove());
|
||||
|
||||
tabs.forEach(tab => {
|
||||
const button = document.createElement('div');
|
||||
const isActive = tab.id === state.id;
|
||||
button.className = `tab${isActive ? ' active' : ''}`;
|
||||
button.setAttribute('role', 'tab');
|
||||
button.setAttribute('aria-selected', String(isActive));
|
||||
button.tabIndex = 0;
|
||||
button.dataset.tabId = String(tab.id);
|
||||
|
||||
const favicon = document.createElement('span');
|
||||
renderFavicon(favicon, tab);
|
||||
|
||||
const title = document.createElement('span');
|
||||
title.className = 'tab-title';
|
||||
title.textContent = tab.title || 'New Tab';
|
||||
|
||||
const loading = document.createElement('span');
|
||||
loading.className = 'tab-loading';
|
||||
loading.hidden = !tab.isLoading;
|
||||
|
||||
const close = document.createElement('button');
|
||||
close.className = 'tab-close';
|
||||
close.type = 'button';
|
||||
close.title = 'Close tab';
|
||||
close.setAttribute('aria-label', `Close ${tab.title || 'New Tab'}`);
|
||||
close.dataset.tabId = String(tab.id);
|
||||
close.innerHTML = '<i data-lucide="x"></i>';
|
||||
|
||||
button.append(favicon, title, loading, close);
|
||||
tabsElement.insertBefore(button, addButton);
|
||||
});
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [tabsElement] });
|
||||
}
|
||||
|
||||
function applyState(nextState) {
|
||||
Object.assign(state, nextState || {});
|
||||
|
||||
const title = state.title || 'New Tab';
|
||||
const url = state.url || '';
|
||||
const addressInput = document.getElementById('address-input');
|
||||
const backButton = document.getElementById('back-button');
|
||||
const forwardButton = document.getElementById('forward-button');
|
||||
const reloadButton = document.getElementById('reload-button');
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
|
||||
document.title = `${title} - Nebula`;
|
||||
renderTabs();
|
||||
backButton.disabled = !state.canGoBack;
|
||||
forwardButton.disabled = !state.canGoForward;
|
||||
const reloadIcon = state.isLoading ? 'x' : 'rotate-cw';
|
||||
reloadButton.dataset.command = state.isLoading ? 'stop' : 'reload';
|
||||
reloadButton.innerHTML = `<i data-lucide="${reloadIcon}"></i>`;
|
||||
if (window.lucide) lucide.createIcons({ nodes: [reloadButton] });
|
||||
|
||||
if (document.activeElement !== addressInput) {
|
||||
addressInput.value = url;
|
||||
}
|
||||
|
||||
progressBar.style.width = `${Math.max(0, Math.min(1, state.progress || 0)) * 100}%`;
|
||||
progressBar.style.opacity = state.isLoading ? '1' : '0';
|
||||
}
|
||||
|
||||
function wireCommands() {
|
||||
document.querySelector('.tabs').addEventListener('click', event => {
|
||||
const close = event.target.closest('.tab-close[data-tab-id]');
|
||||
if (close) {
|
||||
postCommand('close-tab', close.dataset.tabId);
|
||||
return;
|
||||
}
|
||||
|
||||
const tab = event.target.closest('.tab[data-tab-id]');
|
||||
if (tab && !tab.classList.contains('active')) {
|
||||
postCommand('activate-tab', tab.dataset.tabId);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector('.tabs').addEventListener('auxclick', event => {
|
||||
if (event.button !== 1) return;
|
||||
|
||||
const tab = event.target.closest('.tab[data-tab-id]');
|
||||
if (tab) {
|
||||
postCommand('close-tab', tab.dataset.tabId);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-command]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
postCommand(button.dataset.command);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-drag-region]').forEach(region => {
|
||||
region.addEventListener('pointerdown', event => {
|
||||
const interactive = event.target.closest('button, input, .tab, .address-shell');
|
||||
if (event.button === 0 && !interactive) {
|
||||
postCommand('drag');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('address-form').addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
const input = document.getElementById('address-input');
|
||||
const target = toNavigationUrl(input.value);
|
||||
if (target) {
|
||||
postCommand('navigate', target);
|
||||
input.blur();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.NebulaChrome = { applyState, postCommand, toNavigationUrl };
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
wireCommands();
|
||||
applyState(state);
|
||||
});
|
||||
+1
-1
@@ -88,7 +88,7 @@ function applySelectedSearchEngine(engine) {
|
||||
function normalizeNavigationUrl(input) {
|
||||
const value = (input || '').trim();
|
||||
if (!value) return null;
|
||||
if (/^(https?:|file:|data:|blob:)/i.test(value)) return value;
|
||||
if (/^(https?:|file:|data:|blob:|nebula:\/\/)/i.test(value)) return value;
|
||||
if (value.includes('.') && !/\s/.test(value)) return `https://${value}`;
|
||||
return `${searchEngines[selectedSearchEngine]}${encodeURIComponent(value)}`;
|
||||
}
|
||||
|
||||
Vendored
+12
File diff suppressed because one or more lines are too long
+13
-2
@@ -17,6 +17,17 @@ function applyTheme(theme) {
|
||||
setCssVar('--url-bar-border', colors.urlBarBorder, '#3e4652');
|
||||
}
|
||||
|
||||
function sendMenuCommand(cmd) {
|
||||
if (window.electronAPI?.send) {
|
||||
window.electronAPI.send('menu-popup-command', { cmd });
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.nebulaNative?.postMessage) {
|
||||
window.nebulaNative.postMessage(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshZoom() {
|
||||
if (!window.electronAPI?.invoke || !zoomPercentEl) return;
|
||||
try {
|
||||
@@ -34,7 +45,7 @@ window.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-cmd]');
|
||||
if (!btn) return;
|
||||
const cmd = btn.getAttribute('data-cmd');
|
||||
window.electronAPI?.send?.('menu-popup-command', { cmd });
|
||||
sendMenuCommand(cmd);
|
||||
if (cmd === 'zoom-in' || cmd === 'zoom-out') {
|
||||
setTimeout(refreshZoom, 50);
|
||||
}
|
||||
@@ -42,7 +53,7 @@ window.addEventListener('click', (e) => {
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
window.electronAPI?.send?.('menu-popup-command', { cmd: 'close' });
|
||||
sendMenuCommand('close-menu-popup');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@
|
||||
const attemptedUrl = params.get('url');
|
||||
const box = document.getElementById('targetBox');
|
||||
if (attemptedUrl) {
|
||||
box.textContent = decodeURIComponent(attemptedUrl);
|
||||
box.textContent = attemptedUrl;
|
||||
} else {
|
||||
box.textContent = 'Unknown URL';
|
||||
}
|
||||
|
||||
@@ -72,10 +72,6 @@
|
||||
<span class="material-symbols-outlined">bookmarks</span>
|
||||
<span class="nav-label">Bookmarks</span>
|
||||
</button>
|
||||
<button class="nav-item" data-section="history" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">history</span>
|
||||
<span class="nav-label">History</span>
|
||||
</button>
|
||||
<button class="nav-item" data-section="downloads" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">download</span>
|
||||
<span class="nav-label">Downloads</span>
|
||||
@@ -123,13 +119,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent sites -->
|
||||
<div class="recent-sites">
|
||||
<h2 class="subsection-title">Continue Browsing</h2>
|
||||
<div class="horizontal-scroll" id="recentSitesScroll">
|
||||
<!-- Recent sites will be populated dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Browse section (for webview) -->
|
||||
@@ -158,27 +147,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- History section -->
|
||||
<section id="section-history" class="bp-section">
|
||||
<div class="section-header">
|
||||
<h1 class="section-title">History</h1>
|
||||
<p class="section-subtitle">Recently visited sites</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<button class="action-btn" id="clearHistoryBtn" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">delete_sweep</span>
|
||||
<span>Clear History</span>
|
||||
</button>
|
||||
<button class="action-btn" id="refreshHistoryBtn" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">refresh</span>
|
||||
<span>Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="list-container" id="historyList">
|
||||
<!-- History will be populated dynamically -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Downloads section -->
|
||||
<section id="section-downloads" class="bp-section">
|
||||
<div class="section-header">
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nebula Chrome</title>
|
||||
<link rel="stylesheet" href="../css/chrome.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="nebula-chrome" data-drag-region>
|
||||
<div class="title-row" data-drag-region>
|
||||
<div class="tabs" role="tablist" aria-label="Nebula tabs">
|
||||
<button id="active-tab" class="tab active" type="button" role="tab" aria-selected="true">
|
||||
<span id="tab-favicon" class="tab-favicon"></span>
|
||||
<span id="tab-title" class="tab-title">New Tab</span>
|
||||
<span id="tab-loading" class="tab-loading" hidden></span>
|
||||
</button>
|
||||
<button class="tab-add" type="button" data-command="new-tab" title="New tab" aria-label="New tab">
|
||||
<i data-lucide="plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="window-controls" aria-label="Window controls">
|
||||
<button type="button" data-command="minimize" aria-label="Minimize">
|
||||
<i data-lucide="minus"></i>
|
||||
</button>
|
||||
<button type="button" data-command="maximize" aria-label="Maximize">
|
||||
<i data-lucide="square"></i>
|
||||
</button>
|
||||
<button type="button" data-command="close" class="close" aria-label="Close">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<button id="back-button" class="icon-button" type="button" data-command="back" aria-label="Back" disabled>
|
||||
<i data-lucide="chevron-left"></i>
|
||||
</button>
|
||||
<button id="forward-button" class="icon-button" type="button" data-command="forward" aria-label="Forward" disabled>
|
||||
<i data-lucide="chevron-right"></i>
|
||||
</button>
|
||||
<button id="reload-button" class="icon-button" type="button" data-command="reload" aria-label="Reload">
|
||||
<i data-lucide="rotate-cw"></i>
|
||||
</button>
|
||||
<button class="icon-button" type="button" data-command="home" aria-label="Home">
|
||||
<i data-lucide="home"></i>
|
||||
</button>
|
||||
|
||||
<form id="address-form" class="address-shell" autocomplete="off">
|
||||
<div id="progress-bar" class="progress-bar"></div>
|
||||
<label class="sr-only" for="address-input">Search or enter address</label>
|
||||
<input id="address-input" type="text" spellcheck="false" placeholder="Search or enter address">
|
||||
</form>
|
||||
|
||||
<button class="icon-button" type="button" data-command="settings" aria-label="Settings">
|
||||
<i data-lucide="settings"></i>
|
||||
</button>
|
||||
<button class="icon-button menu-button" type="button" data-command="menu-popup" aria-label="Open menu" title="Open menu">
|
||||
<i data-lucide="menu"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../js/lucide.min.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
<script src="../js/chrome.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+939
-205
File diff suppressed because it is too large
Load Diff
@@ -62,6 +62,10 @@
|
||||
const box = document.getElementById('targetBox');
|
||||
if (target) box.textContent = target;
|
||||
function sendNavigate(url, opts){
|
||||
if (opts && opts.insecureBypass && window.nebulaNative && window.nebulaNative.postMessage){
|
||||
window.nebulaNative.postMessage('navigate-insecure', url);
|
||||
return;
|
||||
}
|
||||
if (window.electronAPI && window.electronAPI.sendToHost){
|
||||
window.electronAPI.sendToHost('navigate', url, opts||{});
|
||||
} else if (window.parent && window.parent !== window) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<div id="menu-popup" role="menu">
|
||||
<button data-cmd="open-settings" role="menuitem">Settings</button>
|
||||
<button data-cmd="big-picture" role="menuitem">🎮 Big Picture Mode</button>
|
||||
<button data-cmd="gpu-diagnostics" role="menuitem">GPU Diagnostics</button>
|
||||
<button data-cmd="toggle-devtools" role="menuitem">Toggle Developer Tools</button>
|
||||
<div class="zoom-controls" role="group" aria-label="Zoom controls">
|
||||
<button data-cmd="zoom-out" aria-label="Zoom out">-</button>
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Nebot</title>
|
||||
<link rel="stylesheet" href="../plugins/nebot/page.css" onerror="this.remove()"/>
|
||||
<style>
|
||||
body { margin:0; font-family: system-ui,-apple-system,Segoe UI,Roboto,sans-serif; background:#12141c; color:#e6e8ef; }
|
||||
.fallback { max-width:620px; margin:60px auto; padding:32px 36px; background:#1d222e; border:1px solid rgba(255,255,255,0.08); border-radius:18px; }
|
||||
.fallback h1 { margin:0 0 12px; font-size:28px; background:linear-gradient(90deg,#a48bff,#6cb6ff); -webkit-background-clip:text; color:transparent; }
|
||||
.fallback p { line-height:1.55; }
|
||||
.tip { background:#232b38; padding:10px 14px; border-radius:10px; font-size:13px; margin-top:18px; border:1px solid rgba(255,255,255,0.08); }
|
||||
.err { color:#ff6d7d; font-weight:600; }
|
||||
#mount { min-height:400px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mount"></div>
|
||||
<script>
|
||||
(async function(){
|
||||
// Wait a tick so plugin preload (renderer-preload) runs and exposes window.ollamaChat
|
||||
const mount = document.getElementById('mount');
|
||||
function showFallback(reason){
|
||||
mount.innerHTML = `<div class="fallback">`+
|
||||
`<h1>Nebot</h1>`+
|
||||
`<p>The Nebot plugin page could not load automatically.</p>`+
|
||||
(reason?`<p class='err'>${reason}</p>`:'')+
|
||||
`<div class="tip"><strong>How to fix</strong><br/>1. Ensure the Nebot plugin folder exists at plugins/nebot.<br/>2. Confirm plugin is enabled (manifest enabled: true).<br/>3. Restart the app so the plugin manager registers pages.</div>`+
|
||||
`</div>`;
|
||||
}
|
||||
try {
|
||||
// Try to fetch plugin page HTML directly
|
||||
const res = await fetch('../plugins/nebot/page.html');
|
||||
if(!res.ok){ showFallback('Missing page.html (status '+res.status+').'); return; }
|
||||
const html = await res.text();
|
||||
// Simple sandboxed injection
|
||||
mount.innerHTML = html;
|
||||
// The injected page expects its CSS & JS relative to itself; adjust asset paths
|
||||
const fixLinks = mount.querySelectorAll('link[rel="stylesheet"], script[src]');
|
||||
fixLinks.forEach(el=>{
|
||||
const attr = el.tagName==='SCRIPT'?'src':'href';
|
||||
if(el.getAttribute(attr) && !/plugins\/nebot\//.test(el.getAttribute(attr))){
|
||||
el.setAttribute(attr,'../plugins/nebot/'+el.getAttribute(attr));
|
||||
}
|
||||
});
|
||||
// Inject JS if not already present
|
||||
if(!mount.querySelector('script[data-nebot-page]')){
|
||||
const s=document.createElement('script'); s.dataset.nebotPage='1';
|
||||
// Pass the current URL hash to the page script for debug mode
|
||||
s.src='../plugins/nebot/page.js' + window.location.hash;
|
||||
mount.appendChild(s);
|
||||
}
|
||||
} catch(e){
|
||||
showFallback(e.message||'Unknown error');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -293,12 +293,12 @@
|
||||
|
||||
<div class="customization-group about-actions">
|
||||
<button id="copy-about-btn">Copy diagnostics</button>
|
||||
<a id="github-link" href="https://github.com/Bobbybear007/NebulaBrowser" class="github-btn" rel="noopener noreferrer">
|
||||
<a id="github-link" href="https://gitpub.zambazosmedia.group/#repo/nebula-project/NebulaBrowser" class="github-btn" rel="noopener noreferrer">
|
||||
<!-- GitHub mark (Octicons) MIT License -->
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false" role="img">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.01.08-2.11 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.91.08 2.11.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
|
||||
</svg>
|
||||
<span>GitHub</span>
|
||||
<span>Gitpub</span>
|
||||
</a>
|
||||
<a id="help-link" href="https://nebula.zambazosmedia.group" class="help-btn" rel="noopener noreferrer">
|
||||
<!-- Help icon -->
|
||||
@@ -534,6 +534,12 @@
|
||||
|
||||
async function clearSiteHistory() {
|
||||
try {
|
||||
if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
|
||||
window.electronAPI.sendToHost('clear-site-history');
|
||||
} else if (window.nebulaNative && typeof window.nebulaNative.postMessage === 'function') {
|
||||
window.nebulaNative.postMessage('clear-site-history');
|
||||
}
|
||||
|
||||
// Clear from localStorage
|
||||
localStorage.removeItem('siteHistory');
|
||||
console.log('[SETTINGS DEBUG] Cleared site history from localStorage');
|
||||
|
||||
Reference in New Issue
Block a user