9 Commits

Author SHA1 Message Date
Andrew Zambazos 18bc607d93 Changed GitHub button in settings to Gitpub
Changed GitHub button in settings to Gitpub, yet to do icon
2026-05-16 13:27:25 +12:00
andrew 54216aa133 Persist site history to disk and integrate with settings
Add persistent site history storage and plumbing between the renderer settings UI and the native app. The app now loads/saves site_history.txt in the user data directory (max 200 entries, http/https-only, stored one URL per line) and records visited sites on navigation. Settings pages receive the history via injected JavaScript when the settings page finishes loading, and a "clear-site-history" message from the settings UI clears the on-disk history and updates the renderer.

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