Add NebulaController, tab manager, and CEF clients
Introduce core application structure and browser management: add NebulaController and run entry (src/app/*) to centralize window, tab and CEF lifecycle logic; implement TabManager and NebulaTab (src/browser/*) for tab creation, navigation and state tracking; add URL utilities (NormalizeNavigationInput, JsonEscape) and CEF browser client glue (src/cef/browser_client.cpp/.h) to forward chrome commands and content events. Update app/main.cpp to delegate startup to nebula::app::RunNebula. Add UI assets (chrome.html, chrome.css, chrome.js, lucide, menu-popup updates) and remove obsolete nebot.html. Update CMakeLists to include new sources, add ${CMAKE_SOURCE_DIR}/src to includes and link dwmapi on Windows. Overall this refactors startup and splits responsibilities for cleaner tab and browser lifecycle handling.
This commit is contained in:
@@ -39,6 +39,15 @@ add_subdirectory(
|
||||
|
||||
set(NEBULA_SOURCES
|
||||
app/main.cpp
|
||||
src/app/nebula_controller.cpp
|
||||
src/app/run.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 +63,7 @@ if(MSVC)
|
||||
endif()
|
||||
|
||||
target_include_directories(NebulaBrowser PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/src"
|
||||
"${CEF_ROOT}"
|
||||
"${CEF_ROOT}/include"
|
||||
)
|
||||
@@ -70,6 +80,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,526 @@
|
||||
#include "app/nebula_controller.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <system_error>
|
||||
|
||||
#include "browser/url_utils.h"
|
||||
#include "include/cef_app.h"
|
||||
#include "include/cef_browser.h"
|
||||
#include "include/wrapper/cef_helpers.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::app {
|
||||
namespace {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
return info;
|
||||
}
|
||||
|
||||
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, 218);
|
||||
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;
|
||||
}
|
||||
|
||||
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) {}
|
||||
|
||||
NebulaController::~NebulaController() = default;
|
||||
|
||||
bool NebulaController::Create() {
|
||||
window_ = std::make_unique<nebula::window::NebulaWindow>(this);
|
||||
return window_->Create(instance_, show_command_);
|
||||
}
|
||||
|
||||
void NebulaController::OnWindowCreated() {
|
||||
tabs_.CreateInitialTab(initial_url_.empty() ? nebula::ui::GetHomeUrl() : initial_url_);
|
||||
CreateChromeBrowser();
|
||||
CreateContentBrowser();
|
||||
}
|
||||
|
||||
void NebulaController::OnWindowResized(const nebula::window::BrowserLayout& layout) {
|
||||
UNREFERENCED_PARAMETER(layout);
|
||||
ResizeBrowsers();
|
||||
}
|
||||
|
||||
void NebulaController::OnWindowCloseRequested() {
|
||||
if (closing_) {
|
||||
MaybeFinishShutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
closing_ = true;
|
||||
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);
|
||||
}
|
||||
}
|
||||
MaybeFinishShutdown();
|
||||
}
|
||||
|
||||
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 {
|
||||
tabs_.ClearBrowser(browser);
|
||||
}
|
||||
MaybeFinishShutdown();
|
||||
}
|
||||
|
||||
void NebulaController::OnChromeCommand(const std::string& command, const std::string& payload) {
|
||||
if (command == "navigate") {
|
||||
tabs_.LoadURL(payload);
|
||||
} 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 == "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 == "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) {
|
||||
tabs_.UpdateURL(browser, nebula::ui::IsChromiumNewTabUrl(url) ? nebula::ui::GetHomeUrl() : url);
|
||||
}
|
||||
|
||||
void NebulaController::OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) {
|
||||
tabs_.UpdateTitle(browser, title);
|
||||
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::OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) {
|
||||
tabs_.UpdateFavicon(browser, urls);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void NebulaController::CreateNewTab() {
|
||||
if (auto* tab = tabs_.ActiveTab()) {
|
||||
SetBrowserVisible(tab->browser, false);
|
||||
}
|
||||
|
||||
tabs_.CreateTab(nebula::ui::GetHomeUrl());
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
if (closing_browser) {
|
||||
closing_browser->GetHost()->CloseBrowser(false);
|
||||
}
|
||||
|
||||
if (!tabs_.ActiveTab()) {
|
||||
tabs_.CreateTab(nebula::ui::GetHomeUrl());
|
||||
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;
|
||||
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;
|
||||
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_, 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;
|
||||
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 (!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");
|
||||
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::ResizeBrowsers() {
|
||||
if (!window_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto layout = window_->CurrentLayout();
|
||||
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);
|
||||
}
|
||||
PositionMenuPopup();
|
||||
}
|
||||
|
||||
void NebulaController::SendChromeState(const nebula::browser::NebulaTab& tab) {
|
||||
if (!chrome_browser_) {
|
||||
return;
|
||||
}
|
||||
|
||||
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(tab.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::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,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#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 OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) override;
|
||||
void OnPopupRequested(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 ResizeBrowsers();
|
||||
void SendChromeState(const nebula::browser::NebulaTab& tab);
|
||||
void MaybeFinishShutdown();
|
||||
|
||||
HINSTANCE instance_ = nullptr;
|
||||
std::string initial_url_;
|
||||
int show_command_ = SW_SHOWDEFAULT;
|
||||
bool closing_ = false;
|
||||
bool chrome_ready_ = 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_;
|
||||
};
|
||||
|
||||
} // namespace nebula::app
|
||||
@@ -0,0 +1,54 @@
|
||||
#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 {
|
||||
|
||||
void EnableDpiAwareness() {
|
||||
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
}
|
||||
|
||||
} // 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;
|
||||
}
|
||||
|
||||
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 initial_url = command_line->GetSwitchValue("url");
|
||||
if (nebula::ui::IsEmptyOrChromiumNewTabUrl(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,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,239 @@
|
||||
#include "browser/tab_manager.h"
|
||||
|
||||
#include "browser/url_utils.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();
|
||||
}
|
||||
|
||||
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_;
|
||||
}
|
||||
|
||||
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(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,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "browser/tab.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);
|
||||
NebulaTab* ActiveTab();
|
||||
const NebulaTab* ActiveTab() const;
|
||||
const std::vector<NebulaTab>& Tabs() 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,110 @@
|
||||
#include "browser/url_utils.h"
|
||||
|
||||
#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(const std::string& value) {
|
||||
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:");
|
||||
}
|
||||
|
||||
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,222 @@
|
||||
#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";
|
||||
|
||||
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(frame);
|
||||
UNREFERENCED_PARAMETER(source_process);
|
||||
|
||||
if ((role_ != BrowserRole::Chrome && role_ != BrowserRole::MenuPopup) || !message ||
|
||||
message->GetName().ToString() != kChromeCommandMessage) {
|
||||
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 (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));
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
UNREFERENCED_PARAMETER(httpStatusCode);
|
||||
|
||||
if (role_ == BrowserRole::Content && delegate_ && frame && frame->IsMain()) {
|
||||
delegate_->OnContentLoadProgressChanged(browser, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
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 &&
|
||||
nebula::ui::IsChromiumNewTabUrl(request->GetURL().ToString())) {
|
||||
frame->LoadURL(nebula::ui::GetHomeUrl());
|
||||
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,118 @@
|
||||
#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 OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) = 0;
|
||||
virtual void OnPopupRequested(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;
|
||||
|
||||
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,79 @@
|
||||
#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") {
|
||||
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");
|
||||
}
|
||||
|
||||
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<CefV8Value> native = CefV8Value::CreateObject(nullptr, nullptr);
|
||||
native->SetValue(
|
||||
"postMessage",
|
||||
CefV8Value::CreateFunction("postMessage", new NativeBridgeHandler()),
|
||||
V8_PROPERTY_ATTRIBUTE_NONE);
|
||||
global->SetValue("nebulaNative", native, 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,121 @@
|
||||
#include "ui/paths.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace nebula::ui {
|
||||
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 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;
|
||||
}
|
||||
|
||||
} // 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 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");
|
||||
return path.empty() ? GetHomeUrl() : FilePathToUrl(path);
|
||||
}
|
||||
|
||||
std::string GetHomeUrl() {
|
||||
const auto path = GetUiPagePath(L"home.html");
|
||||
return path.empty() ? "https://www.google.com" : FilePathToUrl(path);
|
||||
}
|
||||
|
||||
std::string GetSettingsUrl() {
|
||||
const auto path = GetUiPagePath(L"settings.html");
|
||||
return path.empty() ? GetHomeUrl() : FilePathToUrl(path);
|
||||
}
|
||||
|
||||
std::string GetBigPictureUrl() {
|
||||
const auto path = GetUiPagePath(L"bigpicture.html");
|
||||
return path.empty() ? GetHomeUrl() : FilePathToUrl(path);
|
||||
}
|
||||
|
||||
std::string GetMenuPopupUrl() {
|
||||
const auto path = GetUiPagePath(L"menu-popup.html");
|
||||
return path.empty() ? GetHomeUrl() : FilePathToUrl(path);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
} // namespace nebula::ui
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace nebula::ui {
|
||||
|
||||
std::filesystem::path GetExecutableDirectory();
|
||||
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 GetBigPictureUrl();
|
||||
std::string GetMenuPopupUrl();
|
||||
|
||||
bool IsInternalHomeUrl(const std::string& url);
|
||||
bool IsChromiumNewTabUrl(const std::string& url);
|
||||
bool IsEmptyOrChromiumNewTabUrl(const std::string& url);
|
||||
|
||||
} // namespace nebula::ui
|
||||
@@ -0,0 +1,426 @@
|
||||
#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;
|
||||
|
||||
RECT GetWorkArea() {
|
||||
RECT work_area = {};
|
||||
SystemParametersInfoW(SPI_GETWORKAREA, 0, &work_area, 0);
|
||||
return work_area;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
} // 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();
|
||||
|
||||
const MARGINS margins = {1, 1, 1, 1};
|
||||
DwmExtendFrameIntoClientArea(hwnd_, &margins);
|
||||
|
||||
ShowWindow(hwnd_, show_command);
|
||||
UpdateWindow(hwnd_);
|
||||
return true;
|
||||
}
|
||||
|
||||
BrowserLayout NebulaWindow::CurrentLayout() const {
|
||||
RECT client = {};
|
||||
if (hwnd_) {
|
||||
GetClientRect(hwnd_, &client);
|
||||
}
|
||||
|
||||
BrowserLayout layout;
|
||||
layout.chrome = {0, 0, client.right, std::min<LONG>(ScaleForDpi(chrome_height_dip_), client.bottom)};
|
||||
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_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ShowWindow(hwnd_, IsZoomed(hwnd_) ? SW_RESTORE : SW_MAXIMIZE);
|
||||
}
|
||||
|
||||
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_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_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);
|
||||
|
||||
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,61 @@
|
||||
#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() const;
|
||||
|
||||
void ResizeChild(HWND child, const RECT& rect) const;
|
||||
void Minimize();
|
||||
void ToggleMaximize();
|
||||
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;
|
||||
UINT dpi_ = 96;
|
||||
int resize_border_dip_ = 8;
|
||||
int chrome_height_dip_ = 104;
|
||||
};
|
||||
|
||||
} // namespace nebula::window
|
||||
@@ -0,0 +1,383 @@
|
||||
: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);
|
||||
}
|
||||
|
||||
/* ── Brand ──────────────────────────────────────────────────── */
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 104px;
|
||||
color: var(--accent-2);
|
||||
font-size: 0.73rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
/* ── 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);
|
||||
}
|
||||
}
|
||||
+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:)/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);
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
<!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="brand" data-drag-region>
|
||||
<img src="../assets/images/branding/Nebula-Icon.svg" alt="" class="brand-icon">
|
||||
<span>Nebula</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
Reference in New Issue
Block a user