#define WIN32_LEAN_AND_MEAN #define NOMINMAX #ifdef _WIN32 #include #include #include #include #include #pragma comment(lib, "dwmapi.lib") #pragma comment(lib, "gdi32.lib") #else #include #include #include #include #include #include #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #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::OpenGLRenderer 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 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& 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 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::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 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 lines; int cr, cc, sr, sc; }; static std::vector 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 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 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 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 s_view; static float s_maxRowW = 0; static std::vector > wrapLineChar(const std::wstring& t, int maxChars) { std::vector > 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 > 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 > 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 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 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 s_fileEntries; static std::vector 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 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 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 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(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 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(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(1, 0), ImXui::Vec2(1, 1) }, CLR_TEXT_DIM, 0.9f, uiF(1.5f)); std::vector 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& 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 wrapDlgText(const std::wstring& text, float maxW) { std::vector 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 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 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& 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& 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 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); 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_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(s_dpy, 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; 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; } if (!s_needRender && XPending(s_dpy) == 0) usleep(1000); } s_renderer.shutdown(); XDestroyWindow(s_dpy, s_win); XCloseDisplay(s_dpy); return 0; } #endif