Step 170: UI event bus
This commit is contained in:
@@ -50,6 +50,7 @@
|
||||
#include "Telemetry.h"
|
||||
#include "UpdateChecker.h"
|
||||
#include "NotificationSystem.h"
|
||||
#include "UIEventBus.h"
|
||||
#include "state/SearchState.h"
|
||||
#include "state/AgentState.h"
|
||||
#include "state/BuildState.h"
|
||||
@@ -174,6 +175,7 @@ struct EditorState {
|
||||
UIFlags ui;
|
||||
|
||||
NotificationSystem notifications;
|
||||
UIEventBus events;
|
||||
HelpPanelState helpPanel;
|
||||
Telemetry telemetry;
|
||||
char outlineFilter[128] = {};
|
||||
@@ -328,10 +330,12 @@ struct EditorState {
|
||||
|
||||
void notify(NotificationLevel level, const std::string& message) {
|
||||
notifications.notify(level, message);
|
||||
events.publish(UIEventType::NotificationPosted, {}, message, ImGui::GetTime());
|
||||
}
|
||||
|
||||
void notify(NotificationLevel level, const std::string& message, const NotificationTarget& target) {
|
||||
notifications.notify(level, message, target);
|
||||
events.publish(UIEventType::NotificationPosted, target.path, message, ImGui::GetTime());
|
||||
}
|
||||
|
||||
void navigateToTarget(const NotificationTarget& target) {
|
||||
@@ -445,8 +449,6 @@ struct EditorState {
|
||||
activeBuffer = state.get();
|
||||
bufferStates[path] = std::move(state);
|
||||
active()->orchestratorDirty = true;
|
||||
library.primitives.setRoot(activeAST());
|
||||
library.primitives.setLanguage(active()->language);
|
||||
recordUndoSnapshot();
|
||||
if (language == "elisp") {
|
||||
emacsState.emacsFunctionIndexDirty = true;
|
||||
@@ -455,9 +457,7 @@ struct EditorState {
|
||||
if (lsp && path.rfind("(untitled", 0) != 0) {
|
||||
lsp->didOpen(toFileUri(path), language, content, activeBuffer->lspVersion);
|
||||
}
|
||||
symbolsPending = true;
|
||||
symbolsLastChange = ImGui::GetTime();
|
||||
if (lsp) lsp->clearDocumentSymbols();
|
||||
events.publish(UIEventType::BufferSwitched, path, {}, ImGui::GetTime());
|
||||
}
|
||||
|
||||
bool jumpToDefinitionLocation(const LSPClient::DefinitionLocation& loc) {
|
||||
@@ -538,14 +538,7 @@ struct EditorState {
|
||||
buffers.switchToBuffer(path);
|
||||
activeBuffer = bufferStates[path].get();
|
||||
if (active()) active()->orchestratorDirty = true;
|
||||
library.primitives.setRoot(activeAST());
|
||||
library.primitives.setLanguage(active()->language);
|
||||
symbolsPending = true;
|
||||
symbolsLastChange = ImGui::GetTime();
|
||||
if (lsp) lsp->clearDocumentSymbols();
|
||||
if (active() && active()->language == "elisp") {
|
||||
emacsState.emacsFunctionIndexDirty = true;
|
||||
}
|
||||
events.publish(UIEventType::BufferSwitched, path, {}, ImGui::GetTime());
|
||||
}
|
||||
|
||||
std::filesystem::path configDir() const {
|
||||
@@ -787,6 +780,28 @@ struct EditorState {
|
||||
loadRecentFiles();
|
||||
loadSettingsFromDisk();
|
||||
registerCommands();
|
||||
events.subscribe(UIEventType::BufferSwitched, [this](const UIEvent&) {
|
||||
if (!active()) return;
|
||||
library.primitives.setRoot(activeAST());
|
||||
library.primitives.setLanguage(active()->language);
|
||||
symbolsPending = true;
|
||||
symbolsLastChange = ImGui::GetTime();
|
||||
if (lsp) lsp->clearDocumentSymbols();
|
||||
if (active()->language == "elisp") {
|
||||
emacsState.emacsFunctionIndexDirty = true;
|
||||
}
|
||||
});
|
||||
events.subscribe(UIEventType::ASTChanged, [this](const UIEvent&) {
|
||||
if (!active()) return;
|
||||
library.primitives.setRoot(activeAST());
|
||||
library.primitives.setLanguage(active()->language);
|
||||
symbolsPending = true;
|
||||
symbolsLastChange = ImGui::GetTime();
|
||||
if (lsp) lsp->clearDocumentSymbols();
|
||||
});
|
||||
events.subscribe(UIEventType::SettingsChanged, [this](const UIEvent&) {
|
||||
applySettingsToState();
|
||||
});
|
||||
startEmacsDaemonFromSettings();
|
||||
initAgentServer();
|
||||
refreshBuildSystem();
|
||||
@@ -1671,6 +1686,9 @@ struct EditorState {
|
||||
std::string prevLang = active()->language;
|
||||
active()->language = lang;
|
||||
active()->mode.setLanguage(lang);
|
||||
if (activeAST()) {
|
||||
library.primitives.setRoot(activeAST());
|
||||
}
|
||||
library.primitives.setLanguage(lang);
|
||||
if (active()->generatedLanguage == prevLang) {
|
||||
active()->generatedLanguage = lang;
|
||||
@@ -1692,6 +1710,11 @@ struct EditorState {
|
||||
active()->bufferMode = BufferManager::BufferMode::Text;
|
||||
buffers.setBufferMode(active()->path, active()->bufferMode);
|
||||
}
|
||||
events.publishDebounced(UIEventType::ASTChanged,
|
||||
active()->path,
|
||||
{},
|
||||
ImGui::GetTime(),
|
||||
0.1);
|
||||
}
|
||||
|
||||
bool handleEmacsKeyChord(const std::string& chord) {
|
||||
@@ -1823,19 +1846,20 @@ struct EditorState {
|
||||
active()->sync.syncNow();
|
||||
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
|
||||
}
|
||||
library.primitives.setRoot(activeAST());
|
||||
library.primitives.setLanguage(active()->language);
|
||||
active()->orchestratorDirty = true;
|
||||
active()->highlightsDirty = true;
|
||||
active()->generatedHighlightsDirty = true;
|
||||
active()->modified = true;
|
||||
symbolsPending = true;
|
||||
symbolsLastChange = ImGui::GetTime();
|
||||
if (lsp) lsp->clearDocumentSymbols();
|
||||
if (lsp && active()->path.rfind("(untitled", 0) != 0) {
|
||||
active()->lspVersion += 1;
|
||||
lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion);
|
||||
}
|
||||
events.publish(UIEventType::FileModified, active()->path, {}, ImGui::GetTime());
|
||||
events.publishDebounced(UIEventType::ASTChanged,
|
||||
active()->path,
|
||||
{},
|
||||
ImGui::GetTime(),
|
||||
0.15);
|
||||
recordUndoSnapshot();
|
||||
}
|
||||
|
||||
|
||||
103
editor/src/UIEventBus.h
Normal file
103
editor/src/UIEventBus.h
Normal file
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
enum class UIEventType {
|
||||
ASTChanged,
|
||||
BufferSwitched,
|
||||
DiagnosticsUpdated,
|
||||
ThemeChanged,
|
||||
SettingsChanged,
|
||||
FileModified,
|
||||
AgentConnected,
|
||||
NotificationPosted
|
||||
};
|
||||
|
||||
struct UIEvent {
|
||||
UIEventType type = UIEventType::ASTChanged;
|
||||
std::string path;
|
||||
std::string detail;
|
||||
double timestamp = 0.0;
|
||||
};
|
||||
|
||||
class UIEventBus {
|
||||
public:
|
||||
using Handler = std::function<void(const UIEvent&)>;
|
||||
|
||||
int subscribe(UIEventType type, Handler handler) {
|
||||
int id = ++nextId;
|
||||
handlers[keyFor(type)].push_back({id, std::move(handler)});
|
||||
return id;
|
||||
}
|
||||
|
||||
void publish(UIEventType type,
|
||||
const std::string& path = {},
|
||||
const std::string& detail = {},
|
||||
double nowSeconds = 0.0) {
|
||||
UIEvent ev;
|
||||
ev.type = type;
|
||||
ev.path = path;
|
||||
ev.detail = detail;
|
||||
ev.timestamp = nowSeconds;
|
||||
dispatch(ev);
|
||||
}
|
||||
|
||||
void publishDebounced(UIEventType type,
|
||||
const std::string& path,
|
||||
const std::string& detail,
|
||||
double nowSeconds,
|
||||
double delaySeconds) {
|
||||
PendingEvent& pendingEvent = pending[keyFor(type)];
|
||||
pendingEvent.event.type = type;
|
||||
pendingEvent.event.path = path;
|
||||
pendingEvent.event.detail = detail;
|
||||
pendingEvent.event.timestamp = nowSeconds;
|
||||
pendingEvent.readyAt = nowSeconds + delaySeconds;
|
||||
}
|
||||
|
||||
void tick(double nowSeconds) {
|
||||
std::vector<int> ready;
|
||||
ready.reserve(pending.size());
|
||||
for (const auto& kv : pending) {
|
||||
if (nowSeconds >= kv.second.readyAt) {
|
||||
ready.push_back(kv.first);
|
||||
}
|
||||
}
|
||||
for (int key : ready) {
|
||||
auto it = pending.find(key);
|
||||
if (it == pending.end()) continue;
|
||||
dispatch(it->second.event);
|
||||
pending.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
struct HandlerEntry {
|
||||
int id = 0;
|
||||
Handler fn;
|
||||
};
|
||||
|
||||
struct PendingEvent {
|
||||
UIEvent event;
|
||||
double readyAt = 0.0;
|
||||
};
|
||||
|
||||
int nextId = 0;
|
||||
std::unordered_map<int, std::vector<HandlerEntry>> handlers;
|
||||
std::unordered_map<int, PendingEvent> pending;
|
||||
|
||||
static int keyFor(UIEventType type) {
|
||||
return static_cast<int>(type);
|
||||
}
|
||||
|
||||
void dispatch(const UIEvent& ev) {
|
||||
auto it = handlers.find(keyFor(ev.type));
|
||||
if (it == handlers.end()) return;
|
||||
for (const auto& entry : it->second) {
|
||||
if (entry.fn) entry.fn(ev);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -253,6 +253,7 @@ int main(int, char**) {
|
||||
state.updateEmacsFunctionIndex();
|
||||
}
|
||||
state.refreshEmacsModeLine(ImGui::GetTime());
|
||||
state.events.tick(ImGui::GetTime());
|
||||
|
||||
// --- Start frame ---
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
|
||||
@@ -9,6 +9,7 @@ static void renderSettingsPanel(EditorState& state) {
|
||||
ImGui::PushFont(state.uiFont);
|
||||
bool settingsChanged = false;
|
||||
bool emacsConfigChanged = false;
|
||||
bool themeChanged = false;
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
int fontSize = state.settings.getFontSize();
|
||||
@@ -57,6 +58,7 @@ static void renderSettingsPanel(EditorState& state) {
|
||||
SetupVSCodeDarkTheme();
|
||||
}
|
||||
settingsChanged = true;
|
||||
themeChanged = true;
|
||||
}
|
||||
|
||||
bool telemetryOptIn = state.settings.getTelemetryOptIn();
|
||||
@@ -158,10 +160,14 @@ static void renderSettingsPanel(EditorState& state) {
|
||||
|
||||
if (settingsChanged) {
|
||||
state.saveSettingsToDisk();
|
||||
state.events.publish(UIEventType::SettingsChanged, {}, {}, ImGui::GetTime());
|
||||
}
|
||||
if (emacsConfigChanged) {
|
||||
state.startEmacsDaemonFromSettings();
|
||||
}
|
||||
if (themeChanged) {
|
||||
state.events.publish(UIEventType::ThemeChanged, {}, {}, ImGui::GetTime());
|
||||
}
|
||||
ImGui::PopFont();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user