Ember
Loading...
Searching...
No Matches
BlackboardScene.cpp
Go to the documentation of this file.
2#include <algorithm>
3#include <cmath>
4#include <wx/dcbuffer.h>
5
6BlackboardScene::BlackboardScene(wxWindow *parent) : m_panel(nullptr), m_scrollPanel(nullptr) {
7 m_panel = new wxPanel(parent, wxID_ANY);
8 m_panel->SetName("BlackboardScene");
9 m_panel->SetBackgroundColour(wxColour(40, 40, 40));
11
12 m_scrollTimer = new wxTimer();
13 m_scrollTimer->Bind(wxEVT_TIMER, &BlackboardScene::OnScrollTimer, this);
14}
15
17 if (m_scrollTimer) {
18 m_scrollTimer->Stop();
19 delete m_scrollTimer;
20 m_scrollTimer = nullptr;
21 }
22}
23
25 wxBoxSizer *sizer = new wxBoxSizer(wxVERTICAL);
26
28 new wxScrolledWindow(m_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxBORDER_NONE);
29 m_scrollPanel->SetBackgroundColour(wxColour(40, 40, 40));
30 m_scrollPanel->SetScrollRate(0, 1);
31 m_scrollPanel->SetBackgroundStyle(wxBG_STYLE_PAINT);
32
33 m_scrollPanel->Bind(wxEVT_PAINT, &BlackboardScene::OnPaint, this);
34 m_scrollPanel->Bind(wxEVT_LEFT_DOWN, &BlackboardScene::OnLeftDown, this);
35 m_scrollPanel->Bind(wxEVT_MOUSEWHEEL, &BlackboardScene::OnMouseWheel, this);
36
37 sizer->Add(m_scrollPanel, 1, wxEXPAND);
38 m_panel->SetSizer(sizer);
39}
40
41void BlackboardScene::SetBlackboards(const std::map<std::string, std::shared_ptr<EmberCore::Blackboard>> &bbs,
42 const std::map<std::string, std::vector<std::string>> &includesMap) {
43 m_blackboards = bbs;
44 m_includesMap = includesMap;
46
47 m_sortedBBIds.clear();
48 m_sortedBBIds.reserve(m_blackboards.size());
49 for (const auto &kv : m_blackboards) {
50 m_sortedBBIds.push_back(kv.first);
51 }
52 std::sort(m_sortedBBIds.begin(), m_sortedBBIds.end());
53
54 if (m_scrollPanel)
55 m_scrollPanel->Refresh();
56}
57
59 m_blackboards.clear();
60 m_includesMap.clear();
61 m_sortedBBIds.clear();
63 m_bbYPositions.clear();
64 if (m_scrollPanel)
65 m_scrollPanel->Refresh();
66}
67
68void BlackboardScene::OnPaint(wxPaintEvent &) {
69 wxAutoBufferedPaintDC dc(m_scrollPanel);
70 m_scrollPanel->DoPrepareDC(dc);
72}
73
75 dc.SetBackground(wxBrush(wxColour(40, 40, 40)));
76 dc.Clear();
77
78 m_bbYPositions.clear();
79
80 if (m_blackboards.empty()) {
81 dc.SetTextForeground(wxColour(150, 150, 150));
82 dc.SetFont(wxFont(11, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_ITALIC, wxFONTWEIGHT_NORMAL));
83 dc.DrawText("No blackboards loaded", 20, 20);
84 return;
85 }
86
87 int panelWidth = m_scrollPanel->GetClientSize().GetWidth();
88 if (panelWidth < 300)
89 panelWidth = 600;
90 int contentWidth = panelWidth - 30;
91 int x = 15;
92 int y = 10;
93
94 // Viewport for culling: only draw items overlapping the visible region
95 int viewY0, viewY1;
96 {
97 int scrollPx = 0;
98 m_scrollPanel->CalcUnscrolledPosition(0, 0, nullptr, &scrollPx);
99 viewY0 = scrollPx;
100 viewY1 = scrollPx + m_scrollPanel->GetClientSize().GetHeight();
101 }
102
103 auto isVisible = [&](int top, int height) { return top + height >= viewY0 && top <= viewY1; };
104
105 // Binary-search style truncation instead of char-by-char removal
106 auto truncate = [&dc](const wxString &text, int maxWidth) -> wxString {
107 if (maxWidth <= 0)
108 return text;
109 if (dc.GetTextExtent(text).GetWidth() <= maxWidth)
110 return text;
111 size_t lo = 0, hi = text.Length();
112 while (lo < hi) {
113 size_t mid = (lo + hi + 1) / 2;
114 if (dc.GetTextExtent(text.Left(mid) + "...").GetWidth() <= maxWidth)
115 lo = mid;
116 else
117 hi = mid - 1;
118 }
119 return lo > 0 ? text.Left(lo) + "..." : "...";
120 };
121
122 // --- Summary Box ---
123 {
124 int summaryHeight = 60;
125 if (isVisible(y, summaryHeight)) {
126 int totalBBs = static_cast<int>(m_blackboards.size());
127 int totalEntries = 0;
128 for (const auto &kv : m_blackboards) {
129 if (kv.second)
130 totalEntries += static_cast<int>(kv.second->GetEntryCount());
131 }
132
133 dc.SetBrush(wxBrush(wxColour(50, 50, 60)));
134 dc.SetPen(wxPen(wxColour(100, 130, 200), 1));
135 dc.DrawRoundedRectangle(x, y, contentWidth, summaryHeight, 5);
136
137 dc.SetTextForeground(wxColour(130, 180, 255));
138 dc.SetFont(wxFont(11, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD));
139 dc.DrawText("Blackboard Overview", x + 10, y + 8);
140
141 dc.SetFont(wxFont(9, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
142 dc.SetTextForeground(wxColour(200, 200, 200));
143 dc.DrawText(wxString::Format("Total: %d blackboard(s), %d entries", totalBBs, totalEntries), x + 15,
144 y + 32);
145 }
146 y += summaryHeight + 15;
147 }
148
149 // Column layout constants
150 int tableX = x + 15;
151 int tableWidth = contentWidth - 40;
152 int col1W = tableWidth * 45 / 100;
153 int col2W = tableWidth * 25 / 100;
154 int col3W = tableWidth - col1W - col2W;
155
156 // Pre-create fonts to avoid repeated construction
157 wxFont headerFont(9, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD);
158 wxFont incFont(9, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
159 wxFont tableHeaderFont(9, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD);
160 wxFont monoFont(9, wxFONTFAMILY_TELETYPE, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
161
162 // --- Per-blackboard cards ---
163 for (const auto &bbId : m_sortedBBIds) {
164 auto bbIt = m_blackboards.find(bbId);
165 if (bbIt == m_blackboards.end() || !bbIt->second)
166 continue;
167
168 const auto &bb = bbIt->second;
169 size_t entryCount = bb->GetEntryCount();
170 bool isCollapsed = m_collapsedBlackboards.count(bbId) > 0;
171
172 // Calculate total height of this card to allow skipping if entirely off-screen
173 int cardHeight = CARD_HEADER_H + 2;
174 if (!isCollapsed && entryCount > 0)
175 cardHeight += ROW_H + static_cast<int>(entryCount) * ROW_H;
176 cardHeight += 10;
177
178 m_bbYPositions[bbId] = y;
179
180 // Skip drawing if entirely outside viewport (but still advance y)
181 if (y > viewY1) {
182 y += cardHeight;
183 continue;
184 }
185 if (y + cardHeight < viewY0) {
186 y += cardHeight;
187 continue;
188 }
189
190 // --- Card header ---
191 if (isVisible(y, CARD_HEADER_H)) {
192 dc.SetBrush(wxBrush(wxColour(70, 50, 90)));
193 dc.SetPen(wxPen(wxColour(150, 100, 200), 1));
194 dc.DrawRoundedRectangle(x + 10, y, contentWidth - 20, CARD_HEADER_H, 3);
195
196 int triX = x + 18;
197 int triY = y + (CARD_HEADER_H / 2) - 4;
198 dc.SetPen(wxPen(wxColour(200, 170, 240), 1));
199 dc.SetBrush(wxBrush(wxColour(200, 170, 240)));
200 if (isCollapsed) {
201 wxPoint tri[3] = {wxPoint(triX, triY), wxPoint(triX, triY + 8), wxPoint(triX + 6, triY + 4)};
202 dc.DrawPolygon(3, tri);
203 } else {
204 wxPoint tri[3] = {wxPoint(triX, triY), wxPoint(triX + 8, triY), wxPoint(triX + 4, triY + 6)};
205 dc.DrawPolygon(3, tri);
206 }
207
208 dc.SetTextForeground(wxColour(220, 180, 255));
209 dc.SetFont(headerFont);
210 wxString headerText =
211 wxString::Format("[%s] (%zu entr%s)", bbId, entryCount, entryCount == 1 ? "y" : "ies");
212 dc.DrawText(headerText, x + 32, y + 4);
213
214 auto incIt = m_includesMap.find(bbId);
215 if (incIt != m_includesMap.end() && !incIt->second.empty()) {
216 dc.SetTextForeground(wxColour(180, 150, 200));
217 dc.SetFont(incFont);
218 wxString incText = "includes: {";
219 for (size_t i = 0; i < incIt->second.size(); ++i) {
220 if (i > 0)
221 incText += " ";
222 incText += wxString::FromUTF8(incIt->second[i]);
223 }
224 incText += "}";
225 int maxW = contentWidth - 250;
226 if (maxW > 0)
227 incText = truncate(incText, maxW);
228 int incTextW = dc.GetTextExtent(incText).GetWidth();
229 dc.DrawText(incText, x + contentWidth - 20 - incTextW, y + 6);
230 }
231 }
232
233 y += CARD_HEADER_H + 2;
234
235 // --- Entry table (only if expanded) ---
236 if (!isCollapsed && entryCount > 0) {
237 // Table header row
238 if (isVisible(y, ROW_H)) {
239 dc.SetBrush(wxBrush(wxColour(55, 55, 65)));
240 dc.SetPen(wxPen(wxColour(80, 80, 100), 1));
241 dc.DrawRectangle(tableX, y, col1W, ROW_H);
242 dc.DrawRectangle(tableX + col1W, y, col2W, ROW_H);
243 dc.DrawRectangle(tableX + col1W + col2W, y, col3W, ROW_H);
244
245 dc.SetTextForeground(wxColour(180, 180, 220));
246 dc.SetFont(tableHeaderFont);
247 dc.DrawText("Key", tableX + 5, y + 4);
248 dc.DrawText("Type", tableX + col1W + 5, y + 4);
249 dc.DrawText("Value", tableX + col1W + col2W + 5, y + 4);
250 }
251 y += ROW_H;
252
253 // Entry rows
254 size_t ei = 0;
255 for (const auto &entryPair : bb->GetEntries()) {
256 const auto &entry = entryPair.second;
257 if (!entry)
258 continue;
259
260 if (isVisible(y, ROW_H)) {
261 wxColour rowBg = (ei % 2 == 0) ? wxColour(45, 45, 50) : wxColour(50, 50, 55);
262 dc.SetBrush(wxBrush(rowBg));
263 dc.SetPen(wxPen(wxColour(60, 60, 70), 1));
264 dc.DrawRectangle(tableX, y, col1W, ROW_H);
265 dc.DrawRectangle(tableX + col1W, y, col2W, ROW_H);
266 dc.DrawRectangle(tableX + col1W + col2W, y, col3W, ROW_H);
267
268 dc.SetFont(monoFont);
269 dc.SetTextForeground(wxColour(200, 200, 200));
270 dc.DrawText(truncate(wxString::FromUTF8(entry->GetKey()), col1W - 10), tableX + 5, y + 4);
271
272 dc.SetTextForeground(wxColour(150, 200, 180));
273 dc.DrawText(truncate(wxString::FromUTF8(entry->GetTypeString()), col2W - 10), tableX + col1W + 5,
274 y + 4);
275
276 dc.SetTextForeground(wxColour(180, 180, 150));
277 dc.DrawText(truncate(wxString::FromUTF8(entry->GetValue()), col3W - 10), tableX + col1W + col2W + 5,
278 y + 4);
279 }
280
281 y += ROW_H;
282 ++ei;
283 }
284 }
285
286 y += 10;
287 }
288
289 m_scrollPanel->SetVirtualSize(panelWidth, y + 20);
290}
291
292void BlackboardScene::OnMouseWheel(wxMouseEvent &event) {
293 if (!m_scrollPanel)
294 return;
295
296 int rotation = event.GetWheelRotation();
297 double delta = -(static_cast<double>(rotation) / event.GetWheelDelta()) * SCROLL_PIXELS_PER_NOTCH;
298
299 m_scrollVelocity += delta;
300
301 int viewStartX, viewStartY;
302 m_scrollPanel->GetViewStart(&viewStartX, &viewStartY);
303 m_scrollY = static_cast<double>(viewStartY);
304
305 if (!m_scrollTimer->IsRunning()) {
306 m_scrollTimer->Start(16);
307 }
308}
309
310void BlackboardScene::OnScrollTimer(wxTimerEvent &) {
311 if (!m_scrollPanel) {
312 m_scrollTimer->Stop();
313 return;
314 }
315
318
319 int virtualH = m_scrollPanel->GetVirtualSize().GetHeight();
320 int clientH = m_scrollPanel->GetClientSize().GetHeight();
321 int maxScroll = std::max(0, virtualH - clientH);
322
323 if (m_scrollY < 0.0)
324 m_scrollY = 0.0;
325 if (m_scrollY > maxScroll)
326 m_scrollY = maxScroll;
327
328 m_scrollPanel->Scroll(-1, static_cast<int>(m_scrollY));
329
330 if (std::abs(m_scrollVelocity) < 0.5) {
331 m_scrollVelocity = 0.0;
332 m_scrollTimer->Stop();
333 }
334}
335
336void BlackboardScene::OnLeftDown(wxMouseEvent &event) {
337 int scrollY = 0;
338 m_scrollPanel->CalcUnscrolledPosition(0, event.GetPosition().y, nullptr, &scrollY);
339
340 for (const auto &kv : m_bbYPositions) {
341 if (scrollY >= kv.second && scrollY < kv.second + CARD_HEADER_H) {
342 if (m_collapsedBlackboards.count(kv.first)) {
343 m_collapsedBlackboards.erase(kv.first);
344 } else {
345 m_collapsedBlackboards.insert(kv.first);
346 }
347 m_scrollPanel->Refresh();
348 return;
349 }
350 }
351
352 event.Skip();
353}
354
355void BlackboardScene::ScrollToBlackboard(const std::string &bbId) {
356 m_collapsedBlackboards.erase(bbId);
357 m_scrollPanel->Refresh();
358 m_scrollPanel->Update();
359
360 auto it = m_bbYPositions.find(bbId);
361 if (it == m_bbYPositions.end())
362 return;
363
364 m_scrollVelocity = 0.0;
365 m_scrollY = static_cast<double>(it->second);
366
367 int virtualH = m_scrollPanel->GetVirtualSize().GetHeight();
368 int clientH = m_scrollPanel->GetClientSize().GetHeight();
369 int maxScroll = std::max(0, virtualH - clientH);
370 if (m_scrollY > maxScroll)
371 m_scrollY = maxScroll;
372
373 m_scrollPanel->Scroll(-1, static_cast<int>(m_scrollY));
374}
wxTimer * m_scrollTimer
void ScrollToBlackboard(const std::string &bbId)
static const int ROW_H
wxScrolledWindow * m_scrollPanel
std::map< std::string, std::shared_ptr< EmberCore::Blackboard > > m_blackboards
static constexpr double SCROLL_FRICTION
void OnMouseWheel(wxMouseEvent &event)
void OnScrollTimer(wxTimerEvent &event)
void OnLeftDown(wxMouseEvent &event)
void DrawBlackboardCards(wxDC &dc)
static const int CARD_HEADER_H
BlackboardScene(wxWindow *parent)
static constexpr double SCROLL_PIXELS_PER_NOTCH
void SetBlackboards(const std::map< std::string, std::shared_ptr< EmberCore::Blackboard > > &bbs, const std::map< std::string, std::vector< std::string > > &includesMap)
std::map< std::string, int > m_bbYPositions
void OnPaint(wxPaintEvent &event)
std::map< std::string, std::vector< std::string > > m_includesMap
std::vector< std::string > m_sortedBBIds
std::set< std::string > m_collapsedBlackboards
~BlackboardScene() override