3056 lines
102 KiB
C++
3056 lines
102 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::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<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
|
|
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
|