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:
+96
-80
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace nebula::cef {
|
||||
enum class BrowserRole {
|
||||
Chrome,
|
||||
Content,
|
||||
BigPicture,
|
||||
MenuPopup,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
+800
-2780
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user