Files
ImXui/examples/NotepadDemo.cpp
T
2026-08-27 17:15:05 -07:00

3090 lines
103 KiB
C++

#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#ifdef _WIN32
#include <windows.h>
#include <windowsx.h>
#include <commdlg.h>
#include <objbase.h>
#include <dwmapi.h>
#pragma comment(lib, "dwmapi.lib")
#pragma comment(lib, "gdi32.lib")
#else
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/cursorfont.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
#endif
#include <ImXui/ImXui.h>
#include <ImXui/Renderer.h>
#include <ImXui/SoftwareRenderer.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cwchar>
#include <cwctype>
#include <cmath>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
#ifndef _WIN32
#define VK_BACK 0x08
#define VK_TAB 0x09
#define VK_RETURN 0x0D
#define VK_ESCAPE 0x1B
#define VK_SPACE 0x20
#define VK_PRIOR 0x21
#define VK_NEXT 0x22
#define VK_END 0x23
#define VK_HOME 0x24
#define VK_LEFT 0x25
#define VK_UP 0x26
#define VK_RIGHT 0x27
#define VK_DOWN 0x28
#define VK_DELETE 0x2E
#define VK_F3 0x72
#define VK_F5 0x74
#endif
#ifdef _WIN32
static ImXui::D3D11Renderer s_renderer;
#else
static ImXui::SoftwareRenderer s_renderer;
#endif
static ImXui::Canvas s_canvas;
static bool s_running = true;
static bool s_rendererReady = false;
#ifdef _WIN32
static HWND s_hwnd = 0;
#else
static Display* s_dpy = nullptr;
static Window s_win = 0;
static GC s_gc = nullptr;
static XImage* s_img = nullptr;
static Visual* s_visual = nullptr;
static int s_depth = 24;
static std::vector<uint8_t> s_blit;
static bool s_ctrlDown = false, s_shiftDown = false;
static bool s_winDrag = false;
static int s_dragDX = 0, s_dragDY = 0;
static int s_savedX = 0, s_savedY = 0, s_savedW = 0, s_savedH = 0;
static Atom s_atomClipboard = 0, s_atomUtf8 = 0;
static std::string s_clipUtf8;
static bool s_pastePending = false;
#endif
static const wchar_t* FONT_UI = L"Segoe UI";
static const wchar_t* FONT_MONO = L"Consolas";
static const float CLR_WINDOW[3] = { 0.13f, 0.14f, 0.17f };
static const float CLR_EDITOR[3] = { 0.11f, 0.12f, 0.15f };
static const float CLR_PANEL[3] = { 0.17f, 0.19f, 0.23f };
static const float CLR_PANEL_HOT[3]= { 0.22f, 0.25f, 0.31f };
static const float CLR_MENU_BG[3] = { 0.20f, 0.22f, 0.27f };
static const float CLR_MENU_HOT[3] = { 0.25f, 0.31f, 0.44f };
static const float CLR_TEXT[3] = { 0.92f, 0.93f, 0.95f };
static const float CLR_TEXT_DIM[3] = { 0.62f, 0.65f, 0.70f };
static const float CLR_CARET[3] = { 0.95f, 0.95f, 0.95f };
static const float CLR_SEL[3] = { 0.16f, 0.31f, 0.47f };
static const float CLR_CURLINE[3] = { 0.13f, 0.14f, 0.17f };
static const float CLR_ACCENT[3] = { 0.32f, 0.58f, 0.96f };
static const float CLR_ACCENT_DIM[3] = { 0.22f, 0.40f, 0.70f };
static const float CLR_BORDER[3] = { 0.10f, 0.11f, 0.13f };
static const float CLR_MATCH[3] = { 0.42f, 0.34f, 0.14f };
static const float CLR_INPUT[3] = { 0.07f, 0.08f, 0.10f };
static const float CLR_SCROLL[3] = { 0.24f, 0.26f, 0.31f };
static const float CLR_SCROLL_THUMB[3] = { 0.42f, 0.45f, 0.52f };
static void qd(float x, float y, float w, float h, const float* c, float a = 1.0f) {
if (w <= 0 || h <= 0) return;
s_renderer.drawColoredQuad(x, y, w, h, c[0], c[1], c[2], a);
}
static void qr(const ImXui::Rect& r, const float* c, float a = 1.0f) {
qd(r.x, r.y, r.w, r.h, c, a);
}
static void lineFig(const ImXui::Rect& r, const std::vector<ImXui::Vec2>& pts,
const float* c, float a, float sw) {
s_renderer.drawFigure(r.x, r.y, r.w, r.h, pts, false,
0, 0, 0, 0, c[0], c[1], c[2], a, sw);
}
static bool inRect(const ImXui::Rect& r, float x, float y) {
return x >= r.x && x < r.right() && y >= r.y && y < r.bottom();
}
static void txt(float x, float y, float w, float h, const std::wstring& t,
const std::wstring& font, float sz, const float* c,
float a = 1.0f, bool centered = false) {
if (t.empty()) return;
s_renderer.drawTextString(x, y, w, h, t, font, sz, c[0], c[1], c[2], a, centered);
}
#ifdef _WIN32
static HDC s_measDC = nullptr;
static std::map<std::wstring, HFONT> s_measFonts;
static HFONT measFont(const std::wstring& family, float size) {
wchar_t key[128];
swprintf(key, 128, L"%s|%.1f", family.c_str(), size);
std::map<std::wstring, HFONT>::iterator it = s_measFonts.find(key);
if (it != s_measFonts.end()) return it->second;
HFONT f = CreateFontW(-(int)(size + 0.5f), 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS,
NONANTIALIASED_QUALITY, DEFAULT_PITCH, family.c_str());
s_measFonts[key] = f;
return f;
}
static float textW(const std::wstring& t, const std::wstring& family, float size) {
if (t.empty()) return 0;
if (!s_measDC) s_measDC = CreateCompatibleDC(nullptr);
HFONT old = (HFONT)SelectObject(s_measDC, measFont(family, size));
SIZE sz = {};
GetTextExtentPoint32W(s_measDC, t.c_str(), (int)t.size(), &sz);
SelectObject(s_measDC, old);
return (float)sz.cx;
}
#else
// the soft renderer holds one font; family is ignored here
static float textW(const std::wstring& t, const std::wstring&, float size) {
if (t.empty()) return 0;
return s_renderer.measureText(t, size);
}
#endif
static std::vector<std::wstring> s_lines;
static int s_caretRow = 0, s_caretCol = 0;
static int s_selRow = 0, s_selCol = 0;
static int s_scrollTop = 0;
static float s_scrollX = 0;
static bool s_wrap = true;
static bool s_dirty = false;
static bool s_statusBarVisible = true;
static std::string s_filePath;
static bool s_typingRun = false;
static int s_fontIdx = 1;
static const float s_fontSizes[5] = { 13, 15, 17, 19, 21 };
static float s_fontSize = 15.0f;
static float s_charW = 8.0f;
static float s_lineH = 20.0f;
static float s_uiScale = 1.0f;
static float uiF(float px) { return px * s_uiScale; }
static unsigned long nowMs() {
#ifdef _WIN32
return GetTickCount();
#else
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (unsigned long)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
#endif
}
static void updateFontMetrics() {
float eff = s_fontSize * s_uiScale;
s_charW = textW(L"M", FONT_MONO, eff);
#ifdef _WIN32
if (s_measDC) {
HFONT old = (HFONT)SelectObject(s_measDC, measFont(FONT_MONO, eff));
TEXTMETRICW tm = {};
GetTextMetricsW(s_measDC, &tm);
SelectObject(s_measDC, old);
s_lineH = (float)tm.tmHeight + 3.0f * s_uiScale;
} else {
s_lineH = eff * 1.35f;
}
#else
s_lineH = s_renderer.lineHeight(eff) + 3.0f * s_uiScale;
#endif
}
struct Snap {
std::vector<std::wstring> lines;
int cr, cc, sr, sc;
};
static std::vector<Snap> s_undo, s_redo;
static ImXui::Rect s_rectMenu, s_rectText, s_rectFind, s_rectStatus;
static ImXui::Rect s_editorRect;
enum MenuIdx { MENU_FILE, MENU_EDIT, MENU_FORMAT, MENU_VIEW, MENU_HELP, MENU_COUNT };
static const wchar_t* s_menuLabels[MENU_COUNT] = { L"File", L"Edit", L"Format", L"View", L"Help" };
static std::vector<ImXui::Rect> s_menuRects;
enum Action {
A_NONE = 0,
A_NEW, A_OPEN, A_SAVE, A_SAVEAS, A_EXIT,
A_UNDO, A_CUT, A_COPY, A_PASTE, A_DELETE, A_FIND, A_FINDNEXT, A_SELECTALL, A_TIMEDATE,
A_WRAP, A_STATUSBAR, A_FONTSM, A_FONTMED, A_FONTLG, A_FONTLG2,
A_ABOUT,
};
struct MenuItem {
const wchar_t* label;
const wchar_t* hint;
int action;
bool separator;
MenuItem() : label(0), hint(0), action(A_NONE), separator(false) {}
};
struct MenuDef {
std::vector<MenuItem> items;
};
static MenuDef s_menus[MENU_COUNT];
static int s_openMenu = -1;
static int s_hoverMenu = -1;
static int s_hoverItem = -1;
static void initMenus() {
auto add = [](int menu, const wchar_t* label, const wchar_t* hint, int action) {
MenuItem it; it.label = label; it.hint = hint; it.action = action;
s_menus[menu].items.push_back(it);
};
auto sep = [](int menu) {
MenuItem it; it.separator = true;
s_menus[menu].items.push_back(it);
};
add(MENU_FILE, L"New", L"Ctrl+N", A_NEW);
add(MENU_FILE, L"Open...", L"Ctrl+O", A_OPEN);
add(MENU_FILE, L"Save", L"Ctrl+S", A_SAVE);
add(MENU_FILE, L"Save As...", L"Ctrl+Shift+S", A_SAVEAS);
sep(MENU_FILE);
add(MENU_FILE, L"Exit", L"", A_EXIT);
add(MENU_EDIT, L"Undo", L"Ctrl+Z", A_UNDO);
sep(MENU_EDIT);
add(MENU_EDIT, L"Cut", L"Ctrl+X", A_CUT);
add(MENU_EDIT, L"Copy", L"Ctrl+C", A_COPY);
add(MENU_EDIT, L"Paste", L"Ctrl+V", A_PASTE);
add(MENU_EDIT, L"Delete", L"Del", A_DELETE);
sep(MENU_EDIT);
add(MENU_EDIT, L"Find...", L"Ctrl+F", A_FIND);
add(MENU_EDIT, L"Find Next", L"F3", A_FINDNEXT);
sep(MENU_EDIT);
add(MENU_EDIT, L"Select All", L"Ctrl+A", A_SELECTALL);
add(MENU_EDIT, L"Time/Date", L"F5", A_TIMEDATE);
add(MENU_FORMAT, L"Word Wrap", L"", A_WRAP);
add(MENU_VIEW, L"Status Bar", L"", A_STATUSBAR);
sep(MENU_VIEW);
add(MENU_VIEW, L"Small Font", L"13", A_FONTSM);
add(MENU_VIEW, L"Medium Font", L"15", A_FONTMED);
add(MENU_VIEW, L"Large Font", L"17", A_FONTLG);
add(MENU_VIEW, L"Extra Large Font", L"19", A_FONTLG2);
add(MENU_HELP, L"About Notepad", L"", A_ABOUT);
}
struct Match { int row, col, len; };
static std::vector<Match> s_matches;
static bool s_findVisible = false;
static bool s_findFocus = false;
static std::wstring s_findQuery;
static int s_findHover = -1;
static bool s_findDirty = true;
static ImXui::Rect s_findBox;
static bool s_aboutOpen = false;
static ImXui::Rect s_aboutOk;
static ImXui::Rect s_rectTitle;
static ImXui::Rect s_minBtn, s_maxBtn, s_closeBtn;
static int s_titleHover = -1;
static bool s_maximized = false;
static const float CLR_CLOSE_HOVER[3] = { 0.90f, 0.24f, 0.24f };
enum DlgKind { DlgInfo, DlgError, DlgQuestion };
static void openDialog(DlgKind kind, const std::wstring& title, const std::wstring& msg,
int pendingAction = -1, bool fatal = false);
static bool s_dlgOpen = false;
static DlgKind s_dlgKind = DlgInfo;
static std::wstring s_dlgTitle;
static std::wstring s_dlgMsg;
static int s_dlgPendingAction = -1;
static int s_dlgHover = -1;
static ImXui::Rect s_dlgBtnRects[3];
static bool s_dlgFatal = false;
#ifdef _WIN32
static HCURSOR s_curArrow, s_curIBeam, s_curHand, s_curSizeNS, s_curSizeWE;
#else
static Cursor s_curArrow, s_curIBeam, s_curHand, s_curSizeNS, s_curSizeWE;
#endif
static bool s_selecting = false;
static bool s_dragV = false;
static bool s_dragH = false;
static bool s_hoverAbout = false;
static bool s_needRender = true;
static bool s_blinkOnly = false;
static bool s_minimized = false;
static const unsigned long TIMER_BLINK = 1;
static int s_docRev = 0;
static long long s_viewKey[4] = { -1, -1, -1, -1 };
static float s_lastUiScale = 0;
static float s_lastFontSize = -1;
static float s_lastMenuY = -1;
struct VRow { int line, col, len; };
static std::vector<VRow> s_view;
static float s_maxRowW = 0;
static std::vector<std::pair<int, int> > wrapLineChar(const std::wstring& t, int maxChars) {
std::vector<std::pair<int, int> > out;
if (t.empty()) { out.push_back(std::make_pair(0, 0)); return out; }
if (maxChars <= 1) { out.push_back(std::make_pair(0, (int)t.size())); return out; }
int start = 0, n = (int)t.size();
while (start < n) {
int end = start, cur = 0;
while (end < n) {
int ws = end;
while (ws < n && t[ws] == L' ') ws++;
int we = ws;
while (we < n && t[we] != L' ') we++;
if (we == ws) break;
int need = (end == start) ? (we - ws) : (we - ws + 1);
if (cur + need > maxChars) { if (ws == start) end = we; break; }
cur += need;
end = we;
while (end < n && t[end] == L' ') end++;
}
if (end <= start) end = start + 1;
out.push_back(std::make_pair(start, end - start));
start = end;
}
return out;
}
static int rowsOfLine(int li) {
if (li < 0 || li >= (int)s_lines.size()) return 0;
if (s_wrap) return (int)wrapLineChar(s_lines[li], (int)((s_editorRect.w - 4) / s_charW)).size();
return 1;
}
static void buildView() {
long long keyW = (long long)s_editorRect.w;
long long keyCw = (long long)(s_charW * 1000.0f);
if (s_viewKey[0] == s_docRev && s_viewKey[1] == keyW &&
s_viewKey[2] == keyCw && s_viewKey[3] == (s_wrap ? 1 : 0))
return;
s_view.clear();
s_maxRowW = 0;
int maxChars = (int)((s_editorRect.w - 4) / s_charW);
if (maxChars < 1) maxChars = 1;
for (int li = 0; li < (int)s_lines.size(); li++) {
if (s_wrap) {
std::vector<std::pair<int, int> > segs = wrapLineChar(s_lines[li], maxChars);
for (size_t i = 0; i < segs.size(); i++) {
VRow v; v.line = li; v.col = segs[i].first; v.len = segs[i].second;
s_view.push_back(v);
}
} else {
VRow v; v.line = li; v.col = 0; v.len = (int)s_lines[li].size();
s_view.push_back(v);
float w = (float)v.len * s_charW + 4;
if (w > s_maxRowW) s_maxRowW = w;
}
}
s_viewKey[0] = s_docRev;
s_viewKey[1] = keyW;
s_viewKey[2] = keyCw;
s_viewKey[3] = s_wrap ? 1 : 0;
}
static int viewRowOf(int row, int col) {
int vr = 0;
for (int li = 0; li < row && li < (int)s_lines.size(); li++) vr += rowsOfLine(li);
if (row >= (int)s_lines.size()) return vr;
if (s_wrap) {
std::vector<std::pair<int, int> > segs =
wrapLineChar(s_lines[row], (int)((s_editorRect.w - 4) / s_charW));
int seg = (int)segs.size() - 1;
for (int i = 0; i < (int)segs.size(); i++) {
if (col < segs[i].first + segs[i].second) { seg = i; break; }
}
return vr + seg;
}
return vr;
}
static bool caretVisible() {
if (s_aboutOpen || s_dlgOpen) return false;
if (s_findVisible && s_findFocus) return true;
int cvr = viewRowOf(s_caretRow, s_caretCol);
int visibleRows = std::max(1, (int)(s_editorRect.h / s_lineH));
return cvr >= s_scrollTop && cvr < s_scrollTop + visibleRows;
}
static void caretFromViewRow(int vr, int desiredCol, int& row, int& col) {
if (s_view.empty()) { row = 0; col = 0; return; }
vr = std::max(0, std::min(vr, (int)s_view.size() - 1));
const VRow& v = s_view[vr];
row = v.line;
if (s_wrap) {
col = v.col + std::max(0, std::min(desiredCol - v.col, v.len));
} else {
col = std::min(desiredCol, (int)s_lines[v.line].size());
}
}
static void clampCaret() {
if (s_caretRow < 0) s_caretRow = 0;
if (s_caretRow >= (int)s_lines.size()) s_caretRow = (int)s_lines.size() - 1;
int len = (int)s_lines[s_caretRow].size();
if (s_caretCol > len) s_caretCol = len;
if (s_caretCol < 0) s_caretCol = 0;
if (s_selRow < 0) s_selRow = 0;
if (s_selCol < 0) s_selCol = 0;
}
static bool hasSelection() {
return !(s_selRow == s_caretRow && s_selCol == s_caretCol);
}
static void selBounds(int& br, int& bc, int& ar, int& ac) {
if (s_selRow < s_caretRow || (s_selRow == s_caretRow && s_selCol < s_caretCol)) {
br = s_selRow; bc = s_selCol; ar = s_caretRow; ac = s_caretCol;
} else {
br = s_caretRow; bc = s_caretCol; ar = s_selRow; ac = s_selCol;
}
}
static void setAnchorToCaret() { s_selRow = s_caretRow; s_selCol = s_caretCol; }
static void pushUndo() {
if (s_typingRun) return;
s_redo.clear();
Snap s;
s.lines = s_lines;
s.cr = s_caretRow; s.cc = s_caretCol; s.sr = s_selRow; s.sc = s_selCol;
s_undo.push_back(s);
if (s_undo.size() > 200) s_undo.erase(s_undo.begin());
}
static void markDirty() { s_dirty = true; s_findDirty = true; ++s_docRev; }
static std::wstring docBaseName() {
std::wstring name = L"Untitled";
if (!s_filePath.empty()) {
size_t slash = s_filePath.find_last_of("\\/");
name = std::wstring(s_filePath.begin() + (slash == std::string::npos ? 0 : slash + 1), s_filePath.end());
}
return name;
}
static void updateTitle() {
std::wstring t = docBaseName() + (s_dirty ? L" *" : L"") + L" - Notepad";
#ifdef _WIN32
SetWindowTextW(s_hwnd, t.c_str());
#else
std::string narrow;
for (size_t i = 0; i < t.size(); i++)
if (t[i] < 128) narrow += (char)t[i];
XStoreName(s_dpy, s_win, narrow.c_str());
#endif
}
static void scrollToCaret() {
buildView();
int vr = viewRowOf(s_caretRow, s_caretCol);
int visibleRows = std::max(1, (int)(s_editorRect.h / s_lineH));
if (vr < s_scrollTop) s_scrollTop = vr;
if (vr >= s_scrollTop + visibleRows) s_scrollTop = vr - visibleRows + 1;
if (s_scrollTop < 0) s_scrollTop = 0;
if (!s_wrap) {
float caretX = 4 + (float)s_caretCol * s_charW;
if (s_scrollX > caretX - 8) s_scrollX = std::max(0.0f, caretX - 8);
if (caretX - s_scrollX > s_editorRect.w - 8)
s_scrollX = caretX - s_editorRect.w + 8;
float maxX = std::max(0.0f, s_maxRowW - s_editorRect.w + 4);
if (s_scrollX > maxX) s_scrollX = maxX;
if (s_scrollX < 0) s_scrollX = 0;
} else {
s_scrollX = 0;
}
}
static void deleteRangeNoUndo(int br, int bc, int ar, int ac) {
if (br == ar) {
s_lines[br].erase(bc, ac - bc);
} else {
s_lines[br] = s_lines[br].substr(0, bc) + s_lines[ar].substr(ac);
s_lines.erase(s_lines.begin() + br + 1, s_lines.begin() + ar + 1);
}
s_caretRow = br; s_caretCol = bc;
setAnchorToCaret();
}
static void deleteSelectionNoUndo() {
int br, bc, ar, ac;
selBounds(br, bc, ar, ac);
if (br == ar && bc == ac) return;
deleteRangeNoUndo(br, bc, ar, ac);
}
static void insertAtCaret(const std::wstring& text) {
pushUndo();
if (hasSelection()) deleteSelectionNoUndo();
if (text.find(L'\n') == std::wstring::npos) {
s_lines[s_caretRow].insert(s_caretCol, text);
s_caretCol += (int)text.size();
} else {
std::vector<std::wstring> parts;
std::wstring cur;
for (size_t i = 0; i < text.size(); i++) {
if (text[i] == L'\n') { parts.push_back(cur); cur.clear(); }
else cur += text[i];
}
parts.push_back(cur);
std::wstring head = s_lines[s_caretRow].substr(0, s_caretCol);
std::wstring tail = s_lines[s_caretRow].substr(s_caretCol);
std::vector<std::wstring> nl;
nl.push_back(head + parts[0]);
for (size_t i = 1; i + 1 < parts.size(); i++) nl.push_back(parts[i]);
nl.push_back(parts.back() + tail);
s_lines.erase(s_lines.begin() + s_caretRow);
s_lines.insert(s_lines.begin() + s_caretRow, nl.begin(), nl.end());
s_caretRow += (int)parts.size() - 1;
s_caretCol = (int)parts.back().size();
}
setAnchorToCaret();
markDirty();
scrollToCaret();
updateTitle();
}
static void backspace() {
pushUndo();
if (hasSelection()) {
deleteSelectionNoUndo();
} else if (s_caretCol > 0) {
s_lines[s_caretRow].erase(s_caretCol - 1, 1);
s_caretCol--;
} else if (s_caretRow > 0) {
int prevLen = (int)s_lines[s_caretRow - 1].size();
s_lines[s_caretRow - 1] += s_lines[s_caretRow];
s_lines.erase(s_lines.begin() + s_caretRow);
s_caretRow--; s_caretCol = prevLen;
}
setAnchorToCaret();
markDirty();
scrollToCaret();
updateTitle();
}
static void forwardDelete() {
pushUndo();
if (hasSelection()) {
deleteSelectionNoUndo();
} else if (s_caretCol < (int)s_lines[s_caretRow].size()) {
s_lines[s_caretRow].erase(s_caretCol, 1);
} else if (s_caretRow + 1 < (int)s_lines.size()) {
s_lines[s_caretRow] += s_lines[s_caretRow + 1];
s_lines.erase(s_lines.begin() + s_caretRow + 1);
}
setAnchorToCaret();
markDirty();
scrollToCaret();
updateTitle();
}
static void insertTimeDate() {
s_typingRun = false;
wchar_t buf[64];
#ifdef _WIN32
SYSTEMTIME st;
GetLocalTime(&st);
int h12 = st.wHour % 12;
if (h12 == 0) h12 = 12;
swprintf(buf, 64, L"%d:%02d %s %d/%d/%d", h12, st.wMinute,
st.wHour < 12 ? L"AM" : L"PM", st.wMonth, st.wDay, st.wYear);
#else
time_t now = time(nullptr);
struct tm* lt = localtime(&now);
int h12 = lt->tm_hour % 12;
if (h12 == 0) h12 = 12;
swprintf(buf, 64, L"%d:%02d %s %d/%d/%d", h12, lt->tm_min,
lt->tm_hour < 12 ? L"AM" : L"PM", lt->tm_mon + 1, lt->tm_mday, lt->tm_year + 1900);
#endif
insertAtCaret(buf);
}
static int wordLeftCol(const std::wstring& line, int col) {
if (col <= 0) return 0;
int c = col - 1;
while (c > 0 && line[c] == L' ') c--;
while (c > 0 && line[c - 1] != L' ') c--;
return c;
}
static int wordRightCol(const std::wstring& line, int col) {
int n = (int)line.size();
if (col >= n) return n;
int c = col;
while (c < n && line[c] == L' ') c++;
while (c < n && line[c] != L' ') c++;
return c;
}
static void deleteWordLeft() {
s_typingRun = false;
pushUndo();
if (hasSelection()) { deleteSelectionNoUndo(); markDirty(); scrollToCaret(); updateTitle(); return; }
int target = wordLeftCol(s_lines[s_caretRow], s_caretCol);
if (target == s_caretCol && s_caretRow > 0) {
int prevLen = (int)s_lines[s_caretRow - 1].size();
s_lines[s_caretRow - 1] += s_lines[s_caretRow];
s_lines.erase(s_lines.begin() + s_caretRow);
s_caretRow--; s_caretCol = prevLen;
} else {
s_lines[s_caretRow].erase(target, s_caretCol - target);
s_caretCol = target;
}
setAnchorToCaret();
markDirty();
scrollToCaret();
updateTitle();
}
static void deleteWordRight() {
s_typingRun = false;
pushUndo();
if (hasSelection()) { deleteSelectionNoUndo(); markDirty(); scrollToCaret(); updateTitle(); return; }
int target = wordRightCol(s_lines[s_caretRow], s_caretCol);
s_lines[s_caretRow].erase(s_caretCol, target - s_caretCol);
setAnchorToCaret();
markDirty();
scrollToCaret();
updateTitle();
}
static void undo() {
s_typingRun = false;
if (s_undo.empty()) return;
Snap cur;
cur.lines = s_lines; cur.cr = s_caretRow; cur.cc = s_caretCol; cur.sr = s_selRow; cur.sc = s_selCol;
s_redo.push_back(cur);
if (s_redo.size() > 200) s_redo.erase(s_redo.begin());
Snap s = s_undo.back();
s_undo.pop_back();
s_lines = s.lines; s_caretRow = s.cr; s_caretCol = s.cc; s_selRow = s.sr; s_selCol = s.sc;
clampCaret();
markDirty();
scrollToCaret();
updateTitle();
}
static void redo() {
s_typingRun = false;
if (s_redo.empty()) return;
Snap cur;
cur.lines = s_lines; cur.cr = s_caretRow; cur.cc = s_caretCol; cur.sr = s_selRow; cur.sc = s_selCol;
s_undo.push_back(cur);
Snap s = s_redo.back();
s_redo.pop_back();
s_lines = s.lines; s_caretRow = s.cr; s_caretCol = s.cc; s_selRow = s.sr; s_selCol = s.sc;
clampCaret();
markDirty();
scrollToCaret();
updateTitle();
}
static std::string wideToUtf8(const std::wstring& s);
static void copySelection() {
if (!hasSelection()) return;
int br, bc, ar, ac;
selBounds(br, bc, ar, ac);
std::wstring data;
if (br == ar) {
data = s_lines[br].substr(bc, ac - bc);
} else {
data = s_lines[br].substr(bc) + L"\r\n";
for (int i = br + 1; i < ar; i++) data += s_lines[i] + L"\r\n";
data += s_lines[ar].substr(0, ac);
}
#ifdef _WIN32
if (!OpenClipboard(s_hwnd)) return;
EmptyClipboard();
size_t bytes = (data.size() + 1) * sizeof(wchar_t);
HGLOBAL hg = GlobalAlloc(GMEM_MOVEABLE, bytes);
if (hg) {
void* p = GlobalLock(hg);
memcpy(p, data.c_str(), bytes);
GlobalUnlock(hg);
SetClipboardData(CF_UNICODETEXT, hg);
}
CloseClipboard();
#else
s_clipUtf8 = wideToUtf8(data);
XSetSelectionOwner(s_dpy, s_atomClipboard, s_win, CurrentTime);
XFlush(s_dpy);
#endif
}
static void cutSelection() {
if (!hasSelection()) return;
s_typingRun = false;
copySelection();
pushUndo();
deleteSelectionNoUndo();
markDirty();
scrollToCaret();
updateTitle();
}
static void insertClipboardText(const std::wstring& data) {
if (data.empty()) return;
std::wstring clean;
clean.reserve(data.size());
for (size_t i = 0; i < data.size(); i++) {
if (data[i] == L'\r') { if (i + 1 < data.size() && data[i + 1] == L'\n') i++; clean += L'\n'; }
else clean += data[i];
}
insertAtCaret(clean);
}
static void pasteFromClipboard() {
s_typingRun = false;
#ifdef _WIN32
if (!OpenClipboard(s_hwnd)) return;
HANDLE h = GetClipboardData(CF_UNICODETEXT);
std::wstring data;
if (h) {
const wchar_t* p = (const wchar_t*)GlobalLock(h);
if (p) { data = p; GlobalUnlock(h); }
}
CloseClipboard();
insertClipboardText(data);
#else
s_pastePending = true;
XConvertSelection(s_dpy, s_atomClipboard, s_atomUtf8, s_atomUtf8, s_win, CurrentTime);
XFlush(s_dpy);
#endif
}
static std::wstring toLower(const std::wstring& s) {
std::wstring t = s;
for (size_t i = 0; i < t.size(); i++) t[i] = (wchar_t)towlower(t[i]);
return t;
}
static void findMatches() {
s_matches.clear();
s_findDirty = false;
std::wstring q = toLower(s_findQuery);
if (q.empty()) return;
for (int li = 0; li < (int)s_lines.size(); li++) {
std::wstring line = toLower(s_lines[li]);
size_t pos = 0;
while ((pos = line.find(q, pos)) != std::wstring::npos) {
Match m; m.row = li; m.col = (int)pos; m.len = (int)q.size();
s_matches.push_back(m);
pos += q.size();
}
}
}
static void findNext(bool backward) {
if (s_findDirty) findMatches();
if (s_matches.empty()) return;
int idx = -1;
if (backward) {
for (int i = (int)s_matches.size() - 1; i >= 0; i--) {
const Match& m = s_matches[i];
if (m.row < s_caretRow || (m.row == s_caretRow && m.col < s_caretCol)) { idx = i; break; }
}
if (idx < 0) idx = (int)s_matches.size() - 1;
} else {
for (int i = 0; i < (int)s_matches.size(); i++) {
const Match& m = s_matches[i];
if (m.row > s_caretRow || (m.row == s_caretRow && m.col >= s_caretCol + 1)) { idx = i; break; }
}
if (idx < 0) idx = 0;
}
const Match& m = s_matches[idx];
s_typingRun = false;
s_selRow = m.row; s_selCol = m.col;
s_caretRow = m.row; s_caretCol = m.col + m.len;
scrollToCaret();
}
static std::string wideToUtf8(const std::wstring& s) {
std::string out;
for (size_t i = 0; i < s.size(); i++) {
unsigned int cp = (unsigned int)s[i];
if (cp < 0x80) out += (char)cp;
else if (cp < 0x800) {
out += (char)(0xC0 | (cp >> 6));
out += (char)(0x80 | (cp & 0x3F));
} else {
out += (char)(0xE0 | (cp >> 12));
out += (char)(0x80 | ((cp >> 6) & 0x3F));
out += (char)(0x80 | (cp & 0x3F));
}
}
return out;
}
static bool utf8ToWideSafe(const std::string& s, std::wstring& out) {
out.clear();
size_t i = 0;
while (i < s.size()) {
unsigned char c = (unsigned char)s[i];
unsigned int cp;
int need;
if (c < 0x80) { cp = c; need = 0; }
else if ((c & 0xE0) == 0xC0) { cp = c & 0x1F; need = 1; }
else if ((c & 0xF0) == 0xE0) { cp = c & 0x0F; need = 2; }
else if ((c & 0xF8) == 0xF0) { cp = c & 0x07; need = 3; }
else return false;
for (int k = 1; k <= need; k++) {
if (i + k >= s.size()) return false;
unsigned char cc = (unsigned char)s[i + k];
if ((cc & 0xC0) != 0x80) return false;
cp = (cp << 6) | (cc & 0x3F);
}
if (cp > 0x10FFFF) return false;
out += (wchar_t)cp;
i += need + 1;
}
return true;
}
static bool readFileBytes(const std::string& path, std::string& out) {
FILE* f = fopen(path.c_str(), "rb");
if (!f) return false;
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
if (sz < 0) { fclose(f); return false; }
out.resize((size_t)sz);
size_t rd = fread(&out[0], 1, (size_t)sz, f);
fclose(f);
return rd == (size_t)sz;
}
static void setDoc(const std::wstring& text) {
s_lines.clear();
std::wstring cur;
for (size_t i = 0; i < text.size(); i++) {
if (text[i] == L'\n') {
if (!cur.empty() && cur[cur.size() - 1] == L'\r') cur.erase(cur.size() - 1);
s_lines.push_back(cur);
cur.clear();
} else cur += text[i];
}
if (!cur.empty() && cur[cur.size() - 1] == L'\r') cur.erase(cur.size() - 1);
s_lines.push_back(cur);
if (s_lines.empty()) s_lines.push_back(L"");
s_caretRow = 0; s_caretCol = 0;
setAnchorToCaret();
s_scrollTop = 0; s_scrollX = 0;
s_undo.clear(); s_redo.clear();
s_typingRun = false;
s_dirty = false;
s_findDirty = true;
++s_docRev;
scrollToCaret();
updateTitle();
}
static void loadFile(const std::string& path) {
std::string bytes;
if (!readFileBytes(path, bytes)) {
openDialog(DlgError, L"Notepad", L"Could not open the file.");
return;
}
std::wstring text;
size_t off = 0;
if (bytes.size() >= 3 && (unsigned char)bytes[0] == 0xEF &&
(unsigned char)bytes[1] == 0xBB && (unsigned char)bytes[2] == 0xBF)
off = 3;
if (!utf8ToWideSafe(bytes.substr(off), text)) {
#ifdef _WIN32
int n = MultiByteToWideChar(CP_ACP, 0, bytes.data() + off, (int)(bytes.size() - off), NULL, 0);
text.resize(n > 0 ? n : 0);
if (n > 0) MultiByteToWideChar(CP_ACP, 0, bytes.data() + off, (int)(bytes.size() - off), &text[0], n);
#else
for (size_t i = off; i < bytes.size(); i++) text += (wchar_t)(unsigned char)bytes[i];
#endif
}
s_filePath = path;
setDoc(text);
}
static bool saveFileTo(const std::string& path) {
std::wstring joined;
for (size_t i = 0; i < s_lines.size(); i++) {
if (i) joined += L'\n';
joined += s_lines[i];
}
std::string body = wideToUtf8(joined);
std::string out;
out.push_back((char)0xEF); out.push_back((char)0xBB); out.push_back((char)0xBF);
out += body;
FILE* f = fopen(path.c_str(), "wb");
if (!f) {
openDialog(DlgError, L"Notepad", L"Could not save the file.");
return false;
}
fwrite(out.data(), 1, out.size(), f);
fclose(f);
s_filePath = path;
s_dirty = false;
updateTitle();
return true;
}
#ifndef _WIN32
static bool s_fileDlgOpen = false;
static bool s_fileDlgSave = false;
static std::string s_fileDir;
static std::vector<std::string> s_fileEntries;
static std::vector<bool> s_fileIsDir;
static int s_fileScroll = 0;
static int s_fileSel = -1;
static std::wstring s_fileName;
static int s_fileHover = -1;
static ImXui::Rect s_fdList, s_fdInput, s_fdOk, s_fdCancel;
static std::wstring utf8ToWideLoose(const std::string& s) {
std::wstring out;
if (utf8ToWideSafe(s, out)) return out;
for (size_t i = 0; i < s.size(); i++) out += (wchar_t)(unsigned char)s[i];
return out;
}
static bool fileEntryIsDir(const std::string& p) {
struct stat st;
return stat(p.c_str(), &st) == 0 && S_ISDIR(st.st_mode);
}
static void fileDialogRefresh() {
s_fileEntries.clear();
s_fileIsDir.clear();
std::vector<std::string> dirs, files;
DIR* d = opendir(s_fileDir.c_str());
if (d) {
struct dirent* e;
while ((e = readdir(d))) {
std::string n = e->d_name;
if (n == "." || n == "..") continue;
std::string full = s_fileDir + "/" + n;
if (fileEntryIsDir(full)) dirs.push_back(n + "/");
else files.push_back(n);
}
closedir(d);
}
std::sort(dirs.begin(), dirs.end());
std::sort(files.begin(), files.end());
s_fileEntries.push_back("../");
s_fileIsDir.push_back(true);
for (size_t i = 0; i < dirs.size(); i++) { s_fileEntries.push_back(dirs[i]); s_fileIsDir.push_back(true); }
for (size_t i = 0; i < files.size(); i++) { s_fileEntries.push_back(files[i]); s_fileIsDir.push_back(false); }
s_fileScroll = 0;
s_fileSel = -1;
}
static void fileDialogOpen(bool saveMode) {
s_fileDlgOpen = true;
s_fileDlgSave = saveMode;
s_fileName.clear();
if (s_fileDir.empty()) {
char cwd[4096];
if (getcwd(cwd, sizeof(cwd))) s_fileDir = cwd;
}
fileDialogRefresh();
s_fileHover = -1;
}
static void fileDialogOk() {
std::string name;
for (size_t i = 0; i < s_fileName.size(); i++)
if (s_fileName[i] < 128) name += (char)s_fileName[i];
if (name.empty()) return;
std::string path = s_fileDir + "/" + name;
if (s_fileDlgSave) {
if (path.find('.') == std::string::npos) path += ".txt";
s_fileDlgOpen = false;
saveFileTo(path);
} else {
s_fileDlgOpen = false;
loadFile(path);
}
}
static void drawFileDialog() {
if (!s_fileDlgOpen) return;
int W = s_renderer.getWidth(), H = s_renderer.getHeight();
qd(0, 0, (float)W, (float)H, CLR_WINDOW, 0.55f);
float pw = uiF(520), ph = uiF(360);
ImXui::Rect panel((float)W / 2 - pw / 2, (float)H / 2 - ph / 2, pw, ph);
qr(panel, CLR_MENU_BG);
qd(panel.x, panel.y, panel.w, 1, CLR_ACCENT_DIM);
qd(panel.x, panel.bottom() - 1, panel.w, 1, CLR_BORDER);
qd(panel.x, panel.y, 1, panel.h, CLR_BORDER);
qd(panel.right() - 1, panel.y, 1, panel.h, CLR_BORDER);
txt(panel.x + uiF(16), panel.y + uiF(12), pw - uiF(32), uiF(22),
s_fileDlgSave ? L"Save As" : L"Open", FONT_UI, uiF(14), CLR_TEXT);
std::wstring dirLabel = utf8ToWideLoose(s_fileDir);
while (!dirLabel.empty() && textW(dirLabel, FONT_UI, uiF(11)) > pw - uiF(40)) dirLabel.erase(0, 1);
txt(panel.x + uiF(16), panel.y + uiF(40), pw - uiF(32), uiF(16), dirLabel, FONT_UI, uiF(11), CLR_TEXT_DIM);
s_fdList = ImXui::Rect(panel.x + uiF(12), panel.y + uiF(62), pw - uiF(24), uiF(205));
qr(s_fdList, CLR_INPUT);
qd(s_fdList.x, s_fdList.y, s_fdList.w, 1, CLR_BORDER);
qd(s_fdList.x, s_fdList.bottom() - 1, s_fdList.w, 1, CLR_BORDER);
qd(s_fdList.x, s_fdList.y, 1, s_fdList.h, CLR_BORDER);
qd(s_fdList.right() - 1, s_fdList.y, 1, s_fdList.h, CLR_BORDER);
const float rowH = uiF(18);
int visible = std::max(1, (int)(s_fdList.h / rowH));
int maxScroll = std::max(0, (int)s_fileEntries.size() - visible);
if (s_fileScroll > maxScroll) s_fileScroll = maxScroll;
for (int i = s_fileScroll; i < s_fileScroll + visible && i < (int)s_fileEntries.size(); i++) {
float y = s_fdList.y + 2 + (float)(i - s_fileScroll) * rowH;
if (s_fileHover == i || s_fileSel == i)
qd(s_fdList.x, y, s_fdList.w, rowH, s_fileHover == i ? CLR_MENU_HOT : CLR_PANEL_HOT);
std::wstring name = utf8ToWideLoose(s_fileEntries[i]);
txt(s_fdList.x + uiF(6), y, s_fdList.w - uiF(12), uiF(16), name, FONT_UI, uiF(11),
s_fileIsDir[i] ? CLR_TEXT : CLR_TEXT_DIM);
}
if ((int)s_fileEntries.size() > visible) {
float thumbH = std::max(uiF(24.0f), s_fdList.h * (float)visible / (float)s_fileEntries.size());
float maxTop = (float)((int)s_fileEntries.size() - visible);
float thumbY = s_fdList.y + (s_fdList.h - thumbH) * (float)s_fileScroll / maxTop;
qd(s_fdList.right() - uiF(8), thumbY, uiF(8), thumbH, CLR_SCROLL_THUMB);
}
s_fdInput = ImXui::Rect(panel.x + uiF(12), s_fdList.bottom() + uiF(10), pw - uiF(24), uiF(26));
qr(s_fdInput, CLR_INPUT);
qd(s_fdInput.x, s_fdInput.y, s_fdInput.w, 1, CLR_BORDER);
qd(s_fdInput.x, s_fdInput.bottom() - 1, s_fdInput.w, 1, CLR_BORDER);
qd(s_fdInput.x, s_fdInput.y, 1, s_fdInput.h, CLR_BORDER);
qd(s_fdInput.right() - 1, s_fdInput.y, 1, s_fdInput.h, CLR_BORDER);
if (s_fileName.empty()) {
txt(s_fdInput.x + uiF(8), s_fdInput.y + uiF(5), s_fdInput.w - uiF(16), uiF(16),
s_fileDlgSave ? L"filename.txt" : L"(type a filename)", FONT_UI, uiF(12), CLR_TEXT_DIM);
} else {
txt(s_fdInput.x + uiF(8), s_fdInput.y + uiF(5), s_fdInput.w - uiF(16), uiF(16),
s_fileName, FONT_UI, uiF(12), CLR_TEXT);
if ((int)(nowMs() / 530) % 2 == 0) {
float cw = textW(s_fileName, FONT_UI, uiF(12));
qd(s_fdInput.x + uiF(8) + cw + 1, s_fdInput.y + uiF(6), 2, s_fdInput.h - uiF(12),
CLR_CARET, 0.9f);
}
}
float bw = uiF(84), bh = uiF(28);
s_fdCancel = ImXui::Rect(panel.right() - bw - uiF(12), s_fdInput.bottom() + uiF(12), bw, bh);
s_fdOk = ImXui::Rect(s_fdCancel.x - bw - uiF(10), s_fdCancel.y, bw, bh);
auto btn = [&](const ImXui::Rect& r, const wchar_t* label, bool hot) {
if (hot) qr(r, CLR_MENU_HOT);
else {
qd(r.x, r.y, r.w, 1, CLR_BORDER);
qd(r.x, r.bottom() - 1, r.w, 1, CLR_BORDER);
qd(r.x, r.y, 1, r.h, CLR_BORDER);
qd(r.right() - 1, r.y, 1, r.h, CLR_BORDER);
}
txt(r.x, r.y + uiF(5), r.w, uiF(18), label, FONT_UI, uiF(12), CLR_TEXT, 1.0f, true);
};
btn(s_fdOk, s_fileDlgSave ? L"Save" : L"Open", s_fileHover == -2);
btn(s_fdCancel, L"Cancel", s_fileHover == -1);
}
static void fileDialogHover(float mx, float my) {
s_fileHover = -3;
if (inRect(s_fdOk, mx, my)) s_fileHover = -2;
else if (inRect(s_fdCancel, mx, my)) s_fileHover = -1;
else if (inRect(s_fdList, mx, my)) {
int idx = s_fileScroll + (int)((my - s_fdList.y) / uiF(18));
if (idx >= 0 && idx < (int)s_fileEntries.size()) s_fileHover = idx;
}
}
static void fileDialogMouse(float mx, float my) {
if (inRect(s_fdOk, mx, my)) { fileDialogOk(); return; }
if (inRect(s_fdCancel, mx, my)) { s_fileDlgOpen = false; return; }
if (inRect(s_fdList, mx, my)) {
int idx = s_fileScroll + (int)((my - s_fdList.y) / uiF(18));
if (idx < 0 || idx >= (int)s_fileEntries.size()) return;
const std::string& en = s_fileEntries[idx];
if (s_fileIsDir[idx]) {
if (en == "../") {
size_t slash = s_fileDir.find_last_of('/');
if (slash != std::string::npos && slash > 0) s_fileDir = s_fileDir.substr(0, slash);
else s_fileDir = "/";
} else {
std::string n = en.substr(0, en.size() - 1);
s_fileDir = s_fileDir + "/" + n;
}
fileDialogRefresh();
} else {
s_fileSel = idx;
s_fileName.clear();
for (size_t i = 0; i < en.size(); i++) s_fileName += (wchar_t)(unsigned char)en[i];
}
}
}
#endif
static bool saveAsDialog();
static bool doSave() {
if (s_filePath.empty()) return saveAsDialog();
return saveFileTo(s_filePath);
}
static bool saveAsDialog() {
#ifdef _WIN32
char buf[MAX_PATH * 4] = {};
OPENFILENAMEA ofn = {};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = s_hwnd;
ofn.lpstrFilter = "Text Files\0*.txt\0All Files\0*.*\0";
ofn.lpstrFile = buf;
ofn.nMaxFile = sizeof(buf);
ofn.lpstrDefExt = "txt";
ofn.Flags = OFN_OVERWRITEPROMPT | OFN_NOCHANGEDIR;
if (!GetSaveFileNameA(&ofn)) return false;
return saveFileTo(buf);
#else
fileDialogOpen(true);
return false;
#endif
}
static void openFileDialog() {
#ifdef _WIN32
char buf[MAX_PATH * 4] = {};
OPENFILENAMEA ofn = {};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = s_hwnd;
ofn.lpstrFilter = "Text Files\0*.txt\0All Files\0*.*\0";
ofn.lpstrFile = buf;
ofn.nMaxFile = sizeof(buf);
ofn.Flags = OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR;
if (!GetOpenFileNameA(&ofn)) return;
loadFile(buf);
#else
fileDialogOpen(false);
#endif
}
static void runAfterDialog(int action) {
switch (action) {
case A_NEW: s_filePath.clear(); setDoc(L""); break;
case A_OPEN: openFileDialog(); break;
case A_EXIT:
#ifdef _WIN32
DestroyWindow(s_hwnd);
#else
s_running = false;
#endif
break;
default: break;
}
}
static void requestWithSaveCheck(int action) {
if (!s_dirty) { runAfterDialog(action); return; }
openDialog(DlgQuestion, L"Notepad",
L"Do you want to save changes to " + docBaseName() + L"?", action);
}
static void requestClose() {
if (s_dlgOpen) return;
s_needRender = true;
requestWithSaveCheck(A_EXIT);
}
static void toggleWrap() {
s_wrap = !s_wrap;
if (s_wrap) s_scrollX = 0;
buildView();
scrollToCaret();
}
static void setFontIdx(int idx) {
s_fontIdx = idx;
s_fontSize = s_fontSizes[idx];
updateFontMetrics();
buildView();
scrollToCaret();
}
static void doAction(int a) {
switch (a) {
case A_NEW:
requestWithSaveCheck(A_NEW);
break;
case A_OPEN:
requestWithSaveCheck(A_OPEN);
break;
case A_SAVE: doSave(); break;
case A_SAVEAS: saveAsDialog(); break;
case A_EXIT:
#ifdef _WIN32
PostMessageW(s_hwnd, WM_CLOSE, 0, 0);
#else
requestClose();
#endif
break;
case A_UNDO: undo(); break;
case A_CUT: cutSelection(); break;
case A_COPY: copySelection(); break;
case A_PASTE: pasteFromClipboard(); break;
case A_DELETE:
s_typingRun = false;
if (hasSelection()) { pushUndo(); deleteSelectionNoUndo(); markDirty(); scrollToCaret(); updateTitle(); }
break;
case A_FIND:
s_findVisible = true;
s_findFocus = true;
s_findDirty = true;
s_typingRun = false;
break;
case A_FINDNEXT:
#ifdef _WIN32
findNext((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0);
#else
findNext(s_shiftDown);
#endif
break;
case A_SELECTALL:
s_typingRun = false;
if (!s_lines.empty()) {
s_selRow = 0; s_selCol = 0;
s_caretRow = (int)s_lines.size() - 1;
s_caretCol = (int)s_lines.back().size();
}
scrollToCaret();
break;
case A_TIMEDATE: insertTimeDate(); break;
case A_WRAP: toggleWrap(); break;
case A_STATUSBAR:
s_statusBarVisible = !s_statusBarVisible;
if (s_canvas.getScene()) {
if (ImXui::Element* sb = s_canvas.getScene()->getRootElement()->getChildById(L"chrome"))
if (ImXui::Element* bar = sb->getChildById(L"statusBar"))
bar->setShow(s_statusBarVisible);
}
break;
case A_FONTSM: setFontIdx(0); break;
case A_FONTMED: setFontIdx(1); break;
case A_FONTLG: setFontIdx(2); break;
case A_FONTLG2: setFontIdx(3); break;
case A_ABOUT: s_aboutOpen = true; break;
default: break;
}
}
static bool loadChrome() {
std::vector<std::string> candidates;
candidates.push_back("examples/assets/notepad.xui");
candidates.push_back("assets/notepad.xui");
candidates.push_back("notepad.xui");
#ifdef _WIN32
char exe[MAX_PATH] = {0};
if (GetModuleFileNameA(0, exe, MAX_PATH)) {
std::string p(exe);
size_t slash = p.find_last_of("\\/");
std::string dir = (slash == std::string::npos) ? "" : p.substr(0, slash + 1);
candidates.push_back(dir + "examples/assets/notepad.xui");
candidates.push_back(dir + "assets/notepad.xui");
candidates.push_back(dir + "notepad.xui");
}
#else
char exe[4096];
ssize_t n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
if (n > 0) {
exe[n] = 0;
std::string p(exe);
size_t slash = p.find_last_of('/');
std::string dir = (slash == std::string::npos) ? "" : p.substr(0, slash + 1);
candidates.push_back(dir + "examples/assets/notepad.xui");
candidates.push_back(dir + "assets/notepad.xui");
candidates.push_back(dir + "notepad.xui");
}
#endif
for (size_t i = 0; i < candidates.size(); i++) {
if (s_canvas.loadFromXui(candidates[i]) == ImXui::Result_Success)
return true;
}
return false;
}
static void layoutUI() {
ImXui::Scene* scene = s_canvas.getScene();
if (!scene) return;
ImXui::Element* root = scene->getRootElement();
if (!root) return;
#ifdef _WIN32
RECT rc;
GetClientRect(s_hwnd, &rc);
float W = (float)(rc.right - rc.left);
float H = (float)(rc.bottom - rc.top);
#else
float W = (float)s_renderer.getWidth();
float H = (float)s_renderer.getHeight();
#endif
if (W < 40 || H < 40) return;
ImXui::Element* chrome = root->getChildById(L"chrome");
if (!chrome) return;
s_uiScale = std::max(0.75f, std::min(3.0f, std::min(W / 1280.0f, H / 720.0f)));
const float titleH = uiF(30.0f);
const float menuH = uiF(28.0f);
const float findH = uiF(40.0f);
const float statusH = uiF(24.0f);
ImXui::Element* tb = chrome->getChildById(L"titleBar");
ImXui::Element* mb = chrome->getChildById(L"menuBar");
ImXui::Element* ta = chrome->getChildById(L"textArea");
ImXui::Element* fb = chrome->getChildById(L"findBar");
ImXui::Element* sb = chrome->getChildById(L"statusBar");
if (tb) { tb->setPosition(ImXui::Vec3(0, 0, 0)); tb->setBounds(1280, titleH); }
if (mb) { mb->setPosition(ImXui::Vec3(0, titleH, 0)); mb->setBounds(1280, menuH); }
if (ta) { ta->setPosition(ImXui::Vec3(0, titleH + menuH, 0)); ta->setBounds(1280, 720.0f - titleH - menuH - statusH); }
if (fb) { fb->setPosition(ImXui::Vec3(0, titleH + menuH, 0)); fb->setBounds(1280, findH); }
if (sb) { sb->setPosition(ImXui::Vec3(0, 720.0f - statusH, 0)); sb->setBounds(1280, statusH); }
chrome->computeLayout(ImXui::Rect(0, 0, W, H));
if (tb) s_rectTitle = tb->getComputedRect();
if (mb) s_rectMenu = mb->getComputedRect();
if (ta) s_rectText = ta->getComputedRect();
if (fb) s_rectFind = fb->getComputedRect();
if (sb) s_rectStatus = sb->getComputedRect();
const float btnW = uiF(46);
s_minBtn = ImXui::Rect(W - btnW * 3, s_rectTitle.y, btnW, s_rectTitle.h);
s_maxBtn = ImXui::Rect(W - btnW * 2, s_rectTitle.y, btnW, s_rectTitle.h);
s_closeBtn = ImXui::Rect(W - btnW, s_rectTitle.y, btnW, s_rectTitle.h);
s_editorRect = s_rectText;
if (s_findVisible && s_rectFind.h > 0 && s_rectFind.bottom() <= s_rectText.bottom()) {
s_editorRect.y = s_rectFind.bottom();
s_editorRect.h = s_rectText.bottom() - s_rectFind.bottom();
}
if (s_statusBarVisible) {
s_editorRect.h = std::min(s_editorRect.h, s_rectStatus.y - s_editorRect.y);
}
if (s_uiScale != s_lastUiScale || s_fontSize != s_lastFontSize ||
s_rectMenu.y != s_lastMenuY) {
s_lastUiScale = s_uiScale;
s_lastFontSize = s_fontSize;
s_lastMenuY = s_rectMenu.y;
updateFontMetrics();
s_menuRects.clear();
float x = uiF(6);
for (int i = 0; i < MENU_COUNT; i++) {
float w = textW(s_menuLabels[i], FONT_UI, uiF(12)) + uiF(22);
s_menuRects.push_back(ImXui::Rect(x, s_rectMenu.y + uiF(4), w, s_rectMenu.h - uiF(8)));
x += w;
}
}
}
static void drawCloseIcon(float cx, float cy) {
float r = uiF(4);
std::vector<ImXui::Vec2> d1, d2;
d1.push_back(ImXui::Vec2(0, 0)); d1.push_back(ImXui::Vec2(1, 1));
d2.push_back(ImXui::Vec2(1, 0)); d2.push_back(ImXui::Vec2(0, 1));
ImXui::Rect box(cx - r, cy - r, r * 2, r * 2);
lineFig(box, d1, CLR_TEXT_DIM, 0.9f, uiF(1.5f));
lineFig(box, d2, CLR_TEXT_DIM, 0.9f, uiF(1.5f));
}
static void drawMinIcon(float cx, float cy) {
lineFig(ImXui::Rect(cx - uiF(5), cy, uiF(10), uiF(4)),
std::vector<ImXui::Vec2>{ ImXui::Vec2(0, 0.5f), ImXui::Vec2(1, 0.5f) },
CLR_TEXT_DIM, 0.9f, uiF(1.5f));
}
static void drawMaxIcon(float cx, float cy) {
std::vector<ImXui::Vec2> p;
p.push_back(ImXui::Vec2(0, 0)); p.push_back(ImXui::Vec2(1, 0));
p.push_back(ImXui::Vec2(1, 1)); p.push_back(ImXui::Vec2(0, 1));
p.push_back(ImXui::Vec2(0, 0));
lineFig(ImXui::Rect(cx - uiF(5), cy - uiF(5), uiF(10), uiF(10)), p,
CLR_TEXT_DIM, 0.9f, uiF(1.5f));
}
static void drawRestoreIcon(float cx, float cy) {
lineFig(ImXui::Rect(cx - uiF(1), cy - uiF(6), uiF(8), uiF(8)),
std::vector<ImXui::Vec2>{ ImXui::Vec2(0, 0), ImXui::Vec2(1, 0) },
CLR_TEXT_DIM, 0.9f, uiF(1.5f));
lineFig(ImXui::Rect(cx - uiF(1), cy - uiF(6), uiF(8), uiF(8)),
std::vector<ImXui::Vec2>{ ImXui::Vec2(1, 0), ImXui::Vec2(1, 1) },
CLR_TEXT_DIM, 0.9f, uiF(1.5f));
std::vector<ImXui::Vec2> p;
p.push_back(ImXui::Vec2(0, 0)); p.push_back(ImXui::Vec2(1, 0));
p.push_back(ImXui::Vec2(1, 1)); p.push_back(ImXui::Vec2(0, 1));
p.push_back(ImXui::Vec2(0, 0));
lineFig(ImXui::Rect(cx - uiF(5), cy - uiF(2), uiF(8), uiF(8)), p,
CLR_TEXT_DIM, 0.9f, uiF(1.5f));
}
static void drawTitleBar() {
const ImXui::Rect& t = s_rectTitle;
if (t.h <= 0) return;
std::wstring full = docBaseName() + (s_dirty ? L" *" : L"") + L" - Notepad";
std::wstring title = full;
float maxTW = s_minBtn.x - uiF(24);
while (!title.empty() && textW(title, FONT_UI, uiF(12)) > maxTW) title.pop_back();
if (title != full) title += L"\u2026";
txt(t.x + uiF(12), t.y, maxTW, t.h, title, FONT_UI, uiF(12), CLR_TEXT_DIM);
qd(0, t.bottom() - 1, (float)s_renderer.getWidth(), 1, CLR_BORDER);
if (s_titleHover == 0) qr(s_minBtn, CLR_PANEL_HOT);
drawMinIcon(s_minBtn.x + s_minBtn.w * 0.5f, s_minBtn.y + s_minBtn.h * 0.5f);
if (s_titleHover == 1) qr(s_maxBtn, CLR_PANEL_HOT);
float mcx = s_maxBtn.x + s_maxBtn.w * 0.5f, mcy = s_maxBtn.y + s_maxBtn.h * 0.5f;
if (s_maximized) drawRestoreIcon(mcx, mcy); else drawMaxIcon(mcx, mcy);
if (s_titleHover == 2) qr(s_closeBtn, CLR_CLOSE_HOVER);
drawCloseIcon(s_closeBtn.x + s_closeBtn.w * 0.5f, s_closeBtn.y + s_closeBtn.h * 0.5f);
}
static void drawMenuBar() {
const ImXui::Rect& m = s_rectMenu;
if (m.h <= 0) return;
for (int i = 0; i < MENU_COUNT; i++) {
const ImXui::Rect& r = s_menuRects[i];
bool hot = (s_hoverMenu == i) || (s_openMenu == i);
if (hot) qr(r, CLR_MENU_HOT);
txt(r.x + uiF(10), r.y + uiF(3), r.w - uiF(20), uiF(18), s_menuLabels[i],
FONT_UI, uiF(12), hot ? CLR_TEXT : CLR_TEXT_DIM);
}
qd(0, m.bottom() - 1, (float)s_renderer.getWidth(), 1, CLR_ACCENT_DIM, 0.6f);
}
static bool itemChecked(int menu, int idx) {
MenuItem& it = s_menus[menu].items[idx];
if (it.action == A_WRAP) return s_wrap;
if (it.action == A_STATUSBAR) return s_statusBarVisible;
if (it.action == A_FONTSM) return s_fontIdx == 0;
if (it.action == A_FONTMED) return s_fontIdx == 1;
if (it.action == A_FONTLG) return s_fontIdx == 2;
if (it.action == A_FONTLG2) return s_fontIdx == 3;
return false;
}
static void drawDropdown(int menu) {
const std::vector<MenuItem>& items = s_menus[menu].items;
const float fLabel = uiF(12), fHint = uiF(10), fCheck = uiF(11);
float maxLabel = 0, maxHint = 0;
for (size_t i = 0; i < items.size(); i++) {
if (items[i].separator) continue;
float lw = textW(items[i].label, FONT_UI, fLabel);
if (lw > maxLabel) maxLabel = lw;
if (items[i].hint && items[i].hint[0]) {
float hw = textW(items[i].hint, FONT_UI, fHint);
if (hw > maxHint) maxHint = hw;
}
}
const float itemH = uiF(22.0f);
float w = maxLabel + maxHint + uiF(44);
float h = (float)items.size() * itemH + uiF(8.0f);
ImXui::Rect r(s_menuRects[menu].x, s_rectMenu.bottom() + uiF(2), w, h);
qr(r, CLR_MENU_BG);
qd(r.x, r.y, r.w, 1, CLR_BORDER);
qd(r.x, r.bottom() - 1, r.w, 1, CLR_BORDER);
qd(r.x, r.y, 1, r.h, CLR_BORDER);
qd(r.right() - 1, r.y, 1, r.h, CLR_BORDER);
float y = r.y + uiF(4);
for (size_t i = 0; i < items.size(); i++) {
const MenuItem& it = items[i];
ImXui::Rect ir(r.x + 1, y, r.w - 2, itemH);
if (it.separator) {
qd(r.x + uiF(10), y + itemH * 0.45f, r.w - uiF(20), 1, CLR_BORDER);
y += itemH;
continue;
}
bool hover = (s_hoverItem == (int)i);
if (hover) qr(ir, CLR_MENU_HOT);
if (itemChecked(menu, (int)i))
txt(r.x + uiF(10), y + uiF(3), uiF(14), uiF(16), L"\u2713", FONT_UI, fCheck, CLR_TEXT);
txt(r.x + uiF(26), y + uiF(3), maxLabel + uiF(6), uiF(16), it.label,
FONT_UI, fLabel, CLR_TEXT);
if (it.hint && it.hint[0])
txt(r.right() - maxHint - uiF(12), y + uiF(4), maxHint, uiF(14),
it.hint, FONT_UI, fHint, CLR_TEXT_DIM);
y += itemH;
}
}
static void drawEditor() {
const ImXui::Rect& e = s_editorRect;
if (e.h <= 0 || e.w <= 0) return;
if (s_findDirty) findMatches();
buildView();
int visibleRows = std::max(1, (int)(e.h / s_lineH));
int total = (int)s_view.size();
if (s_scrollTop > total - visibleRows) s_scrollTop = std::max(0, total - visibleRows);
if (s_scrollTop < 0) s_scrollTop = 0;
const float padX = uiF(4.0f);
for (int vr = s_scrollTop; vr < s_scrollTop + visibleRows && vr < total; vr++) {
const VRow& v = s_view[vr];
float y = e.y + uiF(2) + (float)(vr - s_scrollTop) * s_lineH;
if (v.line == s_caretRow && !s_aboutOpen)
qd(e.x, y, e.w, s_lineH, CLR_CURLINE);
if (!s_matches.empty()) {
for (size_t mi = 0; mi < s_matches.size(); mi++) {
const Match& m = s_matches[mi];
if (m.row != v.line) continue;
int s0 = std::max(v.col, m.col);
int s1 = std::min(v.col + v.len, m.col + m.len);
if (s0 >= s1) continue;
bool active = (hasSelection() && m.row == s_selRow && m.col == s_selCol);
if (active) continue;
qd(e.x + padX + (float)s0 * s_charW - s_scrollX, y,
(float)(s1 - s0) * s_charW, s_lineH, CLR_MATCH, 0.55f);
}
}
if (hasSelection()) {
int br, bc, ar, ac;
selBounds(br, bc, ar, ac);
bool inSel = (v.line > br && v.line < ar) ||
(v.line == br && v.line == ar) ||
(v.line == br && br != ar) ||
(v.line == ar && br != ar);
if (inSel) {
int s0 = (v.line == br) ? std::max(v.col, bc) : v.col;
int s1 = (v.line == ar) ? std::min(v.col + v.len, ac) : v.col + v.len;
if (s0 < s1)
qd(e.x + padX + (float)s0 * s_charW - s_scrollX, y,
(float)(s1 - s0) * s_charW, s_lineH, CLR_SEL);
}
}
int c0 = v.col, c1 = v.col + v.len;
if (!s_wrap) {
int sc0 = (int)std::floor(s_scrollX / s_charW);
int sc1 = (int)std::ceil((s_scrollX + e.w - padX) / s_charW);
c0 = std::max(c0, sc0);
c1 = std::min(c1, sc1);
}
if (c0 < c1) {
const std::wstring& line = s_lines[v.line];
txt(e.x + padX + (float)c0 * s_charW - s_scrollX, y + 1,
e.w - padX, s_lineH - 2,
line.substr(c0, c1 - c0), FONT_MONO, s_fontSize * s_uiScale, CLR_TEXT);
}
}
if (!s_aboutOpen && !(s_findVisible && s_findFocus)) {
int cvr = viewRowOf(s_caretRow, s_caretCol);
if (cvr >= s_scrollTop && cvr < s_scrollTop + visibleRows && (int)(nowMs() / 530) % 2 == 0) {
float y = e.y + uiF(2) + (float)(cvr - s_scrollTop) * s_lineH;
float cx = e.x + padX + (float)s_caretCol * s_charW - s_scrollX;
qd(cx, y, 2, s_lineH, CLR_CARET, 0.9f);
}
}
if (total > visibleRows) {
const float trackW = uiF(12);
ImXui::Rect track(e.right() - trackW, e.y, trackW, e.h);
qd(track.x, track.y, trackW, track.h, CLR_SCROLL, 0.4f);
float thumbH = std::max(uiF(24.0f), track.h * (float)visibleRows / (float)total);
float maxTop = (float)(total - visibleRows);
float thumbY = track.y + (track.h - thumbH) * (float)s_scrollTop / maxTop;
qd(track.x, thumbY, trackW, thumbH, CLR_SCROLL_THUMB);
}
if (!s_wrap && s_maxRowW > e.w) {
ImXui::Rect track(e.x, e.bottom() - uiF(12), e.w, uiF(12));
qd(track.x, track.y, track.w, track.h, CLR_SCROLL, 0.4f);
float thumbW = std::max(uiF(24.0f), track.w * e.w / s_maxRowW);
float maxX = std::max(1.0f, s_maxRowW - e.w);
float thumbX = track.x + (track.w - thumbW) * s_scrollX / maxX;
qd(thumbX, track.y, thumbW, track.h, CLR_SCROLL_THUMB);
}
}
static void drawFindBar() {
if (!s_findVisible) return;
const ImXui::Rect& f = s_rectFind;
if (f.h <= 0) return;
qd(0, f.bottom() - 1, (float)s_renderer.getWidth(), 1, CLR_BORDER);
txt(f.x + uiF(10), f.y + uiF(10), uiF(52), uiF(20), L"Find:", FONT_UI, uiF(12), CLR_TEXT_DIM);
s_findBox = ImXui::Rect(f.x + uiF(62), f.y + uiF(6), uiF(340), f.h - uiF(12));
qr(s_findBox, CLR_INPUT);
qd(s_findBox.x, s_findBox.y, s_findBox.w, 1, CLR_BORDER);
qd(s_findBox.x, s_findBox.bottom() - 1, s_findBox.w, 1, CLR_BORDER);
qd(s_findBox.x, s_findBox.y, 1, s_findBox.h, CLR_BORDER);
qd(s_findBox.right() - 1, s_findBox.y, 1, s_findBox.h, CLR_BORDER);
if (s_findFocus) {
txt(s_findBox.x + uiF(8), s_findBox.y + uiF(5), s_findBox.w - uiF(16), uiF(20),
s_findQuery, FONT_MONO, uiF(13), CLR_TEXT);
if ((int)(nowMs() / 530) % 2 == 0) {
float cw = textW(s_findQuery, FONT_MONO, uiF(13));
qd(s_findBox.x + uiF(8) + cw + 1, s_findBox.y + uiF(6), 2, s_findBox.h - uiF(12),
CLR_CARET, 0.9f);
}
} else {
txt(s_findBox.x + uiF(8), s_findBox.y + uiF(5), s_findBox.w - uiF(16), uiF(20),
s_findQuery.empty() ? L"(click to type)" : s_findQuery,
FONT_MONO, uiF(13), s_findQuery.empty() ? CLR_TEXT_DIM : CLR_TEXT);
}
auto btn = [&](float x, float w, const wchar_t* label, int id) {
ImXui::Rect r(x, f.y + uiF(8), w, f.h - uiF(16));
if (s_findHover == id) {
qr(r, CLR_PANEL_HOT);
} else {
qd(r.x, r.y, r.w, 1, CLR_BORDER);
qd(r.x, r.bottom() - 1, r.w, 1, CLR_BORDER);
qd(r.x, r.y, 1, r.h, CLR_BORDER);
qd(r.right() - 1, r.y, 1, r.h, CLR_BORDER);
}
txt(r.x, r.y + uiF(4), r.w, uiF(16), label, FONT_UI, uiF(11), CLR_TEXT, 1.0f, true);
};
float bx = s_findBox.right() + uiF(10);
btn(bx, uiF(52), L"Next", 0);
btn(bx + uiF(58), uiF(52), L"Prev", 1);
btn(bx + uiF(116), uiF(56), L"Close", 2);
if (s_findDirty) findMatches();
std::wstring count = s_findQuery.empty() ? L"" :
(std::to_wstring((long long)s_matches.size()) + (s_matches.size() == 1 ? L" match" : L" matches"));
txt(bx + uiF(180), f.y + uiF(10), uiF(220), uiF(18), count, FONT_UI, uiF(11), CLR_TEXT_DIM);
}
static void drawStatusBar() {
if (!s_statusBarVisible) return;
const ImXui::Rect& s = s_rectStatus;
if (s.h <= 0) return;
qd(0, s.y, (float)s_renderer.getWidth(), 1, CLR_BORDER);
std::wstring name = L"Untitled";
if (!s_filePath.empty()) {
size_t slash = s_filePath.find_last_of("\\/");
name = std::wstring(s_filePath.begin() + (slash == std::string::npos ? 0 : slash + 1), s_filePath.end());
}
txt(s.x + uiF(10), s.y + uiF(4), uiF(360), uiF(16), name + (s_dirty ? L" *" : L""),
FONT_UI, uiF(11), s_dirty ? CLR_TEXT : CLR_TEXT_DIM);
std::wstring right;
right += s_wrap ? L"Wrap" : L"No Wrap";
right += L" UTF-8";
int chars = 0;
for (size_t i = 0; i < s_lines.size(); i++) chars += (int)s_lines[i].size();
if (!s_lines.empty()) chars += (int)s_lines.size() - 1;
right += L" " + std::to_wstring((long long)chars) + L" chars";
right += L" Ln " + std::to_wstring((long long)(s_caretRow + 1)) +
L", Col " + std::to_wstring((long long)(s_caretCol + 1));
float rw = textW(right, FONT_UI, uiF(11));
txt(s.right() - rw - uiF(10), s.y + uiF(4), rw + uiF(4), uiF(16), right, FONT_UI, uiF(11), CLR_TEXT_DIM);
}
static void drawAbout() {
if (!s_aboutOpen) return;
int W = s_renderer.getWidth(), H = s_renderer.getHeight();
qd(0, 0, (float)W, (float)H, CLR_WINDOW, 0.55f);
float pw = uiF(420), ph = uiF(240);
ImXui::Rect panel((float)W / 2 - pw / 2, (float)H / 2 - ph / 2, pw, ph);
qr(panel, CLR_MENU_BG);
qd(panel.x, panel.y, panel.w, 1, CLR_ACCENT_DIM);
qd(panel.x, panel.bottom() - 1, panel.w, 1, CLR_BORDER);
qd(panel.x, panel.y, 1, panel.h, CLR_BORDER);
qd(panel.right() - 1, panel.y, 1, panel.h, CLR_BORDER);
txt(panel.x + uiF(18), panel.y + uiF(14), pw - uiF(36), uiF(24), L"Notepad", FONT_UI, uiF(16), CLR_TEXT);
txt(panel.x + uiF(18), panel.y + uiF(52), pw - uiF(36), uiF(18),
L"A text editor rendered entirely with ImXui.", FONT_UI, uiF(11), CLR_TEXT_DIM);
txt(panel.x + uiF(18), panel.y + uiF(76), pw - uiF(36), uiF(18),
L"The window chrome is authored as data in notepad.xui", FONT_UI, uiF(11), CLR_TEXT_DIM);
txt(panel.x + uiF(18), panel.y + uiF(96), pw - uiF(36), uiF(18),
L"and loaded at runtime by Canvas::loadFromXui.", FONT_UI, uiF(11), CLR_TEXT_DIM);
txt(panel.x + uiF(18), panel.y + uiF(128), pw - uiF(36), uiF(18),
L"Document text, caret, selection, menus, and status bar", FONT_UI, uiF(11), CLR_TEXT_DIM);
txt(panel.x + uiF(18), panel.y + uiF(148), pw - uiF(36), uiF(18),
L"are drawn with the renderer's immediate API.", FONT_UI, uiF(11), CLR_TEXT_DIM);
s_aboutOk = ImXui::Rect(panel.x + panel.w / 2 - uiF(45), panel.bottom() - uiF(40),
uiF(90), uiF(26));
qr(s_aboutOk, s_hoverAbout ? CLR_MENU_HOT : CLR_PANEL_HOT);
qd(s_aboutOk.x, s_aboutOk.y, s_aboutOk.w, 1, CLR_BORDER);
qd(s_aboutOk.x, s_aboutOk.bottom() - 1, s_aboutOk.w, 1, CLR_BORDER);
qd(s_aboutOk.x, s_aboutOk.y, 1, s_aboutOk.h, CLR_BORDER);
qd(s_aboutOk.right() - 1, s_aboutOk.y, 1, s_aboutOk.h, CLR_BORDER);
txt(s_aboutOk.x, s_aboutOk.y + uiF(4), s_aboutOk.w, uiF(18), L"OK", FONT_UI, uiF(12),
CLR_TEXT, 1.0f, true);
}
static void openDialog(DlgKind kind, const std::wstring& title, const std::wstring& msg,
int pendingAction, bool fatal) {
s_dlgOpen = true;
s_dlgKind = kind;
s_dlgTitle = title;
s_dlgMsg = msg;
s_dlgPendingAction = pendingAction;
s_dlgFatal = fatal;
s_dlgHover = -1;
s_openMenu = -1;
s_aboutOpen = false;
s_dlgBtnRects[0] = ImXui::Rect();
s_dlgBtnRects[1] = ImXui::Rect();
s_dlgBtnRects[2] = ImXui::Rect();
}
static std::vector<std::wstring> wrapDlgText(const std::wstring& text, float maxW) {
std::vector<std::wstring> out;
std::wstring cur;
for (size_t i = 0; i < text.size(); i++) {
if (text[i] == L'\n') { out.push_back(cur); cur.clear(); continue; }
cur += text[i];
}
out.push_back(cur);
std::vector<std::wstring> wrapped;
for (size_t li = 0; li < out.size(); li++) {
const std::wstring& line = out[li];
if (line.empty()) { wrapped.push_back(L""); continue; }
size_t start = 0;
while (start < line.size()) {
size_t end = start;
float w = 0;
while (end < line.size()) {
size_t ws = end;
while (ws < line.size() && line[ws] == L' ') ws++;
size_t we = ws;
while (we < line.size() && line[we] != L' ') we++;
if (we == ws) break;
float sw = (end == start) ? 0 : textW(L" ", FONT_UI, uiF(12));
float ww = textW(line.substr(ws, we - ws), FONT_UI, uiF(12));
if (w + sw + ww > maxW) { if (ws == start) end = we; break; }
w += sw + ww;
end = we;
while (end < line.size() && line[end] == L' ') end++;
}
if (end <= start) end = start + 1;
wrapped.push_back(line.substr(start, end - start));
start = end;
}
}
return wrapped;
}
#ifndef _WIN32
static void maximizeWindow() {
if (s_maximized) return;
XWindowAttributes wa;
XGetWindowAttributes(s_dpy, s_win, &wa);
s_savedX = wa.x; s_savedY = wa.y;
s_savedW = wa.width; s_savedH = wa.height;
Screen* sc = DefaultScreenOfDisplay(s_dpy);
XMoveResizeWindow(s_dpy, s_win, 0, 0, WidthOfScreen(sc), HeightOfScreen(sc));
s_maximized = true;
}
static void restoreWindow() {
if (!s_maximized) return;
XMoveResizeWindow(s_dpy, s_win, s_savedX, s_savedY, s_savedW, s_savedH);
s_maximized = false;
}
static void toggleMaximize() {
if (s_maximized) restoreWindow(); else maximizeWindow();
}
#endif
static void drawDialog() {
if (!s_dlgOpen) return;
int W = s_renderer.getWidth(), H = s_renderer.getHeight();
qd(0, 0, (float)W, (float)H, CLR_WINDOW, 0.55f);
std::vector<std::wstring> lines = wrapDlgText(s_dlgMsg, uiF(380));
float pw = uiF(430);
float ph = uiF(40) + (float)lines.size() * uiF(20) + uiF(66);
ImXui::Rect panel((float)W / 2 - pw / 2, (float)H / 2 - ph / 2, pw, ph);
qr(panel, CLR_MENU_BG);
qd(panel.x, panel.y, panel.w, 1, CLR_ACCENT_DIM);
qd(panel.x, panel.bottom() - 1, panel.w, 1, CLR_BORDER);
qd(panel.x, panel.y, 1, panel.h, CLR_BORDER);
qd(panel.right() - 1, panel.y, 1, panel.h, CLR_BORDER);
txt(panel.x + uiF(16), panel.y + uiF(12), pw - uiF(32), uiF(22), s_dlgTitle,
FONT_UI, uiF(14), CLR_TEXT);
float ly = panel.y + uiF(40);
for (size_t i = 0; i < lines.size(); i++) {
txt(panel.x + uiF(16), ly, pw - uiF(32), uiF(18), lines[i], FONT_UI, uiF(12),
CLR_TEXT_DIM);
ly += uiF(20);
}
const wchar_t* labels[3] = { 0, 0, 0 };
int n = 0;
if (s_dlgKind == DlgQuestion) {
labels[0] = L"Yes"; labels[1] = L"No"; labels[2] = L"Cancel";
n = 3;
} else {
labels[0] = L"OK";
n = 1;
}
float bw = uiF(84), bh = uiF(28);
float total = (float)n * bw + (float)(n - 1) * uiF(10);
float bx = panel.x + (panel.w - total) * 0.5f;
float by = panel.bottom() - uiF(38);
for (int i = 0; i < n; i++) {
s_dlgBtnRects[i] = ImXui::Rect(bx, by, bw, bh);
ImXui::Rect& r = s_dlgBtnRects[i];
if (s_dlgHover == i) {
qr(r, CLR_MENU_HOT);
} else {
qd(r.x, r.y, r.w, 1, CLR_BORDER);
qd(r.x, r.bottom() - 1, r.w, 1, CLR_BORDER);
qd(r.x, r.y, 1, r.h, CLR_BORDER);
qd(r.right() - 1, r.y, 1, r.h, CLR_BORDER);
}
txt(r.x, r.y + uiF(5), r.w, uiF(18), labels[i], FONT_UI, uiF(12), CLR_TEXT,
1.0f, true);
bx += bw + uiF(10);
}
}
static void onDialogButton(int idx) {
if (s_dlgKind == DlgQuestion) {
if (idx == 0) {
s_dlgOpen = false;
int a = s_dlgPendingAction;
s_dlgPendingAction = -1;
if (doSave()) runAfterDialog(a);
} else if (idx == 1) {
s_dlgOpen = false;
int a = s_dlgPendingAction;
s_dlgPendingAction = -1;
runAfterDialog(a);
} else {
s_dlgOpen = false;
s_dlgPendingAction = -1;
}
} else {
s_dlgOpen = false;
if (s_dlgFatal) {
s_dlgFatal = false;
#ifdef _WIN32
PostMessageW(s_hwnd, WM_CLOSE, 0, 0);
#else
s_running = false;
#endif
}
}
}
static ImXui::Rect menuDropdownRect(int menu) {
const std::vector<MenuItem>& items = s_menus[menu].items;
const float fLabel = uiF(12), fHint = uiF(10);
float maxLabel = 0, maxHint = 0;
for (size_t i = 0; i < items.size(); i++) {
if (items[i].separator) continue;
float lw = textW(items[i].label, FONT_UI, fLabel);
if (lw > maxLabel) maxLabel = lw;
if (items[i].hint && items[i].hint[0]) {
float hw = textW(items[i].hint, FONT_UI, fHint);
if (hw > maxHint) maxHint = hw;
}
}
float w = maxLabel + maxHint + uiF(44);
float h = (float)items.size() * uiF(22.0f) + uiF(8.0f);
return ImXui::Rect(s_menuRects[menu].x, s_rectMenu.bottom() + uiF(2), w, h);
}
static void caretFromPoint(float mx, float my) {
const ImXui::Rect& e = s_editorRect;
if (e.h <= 0) return;
int visibleRows = std::max(1, (int)(e.h / s_lineH));
int vr = s_scrollTop + (int)((my - e.y - uiF(2)) / s_lineH);
vr = std::max(0, std::min(vr, (int)s_view.size() - 1));
const VRow& v = s_view[vr];
float x = mx - e.x - uiF(4) + s_scrollX;
int col = v.col;
if (x <= 0) col = v.col;
else {
int c = (int)(x / s_charW);
col = v.col + std::max(0, std::min(c, v.len));
}
s_caretRow = v.line;
s_caretCol = col;
}
static void updateHover(float mx, float my) {
s_hoverMenu = -1;
s_hoverItem = -1;
s_findHover = -1;
s_hoverAbout = false;
s_titleHover = -1;
s_dlgHover = -1;
#ifndef _WIN32
if (s_fileDlgOpen) {
fileDialogHover(mx, my);
return;
}
#endif
if (s_dlgOpen) {
for (int i = 0; i < 3; i++) {
if (s_dlgBtnRects[i].w > 0 && inRect(s_dlgBtnRects[i], mx, my)) { s_dlgHover = i; break; }
}
return;
}
if (s_aboutOpen) {
s_hoverAbout = inRect(s_aboutOk, mx, my);
return;
}
if (my <= s_rectTitle.bottom()) {
if (inRect(s_minBtn, mx, my)) s_titleHover = 0;
else if (inRect(s_maxBtn, mx, my)) s_titleHover = 1;
else if (inRect(s_closeBtn, mx, my)) s_titleHover = 2;
return;
}
if (my >= s_rectMenu.y && my <= s_rectMenu.bottom()) {
for (int i = 0; i < MENU_COUNT; i++) {
if (inRect(s_menuRects[i], mx, my)) { s_hoverMenu = i; break; }
}
} if (s_openMenu >= 0) {
ImXui::Rect dr = menuDropdownRect(s_openMenu);
if (inRect(dr, mx, my)) {
int idx = (int)((my - dr.y - uiF(4)) / uiF(22));
if (idx >= 0 && idx < (int)s_menus[s_openMenu].items.size() &&
!s_menus[s_openMenu].items[idx].separator)
s_hoverItem = idx;
}
return;
}
if (s_findVisible) {
const ImXui::Rect& f = s_rectFind;
if (my >= f.y && my <= f.bottom()) {
float bx = s_findBox.right() + uiF(10);
if (mx >= bx && mx < bx + uiF(52)) s_findHover = 0;
else if (mx >= bx + uiF(58) && mx < bx + uiF(110)) s_findHover = 1;
else if (mx >= bx + uiF(116) && mx < bx + uiF(172)) s_findHover = 2;
}
}
}
static void handleLButtonDown(float mx, float my) {
updateHover(mx, my);
#ifndef _WIN32
if (s_fileDlgOpen) {
fileDialogMouse(mx, my);
return;
}
#endif
if (s_dlgOpen) {
if (s_dlgHover >= 0) onDialogButton(s_dlgHover);
return;
}
if (s_aboutOpen) {
if (inRect(s_aboutOk, mx, my)) s_aboutOpen = false;
return;
}
if (my <= s_rectTitle.bottom()) {
s_openMenu = -1;
if (s_titleHover == 0) {
#ifdef _WIN32
ShowWindow(s_hwnd, SW_MINIMIZE);
#else
XIconifyWindow(s_dpy, s_win, DefaultScreen(s_dpy));
#endif
} else if (s_titleHover == 1) {
#ifdef _WIN32
ShowWindow(s_hwnd, s_maximized ? SW_RESTORE : SW_MAXIMIZE);
#else
toggleMaximize();
#endif
} else if (s_titleHover == 2) {
#ifdef _WIN32
PostMessageW(s_hwnd, WM_CLOSE, 0, 0);
#else
requestClose();
#endif
} else {
#ifdef _WIN32
ReleaseCapture(); SendMessageW(s_hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0);
#else
s_winDrag = true;
s_dragDX = (int)mx; s_dragDY = (int)my;
#endif
}
return;
}
if (my >= s_rectMenu.y && my <= s_rectMenu.bottom() || s_openMenu >= 0) {
if (s_openMenu >= 0) {
ImXui::Rect dr = menuDropdownRect(s_openMenu);
if (inRect(dr, mx, my)) {
if (s_hoverItem >= 0) {
int a = s_menus[s_openMenu].items[s_hoverItem].action;
s_openMenu = -1;
doAction(a);
}
return;
}
s_openMenu = -1;
}
if (my >= s_rectMenu.y && my <= s_rectMenu.bottom()) {
int hit = -1;
for (int i = 0; i < MENU_COUNT; i++)
if (inRect(s_menuRects[i], mx, my)) { hit = i; break; }
s_openMenu = (hit >= 0 && hit == s_openMenu) ? -1 : hit;
}
return;
}
if (s_findVisible && my >= s_rectFind.y && my <= s_rectFind.bottom()) {
s_findFocus = inRect(s_findBox, mx, my);
if (s_findHover == 0) { findNext(false); return; }
if (s_findHover == 1) { findNext(true); return; }
if (s_findHover == 2) { s_findVisible = false; return; }
return;
}
s_findFocus = false;
const ImXui::Rect& e = s_editorRect;
if (my >= e.y && my <= e.bottom() && mx >= e.x && mx <= e.right()) {
int total = (int)s_view.size();
int visibleRows = std::max(1, (int)(e.h / s_lineH));
if (total > visibleRows && mx >= e.right() - uiF(12)) {
s_dragV = true;
float trackH = e.h;
float maxTop = (float)(total - visibleRows);
s_scrollTop = (int)((my - e.y) / trackH * maxTop);
s_scrollTop = std::max(0, std::min(s_scrollTop, total - visibleRows));
return;
}
if (!s_wrap && s_maxRowW > e.w && my >= e.bottom() - uiF(12)) {
s_dragH = true;
float maxX = std::max(1.0f, s_maxRowW - e.w);
s_scrollX = (mx - e.x) / e.w * maxX;
s_scrollX = std::max(0.0f, std::min(s_scrollX, maxX));
return;
}
s_typingRun = false;
caretFromPoint(mx, my);
setAnchorToCaret();
s_selecting = true;
scrollToCaret();
}
}
static void handleMouseMove(float mx, float my) {
updateHover(mx, my);
if (s_selecting) {
caretFromPoint(mx, my);
scrollToCaret();
return;
}
if (s_dragV) {
const ImXui::Rect& e = s_editorRect;
int total = (int)s_view.size();
int visibleRows = std::max(1, (int)(e.h / s_lineH));
if (total > visibleRows) {
float maxTop = (float)(total - visibleRows);
s_scrollTop = (int)((my - e.y) / e.h * maxTop);
s_scrollTop = std::max(0, std::min(s_scrollTop, total - visibleRows));
}
return;
}
if (s_dragH) {
const ImXui::Rect& e = s_editorRect;
if (s_maxRowW > e.w) {
float maxX = std::max(1.0f, s_maxRowW - e.w);
s_scrollX = (mx - e.x) / e.w * maxX;
s_scrollX = std::max(0.0f, std::min(s_scrollX, maxX));
}
return;
}
}
#ifdef _WIN32
static HCURSOR cursorForPoint(float mx, float my) {
#else
static Cursor cursorForPoint(float mx, float my) {
#endif
#ifndef _WIN32
if (s_fileDlgOpen) {
if (inRect(s_fdOk, mx, my) || inRect(s_fdCancel, mx, my)) return s_curHand;
if (inRect(s_fdList, mx, my)) return s_curHand;
return s_curArrow;
}
#endif
if (s_dlgOpen) {
for (int i = 0; i < 3; i++) {
if (s_dlgBtnRects[i].w > 0 && inRect(s_dlgBtnRects[i], mx, my))
return s_curHand;
}
return s_curArrow;
}
if (s_aboutOpen)
return inRect(s_aboutOk, mx, my) ? s_curHand : s_curArrow;
if (my <= s_rectTitle.bottom()) {
if (inRect(s_minBtn, mx, my) || inRect(s_maxBtn, mx, my) || inRect(s_closeBtn, mx, my))
return s_curHand;
return s_curArrow;
}
if (my <= s_rectMenu.bottom()) {
for (int i = 0; i < MENU_COUNT; i++)
if (inRect(s_menuRects[i], mx, my)) return s_curHand;
return s_curArrow;
}
if (s_openMenu >= 0) {
ImXui::Rect dr = menuDropdownRect(s_openMenu);
return inRect(dr, mx, my) ? s_curHand : s_curArrow;
}
if (s_findVisible && my <= s_rectFind.bottom()) {
if (inRect(s_findBox, mx, my)) return s_curIBeam;
float bx = s_findBox.right() + uiF(10);
if ((mx >= bx && mx < bx + uiF(52)) ||
(mx >= bx + uiF(58) && mx < bx + uiF(110)) ||
(mx >= bx + uiF(116) && mx < bx + uiF(172)))
return s_curHand;
return s_curArrow;
}
if (my >= s_editorRect.y && my <= s_editorRect.bottom() &&
mx >= s_editorRect.x && mx <= s_editorRect.right()) {
int total = (int)s_view.size();
int visibleRows = std::max(1, (int)(s_editorRect.h / s_lineH));
if (total > visibleRows && mx >= s_editorRect.right() - uiF(12)) {
float thumbH = std::max(uiF(24.0f), s_editorRect.h * (float)visibleRows / (float)total);
float maxTop = (float)(total - visibleRows);
float thumbY = s_editorRect.y + (s_editorRect.h - thumbH) * (float)s_scrollTop / maxTop;
if (my >= thumbY && my <= thumbY + thumbH) return s_curSizeNS;
return s_curArrow;
}
if (!s_wrap && s_maxRowW > s_editorRect.w && my >= s_editorRect.bottom() - uiF(12)) {
float thumbW = std::max(uiF(24.0f), s_editorRect.w * s_editorRect.w / s_maxRowW);
float maxX = std::max(1.0f, s_maxRowW - s_editorRect.w);
float thumbX = s_editorRect.x + (s_editorRect.w - thumbW) * s_scrollX / maxX;
if (mx >= thumbX && mx <= thumbX + thumbW) return s_curSizeWE;
return s_curArrow;
}
return s_curIBeam;
}
return s_curArrow;
}
static void handleWheel(int delta, bool ctrl) {
#ifndef _WIN32
if (s_fileDlgOpen) {
s_fileScroll += (delta > 0) ? -3 : 3;
s_fileScroll = std::max(0, s_fileScroll);
return;
}
#endif
if (s_dlgOpen || s_aboutOpen) return;
if (ctrl) {
int idx = s_fontIdx;
idx += (delta > 0) ? 1 : -1;
idx = std::max(0, std::min(idx, 3));
if (idx != s_fontIdx) setFontIdx(idx);
return;
}
int visibleRows = std::max(1, (int)(s_editorRect.h / s_lineH));
int total = (int)s_view.size();
if (total > visibleRows) {
int step = (delta > 0) ? -3 : 3;
s_scrollTop += step;
s_scrollTop = std::max(0, std::min(s_scrollTop, total - visibleRows));
}
}
static void handleChar(wchar_t c) {
#ifndef _WIN32
if (s_fileDlgOpen) {
if (c == 0x08) {
if (!s_fileName.empty()) s_fileName.erase(s_fileName.end() - 1);
} else if (c >= 32 && c != 127) {
s_fileName += c;
}
return;
}
#endif
if (s_dlgOpen || s_aboutOpen || s_openMenu >= 0) return;
if (s_findVisible && s_findFocus) {
if (c == 0x08) {
if (!s_findQuery.empty()) s_findQuery.erase(s_findQuery.end() - 1);
s_findDirty = true;
} else if (c >= 32) {
s_findQuery += c;
s_findDirty = true;
}
return;
}
if (c == 0x08) backspace();
else if (c == 0x7F) deleteWordLeft();
else if (c == '\r') { s_typingRun = false; insertAtCaret(L"\n"); }
else if (c == '\t') { s_typingRun = false; insertAtCaret(L" "); }
else if (c >= 32) { s_typingRun = true; std::wstring s(1, c); insertAtCaret(s); }
}
static void handleKeyDown(int vk, bool ctrl, bool shift) {
#ifndef _WIN32
if (s_fileDlgOpen) {
if (vk == VK_ESCAPE) { s_fileDlgOpen = false; return; }
if (vk == VK_RETURN) { fileDialogOk(); return; }
if (vk == VK_UP || vk == VK_DOWN) {
int n = (int)s_fileEntries.size();
if (n > 0) {
if (s_fileSel < 0) s_fileSel = (vk == VK_UP) ? n - 1 : 0;
else s_fileSel += (vk == VK_UP) ? -1 : 1;
s_fileSel = std::max(0, std::min(s_fileSel, n - 1));
const std::string& en = s_fileEntries[s_fileSel];
s_fileName.clear();
for (size_t i = 0; i < en.size(); i++) s_fileName += (wchar_t)(unsigned char)en[i];
int visible = std::max(1, (int)(s_fdList.h / uiF(18)));
if (s_fileSel < s_fileScroll) s_fileScroll = s_fileSel;
if (s_fileSel >= s_fileScroll + visible) s_fileScroll = s_fileSel - visible + 1;
}
}
return;
}
#endif
if (s_dlgOpen) {
if (vk == VK_ESCAPE) onDialogButton(s_dlgKind == DlgQuestion ? 2 : 0);
else if (vk == VK_RETURN) onDialogButton(0);
return;
}
if (s_aboutOpen) {
if (vk == VK_ESCAPE || vk == VK_RETURN || vk == VK_SPACE) s_aboutOpen = false;
return;
}
if (s_openMenu >= 0) {
switch (vk) {
case VK_ESCAPE: s_openMenu = -1; return;
case VK_LEFT: case VK_RIGHT: {
int dir = (vk == VK_LEFT) ? -1 : 1;
int next = s_openMenu + dir;
if (next < 0) next = MENU_COUNT - 1;
if (next >= MENU_COUNT) next = 0;
s_openMenu = next; s_hoverItem = -1;
return;
}
case VK_UP: case VK_DOWN: {
const std::vector<MenuItem>& items = s_menus[s_openMenu].items;
int n = (int)items.size();
int start = (vk == VK_DOWN) ? (s_hoverItem < 0 ? 0 : s_hoverItem + 1)
: (s_hoverItem < 0 ? n - 1 : s_hoverItem - 1);
for (int i = 0; i < n; i++) {
int idx = (start + i * ((vk == VK_DOWN) ? 1 : -1) + n * n) % n;
if (!items[idx].separator) { s_hoverItem = idx; break; }
}
return;
}
case VK_RETURN:
if (s_hoverItem >= 0) {
int a = s_menus[s_openMenu].items[s_hoverItem].action;
s_openMenu = -1;
doAction(a);
}
return;
}
return;
}
if (s_findVisible) {
if (vk == VK_ESCAPE) { s_findVisible = false; return; }
if (vk == VK_RETURN) { findNext(shift); return; }
if (vk == VK_TAB) { s_findFocus = false; return; }
if (vk == VK_F3) { findNext(shift); return; }
}
if (ctrl && vk == 'Z') { undo(); return; }
if (ctrl && vk == 'Y') { redo(); return; }
if (ctrl && vk == 'X') { cutSelection(); return; }
if (ctrl && vk == 'C') { copySelection(); return; }
if (ctrl && vk == 'V') { pasteFromClipboard(); return; }
if (ctrl && vk == 'A') { doAction(A_SELECTALL); return; }
if (ctrl && vk == 'F') { doAction(A_FIND); return; }
if (ctrl && vk == 'N') { doAction(A_NEW); return; }
if (ctrl && vk == 'O') { doAction(A_OPEN); return; }
if (ctrl && vk == 'S') { if (shift) doAction(A_SAVEAS); else doAction(A_SAVE); return; }
if (vk == VK_F3) { findNext(shift); return; }
if (vk == VK_F5) { insertTimeDate(); return; }
if (s_findVisible && s_findFocus) return;
int vr = viewRowOf(s_caretRow, s_caretCol);
int visibleRows = std::max(1, (int)(s_editorRect.h / s_lineH));
switch (vk) {
case VK_LEFT: {
int oldCol = s_caretCol;
if (ctrl) {
s_typingRun = false;
if (hasSelection() && !shift) {
int br, bc, ar, ac; selBounds(br, bc, ar, ac);
s_caretRow = br; s_caretCol = bc;
} else {
s_caretCol = wordLeftCol(s_lines[s_caretRow], s_caretCol);
if (s_caretCol == 0 && s_caretRow > 0 && oldCol == 0) { s_caretRow--; s_caretCol = (int)s_lines[s_caretRow].size(); }
}
} else if (s_caretCol > 0) s_caretCol--;
else if (s_caretRow > 0) { s_caretRow--; s_caretCol = (int)s_lines[s_caretRow].size(); }
break;
}
case VK_RIGHT:
if (ctrl) {
s_typingRun = false;
if (hasSelection() && !shift) {
int br, bc, ar, ac; selBounds(br, bc, ar, ac);
s_caretRow = ar; s_caretCol = ac;
} else {
s_caretCol = wordRightCol(s_lines[s_caretRow], s_caretCol);
if (s_caretCol == (int)s_lines[s_caretRow].size() && s_caretRow + 1 < (int)s_lines.size()) {
s_caretRow++; s_caretCol = 0;
}
}
} else if (s_caretCol < (int)s_lines[s_caretRow].size()) s_caretCol++;
else if (s_caretRow + 1 < (int)s_lines.size()) { s_caretRow++; s_caretCol = 0; }
break;
case VK_UP:
if (ctrl) s_scrollTop--;
else { vr--; caretFromViewRow(vr, s_caretCol, s_caretRow, s_caretCol); }
break;
case VK_DOWN:
if (ctrl) s_scrollTop++;
else { vr++; caretFromViewRow(vr, s_caretCol, s_caretRow, s_caretCol); }
break;
case VK_HOME:
s_caretCol = 0;
if (ctrl) { s_caretRow = 0; }
break;
case VK_END:
s_caretCol = (int)s_lines[s_caretRow].size();
if (ctrl) { s_caretRow = (int)s_lines.size() - 1; s_caretCol = (int)s_lines[s_caretRow].size(); }
break;
case VK_PRIOR: {
s_caretRow = s_caretRow - visibleRows; if (s_caretRow < 0) s_caretRow = 0;
s_scrollTop -= visibleRows; if (s_scrollTop < 0) s_scrollTop = 0;
int len = (int)s_lines[s_caretRow].size();
if (s_caretCol > len) s_caretCol = len;
break;
}
case VK_NEXT: {
int last = (int)s_lines.size() - 1;
s_caretRow = s_caretRow + visibleRows; if (s_caretRow > last) s_caretRow = last;
s_scrollTop += visibleRows;
int total = (int)s_view.size();
int maxTop = std::max(0, total - visibleRows);
if (s_scrollTop > maxTop) s_scrollTop = maxTop;
int len = (int)s_lines[s_caretRow].size();
if (s_caretCol > len) s_caretCol = len;
break;
}
case VK_DELETE:
if (ctrl) deleteWordRight();
else { s_typingRun = false; forwardDelete(); }
break;
default:
return;
}
clampCaret();
if (!shift) setAnchorToCaret();
if (ctrl && (vk == VK_UP || vk == VK_DOWN)) {
int total = (int)s_view.size();
int maxTop = std::max(0, total - visibleRows);
if (s_scrollTop < 0) s_scrollTop = 0;
if (s_scrollTop > maxTop) s_scrollTop = maxTop;
}
scrollToCaret();
}
#ifdef _WIN32
static LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM w, LPARAM l) {
switch (msg) {
case WM_DESTROY:
s_running = false;
PostQuitMessage(0);
return 0;
case WM_TIMER:
if (w == TIMER_BLINK && caretVisible()) { s_needRender = true; s_blinkOnly = true; }
return 0;
case WM_CLOSE:
requestClose();
return 0;
case WM_SIZE:
s_minimized = (w == SIZE_MINIMIZED);
if (w == SIZE_MINIMIZED) return 0;
s_maximized = (w == SIZE_MAXIMIZED);
if (!s_rendererReady) return 0;
s_needRender = true;
s_renderer.resize(LOWORD(l), HIWORD(l));
layoutUI();
return 0;
case WM_LBUTTONDBLCLK: {
float mx = (float)LOWORD(l), my = (float)HIWORD(l);
if (my <= s_rectTitle.bottom() &&
!(inRect(s_minBtn, mx, my) || inRect(s_maxBtn, mx, my) || inRect(s_closeBtn, mx, my))) {
ShowWindow(hwnd, s_maximized ? SW_RESTORE : SW_MAXIMIZE);
return 0;
}
return 0;
}
case WM_LBUTTONDOWN:
s_needRender = true; s_blinkOnly = false;
handleLButtonDown((float)LOWORD(l), (float)HIWORD(l));
return 0;
case WM_MOUSEMOVE:
s_needRender = true; s_blinkOnly = false;
handleMouseMove((float)LOWORD(l), (float)HIWORD(l));
return 0;
case WM_LBUTTONUP:
s_needRender = true; s_blinkOnly = false;
s_selecting = false;
s_dragV = false;
s_dragH = false;
return 0;
case WM_SETCURSOR:
if (LOWORD(l) == HTCLIENT) {
POINT pt;
GetCursorPos(&pt);
ScreenToClient(hwnd, &pt);
HCURSOR cur = cursorForPoint((float)pt.x, (float)pt.y);
if (cur) { SetCursor(cur); return TRUE; }
}
return DefWindowProcW(hwnd, msg, w, l);
case WM_NCHITTEST: {
POINT pt = { GET_X_LPARAM(l), GET_Y_LPARAM(l) };
ScreenToClient(hwnd, &pt);
RECT rc; GetClientRect(hwnd, &rc);
float mx = (float)pt.x, my = (float)pt.y;
float W = (float)(rc.right - rc.left), H = (float)(rc.bottom - rc.top);
const float B = 8.0f;
if (!s_maximized) {
bool lft = mx < B, rgt = mx > W - B, top = my < B, bot = my > H - B;
if (top && lft) return HTTOPLEFT;
if (top && rgt) return HTTOPRIGHT;
if (bot && lft) return HTBOTTOMLEFT;
if (bot && rgt) return HTBOTTOMRIGHT;
if (lft) return HTLEFT;
if (rgt) return HTRIGHT;
if (top) return HTTOP;
if (bot) return HTBOTTOM;
}
return HTCLIENT;
}
case WM_NCCALCSIZE:
return 0;
case WM_GETMINMAXINFO: {
MINMAXINFO* mmi = (MINMAXINFO*)l;
HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
MONITORINFO mi;
mi.cbSize = sizeof(mi);
if (GetMonitorInfo(mon, &mi)) {
RECT work = mi.rcWork;
mmi->ptMaxPosition.x = work.left;
mmi->ptMaxPosition.y = work.top;
mmi->ptMaxSize.x = work.right - work.left;
mmi->ptMaxSize.y = work.bottom - work.top;
}
return 0;
}
case WM_ERASEBKGND:
return 1;
case WM_MOUSEWHEEL: {
s_needRender = true; s_blinkOnly = false;
int delta = GET_WHEEL_DELTA_WPARAM(w);
bool ctrl = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
handleWheel(delta, ctrl);
return 0;
}
case WM_CHAR:
s_needRender = true; s_blinkOnly = false;
handleChar((wchar_t)w);
return 0;
case WM_KEYDOWN: {
s_needRender = true; s_blinkOnly = false;
bool ctrl = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
bool shift = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
handleKeyDown((int)w, ctrl, shift);
return 0;
}
default:
return DefWindowProc(hwnd, msg, w, l);
}
}
#endif
#ifdef _WIN32
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow) {
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
s_curArrow = LoadCursor(nullptr, IDC_ARROW);
s_curIBeam = LoadCursor(nullptr, IDC_IBEAM);
s_curHand = LoadCursor(nullptr, IDC_HAND);
s_curSizeNS = LoadCursor(nullptr, IDC_SIZENS);
s_curSizeWE = LoadCursor(nullptr, IDC_SIZEWE);
WNDCLASSEXW wc = {};
wc.cbSize = sizeof(wc);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = wndProc;
wc.hInstance = hInstance;
wc.hCursor = s_curArrow;
wc.hbrBackground = CreateSolidBrush(RGB(32, 36, 44));
wc.lpszClassName = L"NotepadDemoClass";
RegisterClassExW(&wc);
HWND hwnd = CreateWindowExW(0, L"NotepadDemoClass", L"Notepad",
WS_POPUP | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX,
CW_USEDEFAULT, CW_USEDEFAULT,
1280, 800, nullptr, nullptr, hInstance, nullptr);
if (!hwnd) return 1;
s_hwnd = hwnd;
{
DWORD corner = 2;
DwmSetWindowAttribute(hwnd, 33 ,
&corner, sizeof(corner));
}
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
if (!s_renderer.init(hwnd, 1280, 800)) {
fprintf(stderr, "Renderer init failed\n");
return 1;
}
s_renderer.initTextRendering();
{
RECT cr;
GetClientRect(s_hwnd, &cr);
s_renderer.resize(cr.right, cr.bottom);
}
if (!loadChrome()) {
fprintf(stderr, "Failed to load notepad.xui; check that it ships next to the exe.\n");
openDialog(DlgError, L"Notepad",
L"Could not find notepad.xui (the data-driven chrome).", -1, true);
}
initMenus();
s_lines.push_back(L"Welcome to Notepad, rendered entirely with ImXui.");
s_lines.push_back(L"");
s_lines.push_back(L"The window chrome - menu bar, editor, status bar, find bar -");
s_lines.push_back(L"is authored as data in examples/assets/notepad.xui and loaded");
s_lines.push_back(L"at runtime by Canvas::loadFromXui.");
s_lines.push_back(L"");
s_lines.push_back(L"Try: Ctrl+F to find, Ctrl+Z/Ctrl+Y to undo/redo, F5 for time/date,");
s_lines.push_back(L"Ctrl+wheel to zoom, and Format > Word Wrap to toggle wrapping.");
s_caretRow = 0; s_caretCol = 0;
setAnchorToCaret();
setFontIdx(1);
layoutUI();
scrollToCaret();
s_rendererReady = true;
MSG msg = {};
SetTimer(hwnd, TIMER_BLINK, 265, nullptr);
while (s_running && msg.message != WM_QUIT) {
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
if (s_needRender && !s_minimized) {
s_needRender = false;
s_renderer.beginFrame(CLR_WINDOW[0], CLR_WINDOW[1], CLR_WINDOW[2], 1.0f);
layoutUI();
if (s_canvas.getScene()) s_renderer.renderScene(s_canvas.getScene());
drawTitleBar();
drawMenuBar();
drawEditor();
drawFindBar();
drawStatusBar();
if (s_openMenu >= 0) drawDropdown(s_openMenu);
drawAbout();
drawDialog();
s_renderer.endFrame(!s_blinkOnly);
s_blinkOnly = false;
}
if (s_running && msg.message != WM_QUIT && !s_needRender)
WaitMessage();
}
KillTimer(hwnd, TIMER_BLINK);
s_renderer.shutdown();
CoUninitialize();
return 0;
}
#else
// 24-bit visuals expect BGRA byte order on x86, the renderer gives RGBA
static void swizzleToBlit() {
const uint8_t* fb = s_renderer.getFramebuffer();
size_t n = s_blit.size();
for (size_t i = 0; i < n; i += 4) {
s_blit[i] = fb[i + 2];
s_blit[i + 1] = fb[i + 1];
s_blit[i + 2] = fb[i];
s_blit[i + 3] = 255;
}
}
static void rebuildImage() {
if (s_img) XDestroyImage(s_img);
int w = s_renderer.getWidth();
int h = s_renderer.getHeight();
s_blit.resize((size_t)w * h * 4);
s_img = XCreateImage(s_dpy, s_visual, s_depth, ZPixmap, 0,
(char*)s_blit.data(), w, h, 32, w * 4);
}
static int x11Vk(KeySym ks, bool ctrl) {
switch (ks) {
case XK_Left: return VK_LEFT;
case XK_Right: return VK_RIGHT;
case XK_Up: return VK_UP;
case XK_Down: return VK_DOWN;
case XK_Home: return VK_HOME;
case XK_End: return VK_END;
case XK_Prior: return VK_PRIOR;
case XK_Next: return VK_NEXT;
case XK_Delete: return VK_DELETE;
case XK_Escape: return VK_ESCAPE;
case XK_Tab: return VK_TAB;
case XK_Return: return VK_RETURN;
case XK_F3: return VK_F3;
case XK_F5: return VK_F5;
default: break;
}
if (ctrl) {
if (ks >= XK_a && ks <= XK_z) return (int)('A' + (ks - XK_a));
if (ks >= XK_A && ks <= XK_Z) return (int)ks;
if (ks >= XK_0 && ks <= XK_9) return (int)ks;
}
return 0;
}
static void applyCursor(int x, int y) {
XDefineCursor(s_dpy, s_win, cursorForPoint((float)x, (float)y));
}
static void handleXEvent(XEvent& ev, Atom wmDelete) {
switch (ev.type) {
case Expose:
s_needRender = true;
break;
case ConfigureNotify:
if (ev.xconfigure.width > 0 && ev.xconfigure.height > 0 &&
(ev.xconfigure.width != s_renderer.getWidth() ||
ev.xconfigure.height != s_renderer.getHeight())) {
s_renderer.resize(ev.xconfigure.width, ev.xconfigure.height);
rebuildImage();
layoutUI();
s_needRender = true;
}
break;
case KeyPress: {
s_needRender = true; s_blinkOnly = false;
bool ctrl = (ev.xkey.state & ControlMask) != 0;
bool shift = (ev.xkey.state & ShiftMask) != 0;
s_ctrlDown = ctrl; s_shiftDown = shift;
KeySym ks = XLookupKeysym(&ev.xkey, 0);
if (ks == XK_BackSpace) { handleChar(0x08); break; }
int vk = x11Vk(ks, ctrl);
if (vk) handleKeyDown(vk, ctrl, shift);
if (ks == XK_Return) handleChar('\r');
else if (ks == XK_Tab) handleChar('\t');
else if (!vk && !ctrl) {
char buf[16];
int n = XLookupString(&ev.xkey, buf, sizeof(buf), nullptr, nullptr);
for (int i = 0; i < n; i++) {
unsigned char c = (unsigned char)buf[i];
if (c >= 32 && c != 127) handleChar((wchar_t)c);
}
}
break;
}
case KeyRelease: {
KeySym ks = XLookupKeysym(&ev.xkey, 0);
if (ks == XK_Control_L || ks == XK_Control_R) s_ctrlDown = false;
if (ks == XK_Shift_L || ks == XK_Shift_R) s_shiftDown = false;
break;
}
case ButtonPress: {
s_needRender = true; s_blinkOnly = false;
int x = ev.xbutton.x, y = ev.xbutton.y;
if (ev.xbutton.button == 4 || ev.xbutton.button == 5) {
handleWheel(ev.xbutton.button == 4 ? 120 : -120, s_ctrlDown);
break;
}
if (ev.xbutton.button == 1) {
static Time s_lastClick = 0;
bool dbl = (ev.xbutton.time - s_lastClick) < 300;
s_lastClick = ev.xbutton.time;
if (dbl && y <= s_rectTitle.bottom() &&
!(inRect(s_minBtn, (float)x, (float)y) || inRect(s_maxBtn, (float)x, (float)y) ||
inRect(s_closeBtn, (float)x, (float)y))) {
toggleMaximize();
break;
}
if (s_winDrag) s_winDrag = false;
handleLButtonDown((float)x, (float)y);
}
break;
}
case ButtonRelease:
s_needRender = true; s_blinkOnly = false;
s_selecting = false;
s_dragV = false;
s_dragH = false;
s_winDrag = false;
break;
case MotionNotify: {
s_needRender = true; s_blinkOnly = false;
int x = ev.xmotion.x, y = ev.xmotion.y;
if (s_winDrag && !s_maximized) {
XWindowAttributes wa;
XGetWindowAttributes(s_dpy, s_win, &wa);
XMoveWindow(s_dpy, s_win, wa.x + x - s_dragDX, wa.y + y - s_dragDY);
} else {
handleMouseMove((float)x, (float)y);
}
applyCursor(x, y);
break;
}
case ClientMessage:
if ((Atom)ev.xclient.data.l[0] == wmDelete) requestClose();
break;
case SelectionRequest: {
XSelectionRequestEvent& sr = ev.xselectionrequest;
if (sr.selection == s_atomClipboard) {
XSelectionEvent se;
se.type = SelectionNotify;
se.display = sr.display;
se.requestor = sr.requestor;
se.selection = sr.selection;
se.target = sr.target;
se.time = sr.time;
if (sr.target == s_atomUtf8 && !s_clipUtf8.empty()) {
XChangeProperty(s_dpy, sr.requestor, sr.property, s_atomUtf8, 8,
PropModeReplace,
(const unsigned char*)s_clipUtf8.data(),
(int)s_clipUtf8.size());
se.property = sr.property;
} else {
se.property = None;
}
XSendEvent(s_dpy, sr.requestor, False, 0, (XEvent*)&se);
XFlush(s_dpy);
}
break;
}
case SelectionNotify:
if (ev.xselection.property != None && s_pastePending) {
s_pastePending = false;
Atom type;
int fmt;
unsigned long n, after;
unsigned char* data = nullptr;
if (XGetWindowProperty(s_dpy, s_win, ev.xselection.property, 0, 1 << 20,
True, AnyPropertyType, &type, &fmt, &n, &after,
&data) == Success) {
std::wstring wide;
if (data && n > 0) {
std::string utf8((const char*)data, n);
if (!utf8ToWideSafe(utf8, wide)) {
wide.clear();
for (size_t i = 0; i < utf8.size(); i++)
wide += (wchar_t)(unsigned char)utf8[i];
}
}
if (data) XFree(data);
insertClipboardText(wide);
}
}
break;
}
}
int main() {
s_dpy = XOpenDisplay(nullptr);
if (!s_dpy) {
fprintf(stderr, "cannot open X display\n");
return 1;
}
int screen = DefaultScreen(s_dpy);
s_visual = DefaultVisual(s_dpy, screen);
s_depth = DefaultDepth(s_dpy, screen);
s_atomClipboard = XInternAtom(s_dpy, "CLIPBOARD", False);
s_atomUtf8 = XInternAtom(s_dpy, "UTF8_STRING", False);
Atom wmDelete = XInternAtom(s_dpy, "WM_DELETE_WINDOW", False);
XSetWindowAttributes attr;
attr.colormap = XCreateColormap(s_dpy, RootWindow(s_dpy, screen), s_visual, AllocNone);
attr.background_pixel = 0x20242c;
attr.border_pixel = 0;
s_win = XCreateWindow(s_dpy, RootWindow(s_dpy, screen), 0, 0, 1280, 800, 0,
s_depth, InputOutput, s_visual,
CWColormap | CWBackPixel | CWBorderPixel, &attr);
XStoreName(s_dpy, s_win, "Notepad");
XSetWMProtocols(s_dpy, s_win, &wmDelete, 1);
XSelectInput(s_dpy, s_win, ExposureMask | KeyPressMask | KeyReleaseMask |
ButtonPressMask | ButtonReleaseMask | PointerMotionMask |
StructureNotifyMask);
XMapWindow(s_dpy, s_win);
s_gc = XCreateGC(s_dpy, s_win, 0, nullptr);
s_curArrow = XCreateFontCursor(s_dpy, XC_left_ptr);
s_curIBeam = XCreateFontCursor(s_dpy, XC_xterm);
s_curHand = XCreateFontCursor(s_dpy, XC_hand2);
s_curSizeNS = XCreateFontCursor(s_dpy, XC_sb_v_double_arrow);
s_curSizeWE = XCreateFontCursor(s_dpy, XC_sb_h_double_arrow);
XWindowAttributes wa;
for (int i = 0; i < 50; i++) {
XGetWindowAttributes(s_dpy, s_win, &wa);
if (wa.map_state == IsViewable) break;
usleep(50000);
}
XRaiseWindow(s_dpy, s_win);
XSetInputFocus(s_dpy, s_win, RevertToParent, CurrentTime);
XFlush(s_dpy);
if (!s_renderer.init(nullptr, 1280, 800)) {
fprintf(stderr, "Renderer init failed\n");
return 1;
}
s_renderer.loadSystemFont("Consolas");
s_renderer.initTextRendering();
if (!loadChrome()) {
fprintf(stderr, "Failed to load notepad.xui; check that it ships next to the exe.\n");
openDialog(DlgError, L"Notepad",
L"Could not find notepad.xui (the data-driven chrome).", -1, true);
}
initMenus();
s_lines.push_back(L"Hello, World!");
s_caretRow = 0; s_caretCol = 0;
setAnchorToCaret();
setFontIdx(1);
layoutUI();
scrollToCaret();
s_rendererReady = true;
rebuildImage();
unsigned long lastBlink = nowMs();
while (s_running) {
while (XPending(s_dpy)) {
XEvent ev;
XNextEvent(s_dpy, &ev);
handleXEvent(ev, wmDelete);
}
unsigned long now = nowMs();
if (caretVisible() && now - lastBlink >= 265) {
lastBlink = now;
s_needRender = true;
s_blinkOnly = true;
}
if (s_needRender && !s_minimized) {
s_needRender = false;
s_renderer.beginFrame(CLR_WINDOW[0], CLR_WINDOW[1], CLR_WINDOW[2], 1.0f);
layoutUI();
if (s_canvas.getScene()) s_renderer.renderScene(s_canvas.getScene());
drawTitleBar();
drawMenuBar();
drawEditor();
drawFindBar();
drawStatusBar();
if (s_openMenu >= 0) drawDropdown(s_openMenu);
drawAbout();
drawDialog();
drawFileDialog();
s_renderer.endFrame(false);
s_blinkOnly = false;
swizzleToBlit();
XPutImage(s_dpy, s_win, s_gc, s_img, 0, 0, 0, 0,
s_renderer.getWidth(), s_renderer.getHeight());
XFlush(s_dpy);
}
usleep(5000);
}
s_renderer.shutdown();
XDestroyImage(s_img);
XFreeGC(s_dpy, s_gc);
XDestroyWindow(s_dpy, s_win);
XCloseDisplay(s_dpy);
return 0;
}
#endif