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
+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) {