Add NebulaController, tab manager, and CEF clients

Introduce core application structure and browser management: add NebulaController and run entry (src/app/*) to centralize window, tab and CEF lifecycle logic; implement TabManager and NebulaTab (src/browser/*) for tab creation, navigation and state tracking; add URL utilities (NormalizeNavigationInput, JsonEscape) and CEF browser client glue (src/cef/browser_client.cpp/.h) to forward chrome commands and content events. Update app/main.cpp to delegate startup to nebula::app::RunNebula. Add UI assets (chrome.html, chrome.css, chrome.js, lucide, menu-popup updates) and remove obsolete nebot.html. Update CMakeLists to include new sources, add ${CMAKE_SOURCE_DIR}/src to includes and link dwmapi on Windows. Overall this refactors startup and splits responsibilities for cleaner tab and browser lifecycle handling.
This commit is contained in:
Andrew Zambazos
2026-05-14 10:18:51 +12:00
parent 207a849f06
commit a8786b4c1c
23 changed files with 2835 additions and 330 deletions
+13
View File
@@ -0,0 +1,13 @@
#include "browser/tab.h"
namespace nebula::browser {
bool NebulaTab::CanGoBack() const {
return browser && browser->CanGoBack();
}
bool NebulaTab::CanGoForward() const {
return browser && browser->CanGoForward();
}
} // namespace nebula::browser
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <string>
#include <vector>
#include "include/cef_browser.h"
namespace nebula::browser {
struct NebulaTab {
int id = 1;
std::string url;
std::string title = "New Tab";
bool is_loading = false;
double load_progress = 0.0;
std::string favicon_url;
CefRefPtr<CefBrowser> browser;
bool CanGoBack() const;
bool CanGoForward() const;
};
} // namespace nebula::browser
+239
View File
@@ -0,0 +1,239 @@
#include "browser/tab_manager.h"
#include "browser/url_utils.h"
namespace nebula::browser {
TabManager::TabManager(TabObserver* observer) : observer_(observer) {}
NebulaTab& TabManager::CreateInitialTab(std::string initial_url) {
tabs_.clear();
NebulaTab tab;
tab.id = next_tab_id_++;
tab.url = std::move(initial_url);
tabs_.push_back(std::move(tab));
active_tab_id_ = tabs_.front().id;
Notify();
return tabs_.front();
}
NebulaTab& TabManager::CreateTab(std::string url) {
NebulaTab tab;
tab.id = next_tab_id_++;
tab.url = std::move(url);
tabs_.push_back(std::move(tab));
active_tab_id_ = tabs_.back().id;
Notify();
return tabs_.back();
}
NebulaTab* TabManager::ActiveTab() {
for (auto& tab : tabs_) {
if (tab.id == active_tab_id_) {
return &tab;
}
}
return nullptr;
}
const NebulaTab* TabManager::ActiveTab() const {
for (const auto& tab : tabs_) {
if (tab.id == active_tab_id_) {
return &tab;
}
}
return nullptr;
}
const std::vector<NebulaTab>& TabManager::Tabs() const {
return tabs_;
}
bool TabManager::ActivateTab(int tab_id) {
if (!FindTab(tab_id)) {
return false;
}
active_tab_id_ = tab_id;
Notify();
return true;
}
CefRefPtr<CefBrowser> TabManager::CloseTab(int tab_id) {
for (auto it = tabs_.begin(); it != tabs_.end(); ++it) {
if (it->id != tab_id) {
continue;
}
CefRefPtr<CefBrowser> browser = it->browser;
const bool was_active = it->id == active_tab_id_;
const auto next_it = tabs_.erase(it);
if (tabs_.empty()) {
active_tab_id_ = 0;
} else if (was_active) {
active_tab_id_ = next_it != tabs_.end() ? next_it->id : tabs_.back().id;
}
Notify();
return browser;
}
return nullptr;
}
void TabManager::SetActiveBrowser(CefRefPtr<CefBrowser> browser) {
if (NebulaTab* tab = ActiveTab()) {
tab->browser = browser;
if (browser && tab->url.empty()) {
tab->url = browser->GetMainFrame()->GetURL();
}
Notify();
}
}
bool TabManager::OwnsBrowser(CefRefPtr<CefBrowser> browser) const {
if (!browser) {
return false;
}
for (const auto& tab : tabs_) {
if (tab.browser && tab.browser->IsSame(browser)) {
return true;
}
}
return false;
}
bool TabManager::HasOpenBrowsers() const {
for (const auto& tab : tabs_) {
if (tab.browser) {
return true;
}
}
return false;
}
void TabManager::ClearBrowser(CefRefPtr<CefBrowser> browser) {
if (NebulaTab* tab = FindTab(browser)) {
tab->browser = nullptr;
Notify();
}
}
void TabManager::LoadURL(const std::string& input) {
NebulaTab* tab = ActiveTab();
if (!tab || !tab->browser) {
return;
}
const std::string target = NormalizeNavigationInput(input);
if (target.empty()) {
return;
}
tab->url = target;
tab->favicon_url.clear();
tab->browser->GetMainFrame()->LoadURL(target);
Notify();
}
void TabManager::GoBack() {
NebulaTab* tab = ActiveTab();
if (tab && tab->browser && tab->browser->CanGoBack()) {
tab->browser->GoBack();
}
}
void TabManager::GoForward() {
NebulaTab* tab = ActiveTab();
if (tab && tab->browser && tab->browser->CanGoForward()) {
tab->browser->GoForward();
}
}
void TabManager::Reload() {
NebulaTab* tab = ActiveTab();
if (tab && tab->browser) {
tab->browser->Reload();
}
}
void TabManager::StopLoad() {
NebulaTab* tab = ActiveTab();
if (tab && tab->browser) {
tab->browser->StopLoad();
}
}
void TabManager::UpdateURL(CefRefPtr<CefBrowser> browser, std::string url) {
if (NebulaTab* tab = FindTab(browser)) {
tab->url = std::move(url);
Notify();
}
}
void TabManager::UpdateTitle(CefRefPtr<CefBrowser> browser, std::string title) {
if (NebulaTab* tab = FindTab(browser)) {
tab->title = title.empty() ? "New Tab" : std::move(title);
Notify();
}
}
void TabManager::UpdateLoadingState(CefRefPtr<CefBrowser> browser, bool is_loading) {
if (NebulaTab* tab = FindTab(browser)) {
tab->is_loading = is_loading;
if (is_loading) {
tab->favicon_url.clear();
}
if (!is_loading) {
tab->load_progress = 1.0;
}
Notify();
}
}
void TabManager::UpdateLoadProgress(CefRefPtr<CefBrowser> browser, double progress) {
if (NebulaTab* tab = FindTab(browser)) {
tab->load_progress = progress;
Notify();
}
}
void TabManager::UpdateFavicon(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) {
if (NebulaTab* tab = FindTab(browser)) {
tab->favicon_url = urls.empty() ? std::string{} : urls.front();
Notify();
}
}
void TabManager::Notify() {
const NebulaTab* tab = ActiveTab();
if (observer_ && tab) {
observer_->OnActiveTabChanged(*tab);
}
}
NebulaTab* TabManager::FindTab(int tab_id) {
for (auto& tab : tabs_) {
if (tab.id == tab_id) {
return &tab;
}
}
return nullptr;
}
NebulaTab* TabManager::FindTab(CefRefPtr<CefBrowser> browser) {
if (!browser) {
return nullptr;
}
for (auto& tab : tabs_) {
if (tab.browser && tab.browser->IsSame(browser)) {
return &tab;
}
}
return nullptr;
}
} // namespace nebula::browser
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include <string>
#include <vector>
#include "browser/tab.h"
namespace nebula::browser {
class TabObserver {
public:
virtual ~TabObserver() = default;
virtual void OnActiveTabChanged(const NebulaTab& tab) = 0;
};
class TabManager {
public:
explicit TabManager(TabObserver* observer);
NebulaTab& CreateInitialTab(std::string initial_url);
NebulaTab& CreateTab(std::string url);
NebulaTab* ActiveTab();
const NebulaTab* ActiveTab() const;
const std::vector<NebulaTab>& Tabs() const;
bool ActivateTab(int tab_id);
CefRefPtr<CefBrowser> CloseTab(int tab_id);
void SetActiveBrowser(CefRefPtr<CefBrowser> browser);
bool OwnsBrowser(CefRefPtr<CefBrowser> browser) const;
void ClearBrowser(CefRefPtr<CefBrowser> browser);
bool HasOpenBrowsers() const;
void LoadURL(const std::string& input);
void GoBack();
void GoForward();
void Reload();
void StopLoad();
void UpdateURL(CefRefPtr<CefBrowser> browser, std::string url);
void UpdateTitle(CefRefPtr<CefBrowser> browser, std::string title);
void UpdateLoadingState(CefRefPtr<CefBrowser> browser, bool is_loading);
void UpdateLoadProgress(CefRefPtr<CefBrowser> browser, double progress);
void UpdateFavicon(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls);
private:
void Notify();
NebulaTab* FindTab(int tab_id);
NebulaTab* FindTab(CefRefPtr<CefBrowser> browser);
TabObserver* observer_ = nullptr;
std::vector<NebulaTab> tabs_;
int active_tab_id_ = 0;
int next_tab_id_ = 1;
};
} // namespace nebula::browser
+110
View File
@@ -0,0 +1,110 @@
#include "browser/url_utils.h"
#include <cctype>
#include <iomanip>
#include <sstream>
namespace nebula::browser {
namespace {
constexpr char kSearchUrl[] = "https://www.google.com/search?q=";
std::string Trim(std::string value) {
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.front()))) {
value.erase(value.begin());
}
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.back()))) {
value.pop_back();
}
return value;
}
bool StartsWithScheme(const std::string& value) {
return value.starts_with("http://") ||
value.starts_with("https://") ||
value.starts_with("file:") ||
value.starts_with("data:") ||
value.starts_with("blob:") ||
value.starts_with("chrome:");
}
bool LooksLikeHostName(const std::string& value) {
return value.find('.') != std::string::npos &&
value.find_first_of(" \t\r\n") == std::string::npos;
}
std::string UrlEncodeSearch(const std::string& value) {
std::ostringstream encoded;
encoded << std::hex << std::uppercase;
for (unsigned char ch : value) {
if (std::isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '~') {
encoded << static_cast<char>(ch);
} else if (ch == ' ') {
encoded << '+';
} else {
encoded << '%' << std::setw(2) << std::setfill('0') << static_cast<int>(ch);
}
}
return encoded.str();
}
} // namespace
std::string NormalizeNavigationInput(const std::string& input) {
const std::string value = Trim(input);
if (value.empty()) {
return {};
}
if (StartsWithScheme(value)) {
return value;
}
if (LooksLikeHostName(value)) {
return "https://" + value;
}
return std::string(kSearchUrl) + UrlEncodeSearch(value);
}
std::string JsonEscape(const std::string& value) {
std::ostringstream escaped;
for (unsigned char ch : value) {
switch (ch) {
case '\\':
escaped << "\\\\";
break;
case '"':
escaped << "\\\"";
break;
case '\b':
escaped << "\\b";
break;
case '\f':
escaped << "\\f";
break;
case '\n':
escaped << "\\n";
break;
case '\r':
escaped << "\\r";
break;
case '\t':
escaped << "\\t";
break;
default:
if (ch < 0x20) {
escaped << "\\u" << std::hex << std::uppercase << std::setw(4)
<< std::setfill('0') << static_cast<int>(ch);
} else {
escaped << static_cast<char>(ch);
}
break;
}
}
return escaped.str();
}
} // namespace nebula::browser
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <string>
namespace nebula::browser {
std::string NormalizeNavigationInput(const std::string& input);
std::string JsonEscape(const std::string& value);
} // namespace nebula::browser