RunicVTT Open Source Virtual Tabletop for TTRPG using P2P
Loading...
Searching...
No Matches
NoteEditorUI.cpp
Go to the documentation of this file.
1#include "NoteEditorUI.h"
2#include "ImGuiToaster.h"
3#include <algorithm>
4
5//static bool InputTextMultilineString(const char* label,
6// std::string* str,
7// const ImVec2& size = ImVec2(0, 0),
8// ImGuiInputTextFlags flags = 0)
9//{
10// IM_ASSERT(str);
11// auto cb = [](ImGuiInputTextCallbackData* data) -> int
12// {
13// if (data->EventFlag == ImGuiInputTextFlags_CallbackResize)
14// {
15// auto* s = static_cast<std::string*>(data->UserData);
16// s->resize(static_cast<size_t>(data->BufTextLen));
17// data->Buf = s->data();
18// }
19// return 0;
20// };
21//
22// flags |= ImGuiInputTextFlags_CallbackResize;
23// flags |= ImGuiInputTextFlags_NoHorizontalScroll;
24//
25// return ImGui::InputTextMultiline(label, str->data(), str->size() + 1, size, flags, cb, str);
26//}
27
29{
30 std::string* buf = nullptr;
31 float max_px = 0.f;
32};
33
34static int InputTextMultilineWrapCallback(ImGuiInputTextCallbackData* data)
35{
36 auto* ctx = static_cast<InputTextWrapCtx*>(data->UserData);
37 if (!ctx || !ctx->buf)
38 return 0;
39
40 // Keep Dear ImGui's string-resize contract
41 if (data->EventFlag == ImGuiInputTextFlags_CallbackResize)
42 {
43 ctx->buf->resize(static_cast<size_t>(data->BufTextLen));
44 data->Buf = ctx->buf->data();
45 return 0;
46 }
47
48 if (data->EventFlag == ImGuiInputTextFlags_CallbackEdit)
49 {
50 // Current line range [start, end)
51 const int len = data->BufTextLen;
52 int cur = data->CursorPos;
53 int start = cur;
54 while (start > 0 && data->Buf[start - 1] != '\n')
55 start--;
56 int end = cur;
57 while (end < len && data->Buf[end] != '\n')
58 end++;
59
60 const char* line_start = data->Buf + start;
61 const char* line_end = data->Buf + end;
62
63 // If current line fits, done.
64 ImVec2 sz = ImGui::CalcTextSize(line_start, line_end, false, FLT_MAX);
65 if (sz.x <= ctx->max_px || ctx->max_px <= 0.0f)
66 return 0;
67
68 // Find a nice break point (space/tab/hyphen/slash) before cursor
69 int break_pos = -1;
70 for (int i = cur - 1; i >= start; --i)
71 {
72 char c = data->Buf[i];
73 if (c == ' ' || c == '\t' || c == '-' || c == '/')
74 {
75 // Would the line up to here fit?
76 ImVec2 sz2 = ImGui::CalcTextSize(line_start, data->Buf + i, false, FLT_MAX);
77 if (sz2.x <= ctx->max_px)
78 {
79 break_pos = i;
80 break;
81 }
82 }
83 }
84
85 if (break_pos >= start)
86 {
87 // Replace that separator with a newline
88 data->DeleteChars(break_pos, 1);
89 data->InsertChars(break_pos, "\n");
90 // Let Dear ImGui re-evaluate next frame; avoid chaining multiple edits now
91 return 0;
92 }
93 else
94 {
95 // Force break at cursor if no separator found that fits
96 data->InsertChars(cur, "\n");
97 data->CursorPos = cur + 1;
98 return 0;
99 }
100 }
101 return 0;
102}
103
104static bool InputTextMultilineString_HardWrap(const char* label,
105 std::string* str,
106 const ImVec2& size,
107 float max_px_line,
108 ImGuiInputTextFlags flags = 0)
109{
110 IM_ASSERT(str);
111 if (str->capacity() == 0)
112 str->reserve(1);
113
115 ctx.buf = str;
116 ctx.max_px = std::max(0.0f, max_px_line);
117
118 flags |= ImGuiInputTextFlags_CallbackResize;
119 flags |= ImGuiInputTextFlags_CallbackEdit;
120 flags |= ImGuiInputTextFlags_NoHorizontalScroll;
121
122 return ImGui::InputTextMultiline(label,
123 str->data(),
124 str->capacity() + 1, // bind to capacity; resize callback will keep this valid
125 size,
126 flags,
128 &ctx);
129}
130
131NoteEditorUI::NoteEditorUI(std::shared_ptr<NotesManager> notes_manager, std::shared_ptr<ImGuiToaster> toaster) :
132 notes_manager(std::move(notes_manager)), toaster_(std::move(toaster)) {}
133
134void NoteEditorUI::setActiveTable(std::optional<std::string> tableName)
135{
136 (void)tableName; // reserved for future save-locations UX
137}
138
140{
141 if (!visible_)
142 return;
143
144 const ImU32 winBg = IM_COL32(16, 16, 18, 255);
145 ImGui::PushStyleColor(ImGuiCol_WindowBg, winBg);
146 ImGui::SetNextWindowSizeConstraints(ImVec2(800, 600), ImVec2(FLT_MAX, FLT_MAX));
147 bool open = true;
148 ImGui::Begin("Notes", &open);
149
150 const float fullW = ImGui::GetContentRegionAvail().x;
151 const float fullH = ImGui::GetContentRegionAvail().y;
152
153 ImGui::BeginChild("##Dir", ImVec2(leftWidth_, fullH), true);
154 renderDirectory_(fullH);
155 ImGui::EndChild();
156
157 ImGui::SameLine();
158 ImGui::InvisibleButton("##splitter", ImVec2(6, fullH));
159 if (ImGui::IsItemActive())
160 {
161 leftWidth_ += ImGui::GetIO().MouseDelta.x;
162 leftWidth_ = std::clamp(leftWidth_, 180.0f, fullW - 240.0f);
163 }
164 ImGui::SameLine();
165
166 // Let ImGui compute exact remaining width to avoid h-scroll.
167 ImGui::BeginChild("##Tabs", ImVec2(0.f, fullH), true,
168 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
169 renderTabsArea_(/*unused*/ 0.f, fullH);
170 ImGui::EndChild();
171
172 ImGui::End();
173 ImGui::PopStyleColor(1); // <-- only 1 was pushed
174
175 if (!open)
176 visible_ = false;
177
178 // ---- CREATE NOTE MODAL ----
180 ImGui::OpenPopup("Create Note"); // one-shot
181
182 bool createOpen = true;
183 if (ImGui::BeginPopupModal("Create Note", &createOpen, ImGuiWindowFlags_AlwaysAutoResize))
184 {
185 ImGui::InputText("Title", createTitle_, sizeof(createTitle_));
187 ImGui::InputText("Author", createAuthor_, sizeof(createAuthor_));
189
190 if (ImGui::Button("Create"))
191 {
192 std::string title = createTitle_[0] ? createTitle_ : "Untitled";
193 std::string author = createAuthor_[0] ? createAuthor_ : "unknown";
194 auto id = notes_manager->createNote(title, author);
195 openOrFocusTab(id);
196 createTitle_[0] = 0;
197 createAuthor_[0] = 0;
198 showCreatePopup_ = false;
199 ImGui::CloseCurrentPopup();
200 }
201 ImGui::SameLine();
202 if (ImGui::Button("Cancel"))
203 {
204 showCreatePopup_ = false;
205 ImGui::CloseCurrentPopup();
206 }
207
208 ImGui::EndPopup();
209 }
210 else
211 {
212 // If not visible anymore (closed via ESC or X), clear flag
213 showCreatePopup_ = false;
214 }
215
216 // ---- DELETE NOTE MODAL ----
218 ImGui::OpenPopup("Delete Note?");
219
220 bool delOpen = true;
221 if (ImGui::BeginPopupModal("Delete Note?", &delOpen, ImGuiWindowFlags_AlwaysAutoResize))
222 {
223 static bool alsoDisk = false;
224 ImGui::Checkbox("Also delete from disk", &alsoDisk);
225
226 if (ImGui::Button("Delete"))
227 {
228 if (!pendingDeleteUuid_.empty())
229 {
230 notes_manager->deleteNote(pendingDeleteUuid_, alsoDisk);
231 auto it = std::find(openTabs_.begin(), openTabs_.end(), pendingDeleteUuid_);
232 if (it != openTabs_.end())
233 {
234 int idx = (int)std::distance(openTabs_.begin(), it);
235 closeTab_(idx);
236 }
237 }
238 pendingDeleteUuid_.clear();
239 alsoDisk = false;
240 showDeletePopup_ = false;
241 ImGui::CloseCurrentPopup();
242 }
243 ImGui::SameLine();
244 if (ImGui::Button("Cancel"))
245 {
246 pendingDeleteUuid_.clear();
247 showDeletePopup_ = false;
248 ImGui::CloseCurrentPopup();
249 }
250
251 ImGui::EndPopup();
252 }
253 else
254 {
255 // ESC or close -> reset state
256 if (!delOpen)
257 {
258 pendingDeleteUuid_.clear();
259 showDeletePopup_ = false;
260 }
261 }
262}
263void NoteEditorUI::renderDirectory_(float /*height*/)
264{
265 if (ImGui::Button("New"))
266 {
267 showCreatePopup_ = true;
268 }
269 ImGui::SameLine();
270 if (ImGui::Button("Reload"))
271 {
272 notes_manager->loadAllFromDisk();
273 }
274 ImGui::Separator();
275
276 ImGui::SetNextItemWidth(-1);
277 ImGui::InputTextWithHint("##search", "Search title/author/text...", searchBuf_, sizeof(searchBuf_));
279 ImGui::Separator();
280
281 if (ImGui::CollapsingHeader("My Notes", ImGuiTreeNodeFlags_DefaultOpen))
282 {
283 auto mine = notes_manager->listMyNotes();
284 if (mine.empty())
285 {
286 ImGui::TextDisabled("(none)");
287 }
288 else
289 {
290 for (auto& n : mine)
291 {
292 if (!n || !filterMatch_(*n))
293 continue;
294 ImGui::PushID(n->uuid.c_str());
295
296 const bool selected = (std::find(openTabs_.begin(), openTabs_.end(), n->uuid) != openTabs_.end());
297 const std::string label = n->title.empty() ? n->uuid : n->title; // ← no '*' or '(unsaved)'
298 if (ImGui::Selectable(label.c_str(), selected))
299 openOrFocusTab(n->uuid);
300
301 ImGui::PopID();
302 }
303 }
304 }
305
306 ImGui::Separator();
307
308 //if (ImGui::CollapsingHeader("Shared Inbox", ImGuiTreeNodeFlags_DefaultOpen))
309 //{
310 // auto inbox = notes_manager->listInbox();
311 // if (inbox.empty())
312 // {
313 // ImGui::TextDisabled("(empty)");
314 // }
315 // else
316 // {
317 // for (auto& n : inbox)
318 // {
319 // if (!n || !filterMatch_(*n))
320 // continue;
321 // ImGui::PushID(n->uuid.c_str());
322
323 // const bool selected = (std::find(openTabs_.begin(), openTabs_.end(), n->uuid) != openTabs_.end());
324 // const std::string label = (n->title.empty() ? n->uuid : n->title) + " [from: " + (n->shared_from ? *n->shared_from : "?") + "]";
325 // if (ImGui::Selectable(label.c_str(), selected))
326 // openOrFocusTab(n->uuid);
327
328 // ImGui::SameLine();
329 // if (ImGui::SmallButton("Save locally"))
330 // actSaveInboxToLocal_(n->uuid);
331
332 // ImGui::PopID();
333 // }
334 // }
335 //}
336}
337
338void NoteEditorUI::renderTabsArea_(float /*width*/, float /*height*/)
339{
340 if (openTabs_.empty())
341 {
342 ImGui::TextDisabled("No notes open. Select a note on the left or create a new one.");
343 return;
344 }
345
346 if (ImGui::BeginTabBar("##NoteTabs",
347 ImGuiTabBarFlags_AutoSelectNewTabs |
348 ImGuiTabBarFlags_Reorderable))
349 {
350 for (int i = 0; i < (int)openTabs_.size(); ++i)
351 {
352 const std::string& uuid = openTabs_[i];
353 auto n = notes_manager->getNote(uuid);
354 if (!n)
355 {
356 if (currentTabIndex_ == i)
357 currentTabIndex_ = -1;
358 closeTab_(i);
359 --i;
360 continue;
361 }
362
363 // Visible label stays constant; ID is after '###'
364 const std::string visible = n->title.empty() ? n->uuid : n->title;
365 const std::string label = visible + "###" + uuid;
366
367 ImGuiTabItemFlags tif = 0;
368 if (n->dirty)
369 tif |= ImGuiTabItemFlags_UnsavedDocument;
370
371 bool open = true;
372 if (ImGui::BeginTabItem(label.c_str(), &open, tif))
373 {
375
376 const float availW = ImGui::GetContentRegionAvail().x;
377 const float availH = ImGui::GetContentRegionAvail().y;
378 renderOneTab_(uuid, availW, availH);
379
380 ImGui::EndTabItem();
381 }
382 if (!open)
383 {
384 closeTab_(i);
385 if (currentTabIndex_ >= (int)openTabs_.size())
386 currentTabIndex_ = (int)openTabs_.size() - 1;
387 --i;
388 }
389 }
390 ImGui::EndTabBar();
391 }
392}
393
394void NoteEditorUI::renderOneTab_(const std::string& uuid, float availW, float /*availH*/)
395{
396 auto n = notes_manager->getNote(uuid);
397 if (!n)
398 return;
399
400 ImGuiIO& io = ImGui::GetIO();
401 const bool windowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_RootAndChildWindows);
402 if (windowFocused && io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_S, false))
403 actSaveNote_(uuid);
404
405 if (ImGui::Button(n->open_editor ? "Hide Editor" : "Show Editor"))
406 toggleOpenEditor_(uuid);
407 ImGui::SameLine();
408 if (ImGui::Button("Save"))
409 actSaveNote_(uuid);
410 ImGui::SameLine();
411 if (ImGui::Button("Delete"))
412 {
413 pendingDeleteUuid_ = uuid;
414 showDeletePopup_ = true;
415 return;
416 }
417 ImGui::SameLine();
418 ImGui::TextDisabled("%s", n->saved_locally ? n->file_path.filename().string().c_str() : "(unsaved)");
419 ImGui::Separator();
420
421 const float spacing = ImGui::GetStyle().ItemSpacing.x;
422 const float splitterW = 6.f;
423
424 float editorW = 0.f, viewerW = 0.f;
425 if (n->open_editor)
426 {
427 const float shared = availW - splitterW - spacing;
428 editorW = floorf(shared * 0.5f);
429 viewerW = shared - editorW; // exact remainder => no 1px overflow
430 }
431 else
432 {
433 viewerW = availW;
434 }
435
436 // --- Editor ---
437 if (n->open_editor)
438 {
439 ImGui::BeginChild("##editor", ImVec2(editorW, 0.f), true); // 0 height => fill
440 auto& ts = tabState_[uuid];
441 if (!ts.bufferInit)
442 {
443 ts.editBuffer = n->markdown_text;
444 ts.bufferInit = true;
445 }
446
447 ImVec2 editorSize = ImGui::GetContentRegionAvail();
448 float wrap_px = editorSize.x - ImGui::GetStyle().FramePadding.x * 2.0f;
449
450 // hard-wrap (inserts '\n' when exceeding width)
452 &ts.editBuffer,
453 editorSize,
454 wrap_px,
455 ImGuiInputTextFlags_AllowTabInput);
457 ImGui::EndChild();
458
459 ImGui::SameLine();
460 ImGui::InvisibleButton("##split2", ImVec2(splitterW, 0.f));
461 ImGui::SameLine();
462 }
463
464 // --- Viewer ---
465 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.f, 8.f)); // padding to avoid clipped glyphs
466 ImGui::BeginChild("##viewer", ImVec2(viewerW, 0.f), true);
467 {
468 const auto toMs = NotesManager::toEpochMillis;
469 ImGui::TextDisabled("Author: %s", n->author.c_str());
470 ImGui::SameLine();
471 ImGui::TextDisabled("Created: %lld", (long long)toMs(n->creation_ts));
472 ImGui::SameLine();
473 ImGui::TextDisabled("Updated: %lld", (long long)toMs(n->last_update_ts));
474 ImGui::Separator();
475
476 // only set handlers we need; leave external open to MarkdownRenderer default
477 md_.onRoll = [this](const std::string& expr)
478 {
479 if (chat_manager)
480 chat_manager->tryHandleSlashCommand(chat_manager->generalGroupId_, "/roll " + expr);
481 //if (toaster_)
482 //toaster_->Push(ImGuiToaster::Level::Good, "Roll: " + expr);
483 };
484 md_.resolveNoteRef = [this](const std::string& ref) -> std::string
485 {
486 return notes_manager->resolveRef(ref);
487 };
488 md_.onNoteOpen = [this](const std::string& id)
489 { openTabByUuid(id); };
490
491 const std::string& md = n->open_editor ? tabState_[uuid].editBuffer : n->markdown_text;
492
493 ImGui::PushTextWrapPos(0.f); // wrap md to viewer width (safety; imgui_md usually wraps, this enforces)
494 if (!md.empty())
495 md_.print(md.c_str(), md.c_str() + md.size());
496 else
497 ImGui::TextDisabled("(empty)");
498 ImGui::PopTextWrapPos();
499 }
500 ImGui::EndChild();
501 ImGui::PopStyleVar();
502
503 // Propagate editor changes to manager
504 if (n->open_editor)
505 {
506 auto& ts = tabState_[uuid];
507 if (ts.bufferInit && ts.editBuffer != n->markdown_text)
508 notes_manager->setContent(uuid, ts.editBuffer);
509 }
510}
511
513{
514 static char titleBuf[128] = {0};
515 static char authorBuf[128] = {0};
516 ImGui::OpenPopup("Create Note");
517 if (ImGui::BeginPopupModal("Create Note", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
518 {
519 ImGui::InputText("Title", titleBuf, sizeof(titleBuf));
521 ImGui::InputText("Author", authorBuf, sizeof(authorBuf));
523 if (ImGui::Button("Create"))
524 {
525 std::string title = titleBuf[0] ? titleBuf : "Untitled";
526 std::string author = authorBuf[0] ? authorBuf : "unknown";
527 auto id = notes_manager->createNote(title, author);
528 openOrFocusTab(id);
529 titleBuf[0] = 0;
530 authorBuf[0] = 0;
531 ImGui::CloseCurrentPopup();
532 }
533 ImGui::SameLine();
534 if (ImGui::Button("Cancel"))
535 {
536 ImGui::CloseCurrentPopup();
537 }
538 ImGui::EndPopup();
539 }
540}
541
542void NoteEditorUI::actSaveNote_(const std::string& uuid)
543{
544 notes_manager->saveNote(uuid);
545}
546
547void NoteEditorUI::actDeleteNote_(const std::string& uuid)
548{
549 ImGui::OpenPopup("Delete Note?");
550 if (ImGui::BeginPopupModal("Delete Note?", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
551 {
552 static bool alsoDisk = false;
553 ImGui::Checkbox("Also delete from disk", &alsoDisk);
554 if (ImGui::Button("Delete"))
555 {
556 notes_manager->deleteNote(uuid, alsoDisk);
557 auto it = std::find(openTabs_.begin(), openTabs_.end(), uuid);
558 if (it != openTabs_.end())
559 {
560 int idx = (int)std::distance(openTabs_.begin(), it);
561 closeTab_(idx);
562 }
563 alsoDisk = false;
564 ImGui::CloseCurrentPopup();
565 }
566 ImGui::SameLine();
567 if (ImGui::Button("Cancel"))
568 {
569 ImGui::CloseCurrentPopup();
570 }
571 ImGui::EndPopup();
572 }
573}
574
575void NoteEditorUI::actSaveInboxToLocal_(const std::string& uuid)
576{
577 notes_manager->saveInboxToLocal(uuid, std::nullopt);
578}
579
580void NoteEditorUI::toggleOpenEditor_(const std::string& uuid)
581{
582 auto n = notes_manager->getNote(uuid);
583 if (!n)
584 return;
585 n->open_editor = !n->open_editor;
586 if (n->open_editor)
587 {
588 auto& ts = tabState_[uuid];
589 ts.bufferInit = false;
590 }
591}
592
593void NoteEditorUI::addTabIfMissing_(const std::string& uuid)
594{
595 if (std::find(openTabs_.begin(), openTabs_.end(), uuid) == openTabs_.end())
596 {
597 openTabs_.push_back(uuid);
598 currentTabIndex_ = (int)openTabs_.size() - 1;
599 if (auto n = notes_manager->getNote(uuid))
600 {
601 auto& ts = tabState_[uuid];
602 ts.editBuffer = n->markdown_text;
603 ts.bufferInit = true;
604 }
605 }
606 else
607 {
608 currentTabIndex_ = (int)std::distance(openTabs_.begin(),
609 std::find(openTabs_.begin(), openTabs_.end(), uuid));
610 }
611}
612
613void NoteEditorUI::openOrFocusTab(const std::string& uuid)
614{
615 auto it = std::find(openTabs_.begin(), openTabs_.end(), uuid);
616 if (it == openTabs_.end())
617 {
618 addTabIfMissing_(uuid); // opens and selects
619 }
620 else
621 {
622 currentTabIndex_ = (int)std::distance(openTabs_.begin(), it);
623 // ImGui will focus the item with BeginTabItem anyway; this ensures index is correct
624 }
625}
626
627void NoteEditorUI::closeTab_(int tabIndex)
628{
629 if (tabIndex < 0 || tabIndex >= (int)openTabs_.size())
630 return;
631 tabState_.erase(openTabs_[tabIndex]);
632 openTabs_.erase(openTabs_.begin() + tabIndex);
633}
634
636{
637 if (searchBuf_[0] == 0)
638 return true;
639 std::string needle = searchBuf_;
640 std::transform(needle.begin(), needle.end(), needle.begin(), ::tolower);
641 auto contains = [&](const std::string& hay)
642 {
643 std::string low = hay;
644 std::transform(low.begin(), low.end(), low.begin(), ::tolower);
645 return low.find(needle) != std::string::npos;
646 };
647 return contains(n.title) || contains(n.author) || contains(n.markdown_text);
648}
649
650void NoteEditorUI::openNoteTab(const std::string& uuid)
651{
652 openOrFocusTab(uuid);
653}
654
655bool NoteEditorUI::openTabByUuid(const std::string& uuid)
656{
657 if (!notes_manager)
658 return false;
659
660 // must exist
661 auto n = notes_manager->getNote(uuid);
662 if (!n)
663 return false;
664
665 // focus if already open
666 auto it = std::find(openTabs_.begin(), openTabs_.end(), uuid);
667 if (it != openTabs_.end())
668 {
669 currentTabIndex_ = static_cast<int>(std::distance(openTabs_.begin(), it));
670 visible_ = true;
671 return true;
672 }
673
674 // otherwise open a new tab
675 openOrFocusTab(uuid);
676 visible_ = true;
677 return true;
678}
static int InputTextMultilineWrapCallback(ImGuiInputTextCallbackData *data)
static bool InputTextMultilineString_HardWrap(const char *label, std::string *str, const ImVec2 &size, float max_px_line, ImGuiInputTextFlags flags=0)
std::function< std::string(const std::string &ref) resolveNoteRef)
std::function< void(const std::string &expr) onRoll)
std::function< void(const std::string &uuid) onNoteOpen)
void actCreateNote_()
void actSaveNote_(const std::string &uuid)
void setActiveTable(std::optional< std::string > tableName)
std::shared_ptr< ChatManager > chat_manager
void renderTabsArea_(float width, float height)
void renderOneTab_(const std::string &uuid, float availW, float availH)
void toggleOpenEditor_(const std::string &uuid)
void closeTab_(int tabIndex)
void renderDirectory_(float height)
void actDeleteNote_(const std::string &uuid)
MarkdownRenderer md_
std::unordered_map< std::string, TabState > tabState_
std::vector< std::string > openTabs_
NoteEditorUI(std::shared_ptr< NotesManager > notes_manager, std::shared_ptr< ImGuiToaster > toaster)
char createAuthor_[128]
void addTabIfMissing_(const std::string &uuid)
bool filterMatch_(const Note &n) const
void actSaveInboxToLocal_(const std::string &uuid)
bool showCreatePopup_
void openNoteTab(const std::string &uuid)
std::shared_ptr< NotesManager > notes_manager
char searchBuf_[128]
void openOrFocusTab(const std::string &uuid)
char createTitle_[128]
bool openTabByUuid(const std::string &uuid)
bool showDeletePopup_
std::string pendingDeleteUuid_
static int64_t toEpochMillis(std::chrono::system_clock::time_point tp)
void TrackThisInput()
std::string * buf