Add Big Picture mode and multi-target build

Introduce a Big Picture mode and support building two app targets. CMakeLists was refactored to add a helper (add_nebula_app_target) and now registers NebulaBrowser and NebulaBigPicture executables, moving UI/assets post-build copying into the helper. A new app/main_bigpicture.cpp entry was added and RunNebula now accepts LaunchOptions (AppMode) with a new LaunchOptions struct. NebulaController was extended heavily to manage a BigPicture browser role: creation, enter/exit mode, layout logic, cursor injection/removal, remote input handlers (mouse move/click/wheel/text), and state syncing. Cef/browser_client updated for the new BigPicture role and message filtering. Platform API gained MoveCursorToBrowserPoint with platform stubs/Win implementation. Big-picture UI files (CSS/JS/HTML) were also updated to support the new mode.
This commit is contained in:
Andrew Zambazos
2026-05-18 22:07:41 +12:00
parent b4d93f24cd
commit d6f15c5dce
16 changed files with 1745 additions and 2903 deletions
+96 -80
View File
@@ -35,7 +35,6 @@ SET_CEF_TARGET_OUT_DIR()
# ------------------------------------------------------------
set(NEBULA_COMMON_SOURCES
app/main.cpp
src/app/nebula_controller.cpp
src/app/run.cpp
src/browser/session_state.cpp
@@ -54,10 +53,6 @@ if(OS_WINDOWS)
src/platform/win/browser_host_win.cpp
src/platform/win/nebula_window_win.cpp
)
add_executable(NebulaBrowser WIN32
${NEBULA_COMMON_SOURCES}
${NEBULA_PLATFORM_SOURCES}
)
elseif(OS_MACOSX)
set(NEBULA_PLATFORM_SOURCES
src/platform/mac/paths_mac.cpp
@@ -65,10 +60,6 @@ elseif(OS_MACOSX)
src/platform/mac/browser_host_mac.cpp
src/platform/mac/nebula_window_mac.cpp
)
add_executable(NebulaBrowser MACOSX_BUNDLE
${NEBULA_COMMON_SOURCES}
${NEBULA_PLATFORM_SOURCES}
)
elseif(OS_LINUX)
set(NEBULA_PLATFORM_SOURCES
src/platform/linux/paths_linux.cpp
@@ -76,86 +67,111 @@ elseif(OS_LINUX)
src/platform/linux/browser_host_linux.cpp
src/platform/linux/nebula_window_linux.cpp
)
add_executable(NebulaBrowser
${NEBULA_COMMON_SOURCES}
${NEBULA_PLATFORM_SOURCES}
)
else()
message(FATAL_ERROR "Unsupported platform.")
endif()
SET_EXECUTABLE_TARGET_PROPERTIES(NebulaBrowser)
add_dependencies(NebulaBrowser libcef_dll_wrapper)
target_include_directories(NebulaBrowser PRIVATE
"${CMAKE_SOURCE_DIR}/src"
"${CEF_ROOT}"
"${CEF_ROOT}/include"
)
ADD_LOGICAL_TARGET("libcef_lib" "${CEF_LIB_RELEASE}" "${CEF_LIB_DEBUG}")
target_link_libraries(NebulaBrowser PRIVATE
libcef_lib
libcef_dll_wrapper
${CEF_STANDARD_LIBS}
)
if(MSVC)
set_property(TARGET NebulaBrowser PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>"
)
endif()
# ------------------------------------------------------------
# Platform-specific CEF runtime deployment
# ------------------------------------------------------------
if(OS_WINDOWS)
target_link_libraries(NebulaBrowser PRIVATE dwmapi)
target_compile_definitions(NebulaBrowser PRIVATE
NOMINMAX
WIN32_LEAN_AND_MEAN
)
COPY_FILES("NebulaBrowser" "${CEF_BINARY_FILES}" "${CEF_BINARY_DIR_RELEASE}" "${CEF_TARGET_OUT_DIR}")
COPY_FILES("NebulaBrowser" "${CEF_RESOURCE_FILES}" "${CEF_RESOURCE_DIR}" "${CEF_TARGET_OUT_DIR}")
elseif(OS_LINUX)
if(OS_LINUX)
FIND_LINUX_LIBRARIES("X11")
COPY_FILES("NebulaBrowser" "${CEF_BINARY_FILES}" "${CEF_BINARY_DIR_RELEASE}" "${CEF_TARGET_OUT_DIR}")
COPY_FILES("NebulaBrowser" "${CEF_RESOURCE_FILES}" "${CEF_RESOURCE_DIR}" "${CEF_TARGET_OUT_DIR}")
elseif(OS_MACOSX)
set(NEBULA_APP "${CEF_TARGET_OUT_DIR}/NebulaBrowser.app")
COPY_MAC_FRAMEWORK(
"NebulaBrowser"
"Chromium Embedded Framework"
"${CEF_BINARY_DIR_RELEASE}"
"${NEBULA_APP}/Contents/Frameworks"
)
COPY_FILES(
"NebulaBrowser"
"${CEF_BINARY_FILES}"
"${CEF_BINARY_DIR_RELEASE}"
"${NEBULA_APP}/Contents/Frameworks"
)
COPY_FILES(
"NebulaBrowser"
"${CEF_RESOURCE_FILES}"
"${CEF_RESOURCE_DIR}"
"${NEBULA_APP}/Contents/Resources"
)
endif()
# ------------------------------------------------------------
# Copy Nebula UI files after build
# ------------------------------------------------------------
function(add_nebula_app_target nebula_target entry_source)
if(OS_WINDOWS)
add_executable(${nebula_target} WIN32
${entry_source}
${NEBULA_COMMON_SOURCES}
${NEBULA_PLATFORM_SOURCES}
)
elseif(OS_MACOSX)
add_executable(${nebula_target} MACOSX_BUNDLE
${entry_source}
${NEBULA_COMMON_SOURCES}
${NEBULA_PLATFORM_SOURCES}
)
elseif(OS_LINUX)
add_executable(${nebula_target}
${entry_source}
${NEBULA_COMMON_SOURCES}
${NEBULA_PLATFORM_SOURCES}
)
endif()
add_custom_command(TARGET NebulaBrowser POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/ui"
"$<TARGET_FILE_DIR:NebulaBrowser>/ui"
SET_EXECUTABLE_TARGET_PROPERTIES(${nebula_target})
add_dependencies(${nebula_target} libcef_dll_wrapper)
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/assets"
"$<TARGET_FILE_DIR:NebulaBrowser>/ui/assets"
target_include_directories(${nebula_target} PRIVATE
"${CMAKE_SOURCE_DIR}/src"
"${CEF_ROOT}"
"${CEF_ROOT}/include"
)
COMMENT "Copying Nebula UI files and assets..."
)
target_link_libraries(${nebula_target} PRIVATE
libcef_lib
libcef_dll_wrapper
${CEF_STANDARD_LIBS}
)
if(MSVC)
set_property(TARGET ${nebula_target} PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>"
)
endif()
# ------------------------------------------------------------
# Platform-specific CEF runtime deployment
# ------------------------------------------------------------
if(OS_WINDOWS)
target_link_libraries(${nebula_target} PRIVATE dwmapi)
target_compile_definitions(${nebula_target} PRIVATE
NOMINMAX
WIN32_LEAN_AND_MEAN
)
COPY_FILES("${nebula_target}" "${CEF_BINARY_FILES}" "${CEF_BINARY_DIR_RELEASE}" "${CEF_TARGET_OUT_DIR}")
COPY_FILES("${nebula_target}" "${CEF_RESOURCE_FILES}" "${CEF_RESOURCE_DIR}" "${CEF_TARGET_OUT_DIR}")
elseif(OS_LINUX)
COPY_FILES("${nebula_target}" "${CEF_BINARY_FILES}" "${CEF_BINARY_DIR_RELEASE}" "${CEF_TARGET_OUT_DIR}")
COPY_FILES("${nebula_target}" "${CEF_RESOURCE_FILES}" "${CEF_RESOURCE_DIR}" "${CEF_TARGET_OUT_DIR}")
elseif(OS_MACOSX)
set(NEBULA_APP "${CEF_TARGET_OUT_DIR}/${nebula_target}.app")
COPY_MAC_FRAMEWORK(
"${nebula_target}"
"Chromium Embedded Framework"
"${CEF_BINARY_DIR_RELEASE}"
"${NEBULA_APP}/Contents/Frameworks"
)
COPY_FILES(
"${nebula_target}"
"${CEF_BINARY_FILES}"
"${CEF_BINARY_DIR_RELEASE}"
"${NEBULA_APP}/Contents/Frameworks"
)
COPY_FILES(
"${nebula_target}"
"${CEF_RESOURCE_FILES}"
"${CEF_RESOURCE_DIR}"
"${NEBULA_APP}/Contents/Resources"
)
endif()
# ------------------------------------------------------------
# Copy Nebula UI files after build
# ------------------------------------------------------------
add_custom_command(TARGET ${nebula_target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/ui"
"$<TARGET_FILE_DIR:${nebula_target}>/ui"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/assets"
"$<TARGET_FILE_DIR:${nebula_target}>/ui/assets"
COMMENT "Copying Nebula UI files and assets for ${nebula_target}..."
)
endfunction()
add_nebula_app_target(NebulaBrowser app/main.cpp)
add_nebula_app_target(NebulaBigPicture app/main_bigpicture.cpp)
+2 -2
View File
@@ -12,11 +12,11 @@ int APIENTRY wWinMain(HINSTANCE instance,
UNREFERENCED_PARAMETER(command_line);
const nebula::platform::AppStartup startup{instance, show_command};
return nebula::app::RunNebula(startup);
return nebula::app::RunNebula(startup, {nebula::app::AppMode::Desktop});
}
#else
int main(int argc, char* argv[]) {
const nebula::platform::AppStartup startup{argc, argv};
return nebula::app::RunNebula(startup);
return nebula::app::RunNebula(startup, {nebula::app::AppMode::Desktop});
}
#endif
+22
View File
@@ -0,0 +1,22 @@
#include "app/run.h"
#include "platform/types.h"
#if defined(_WIN32)
#include <windows.h>
int APIENTRY wWinMain(HINSTANCE instance,
HINSTANCE previous_instance,
LPWSTR command_line,
int show_command) {
UNREFERENCED_PARAMETER(previous_instance);
UNREFERENCED_PARAMETER(command_line);
const nebula::platform::AppStartup startup{instance, show_command};
return nebula::app::RunNebula(startup, {nebula::app::AppMode::BigPicture});
}
#else
int main(int argc, char* argv[]) {
const nebula::platform::AppStartup startup{argc, argv};
return nebula::app::RunNebula(startup, {nebula::app::AppMode::BigPicture});
}
#endif
+439 -11
View File
@@ -94,6 +94,54 @@ int ParseTabId(const std::string& value) {
return result.ec == std::errc{} && result.ptr == value.data() + value.size() ? tab_id : 0;
}
bool ParseTwoInts(const std::string& value, int& first, int& second) {
const size_t separator = value.find(',');
if (separator == std::string::npos) {
return false;
}
const std::string first_value = value.substr(0, separator);
const std::string second_value = value.substr(separator + 1);
const auto first_result =
std::from_chars(first_value.data(), first_value.data() + first_value.size(), first);
const auto second_result =
std::from_chars(second_value.data(), second_value.data() + second_value.size(), second);
return first_result.ec == std::errc{} && first_result.ptr == first_value.data() + first_value.size() &&
second_result.ec == std::errc{} && second_result.ptr == second_value.data() + second_value.size();
}
bool ParseFourInts(const std::string& value, int& first, int& second, int& third, int& fourth) {
const size_t first_separator = value.find(',');
if (first_separator == std::string::npos) {
return false;
}
const size_t second_separator = value.find(',', first_separator + 1);
if (second_separator == std::string::npos) {
return false;
}
const size_t third_separator = value.find(',', second_separator + 1);
if (third_separator == std::string::npos) {
return false;
}
const std::string first_value = value.substr(0, first_separator);
const std::string second_value = value.substr(first_separator + 1, second_separator - first_separator - 1);
const std::string third_value = value.substr(second_separator + 1, third_separator - second_separator - 1);
const std::string fourth_value = value.substr(third_separator + 1);
const auto first_result =
std::from_chars(first_value.data(), first_value.data() + first_value.size(), first);
const auto second_result =
std::from_chars(second_value.data(), second_value.data() + second_value.size(), second);
const auto third_result =
std::from_chars(third_value.data(), third_value.data() + third_value.size(), third);
const auto fourth_result =
std::from_chars(fourth_value.data(), fourth_value.data() + fourth_value.size(), fourth);
return first_result.ec == std::errc{} && first_result.ptr == first_value.data() + first_value.size() &&
second_result.ec == std::errc{} && second_result.ptr == second_value.data() + second_value.size() &&
third_result.ec == std::errc{} && third_result.ptr == third_value.data() + third_value.size() &&
fourth_result.ec == std::errc{} && fourth_result.ptr == fourth_value.data() + fourth_value.size();
}
std::string WithCacheBuster(std::string url) {
if (url.empty()) {
return url;
@@ -124,9 +172,12 @@ void SetBrowserVisible(CefRefPtr<CefBrowser> browser, bool visible) {
} // namespace
NebulaController::NebulaController(nebula::platform::AppStartup startup, std::string initial_url)
NebulaController::NebulaController(nebula::platform::AppStartup startup,
std::string initial_url,
LaunchOptions launch_options)
: startup_(startup),
initial_url_(std::move(initial_url)),
launch_options_(launch_options),
tabs_(this),
site_history_(LoadSiteHistory()) {}
@@ -138,6 +189,11 @@ bool NebulaController::Create() {
}
void NebulaController::OnWindowCreated() {
big_picture_mode_ = launch_options_.mode == AppMode::BigPicture;
if (big_picture_mode_ && window_) {
window_->SetFullscreen(true);
}
if (initial_url_.empty()) {
tabs_.CreateInitialTab(nebula::ui::GetHomeUrl());
} else {
@@ -145,8 +201,13 @@ void NebulaController::OnWindowCreated() {
}
PersistSession();
CreateChromeBrowser();
if (!big_picture_mode_) {
CreateChromeBrowser();
}
CreateContentBrowser();
if (big_picture_mode_) {
CreateBigPictureBrowser();
}
}
void NebulaController::OnWindowResized(const nebula::window::BrowserLayout& layout) {
@@ -178,6 +239,9 @@ void NebulaController::OnWindowCloseRequested() {
if (chrome_browser_) {
chrome_browser_->GetHost()->CloseBrowser(true);
}
if (big_picture_browser_) {
big_picture_browser_->GetHost()->CloseBrowser(true);
}
if (menu_popup_browser_) {
menu_popup_browser_->GetHost()->CloseBrowser(true);
}
@@ -200,6 +264,12 @@ void NebulaController::OnActiveTabChanged(const nebula::browser::NebulaTab& tab)
if (chrome_ready_) {
SendChromeState(tab);
}
if (big_picture_ready_) {
SendBigPictureState(tab);
}
if (big_picture_mode_) {
InjectBigPictureCursor(tab.browser);
}
}
void NebulaController::OnBrowserCreated(nebula::cef::BrowserRole role, CefRefPtr<CefBrowser> browser) {
@@ -213,6 +283,13 @@ void NebulaController::OnBrowserCreated(nebula::cef::BrowserRole role, CefRefPtr
if (const auto* tab = tabs_.ActiveTab()) {
SendChromeState(*tab);
}
} else if (role == nebula::cef::BrowserRole::BigPicture) {
big_picture_browser_ = browser;
big_picture_ready_ = true;
SetBrowserVisible(big_picture_browser_, big_picture_mode_);
if (const auto* tab = tabs_.ActiveTab()) {
SendBigPictureState(*tab);
}
} else if (role == nebula::cef::BrowserRole::MenuPopup) {
menu_popup_browser_ = browser;
menu_popup_visible_ = true;
@@ -229,6 +306,11 @@ void NebulaController::OnBrowserClosing(nebula::cef::BrowserRole role, CefRefPtr
if (role == nebula::cef::BrowserRole::Chrome) {
chrome_browser_ = nullptr;
chrome_ready_ = false;
} else if (role == nebula::cef::BrowserRole::BigPicture) {
big_picture_browser_ = nullptr;
big_picture_client_ = nullptr;
big_picture_ready_ = false;
big_picture_mode_ = false;
} else if (role == nebula::cef::BrowserRole::MenuPopup) {
menu_popup_browser_ = nullptr;
menu_popup_client_ = nullptr;
@@ -277,8 +359,7 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
CloseMenuPopup();
tabs_.LoadURL(nebula::ui::GetSettingsUrl());
} else if (command == "big-picture") {
CloseMenuPopup();
tabs_.LoadURL(nebula::ui::GetBigPictureUrl());
EnterBigPictureMode();
} else if (command == "gpu-diagnostics") {
CloseMenuPopup();
tabs_.LoadURL(nebula::ui::GetGpuDiagnosticsUrl());
@@ -306,6 +387,25 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
if (auto* tab = tabs_.ActiveTab(); tab && tab->browser) {
InjectSettingsHistory(tab->browser);
}
if (auto* tab = tabs_.ActiveTab()) {
SendBigPictureState(*tab);
}
} else if (command == "clear-search-history") {
if (auto* tab = tabs_.ActiveTab()) {
SendBigPictureState(*tab);
}
} else if (command == "bigpicture-mouse-move") {
SendBigPictureMouseMove(payload);
} else if (command == "bigpicture-click") {
SendBigPictureMouseClick(payload, false);
} else if (command == "bigpicture-right-click") {
SendBigPictureMouseClick(payload, true);
} else if (command == "bigpicture-scroll") {
SendBigPictureMouseWheel(payload);
} else if (command == "bigpicture-text") {
SendBigPictureText(payload);
} else if (command == "bigpicture-browse-visible") {
SetBigPictureBrowseVisible(payload == "1" || payload == "true");
} else if (command == "minimize" && window_) {
window_->Minimize();
} else if (command == "maximize" && window_) {
@@ -313,7 +413,11 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
} else if (command == "close" && window_) {
OnWindowCloseRequested();
} else if (command == "exit-bigpicture" && window_) {
OnWindowCloseRequested();
if (launch_options_.mode == AppMode::BigPicture) {
OnWindowCloseRequested();
return;
}
ExitBigPictureMode();
} else if (command == "drag" && window_) {
window_->BeginDrag();
}
@@ -326,6 +430,9 @@ void NebulaController::OnContentAddressChanged(CefRefPtr<CefBrowser> browser, co
? nebula::ui::GetHomeUrl()
: internal_url);
RecordSiteHistory(internal_url);
if (const auto* active_tab = tabs_.ActiveTab()) {
SendBigPictureState(*active_tab);
}
PersistSession();
}
@@ -350,6 +457,7 @@ void NebulaController::OnContentLoadFinished(CefRefPtr<CefBrowser> browser, cons
if (nebula::ui::ToInternalUrl(url).starts_with(nebula::ui::GetSettingsUrl())) {
InjectSettingsHistory(browser);
}
InjectBigPictureCursor(browser);
}
void NebulaController::OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) {
@@ -457,7 +565,7 @@ void NebulaController::CreateChromeBrowser() {
return;
}
const auto layout = window_->CurrentLayout();
const auto layout = CurrentBrowserLayout();
CefBrowserSettings browser_settings = BrowserSettings();
chrome_client_ = new nebula::cef::NebulaBrowserClient(nebula::cef::BrowserRole::Chrome, this);
CefWindowInfo window_info =
@@ -466,6 +574,25 @@ void NebulaController::CreateChromeBrowser() {
window_info, chrome_client_, nebula::ui::GetChromeUrl(), browser_settings, nullptr, nullptr);
}
void NebulaController::CreateBigPictureBrowser() {
if (!window_ || !window_->native_handle()) {
return;
}
const auto layout = CurrentBrowserLayout();
CefBrowserSettings browser_settings = BrowserSettings();
big_picture_client_ = new nebula::cef::NebulaBrowserClient(nebula::cef::BrowserRole::BigPicture, this);
CefWindowInfo window_info =
nebula::platform::MakeChildWindowInfo(window_->native_handle(), layout.chrome);
CefBrowserHost::CreateBrowser(
window_info,
big_picture_client_,
nebula::ui::ResolveInternalUrl(nebula::ui::GetBigPictureUrl()),
browser_settings,
nullptr,
nullptr);
}
void NebulaController::CreateContentBrowser() {
if (!window_ || !window_->native_handle()) {
return;
@@ -473,7 +600,7 @@ void NebulaController::CreateContentBrowser() {
const auto* tab = tabs_.ActiveTab();
const std::string url = tab && !tab->url.empty() ? tab->url : nebula::ui::GetHomeUrl();
const auto layout = window_->CurrentLayout();
const auto layout = CurrentBrowserLayout();
CefBrowserSettings browser_settings = BrowserSettings();
content_client_ = new nebula::cef::NebulaBrowserClient(nebula::cef::BrowserRole::Content, this);
CefWindowInfo window_info =
@@ -482,7 +609,94 @@ void NebulaController::CreateContentBrowser() {
window_info, content_client_, nebula::ui::ResolveInternalUrl(url), browser_settings, nullptr, nullptr);
}
void NebulaController::EnterBigPictureMode() {
if (big_picture_mode_) {
return;
}
CloseMenuPopup();
if (content_fullscreen_) {
SetContentFullscreen(false);
}
big_picture_mode_ = true;
big_picture_browse_visible_ = false;
if (auto* tab = tabs_.ActiveTab()) {
InjectBigPictureCursor(tab->browser);
}
SetBrowserVisible(chrome_browser_, false);
if (big_picture_browser_) {
SetBrowserVisible(big_picture_browser_, true);
if (const auto* tab = tabs_.ActiveTab()) {
SendBigPictureState(*tab);
}
} else {
CreateBigPictureBrowser();
}
ResizeBrowsers();
}
void NebulaController::ExitBigPictureMode() {
if (!big_picture_mode_) {
return;
}
big_picture_mode_ = false;
big_picture_browse_visible_ = false;
if (auto* tab = tabs_.ActiveTab()) {
RemoveBigPictureCursor(tab->browser);
}
SetBrowserVisible(big_picture_browser_, false);
SetBrowserVisible(chrome_browser_, true);
if (const auto* tab = tabs_.ActiveTab()) {
SendChromeState(*tab);
}
ResizeBrowsers();
}
nebula::window::BrowserLayout NebulaController::CurrentBrowserLayout() const {
if (!window_) {
return {};
}
if (!big_picture_mode_) {
return window_->CurrentLayout(!content_fullscreen_);
}
const auto client_size = nebula::platform::ParentClientSize(window_->native_handle());
if (!big_picture_browse_visible_) {
nebula::window::BrowserLayout layout;
layout.chrome = {0, 0, client_size.first, client_size.second};
layout.content = {};
return layout;
}
nebula::window::BrowserLayout layout;
layout.chrome = {0, 0, client_size.first, client_size.second};
// left_margin must clear the 220px sidebar; right_margin must clear the
// native-browser-panel (clamp(168,18vw,260) + 20px right inset ≈ 280px max).
// top/bottom margins must clear the header (~68px) and footer (~56px).
const int left_margin = nebula::platform::ScaleForParentWindow(window_->native_handle(), 224);
const int right_margin = nebula::platform::ScaleForParentWindow(window_->native_handle(), 284);
const int top_margin = nebula::platform::ScaleForParentWindow(window_->native_handle(), 68);
const int bottom_margin = nebula::platform::ScaleForParentWindow(window_->native_handle(), 56);
const int available_width = std::max(0, client_size.first - left_margin - right_margin);
const int available_height = std::max(0, client_size.second - top_margin - bottom_margin);
layout.content = {
left_margin,
top_margin,
available_width,
available_height};
return layout;
}
void NebulaController::ToggleMenuPopup() {
if (big_picture_mode_) {
return;
}
if (menu_popup_browser_ && menu_popup_visible_) {
CloseMenuPopup();
return;
@@ -507,7 +721,7 @@ void NebulaController::CloseMenuPopup() {
}
void NebulaController::CreateMenuPopupBrowser() {
if (!window_ || !window_->native_handle() || content_fullscreen_) {
if (!window_ || !window_->native_handle() || content_fullscreen_ || big_picture_mode_) {
return;
}
@@ -591,6 +805,110 @@ void NebulaController::FreshReload() {
tabs_.LoadURL(WithCacheBuster(tab->url));
}
void NebulaController::SendBigPictureMouseMove(const std::string& payload) {
auto* tab = tabs_.ActiveTab();
if (!tab || !tab->browser) {
return;
}
int x = 0;
int y = 0;
if (!ParseTwoInts(payload, x, y)) {
return;
}
CefMouseEvent event = {};
event.x = x;
event.y = y;
nebula::platform::MoveCursorToBrowserPoint(tab->browser->GetHost()->GetWindowHandle(), x, y);
tab->browser->GetHost()->SendMouseMoveEvent(event, false);
}
void NebulaController::SendBigPictureMouseClick(const std::string& payload, bool right_click) {
auto* tab = tabs_.ActiveTab();
if (!tab || !tab->browser) {
return;
}
int x = 0;
int y = 0;
if (!ParseTwoInts(payload, x, y)) {
return;
}
CefMouseEvent event = {};
event.x = x;
event.y = y;
const auto button = right_click ? MBT_RIGHT : MBT_LEFT;
nebula::platform::MoveCursorToBrowserPoint(tab->browser->GetHost()->GetWindowHandle(), x, y);
tab->browser->GetHost()->SendMouseMoveEvent(event, false);
tab->browser->GetHost()->SendMouseClickEvent(event, button, false, 1);
tab->browser->GetHost()->SendMouseClickEvent(event, button, true, 1);
}
void NebulaController::SendBigPictureMouseWheel(const std::string& payload) {
auto* tab = tabs_.ActiveTab();
if (!tab || !tab->browser) {
return;
}
int delta_x = 0;
int delta_y = 0;
int x = 0;
int y = 0;
const bool has_pointer = ParseFourInts(payload, delta_x, delta_y, x, y);
if (!has_pointer && !ParseTwoInts(payload, delta_x, delta_y)) {
return;
}
const auto layout = CurrentBrowserLayout();
CefMouseEvent event = {};
event.x = has_pointer ? std::clamp(x, 0, std::max(0, layout.content.width - 1))
: std::max(0, layout.content.width / 2);
event.y = has_pointer ? std::clamp(y, 0, std::max(0, layout.content.height - 1))
: std::max(0, layout.content.height / 2);
tab->browser->GetHost()->SendMouseWheelEvent(event, delta_x, delta_y);
}
void NebulaController::SendBigPictureText(const std::string& payload) {
auto* tab = tabs_.ActiveTab();
if (!tab || !tab->browser) {
return;
}
const std::string script =
"(function(){"
"const el=document.activeElement;"
"if(!el)return;"
"const value=\"" + nebula::browser::JsonEscape(payload) + "\";"
"const editable=el.tagName==='INPUT'||el.tagName==='TEXTAREA'||el.isContentEditable;"
"if(!editable)return;"
"if(el.isContentEditable){el.textContent=value;}else{el.value=value;}"
"el.dispatchEvent(new Event('input',{bubbles:true}));"
"el.dispatchEvent(new Event('change',{bubbles:true}));"
"el.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',keyCode:13,bubbles:true}));"
"el.dispatchEvent(new KeyboardEvent('keyup',{key:'Enter',keyCode:13,bubbles:true}));"
"})();";
tab->browser->GetMainFrame()->ExecuteJavaScript(script, tab->url, 0);
}
void NebulaController::SetBigPictureBrowseVisible(bool visible) {
if (big_picture_browse_visible_ == visible) {
return;
}
big_picture_browse_visible_ = visible;
if (visible) {
if (auto* tab = tabs_.ActiveTab()) {
InjectBigPictureCursor(tab->browser);
}
}
ResizeBrowsers();
if (const auto* tab = tabs_.ActiveTab()) {
SendBigPictureState(*tab);
}
}
void NebulaController::SetContentFullscreen(bool fullscreen) {
if (content_fullscreen_ == fullscreen) {
return;
@@ -599,6 +917,7 @@ void NebulaController::SetContentFullscreen(bool fullscreen) {
content_fullscreen_ = fullscreen;
if (fullscreen) {
CloseMenuPopup();
ExitBigPictureMode();
}
SetBrowserVisible(chrome_browser_, !fullscreen);
@@ -613,18 +932,24 @@ void NebulaController::ResizeBrowsers() {
return;
}
const auto layout = window_->CurrentLayout(!content_fullscreen_);
const auto layout = CurrentBrowserLayout();
if (chrome_browser_) {
window_->ResizeChild(
chrome_browser_->GetHost()->GetWindowHandle(),
layout.chrome);
}
if (big_picture_browser_) {
window_->ResizeChild(
big_picture_browser_->GetHost()->GetWindowHandle(),
layout.chrome);
}
if (const auto* tab = tabs_.ActiveTab(); tab && tab->browser) {
SetBrowserVisible(tab->browser, !big_picture_mode_ || big_picture_browse_visible_);
window_->ResizeChild(
tab->browser->GetHost()->GetWindowHandle(),
layout.content);
}
if (!content_fullscreen_) {
if (!content_fullscreen_ && !big_picture_mode_) {
PositionMenuPopup();
}
}
@@ -672,6 +997,71 @@ void NebulaController::SendChromeState(const nebula::browser::NebulaTab& tab) {
chrome_browser_->GetMainFrame()->ExecuteJavaScript(script, nebula::ui::GetChromeUrl(), 0);
}
void NebulaController::SendBigPictureState(const nebula::browser::NebulaTab& tab) {
if (!big_picture_browser_) {
return;
}
const std::string display_url = GetChromeDisplayUrl(tab.url);
double zoom_level = 0.0;
if (tab.browser) {
zoom_level = tab.browser->GetHost()->GetZoomLevel();
}
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) + "\"" +
",\"url\":\"" + nebula::browser::JsonEscape(item.url) + "\"" +
",\"isLoading\":" + std::string(item.is_loading ? "true" : "false") +
",\"favicon\":\"" + nebula::browser::JsonEscape(item.favicon_url) + "\"" +
"}";
}
tabs_json += "]";
std::string history_json = "[";
for (size_t i = 0; i < site_history_.size(); ++i) {
if (i > 0) {
history_json += ",";
}
history_json += "\"" + nebula::browser::JsonEscape(site_history_[i]) + "\"";
}
history_json += "]";
const auto layout = CurrentBrowserLayout();
const std::string script =
"window.NebulaBigPicture && window.NebulaBigPicture.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) + "\"" +
",\"zoomLevel\":" + std::to_string(zoom_level) +
",\"browserLayout\":{"
"\"x\":" + std::to_string(layout.content.x) +
",\"y\":" + std::to_string(layout.content.y) +
",\"width\":" + std::to_string(layout.content.width) +
",\"height\":" + std::to_string(layout.content.height) +
"}" +
",\"tabs\":" + tabs_json +
",\"history\":" + history_json +
"});";
big_picture_browser_->GetMainFrame()->ExecuteJavaScript(
script,
nebula::ui::GetBigPictureUrl(),
0);
}
void NebulaController::RecordSiteHistory(const std::string& url) {
if (!IsSiteHistoryUrl(url)) {
return;
@@ -699,6 +1089,44 @@ void NebulaController::InjectSettingsHistory(CefRefPtr<CefBrowser> browser) {
browser->GetMainFrame()->ExecuteJavaScript(script, nebula::ui::GetSettingsUrl(), 0);
}
void NebulaController::InjectBigPictureCursor(CefRefPtr<CefBrowser> browser) {
if (!big_picture_mode_ || !browser) {
return;
}
static constexpr char kScript[] = R"JS(
(function(){
const id = 'nebula-bigpicture-custom-cursor';
const cursor = 'url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cpath%20d%3D%22M5%203v24l6.6-6.4%204.1%208.5%204.3-2.1-4.1-8.3H25L5%203z%22%20fill%3D%22%2300C6FF%22%20stroke%3D%22%23FFFFFF%22%20stroke-width%3D%222.2%22%20stroke-linejoin%3D%22round%22%2F%3E%3Cpath%20d%3D%22M9%207.7v9.8l2.5-2.4%202%204.1%201.5-.7-2-4h4L9%207.7z%22%20fill%3D%22%237B2EFF%22%20opacity%3D%220.8%22%2F%3E%3C%2Fsvg%3E") 5 3, auto';
const css = 'html, body, body * { cursor: ' + cursor + ' !important; }';
let style = document.getElementById(id);
if (!style) {
style = document.createElement('style');
style.id = id;
(document.head || document.documentElement).appendChild(style);
}
style.textContent = css;
})();
)JS";
browser->GetMainFrame()->ExecuteJavaScript(kScript, browser->GetMainFrame()->GetURL(), 0);
}
void NebulaController::RemoveBigPictureCursor(CefRefPtr<CefBrowser> browser) {
if (!browser) {
return;
}
static constexpr char kScript[] = R"JS(
(function(){
const style = document.getElementById('nebula-bigpicture-custom-cursor');
if (style) {
style.remove();
}
})();
)JS";
browser->GetMainFrame()->ExecuteJavaScript(kScript, browser->GetMainFrame()->GetURL(), 0);
}
void NebulaController::PersistSession() const {
nebula::browser::SaveSessionState(tabs_.Tabs(), tabs_.ActiveTabIndex());
}
@@ -708,7 +1136,7 @@ void NebulaController::MaybeFinishShutdown() {
return;
}
if (chrome_browser_ || menu_popup_browser_ || tabs_.HasOpenBrowsers()) {
if (chrome_browser_ || big_picture_browser_ || menu_popup_browser_ || tabs_.HasOpenBrowsers()) {
return;
}
+22 -1
View File
@@ -5,6 +5,7 @@
#include <unordered_set>
#include <vector>
#include "app/run.h"
#include "browser/tab_manager.h"
#include "cef/browser_client.h"
#include "platform/types.h"
@@ -16,7 +17,9 @@ class NebulaController final : public nebula::window::WindowDelegate,
public nebula::browser::TabObserver,
public nebula::cef::BrowserClientDelegate {
public:
NebulaController(nebula::platform::AppStartup startup, std::string initial_url);
NebulaController(nebula::platform::AppStartup startup,
std::string initial_url,
LaunchOptions launch_options = {});
~NebulaController() override;
bool Create();
@@ -45,7 +48,11 @@ private:
void ActivateTab(int tab_id);
void CloseTab(int tab_id);
void CreateChromeBrowser();
void CreateBigPictureBrowser();
void CreateContentBrowser();
void EnterBigPictureMode();
void ExitBigPictureMode();
nebula::window::BrowserLayout CurrentBrowserLayout() const;
void ToggleMenuPopup();
void CloseMenuPopup();
void CreateMenuPopupBrowser();
@@ -54,27 +61,41 @@ private:
void ToggleDevTools();
void AdjustZoom(double delta);
void FreshReload();
void SendBigPictureMouseMove(const std::string& payload);
void SendBigPictureMouseClick(const std::string& payload, bool right_click);
void SendBigPictureMouseWheel(const std::string& payload);
void SendBigPictureText(const std::string& payload);
void SetBigPictureBrowseVisible(bool visible);
void SetContentFullscreen(bool fullscreen);
void ResizeBrowsers();
void SendChromeState(const nebula::browser::NebulaTab& tab);
void SendBigPictureState(const nebula::browser::NebulaTab& tab);
void RecordSiteHistory(const std::string& url);
void InjectSettingsHistory(CefRefPtr<CefBrowser> browser);
void InjectBigPictureCursor(CefRefPtr<CefBrowser> browser);
void RemoveBigPictureCursor(CefRefPtr<CefBrowser> browser);
void PersistSession() const;
void MaybeFinishShutdown();
bool ForgetClosingTabBrowser(CefRefPtr<CefBrowser> browser);
nebula::platform::AppStartup startup_;
std::string initial_url_;
LaunchOptions launch_options_;
bool closing_ = false;
bool chrome_ready_ = false;
bool big_picture_ready_ = false;
bool big_picture_mode_ = false;
bool big_picture_browse_visible_ = false;
bool content_fullscreen_ = false;
bool menu_popup_visible_ = false;
std::unique_ptr<nebula::window::NebulaWindow> window_;
nebula::browser::TabManager tabs_;
CefRefPtr<CefBrowser> chrome_browser_;
CefRefPtr<CefBrowser> big_picture_browser_;
CefRefPtr<CefBrowser> menu_popup_browser_;
CefRefPtr<nebula::cef::NebulaBrowserClient> chrome_client_;
CefRefPtr<nebula::cef::NebulaBrowserClient> big_picture_client_;
CefRefPtr<nebula::cef::NebulaBrowserClient> content_client_;
CefRefPtr<nebula::cef::NebulaBrowserClient> menu_popup_client_;
std::vector<CefRefPtr<CefBrowser>> closing_tab_browsers_;
+2 -2
View File
@@ -9,7 +9,7 @@
namespace nebula::app {
int RunNebula(const nebula::platform::AppStartup& startup) {
int RunNebula(const nebula::platform::AppStartup& startup, LaunchOptions options) {
nebula::platform::PrepareApp();
const CefMainArgs main_args = nebula::platform::MakeMainArgs(startup);
@@ -41,7 +41,7 @@ int RunNebula(const nebula::platform::AppStartup& startup) {
initial_url = nebula::ui::GetHomeUrl();
}
NebulaController controller(startup, std::move(initial_url));
NebulaController controller(startup, std::move(initial_url), options);
const bool created = controller.Create();
if (created) {
CefRunMessageLoop();
+10 -1
View File
@@ -4,6 +4,15 @@
namespace nebula::app {
int RunNebula(const nebula::platform::AppStartup& startup);
enum class AppMode {
Desktop,
BigPicture,
};
struct LaunchOptions {
AppMode mode = AppMode::Desktop;
};
int RunNebula(const nebula::platform::AppStartup& startup, LaunchOptions options = {});
} // namespace nebula::app
+32 -1
View File
@@ -35,6 +35,33 @@ bool IsBigPictureFrame(CefRefPtr<CefFrame> frame) {
url.starts_with("nebula://big-picture");
}
bool IsBigPictureCommand(const std::string& command) {
return command == "navigate" ||
command == "new-tab" ||
command == "activate-tab" ||
command == "close-tab" ||
command == "back" ||
command == "forward" ||
command == "reload" ||
command == "stop" ||
command == "home" ||
command == "settings" ||
command == "open-settings" ||
command == "big-picture" ||
command == "exit-bigpicture" ||
command == "gpu-diagnostics" ||
command == "zoom-out" ||
command == "zoom-in" ||
command == "clear-site-history" ||
command == "clear-search-history" ||
command == "bigpicture-mouse-move" ||
command == "bigpicture-click" ||
command == "bigpicture-right-click" ||
command == "bigpicture-scroll" ||
command == "bigpicture-text" ||
command == "bigpicture-browse-visible";
}
std::vector<std::string> ToStringVector(const std::vector<CefString>& values) {
std::vector<std::string> result;
result.reserve(values.size());
@@ -62,7 +89,7 @@ bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser
}
if (role_ != BrowserRole::Chrome && role_ != BrowserRole::Content &&
role_ != BrowserRole::MenuPopup) {
role_ != BrowserRole::BigPicture && role_ != BrowserRole::MenuPopup) {
return false;
}
@@ -83,6 +110,10 @@ bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser
!allowed_big_picture_command) {
return false;
}
} else if (role_ == BrowserRole::BigPicture) {
if (!IsBigPictureFrame(frame) || !IsBigPictureCommand(command)) {
return false;
}
}
if (delegate_ && !command.empty()) {
+1
View File
@@ -16,6 +16,7 @@ namespace nebula::cef {
enum class BrowserRole {
Chrome,
Content,
BigPicture,
MenuPopup,
};
+1
View File
@@ -13,6 +13,7 @@ CefWindowInfo MakeDevToolsPopup(NativeWindow parent, const char* title);
void ResizeBrowserWindow(NativeWindow browser_window, const Rect& rect);
void SetBrowserVisible(NativeWindow browser_window, bool visible);
void RaiseBrowserWindow(NativeWindow browser_window);
void MoveCursorToBrowserPoint(NativeWindow browser_window, int x, int y);
Rect MenuPopupRect(NativeWindow parent, const BrowserLayout& layout);
std::string CacheBusterToken();
void DestroyTopLevelWindow(NativeWindow window);
@@ -34,6 +34,12 @@ void RaiseBrowserWindow(NativeWindow browser_window) {
UNREFERENCED_PARAMETER(browser_window);
}
void MoveCursorToBrowserPoint(NativeWindow browser_window, int x, int y) {
UNREFERENCED_PARAMETER(browser_window);
UNREFERENCED_PARAMETER(x);
UNREFERENCED_PARAMETER(y);
}
int ScaleForParentWindow(NativeWindow parent, int value) {
UNREFERENCED_PARAMETER(parent);
return value;
+6
View File
@@ -32,6 +32,12 @@ void RaiseBrowserWindow(NativeWindow browser_window) {
UNREFERENCED_PARAMETER(browser_window);
}
void MoveCursorToBrowserPoint(NativeWindow browser_window, int x, int y) {
UNREFERENCED_PARAMETER(browser_window);
UNREFERENCED_PARAMETER(x);
UNREFERENCED_PARAMETER(y);
}
int ScaleForParentWindow(NativeWindow parent, int value) {
UNREFERENCED_PARAMETER(parent);
return value;
+14
View File
@@ -75,6 +75,20 @@ void RaiseBrowserWindow(NativeWindow browser_window) {
SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
void MoveCursorToBrowserPoint(NativeWindow browser_window, int x, int y) {
const HWND hwnd = AsHwnd(browser_window);
if (!hwnd) {
return;
}
POINT point = {x, y};
if (!ClientToScreen(hwnd, &point)) {
return;
}
SetCursorPos(point.x, point.y);
}
int ScaleForParentWindow(NativeWindow parent, int value) {
const HWND hwnd = AsHwnd(parent);
if (!hwnd) {
+237 -1
View File
@@ -132,6 +132,83 @@ body.mouse-active {
left: -100px;
}
.browser-stage-frame {
position: fixed;
left: var(--browser-stage-x, 20%);
top: var(--browser-stage-y, 10%);
width: var(--browser-stage-width, 60%);
height: var(--browser-stage-height, 80%);
z-index: 3;
pointer-events: none;
border-radius: var(--bp-radius-xl);
box-shadow:
0 0 0 2px rgba(255, 255, 255, 0.08),
0 0 0 6px rgba(123, 46, 255, 0.18),
0 24px 80px rgba(0, 0, 0, 0.55),
0 0 80px var(--bp-primary-glow);
opacity: 1;
transition: opacity var(--bp-transition-normal), box-shadow var(--bp-transition-normal);
}
.browser-stage-frame.hidden {
opacity: 0;
}
.virtual-cursor {
position: fixed;
left: 0;
top: 0;
z-index: 250;
width: 34px;
height: 34px;
pointer-events: none;
transform: translate3d(var(--virtual-cursor-x, 50vw), var(--virtual-cursor-y, 50vh), 0);
transition: opacity var(--bp-transition-fast);
filter: drop-shadow(0 8px 18px rgba(0, 0, 0, 0.55));
}
.virtual-cursor.hidden {
opacity: 0;
}
.virtual-cursor::before {
content: '';
position: absolute;
left: 4px;
top: 3px;
width: 18px;
height: 24px;
background: linear-gradient(135deg, var(--bp-text), var(--bp-accent));
clip-path: polygon(0 0, 0 100%, 38% 76%, 60% 100%, 84% 86%, 62% 62%, 100% 62%);
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.45), 0 0 20px var(--bp-accent-glow);
}
.virtual-cursor-dot {
position: absolute;
left: 10px;
top: 10px;
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--bp-primary);
opacity: 0;
transform: scale(1);
}
.virtual-cursor.clicking .virtual-cursor-dot {
animation: virtual-cursor-click 180ms ease-out;
}
.virtual-cursor.right-clicking .virtual-cursor-dot {
background: var(--bp-warning);
animation: virtual-cursor-click 180ms ease-out;
}
@keyframes virtual-cursor-click {
0% { opacity: 0.95; transform: scale(0.8); }
100% { opacity: 0; transform: scale(4.4); }
}
@keyframes glow-pulse {
0% { transform: scale(1); opacity: 0.3; }
100% { transform: scale(1.2); opacity: 0.5; }
@@ -220,6 +297,15 @@ body.mouse-active {
color: var(--bp-text-muted);
}
.status-icon.controller-status.connected {
color: var(--bp-success);
box-shadow: 0 0 20px rgba(74, 222, 128, 0.25);
}
.status-icon.controller-status.disconnected {
color: var(--bp-text-dim);
}
.status-icon .material-symbols-outlined {
font-size: 20px;
}
@@ -338,8 +424,9 @@ body.mouse-active {
inset: 0;
width: 100%;
height: 100%;
background: var(--bp-bg);
background: transparent;
z-index: 2;
pointer-events: none;
}
.webview-container.hidden {
@@ -353,6 +440,155 @@ body.mouse-active {
border: none;
}
.native-browser-panel {
position: absolute;
top: var(--bp-spacing-md);
right: var(--bp-spacing-md);
bottom: var(--bp-spacing-md);
width: clamp(168px, 18vw, 260px);
overflow-y: auto;
padding: var(--bp-spacing-sm);
background: linear-gradient(180deg, rgba(10,10,15,0.88) 0%, rgba(20,20,31,0.82) 100%);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--bp-radius-lg);
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.35);
backdrop-filter: blur(20px);
pointer-events: auto;
}
.native-page-card {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: var(--bp-spacing-sm);
padding: var(--bp-spacing-sm);
margin-bottom: var(--bp-spacing-md);
background: rgba(20, 20, 31, 0.78);
border: 2px solid var(--bp-border);
border-radius: var(--bp-radius-lg);
cursor: pointer;
}
.native-page-card:focus,
.native-page-card.focused {
outline: none;
border-color: var(--bp-accent);
box-shadow: var(--bp-focus-ring-accent);
}
.native-page-card .material-symbols-outlined {
font-size: 28px;
color: var(--bp-accent);
}
.native-page-card h2 {
font-size: 0.95rem;
color: var(--bp-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.native-page-card p {
font-size: 0.72rem;
color: var(--bp-text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.native-page-card .controller-note {
grid-column: 1 / -1;
margin-top: 4px;
color: var(--bp-accent);
white-space: normal;
}
.browser-actions {
display: grid;
grid-template-columns: 1fr;
gap: var(--bp-spacing-xs);
margin-bottom: var(--bp-spacing-lg);
}
.action-button:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.tab-list {
display: flex;
flex-direction: column;
gap: var(--bp-spacing-xs);
}
.bp-tab {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-areas:
"dot title close"
"dot url close";
align-items: center;
column-gap: var(--bp-spacing-sm);
padding: var(--bp-spacing-sm);
background: var(--bp-surface);
border: 2px solid var(--bp-border);
border-radius: var(--bp-radius-md);
color: var(--bp-text);
cursor: pointer;
text-align: left;
}
.bp-tab.active {
border-color: var(--bp-primary);
background: var(--bp-surface-active);
}
.bp-tab:focus,
.bp-tab.focused {
outline: none;
border-color: var(--bp-accent);
box-shadow: var(--bp-focus-ring-accent);
}
.tab-dot {
grid-area: dot;
color: var(--bp-accent);
}
.tab-text {
grid-area: title;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tab-url {
grid-area: url;
color: var(--bp-text-muted);
font-size: 0.8rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tab-close-inline {
grid-area: close;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: var(--bp-radius-sm);
color: var(--bp-text-muted);
}
.tab-close-inline:hover {
background: var(--bp-danger);
color: var(--bp-text);
}
/* Content area */
.bp-content {
flex: 1;
+817 -2797
View File
File diff suppressed because it is too large Load Diff
+38 -7
View File
@@ -26,6 +26,10 @@
<div class="bg-particles"></div>
<div class="bg-glow"></div>
</div>
<div id="browser-stage-frame" class="browser-stage-frame hidden" aria-hidden="true"></div>
<div id="virtual-cursor" class="virtual-cursor hidden" aria-hidden="true">
<div class="virtual-cursor-dot"></div>
</div>
<!-- Top header bar -->
<header class="bp-header">
@@ -41,6 +45,9 @@
</div>
<div class="header-right">
<div class="status-icons">
<span id="bp-controller-status" class="status-icon controller-status disconnected" title="Controller disconnected">
<span class="material-symbols-outlined">sports_esports</span>
</span>
<span id="bp-wifi" class="status-icon" title="Connected">
<span class="material-symbols-outlined">wifi</span>
</span>
@@ -72,6 +79,10 @@
<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>
@@ -147,6 +158,26 @@
</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 from this profile</p>
</div>
<div class="section-actions">
<button class="action-btn danger" id="bp-clear-history" data-focusable tabindex="0">
<span class="material-symbols-outlined">delete</span>
<span>Clear History</span>
</button>
</div>
<div class="list-container" id="historyList">
<div class="empty-state">
<span class="material-symbols-outlined">history</span>
<p>No browsing history</p>
</div>
</div>
</section>
<!-- Downloads section -->
<section id="section-downloads" class="bp-section">
<div class="section-header">
@@ -332,7 +363,7 @@
</div>
</div>
<div class="option-control">
<button class="action-button" id="bp-clear-history" data-focusable tabindex="0">
<button class="action-button" id="bp-clear-history-settings" data-focusable tabindex="0">
<span class="material-symbols-outlined">delete</span>
<span>Clear</span>
</button>
@@ -403,28 +434,28 @@
<!-- Bottom controller hints -->
<footer class="bp-footer">
<div class="controller-hints">
<div class="controller-hints" id="controller-hints">
<div class="hint">
<span class="controller-btn dpad">
<span class="material-symbols-outlined">gamepad</span>
</span>
<span>Navigate</span>
<span id="hint-navigate">Navigate</span>
</div>
<div class="hint">
<span class="controller-btn a-btn">A</span>
<span>Select</span>
<span id="hint-a">Select</span>
</div>
<div class="hint">
<span class="controller-btn b-btn">B</span>
<span>Back</span>
<span id="hint-b">Back</span>
</div>
<div class="hint">
<span class="controller-btn y-btn">Y</span>
<span>Search</span>
<span id="hint-y">Search</span>
</div>
<div class="hint">
<span class="controller-btn menu-btn"></span>
<span>Menu</span>
<span id="hint-menu">Menu</span>
</div>
</div>
</footer>