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.
This commit is contained in:
2026-05-14 19:11:06 +12:00
parent dd6b3fa70d
commit 10180b7109
17 changed files with 704 additions and 238 deletions
+36 -4
View File
@@ -55,7 +55,7 @@ RECT MenuPopupRect(HWND hwnd, const nebula::window::BrowserLayout& layout) {
GetClientRect(hwnd, &client); GetClientRect(hwnd, &client);
const int width = ScaleForWindow(hwnd, 260); const int width = ScaleForWindow(hwnd, 260);
const int height = ScaleForWindow(hwnd, 218); const int height = ScaleForWindow(hwnd, 258);
const int margin = ScaleForWindow(hwnd, 12); const int margin = ScaleForWindow(hwnd, 12);
const int overlap = ScaleForWindow(hwnd, 2); const int overlap = ScaleForWindow(hwnd, 2);
@@ -103,6 +103,10 @@ std::string WithCacheBuster(std::string url) {
return url + separator + "nebula_cache_bust=" + std::to_string(GetTickCount64()) + fragment; 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) { void SetBrowserVisible(CefRefPtr<CefBrowser> browser, bool visible) {
if (!browser) { if (!browser) {
return; return;
@@ -209,6 +213,12 @@ void NebulaController::OnBrowserClosing(nebula::cef::BrowserRole role, CefRefPtr
void NebulaController::OnChromeCommand(const std::string& command, const std::string& payload) { void NebulaController::OnChromeCommand(const std::string& command, const std::string& payload) {
if (command == "navigate") { if (command == "navigate") {
tabs_.LoadURL(payload); 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") { } else if (command == "new-tab") {
CreateNewTab(); CreateNewTab();
} else if (command == "activate-tab") { } else if (command == "activate-tab") {
@@ -233,6 +243,9 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
} else if (command == "big-picture") { } else if (command == "big-picture") {
CloseMenuPopup(); CloseMenuPopup();
tabs_.LoadURL(nebula::ui::GetBigPictureUrl()); tabs_.LoadURL(nebula::ui::GetBigPictureUrl());
} else if (command == "gpu-diagnostics") {
CloseMenuPopup();
tabs_.LoadURL(nebula::ui::GetGpuDiagnosticsUrl());
} else if (command == "toggle-devtools") { } else if (command == "toggle-devtools") {
ToggleDevTools(); ToggleDevTools();
} else if (command == "zoom-out") { } else if (command == "zoom-out") {
@@ -263,7 +276,10 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
} }
void NebulaController::OnContentAddressChanged(CefRefPtr<CefBrowser> browser, const std::string& url) { void NebulaController::OnContentAddressChanged(CefRefPtr<CefBrowser> browser, const std::string& url) {
tabs_.UpdateURL(browser, nebula::ui::IsChromiumNewTabUrl(url) ? nebula::ui::GetHomeUrl() : url); tabs_.UpdateURL(browser,
nebula::ui::IsChromiumNewTabUrl(url)
? nebula::ui::GetHomeUrl()
: nebula::ui::ToInternalUrl(url));
} }
void NebulaController::OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) { void NebulaController::OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) {
@@ -296,6 +312,20 @@ void NebulaController::OnPopupRequested(CefRefPtr<CefBrowser> browser, const std
: target_url); : 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() { void NebulaController::CreateNewTab() {
if (auto* tab = tabs_.ActiveTab()) { if (auto* tab = tabs_.ActiveTab()) {
SetBrowserVisible(tab->browser, false); SetBrowserVisible(tab->browser, false);
@@ -376,7 +406,8 @@ void NebulaController::CreateContentBrowser() {
CefBrowserSettings browser_settings; CefBrowserSettings browser_settings;
content_client_ = new nebula::cef::NebulaBrowserClient(nebula::cef::BrowserRole::Content, this); content_client_ = new nebula::cef::NebulaBrowserClient(nebula::cef::BrowserRole::Content, this);
CefWindowInfo window_info = ChildWindowInfo(window_->hwnd(), layout.content); CefWindowInfo window_info = ChildWindowInfo(window_->hwnd(), layout.content);
CefBrowserHost::CreateBrowser(window_info, content_client_, url, browser_settings, nullptr, nullptr); CefBrowserHost::CreateBrowser(
window_info, content_client_, nebula::ui::ResolveInternalUrl(url), browser_settings, nullptr, nullptr);
} }
void NebulaController::ToggleMenuPopup() { void NebulaController::ToggleMenuPopup() {
@@ -476,6 +507,7 @@ void NebulaController::SendChromeState(const nebula::browser::NebulaTab& tab) {
return; return;
} }
const std::string display_url = GetChromeDisplayUrl(tab.url);
std::string tabs_json = "["; std::string tabs_json = "[";
const auto& tabs = tabs_.Tabs(); const auto& tabs = tabs_.Tabs();
for (size_t i = 0; i < tabs.size(); ++i) { for (size_t i = 0; i < tabs.size(); ++i) {
@@ -495,7 +527,7 @@ void NebulaController::SendChromeState(const nebula::browser::NebulaTab& tab) {
const std::string script = const std::string script =
"window.NebulaChrome && window.NebulaChrome.applyState({" "window.NebulaChrome && window.NebulaChrome.applyState({"
"\"id\":" + std::to_string(tab.id) + "\"id\":" + std::to_string(tab.id) +
",\"url\":\"" + nebula::browser::JsonEscape(tab.url) + "\"" ",\"url\":\"" + nebula::browser::JsonEscape(display_url) + "\""
",\"title\":\"" + nebula::browser::JsonEscape(tab.title) + "\"" ",\"title\":\"" + nebula::browser::JsonEscape(tab.title) + "\""
",\"isLoading\":" + std::string(tab.is_loading ? "true" : "false") + ",\"isLoading\":" + std::string(tab.is_loading ? "true" : "false") +
",\"progress\":" + std::to_string(tab.load_progress) + ",\"progress\":" + std::to_string(tab.load_progress) +
+3
View File
@@ -2,6 +2,7 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include <unordered_set>
#include <vector> #include <vector>
#include "browser/tab_manager.h" #include "browser/tab_manager.h"
@@ -34,6 +35,7 @@ public:
void OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browser, double progress) override; void OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browser, double progress) override;
void OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) override; void OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) override;
void OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) override; void OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) override;
bool ShouldBypassInsecureWarning(CefRefPtr<CefBrowser> browser, const std::string& target_url) override;
private: private:
void CreateNewTab(); void CreateNewTab();
@@ -65,6 +67,7 @@ private:
CefRefPtr<nebula::cef::NebulaBrowserClient> chrome_client_; CefRefPtr<nebula::cef::NebulaBrowserClient> chrome_client_;
CefRefPtr<nebula::cef::NebulaBrowserClient> content_client_; CefRefPtr<nebula::cef::NebulaBrowserClient> content_client_;
CefRefPtr<nebula::cef::NebulaBrowserClient> menu_popup_client_; CefRefPtr<nebula::cef::NebulaBrowserClient> menu_popup_client_;
std::unordered_set<std::string> insecure_warning_bypasses_;
}; };
} // namespace nebula::app } // namespace nebula::app
+2 -1
View File
@@ -1,6 +1,7 @@
#include "browser/tab_manager.h" #include "browser/tab_manager.h"
#include "browser/url_utils.h" #include "browser/url_utils.h"
#include "ui/paths.h"
namespace nebula::browser { namespace nebula::browser {
@@ -134,7 +135,7 @@ void TabManager::LoadURL(const std::string& input) {
tab->url = target; tab->url = target;
tab->favicon_url.clear(); tab->favicon_url.clear();
tab->browser->GetMainFrame()->LoadURL(target); tab->browser->GetMainFrame()->LoadURL(nebula::ui::ResolveInternalUrl(target));
Notify(); Notify();
} }
+7 -2
View File
@@ -1,5 +1,6 @@
#include "browser/url_utils.h" #include "browser/url_utils.h"
#include <algorithm>
#include <cctype> #include <cctype>
#include <iomanip> #include <iomanip>
#include <sstream> #include <sstream>
@@ -19,13 +20,17 @@ std::string Trim(std::string value) {
return value; return value;
} }
bool StartsWithScheme(const std::string& 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://") || return value.starts_with("http://") ||
value.starts_with("https://") || value.starts_with("https://") ||
value.starts_with("file:") || value.starts_with("file:") ||
value.starts_with("data:") || value.starts_with("data:") ||
value.starts_with("blob:") || value.starts_with("blob:") ||
value.starts_with("chrome:"); value.starts_with("chrome:") ||
value.starts_with("nebula://");
} }
bool LooksLikeHostName(const std::string& value) { bool LooksLikeHostName(const std::string& value) {
+42 -7
View File
@@ -9,6 +9,14 @@ namespace {
constexpr char kChromeCommandMessage[] = "NebulaChromeCommand"; constexpr char kChromeCommandMessage[] = "NebulaChromeCommand";
bool IsInsecureInterstitialFrame(CefRefPtr<CefFrame> frame) {
if (!frame) {
return false;
}
return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://insecure");
}
std::vector<std::string> ToStringVector(const std::vector<CefString>& values) { std::vector<std::string> ToStringVector(const std::vector<CefString>& values) {
std::vector<std::string> result; std::vector<std::string> result;
result.reserve(values.size()); result.reserve(values.size());
@@ -29,17 +37,25 @@ bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser
CefRefPtr<CefProcessMessage> message) { CefRefPtr<CefProcessMessage> message) {
CEF_REQUIRE_UI_THREAD(); CEF_REQUIRE_UI_THREAD();
UNREFERENCED_PARAMETER(browser); UNREFERENCED_PARAMETER(browser);
UNREFERENCED_PARAMETER(frame);
UNREFERENCED_PARAMETER(source_process); UNREFERENCED_PARAMETER(source_process);
if ((role_ != BrowserRole::Chrome && role_ != BrowserRole::MenuPopup) || !message || if (!message || message->GetName().ToString() != kChromeCommandMessage) {
message->GetName().ToString() != kChromeCommandMessage) { return false;
}
if (role_ != BrowserRole::Chrome && role_ != BrowserRole::MenuPopup &&
role_ != BrowserRole::Content) {
return false; return false;
} }
CefRefPtr<CefListValue> args = message->GetArgumentList(); CefRefPtr<CefListValue> args = message->GetArgumentList();
const std::string command = args && args->GetSize() > 0 ? args->GetString(0).ToString() : ""; const std::string command = args && args->GetSize() > 0 ? args->GetString(0).ToString() : "";
const std::string payload = args && args->GetSize() > 1 ? args->GetString(1).ToString() : ""; const std::string payload = args && args->GetSize() > 1 ? args->GetString(1).ToString() : "";
if (role_ == BrowserRole::Content &&
(command != "navigate-insecure" || !IsInsecureInterstitialFrame(frame))) {
return false;
}
if (delegate_ && !command.empty()) { if (delegate_ && !command.empty()) {
delegate_->OnChromeCommand(command, payload); delegate_->OnChromeCommand(command, payload);
return true; return true;
@@ -172,9 +188,14 @@ void NebulaBrowserClient::OnLoadEnd(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame, CefRefPtr<CefFrame> frame,
int httpStatusCode) { int httpStatusCode) {
CEF_REQUIRE_UI_THREAD(); CEF_REQUIRE_UI_THREAD();
UNREFERENCED_PARAMETER(httpStatusCode);
if (role_ == BrowserRole::Content && delegate_ && frame && frame->IsMain()) { 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_->OnContentLoadProgressChanged(browser, 1.0);
} }
} }
@@ -189,12 +210,26 @@ bool NebulaBrowserClient::OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
UNREFERENCED_PARAMETER(user_gesture); UNREFERENCED_PARAMETER(user_gesture);
UNREFERENCED_PARAMETER(is_redirect); UNREFERENCED_PARAMETER(is_redirect);
if (role_ == BrowserRole::Content && frame && frame->IsMain() && request && if (role_ == BrowserRole::Content && frame && frame->IsMain() && request) {
nebula::ui::IsChromiumNewTabUrl(request->GetURL().ToString())) { const std::string url = request->GetURL().ToString();
frame->LoadURL(nebula::ui::GetHomeUrl()); if (nebula::ui::IsChromiumNewTabUrl(url)) {
frame->LoadURL(nebula::ui::ResolveInternalUrl(nebula::ui::GetHomeUrl()));
return true; 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; return false;
} }
+1
View File
@@ -31,6 +31,7 @@ public:
virtual void OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browser, double progress) = 0; virtual void OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browser, double progress) = 0;
virtual void OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) = 0; virtual void OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) = 0;
virtual void OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) = 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, class NebulaBrowserClient final : public CefClient,
+149 -9
View File
@@ -4,10 +4,31 @@
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <string_view>
namespace nebula::ui { namespace nebula::ui {
namespace { 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) { std::string WideToUtf8(const std::wstring& value) {
if (value.empty()) { if (value.empty()) {
return {}; return {};
@@ -31,6 +52,11 @@ std::string GetUrlWithoutDecoration(std::string url) {
return url; 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::string ToLowerAscii(std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch)); return static_cast<char>(std::tolower(ch));
@@ -38,6 +64,66 @@ std::string ToLowerAscii(std::string value) {
return value; 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 } // namespace
std::filesystem::path GetExecutableDirectory() { std::filesystem::path GetExecutableDirectory() {
@@ -77,31 +163,85 @@ std::string FilePathToUrl(std::filesystem::path path) {
std::string GetChromeUrl() { std::string GetChromeUrl() {
const auto path = GetUiPagePath(L"chrome.html"); const auto path = GetUiPagePath(L"chrome.html");
return path.empty() ? GetHomeUrl() : FilePathToUrl(path); const std::string fallback = PageFileUrl(L"home.html");
return path.empty() ? (fallback.empty() ? "https://www.google.com" : fallback) : FilePathToUrl(path);
} }
std::string GetHomeUrl() { std::string GetHomeUrl() {
const auto path = GetUiPagePath(L"home.html"); return InternalUrlForSlug("home");
return path.empty() ? "https://www.google.com" : FilePathToUrl(path);
} }
std::string GetSettingsUrl() { std::string GetSettingsUrl() {
const auto path = GetUiPagePath(L"settings.html"); return InternalUrlForSlug("settings");
return path.empty() ? GetHomeUrl() : FilePathToUrl(path); }
std::string GetDownloadsUrl() {
return InternalUrlForSlug("downloads");
} }
std::string GetBigPictureUrl() { std::string GetBigPictureUrl() {
const auto path = GetUiPagePath(L"bigpicture.html"); return InternalUrlForSlug("bigpicture");
return path.empty() ? GetHomeUrl() : FilePathToUrl(path); }
std::string GetGpuDiagnosticsUrl() {
return InternalUrlForSlug("gpu-diagnostics");
} }
std::string GetMenuPopupUrl() { std::string GetMenuPopupUrl() {
const auto path = GetUiPagePath(L"menu-popup.html"); const auto path = GetUiPagePath(L"menu-popup.html");
return path.empty() ? GetHomeUrl() : FilePathToUrl(path); 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) { bool IsInternalHomeUrl(const std::string& url) {
return GetUrlWithoutDecoration(url) == GetHomeUrl(); 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) { bool IsChromiumNewTabUrl(const std::string& url) {
+8
View File
@@ -11,10 +11,18 @@ std::string FilePathToUrl(std::filesystem::path path);
std::string GetChromeUrl(); std::string GetChromeUrl();
std::string GetHomeUrl(); std::string GetHomeUrl();
std::string GetSettingsUrl(); std::string GetSettingsUrl();
std::string GetDownloadsUrl();
std::string GetBigPictureUrl(); std::string GetBigPictureUrl();
std::string GetGpuDiagnosticsUrl();
std::string GetMenuPopupUrl(); 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 IsInternalHomeUrl(const std::string& url);
bool IsNebulaInternalUrl(const std::string& url);
bool IsHttpUrl(const std::string& url);
bool IsChromiumNewTabUrl(const std::string& url); bool IsChromiumNewTabUrl(const std::string& url);
bool IsEmptyOrChromiumNewTabUrl(const std::string& url); bool IsEmptyOrChromiumNewTabUrl(const std::string& url);
+29 -1
View File
@@ -15,6 +15,7 @@ constexpr wchar_t kChildFrameHitTestParentProp[] = L"NebulaChildFrameHitTestPare
constexpr int kTitleRowHeightDip = 42; constexpr int kTitleRowHeightDip = 42;
constexpr int kWindowControlWidthDip = 46; constexpr int kWindowControlWidthDip = 46;
constexpr int kWindowControlCount = 3; constexpr int kWindowControlCount = 3;
constexpr COLORREF kNoWindowBorderColor = 0xFFFFFFFE;
RECT GetWorkArea() { RECT GetWorkArea() {
RECT work_area = {}; RECT work_area = {};
@@ -56,6 +57,28 @@ bool SetResizeCursor(LRESULT hit) {
return true; 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 } // namespace
NebulaWindow::NebulaWindow(WindowDelegate* delegate) : delegate_(delegate) {} NebulaWindow::NebulaWindow(WindowDelegate* delegate) : delegate_(delegate) {}
@@ -92,8 +115,9 @@ bool NebulaWindow::Create(HINSTANCE instance, int show_command) {
} }
UpdateDpi(); UpdateDpi();
ApplyWindowFrameStyle(hwnd_);
const MARGINS margins = {1, 1, 1, 1}; const MARGINS margins = {0, 0, 0, 0};
DwmExtendFrameIntoClientArea(hwnd_, &margins); DwmExtendFrameIntoClientArea(hwnd_, &margins);
ShowWindow(hwnd_, show_command); ShowWindow(hwnd_, show_command);
@@ -265,6 +289,10 @@ LRESULT NebulaWindow::WndProc(UINT message, WPARAM wparam, LPARAM lparam) {
} }
break; break;
case WM_NCACTIVATE:
ApplyWindowFrameStyle(hwnd_);
return TRUE;
case WM_ERASEBKGND: case WM_ERASEBKGND:
return 1; return 1;
-20
View File
@@ -78,26 +78,6 @@ button:disabled {
background: var(--bg); background: var(--bg);
} }
/* ── Brand ──────────────────────────────────────────────────── */
.brand {
display: flex;
align-items: center;
gap: 7px;
min-width: 104px;
color: var(--accent-2);
font-size: 0.73rem;
font-weight: 800;
letter-spacing: 0.15em;
text-transform: uppercase;
opacity: 0.85;
}
.brand-icon {
width: 17px;
height: 17px;
}
/* ── Tabs ───────────────────────────────────────────────────── */ /* ── Tabs ───────────────────────────────────────────────────── */
.tabs { .tabs {
+1 -1
View File
@@ -15,7 +15,7 @@ const state = {
function toNavigationUrl(input) { function toNavigationUrl(input) {
const value = (input || '').trim(); const value = (input || '').trim();
if (!value) return null; if (!value) return null;
if (/^(https?:|file:|data:|blob:|chrome:)/i.test(value)) return value; if (/^(https?:|file:|data:|blob:|chrome:|nebula:\/\/)/i.test(value)) return value;
if (value.includes('.') && !/\s/.test(value)) return `https://${value}`; if (value.includes('.') && !/\s/.test(value)) return `https://${value}`;
return `${SEARCH_URL}${encodeURIComponent(value)}`; return `${SEARCH_URL}${encodeURIComponent(value)}`;
} }
+1 -1
View File
@@ -88,7 +88,7 @@ function applySelectedSearchEngine(engine) {
function normalizeNavigationUrl(input) { function normalizeNavigationUrl(input) {
const value = (input || '').trim(); const value = (input || '').trim();
if (!value) return null; 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}`; if (value.includes('.') && !/\s/.test(value)) return `https://${value}`;
return `${searchEngines[selectedSearchEngine]}${encodeURIComponent(value)}`; return `${searchEngines[selectedSearchEngine]}${encodeURIComponent(value)}`;
} }
+1 -1
View File
@@ -61,7 +61,7 @@
const attemptedUrl = params.get('url'); const attemptedUrl = params.get('url');
const box = document.getElementById('targetBox'); const box = document.getElementById('targetBox');
if (attemptedUrl) { if (attemptedUrl) {
box.textContent = decodeURIComponent(attemptedUrl); box.textContent = attemptedUrl;
} else { } else {
box.textContent = 'Unknown URL'; box.textContent = 'Unknown URL';
} }
-5
View File
@@ -9,11 +9,6 @@
<body> <body>
<div class="nebula-chrome" data-drag-region> <div class="nebula-chrome" data-drag-region>
<div class="title-row" data-drag-region> <div class="title-row" data-drag-region>
<div class="brand" data-drag-region>
<img src="../assets/images/branding/Nebula-Icon.svg" alt="" class="brand-icon">
<span>Nebula</span>
</div>
<div class="tabs" role="tablist" aria-label="Nebula tabs"> <div class="tabs" role="tablist" aria-label="Nebula tabs">
<button id="active-tab" class="tab active" type="button" role="tab" aria-selected="true"> <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-favicon" class="tab-favicon"></span>
+406 -173
View File
@@ -1,231 +1,464 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GPU Diagnostics - Nebula Browser</title> <title>GPU Diagnostics - Nebula Browser</title>
<style> <style>
:root {
color-scheme: dark;
--bg: #0b0d10;
--panel: #151922;
--panel-soft: #1d2330;
--text: #eef2ff;
--muted: #9aa7bd;
--good: #2fbf71;
--warn: #f4b740;
--bad: #ef5b5b;
--accent: #7b2eff;
--border: rgba(255, 255, 255, 0.12);
}
* { box-sizing: border-box; }
body { body {
font-family: Arial, sans-serif; margin: 0;
margin: 20px; min-height: 100vh;
background: #f5f5f5; background:
radial-gradient(circle at top left, rgba(123, 46, 255, 0.26), transparent 32rem),
radial-gradient(circle at bottom right, rgba(0, 198, 255, 0.18), transparent 30rem),
var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
} }
.container { .container {
max-width: 800px; width: min(1100px, calc(100vw - 32px));
margin: 0 auto; margin: 0 auto;
background: white; padding: 32px 0;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
} }
header {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-start;
margin-bottom: 20px;
}
h1, h2, h3, p { margin-top: 0; }
h1 {
margin-bottom: 8px;
font-size: clamp(2rem, 4vw, 3.4rem);
}
.lede {
max-width: 720px;
color: var(--muted);
line-height: 1.6;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 16px;
}
.card {
background: color-mix(in srgb, var(--panel) 88%, transparent);
border: 1px solid var(--border);
border-radius: 18px;
padding: 18px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.22);
}
.status { .status {
padding: 10px; display: inline-flex;
margin: 10px 0; align-items: center;
border-radius: 4px; gap: 8px;
margin-bottom: 12px;
padding: 6px 10px;
border-radius: 999px;
background: var(--panel-soft);
color: var(--muted);
font-weight: 650;
} }
.status.good { background: #d4edda; color: #155724; }
.status.warning { background: #fff3cd; color: #856404; } .status::before {
.status.error { background: #f8d7da; color: #721c24; } content: "";
width: 9px;
height: 9px;
border-radius: 50%;
background: currentColor;
}
.good { color: var(--good); }
.warning { color: var(--warn); }
.error { color: var(--bad); }
canvas {
display: block;
width: 100%;
height: auto;
margin: 12px 0;
border: 1px solid var(--border);
border-radius: 14px;
background: #03050a;
}
button { button {
background: #007bff; border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border));
color: white; border-radius: 12px;
border: none; background: color-mix(in srgb, var(--accent) 26%, var(--panel));
padding: 8px 16px; color: var(--text);
border-radius: 4px;
cursor: pointer; cursor: pointer;
margin: 5px; font: inherit;
font-weight: 650;
padding: 10px 14px;
} }
button:hover { background: #0056b3; }
button:hover {
filter: brightness(1.12);
}
dl {
display: grid;
grid-template-columns: minmax(130px, auto) 1fr;
gap: 8px 12px;
margin: 0;
}
dt {
color: var(--muted);
}
dd {
margin: 0;
overflow-wrap: anywhere;
}
pre { pre {
background: #f8f9fa; max-height: 420px;
padding: 10px; overflow: auto;
border-radius: 4px; margin: 0;
overflow-x: auto; padding: 14px;
border-radius: 14px;
background: #05070d;
color: #d6e2ff;
font-size: 12px; font-size: 12px;
} line-height: 1.5;
.canvas-test { white-space: pre-wrap;
border: 1px solid #ccc;
margin: 10px 0;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <main class="container">
<header>
<div>
<h1>GPU Diagnostics</h1> <h1>GPU Diagnostics</h1>
<p class="lede">
Quick CEF-compatible checks for WebGL, WebGL2, Canvas 2D, GPU renderer strings,
and browser runtime details. Use this page to verify graphics support after GPU
flag or runtime changes.
</p>
</div>
<button id="refresh-button" type="button">Run Again</button>
</header>
<div id="gpu-status" class="status"> <section class="grid" aria-label="GPU test summary">
<h3>GPU Status</h3> <article class="card">
<p>Loading GPU information...</p> <div id="overall-status" class="status">Waiting</div>
</div> <h2>Summary</h2>
<dl id="summary-list"></dl>
</article>
<div class="status"> <article class="card">
<h3>WebGL Test</h3> <div id="webgl-status" class="status">Waiting</div>
<canvas id="webgl-canvas" class="canvas-test" width="300" height="150"></canvas> <h2>WebGL</h2>
<p id="webgl-status">Testing WebGL...</p> <canvas id="webgl-canvas" width="480" height="240"></canvas>
</div> <p id="webgl-message"></p>
</article>
<div class="status"> <article class="card">
<h3>Canvas 2D Acceleration Test</h3> <div id="webgl2-status" class="status">Waiting</div>
<canvas id="canvas2d" class="canvas-test" width="300" height="150"></canvas> <h2>WebGL2</h2>
<p id="canvas2d-status">Testing Canvas 2D...</p> <canvas id="webgl2-canvas" width="480" height="240"></canvas>
</div> <p id="webgl2-message"></p>
</article>
<div> <article class="card">
<h3>Actions</h3> <div id="canvas2d-status" class="status">Waiting</div>
<button onclick="refreshGPUInfo()">Refresh GPU Info</button> <h2>Canvas 2D</h2>
<button onclick="forceGC()">Force Garbage Collection</button> <canvas id="canvas2d" width="480" height="240"></canvas>
<button onclick="applyFallback(1)">Apply GPU Fallback Level 1</button> <p id="canvas2d-message"></p>
<button onclick="applyFallback(2)">Apply GPU Fallback Level 2</button> </article>
</div> </section>
<div> <section class="card" style="margin-top: 16px;">
<h3>Detailed GPU Information</h3> <h2>Detailed Information</h2>
<pre id="gpu-details">Loading...</pre> <pre id="gpu-details">Waiting for diagnostics...</pre>
</div> </section>
</div> </main>
<script> <script>
async function refreshGPUInfo() { function setStatus(id, level, text) {
try { const element = document.getElementById(id);
if (!window.electronAPI?.invoke) { element.className = `status ${level}`;
const statusDiv = document.getElementById('gpu-status'); element.textContent = text;
const detailsDiv = document.getElementById('gpu-details');
statusDiv.className = 'status warning';
statusDiv.innerHTML = '<h3>GPU Status</h3><p>Native GPU diagnostics are not exposed to this CEF page.</p>';
detailsDiv.textContent = navigator.userAgent;
return;
}
const gpuInfo = await window.electronAPI.invoke('get-gpu-info');
const statusDiv = document.getElementById('gpu-status');
const detailsDiv = document.getElementById('gpu-details');
if (gpuInfo.error) {
statusDiv.className = 'status error';
statusDiv.innerHTML = `<h3>GPU Status</h3><p>Error: ${gpuInfo.error}</p>`;
} else {
const isGPUWorking = checkGPUFeatures(gpuInfo.featureStatus);
statusDiv.className = `status ${isGPUWorking ? 'good' : 'warning'}`;
statusDiv.innerHTML = `
<h3>GPU Status</h3>
<p><strong>Hardware Acceleration:</strong> ${isGPUWorking ? 'Enabled' : 'Disabled/Limited'}</p>
<p><strong>Fallback Level:</strong> ${gpuInfo.fallbackStatus?.fallbackLevel || 0}</p>
<p><strong>GPU Enabled:</strong> ${gpuInfo.fallbackStatus?.gpuEnabled ? 'Yes' : 'No'}</p>
`;
} }
detailsDiv.textContent = JSON.stringify(gpuInfo, null, 2); function setMessage(id, text) {
} catch (err) { document.getElementById(id).textContent = text;
console.error('Failed to get GPU info:', err);
document.getElementById('gpu-status').innerHTML = `<h3>GPU Status</h3><p>Error: ${err.message}</p>`;
}
} }
function checkGPUFeatures(features) { function formatValue(value) {
const criticalFeatures = ['gpu_compositing', 'webgl', 'webgl2']; if (value === undefined || value === null || value === '') return 'Unavailable';
return criticalFeatures.some(feature => if (Array.isArray(value)) return value.length ? value.join(', ') : 'Unavailable';
features[feature] && !features[feature].includes('disabled') return String(value);
);
} }
async function forceGC() { function collectContextAttributes(gl) {
try { const attributes = gl.getContextAttributes?.();
if (!window.electronAPI?.invoke) { if (!attributes) return {};
alert('Garbage collection is managed by CEF in this build.'); return {
return; alpha: attributes.alpha,
} antialias: attributes.antialias,
await window.electronAPI.invoke('force-gc'); depth: attributes.depth,
alert('Garbage collection completed'); desynchronized: attributes.desynchronized,
} catch (err) { failIfMajorPerformanceCaveat: attributes.failIfMajorPerformanceCaveat,
alert('Failed to force GC: ' + err.message); powerPreference: attributes.powerPreference,
} premultipliedAlpha: attributes.premultipliedAlpha,
preserveDrawingBuffer: attributes.preserveDrawingBuffer,
stencil: attributes.stencil
};
} }
async function applyFallback(level) { function collectRendererInfo(gl) {
try { const info = {
if (!window.electronAPI?.invoke) { version: gl.getParameter(gl.VERSION),
alert('GPU fallback settings are managed by the native CEF app.'); shadingLanguageVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION),
return; vendor: gl.getParameter(gl.VENDOR),
} renderer: gl.getParameter(gl.RENDERER),
const result = await window.electronAPI.invoke('apply-gpu-fallback', level); maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
if (result.success) { maxRenderbufferSize: gl.getParameter(gl.MAX_RENDERBUFFER_SIZE),
alert(`Applied GPU fallback level ${level}. App restart may be required.`); maxViewportDims: Array.from(gl.getParameter(gl.MAX_VIEWPORT_DIMS)),
} else { contextAttributes: collectContextAttributes(gl)
alert('Failed to apply fallback: ' + result.error); };
}
} catch (err) { const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
alert('Failed to apply fallback: ' + err.message); if (debugInfo) {
} info.unmaskedVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
info.unmaskedRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
} }
// Test WebGL return info;
function testWebGL() { }
const canvas = document.getElementById('webgl-canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
const status = document.getElementById('webgl-status');
if (gl) { function createShader(gl, type, source) {
// Draw a simple triangle const shader = gl.createShader(type);
const vertexShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(shader, source);
gl.shaderSource(vertexShader, ` gl.compileShader(shader);
attribute vec2 position; if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const log = gl.getShaderInfoLog(shader) || 'Unknown shader compile error';
gl.deleteShader(shader);
throw new Error(log);
}
return shader;
}
function drawWebGL(canvasId, version) {
const canvas = document.getElementById(canvasId);
const contextNames = version === 2
? ['webgl2']
: ['webgl', 'experimental-webgl'];
const gl = contextNames.map(name => canvas.getContext(name)).find(Boolean);
if (!gl) {
return { ok: false, error: `WebGL${version === 2 ? '2' : ''} context unavailable` };
}
const vertexSource = version === 2
? `#version 300 es
in vec2 position;
void main() { void main() {
gl_Position = vec4(position, 0.0, 1.0); gl_Position = vec4(position, 0.0, 1.0);
} }`
`); : `attribute vec2 position;
gl.compileShader(vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, `
precision mediump float;
void main() { void main() {
gl_Color = vec4(0.0, 1.0, 0.0, 1.0); gl_Position = vec4(position, 0.0, 1.0);
}`;
const fragmentSource = version === 2
? `#version 300 es
precision mediump float;
out vec4 outColor;
void main() {
outColor = vec4(0.12, 0.85, 0.48, 1.0);
}`
: `precision mediump float;
void main() {
gl_FragColor = vec4(0.12, 0.85, 0.48, 1.0);
}`;
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(program) || 'Unknown WebGL link error');
} }
`);
gl.compileShader(fragmentShader);
status.textContent = 'WebGL: Available ✓'; const buffer = gl.createBuffer();
status.parentElement.className = 'status good'; gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-0.85, -0.75, 0.85, -0.75, 0.0, 0.85]),
gl.STATIC_DRAW);
// Clear with green color to show it's working const position = gl.getAttribLocation(program, 'position');
gl.clearColor(0.0, 0.8, 0.0, 1.0); gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0.03, 0.05, 0.1, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT); gl.clear(gl.COLOR_BUFFER_BIT);
} else { gl.useProgram(program);
status.textContent = 'WebGL: Not Available ✗'; gl.enableVertexAttribArray(position);
status.parentElement.className = 'status error'; gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
} gl.drawArrays(gl.TRIANGLES, 0, 3);
return {
ok: true,
renderer: collectRendererInfo(gl)
};
} }
// Test Canvas 2D
function testCanvas2D() { function testCanvas2D() {
const canvas = document.getElementById('canvas2d'); const canvas = document.getElementById('canvas2d');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const status = document.getElementById('canvas2d-status'); if (!ctx) {
return { ok: false, error: 'Canvas 2D context unavailable' };
}
const start = performance.now();
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, '#7b2eff');
gradient.addColorStop(0.55, '#00c6ff');
gradient.addColorStop(1, '#2fbf71');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < 900; i += 1) {
const x = (i * 29) % canvas.width;
const y = (i * 47) % canvas.height;
ctx.fillStyle = `rgba(${(i * 3) % 255}, ${(i * 7) % 255}, 255, 0.17)`;
ctx.beginPath();
ctx.arc(x, y, 10 + (i % 16), 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = '#ffffff';
ctx.font = '600 26px system-ui, sans-serif';
ctx.fillText('Canvas 2D OK', 24, 54);
return {
ok: true,
drawTimeMs: Math.round((performance.now() - start) * 100) / 100
};
}
function renderSummary(info) {
const rows = [
['WebGL', info.webgl.ok ? 'Available' : 'Unavailable'],
['WebGL2', info.webgl2.ok ? 'Available' : 'Unavailable'],
['Canvas 2D', info.canvas2d.ok ? `Working (${info.canvas2d.drawTimeMs} ms)` : 'Unavailable'],
['Renderer', info.webgl.renderer?.unmaskedRenderer || info.webgl2.renderer?.unmaskedRenderer || info.webgl.renderer?.renderer],
['Vendor', info.webgl.renderer?.unmaskedVendor || info.webgl2.renderer?.unmaskedVendor || info.webgl.renderer?.vendor],
['User Agent', navigator.userAgent]
];
const summaryList = document.getElementById('summary-list');
summaryList.replaceChildren();
rows.forEach(([label, value]) => {
const term = document.createElement('dt');
const description = document.createElement('dd');
term.textContent = label;
description.textContent = formatValue(value);
summaryList.append(term, description);
});
}
function runDiagnostics() {
const info = {
generatedAt: new Date().toISOString(),
pageUrl: location.href,
browser: {
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
hardwareConcurrency: navigator.hardwareConcurrency,
deviceMemoryGb: navigator.deviceMemory,
maxTouchPoints: navigator.maxTouchPoints,
webdriver: navigator.webdriver
},
screen: {
width: screen.width,
height: screen.height,
availWidth: screen.availWidth,
availHeight: screen.availHeight,
colorDepth: screen.colorDepth,
pixelDepth: screen.pixelDepth,
devicePixelRatio: window.devicePixelRatio
},
webgl: {},
webgl2: {},
canvas2d: {}
};
try { try {
// Draw some graphics to test acceleration info.webgl = drawWebGL('webgl-canvas', 1);
const gradient = ctx.createLinearGradient(0, 0, 300, 0); setStatus('webgl-status', info.webgl.ok ? 'good' : 'error', info.webgl.ok ? 'Available' : 'Unavailable');
gradient.addColorStop(0, '#ff0000'); setMessage('webgl-message', info.webgl.ok
gradient.addColorStop(1, '#0000ff'); ? formatValue(info.webgl.renderer.unmaskedRenderer || info.webgl.renderer.renderer)
: info.webgl.error);
ctx.fillStyle = gradient; } catch (error) {
ctx.fillRect(0, 0, 300, 150); info.webgl = { ok: false, error: error.message };
setStatus('webgl-status', 'error', 'Failed');
ctx.fillStyle = 'white'; setMessage('webgl-message', error.message);
ctx.font = '20px Arial';
ctx.fillText('Canvas 2D Working!', 50, 80);
status.textContent = 'Canvas 2D: Working ✓';
status.parentElement.className = 'status good';
} catch (err) {
status.textContent = 'Canvas 2D: Error - ' + err.message;
status.parentElement.className = 'status error';
}
} }
// Initialize tests try {
window.addEventListener('DOMContentLoaded', () => { info.webgl2 = drawWebGL('webgl2-canvas', 2);
refreshGPUInfo(); setStatus('webgl2-status', info.webgl2.ok ? 'good' : 'warning', info.webgl2.ok ? 'Available' : 'Unavailable');
testWebGL(); setMessage('webgl2-message', info.webgl2.ok
testCanvas2D(); ? formatValue(info.webgl2.renderer.unmaskedRenderer || info.webgl2.renderer.renderer)
}); : info.webgl2.error);
} catch (error) {
info.webgl2 = { ok: false, error: error.message };
setStatus('webgl2-status', 'warning', 'Failed');
setMessage('webgl2-message', error.message);
}
try {
info.canvas2d = testCanvas2D();
setStatus('canvas2d-status', info.canvas2d.ok ? 'good' : 'error', info.canvas2d.ok ? 'Working' : 'Unavailable');
setMessage('canvas2d-message', info.canvas2d.ok
? `Draw stress test completed in ${info.canvas2d.drawTimeMs} ms.`
: info.canvas2d.error);
} catch (error) {
info.canvas2d = { ok: false, error: error.message };
setStatus('canvas2d-status', 'error', 'Failed');
setMessage('canvas2d-message', error.message);
}
const healthy = info.webgl.ok && info.canvas2d.ok;
setStatus('overall-status', healthy ? 'good' : 'warning', healthy ? 'GPU Path Looks Good' : 'GPU Path Limited');
renderSummary(info);
document.getElementById('gpu-details').textContent = JSON.stringify(info, null, 2);
}
document.getElementById('refresh-button').addEventListener('click', runDiagnostics);
window.addEventListener('DOMContentLoaded', runDiagnostics);
</script> </script>
</body> </body>
</html> </html>
+4
View File
@@ -62,6 +62,10 @@
const box = document.getElementById('targetBox'); const box = document.getElementById('targetBox');
if (target) box.textContent = target; if (target) box.textContent = target;
function sendNavigate(url, opts){ 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){ if (window.electronAPI && window.electronAPI.sendToHost){
window.electronAPI.sendToHost('navigate', url, opts||{}); window.electronAPI.sendToHost('navigate', url, opts||{});
} else if (window.parent && window.parent !== window) { } else if (window.parent && window.parent !== window) {
+1
View File
@@ -9,6 +9,7 @@
<div id="menu-popup" role="menu"> <div id="menu-popup" role="menu">
<button data-cmd="open-settings" role="menuitem">Settings</button> <button data-cmd="open-settings" role="menuitem">Settings</button>
<button data-cmd="big-picture" role="menuitem">🎮 Big Picture Mode</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> <button data-cmd="toggle-devtools" role="menuitem">Toggle Developer Tools</button>
<div class="zoom-controls" role="group" aria-label="Zoom controls"> <div class="zoom-controls" role="group" aria-label="Zoom controls">
<button data-cmd="zoom-out" aria-label="Zoom out">-</button> <button data-cmd="zoom-out" aria-label="Zoom out">-</button>