From 67872c6b985d2f2f457c19e7588d8c04b33a42d7 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Wed, 25 Mar 2026 21:43:41 -0500 Subject: [PATCH] First pass of major debugging. At a glance, everything is working again. --- apps/dvxdemo/dvxdemo.c | 1 + apps/imgview/imgview.c | 6 ++ apps/notepad/notepad.c | 12 +++ apps/progman/progman.c | 4 +- config/dvx.ini | 6 ++ core/dvxApp.c | 88 ++++++++++++++++- core/dvxApp.h | 7 +- core/dvxCursor.h | 52 +++++++++- core/dvxWidgetPlugin.h | 2 + core/platform/dvxPlatform.h | 7 ++ core/platform/dvxPlatformDos.c | 122 +++++++++++++++++++---- core/widgetCore.c | 7 ++ loader/loaderMain.c | 31 +++--- shell/Makefile | 2 +- shell/shellApp.c | 39 ++++---- shell/shellApp.h | 21 ---- shell/shellExport.c | 119 ---------------------- shell/shellInfo.c | 8 +- shell/shellMain.c | 94 ++++++------------ texthelp/textHelp.c | 29 ++++++ texthelp/textHelp.h | 6 ++ widgets/widgetAnsiTerm.c | 53 ++++++++++ widgets/widgetComboBox.c | 73 +++++++++++++- widgets/widgetDropdown.c | 34 ++++++- widgets/widgetListBox.c | 121 ++++++++++++++++++++++- widgets/widgetListView.c | 169 ++++++++++++++++++++++++++++++- widgets/widgetScrollPane.c | 80 +++++++++++++-- widgets/widgetSplitter.c | 14 +++ widgets/widgetTextInput.c | 162 +++++++++++++++++++++++++++++- widgets/widgetTimer.c | 27 +---- widgets/widgetTimer.h | 3 - widgets/widgetTreeView.c | 175 ++++++++++++++++++++++++++++++++- 32 files changed, 1246 insertions(+), 328 deletions(-) delete mode 100644 shell/shellExport.c diff --git a/apps/dvxdemo/dvxdemo.c b/apps/dvxdemo/dvxdemo.c index 8810bdc..fd8ad31 100644 --- a/apps/dvxdemo/dvxdemo.c +++ b/apps/dvxdemo/dvxdemo.c @@ -28,6 +28,7 @@ #include "widgetImageButton.h" #include "widgetLabel.h" #include "widgetListBox.h" +#include "widgetListView.h" #include "widgetProgressBar.h" #include "widgetRadio.h" #include "widgetScrollPane.h" diff --git a/apps/imgview/imgview.c b/apps/imgview/imgview.c index 390a718..c79fb7e 100644 --- a/apps/imgview/imgview.c +++ b/apps/imgview/imgview.c @@ -178,10 +178,13 @@ static void loadAndDisplay(const char *path) { stbi_image_free(sImgRgb); sImgRgb = NULL; + dvxSetBusy(sAc, true); + int32_t channels; sImgRgb = stbi_load(path, &sImgW, &sImgH, &channels, 3); if (!sImgRgb) { + dvxSetBusy(sAc, false); dvxMessageBox(sAc, "Error", "Could not load image.", MB_OK | MB_ICONERROR); return; } @@ -202,6 +205,7 @@ static void loadAndDisplay(const char *path) { // Scale and repaint buildScaled(sWin->contentW, sWin->contentH); + dvxSetBusy(sAc, false); RectT fullRect = {0, 0, sWin->contentW, sWin->contentH}; sWin->onPaint(sWin, &fullRect); @@ -242,7 +246,9 @@ static void onPaint(WindowT *win, RectT *dirty) { // scaled image (or dark background) to avoid expensive per-frame scaling. if (sImgRgb && sAc->stack.resizeWindow < 0) { if (sLastFitW != win->contentW || sLastFitH != win->contentH) { + dvxSetBusy(sAc, true); buildScaled(win->contentW, win->contentH); + dvxSetBusy(sAc, false); } } diff --git a/apps/notepad/notepad.c b/apps/notepad/notepad.c index bb87ac6..7049eac 100644 --- a/apps/notepad/notepad.c +++ b/apps/notepad/notepad.c @@ -313,15 +313,27 @@ static void onMenu(WindowT *win, int32_t menuId) { break; case CMD_CUT: + if (sTextArea && sTextArea->wclass && sTextArea->wclass->onKey) { + sTextArea->wclass->onKey(sTextArea, 24, 0); // Ctrl+X + } break; case CMD_COPY: + if (sTextArea && sTextArea->wclass && sTextArea->wclass->onKey) { + sTextArea->wclass->onKey(sTextArea, 3, 0); // Ctrl+C + } break; case CMD_PASTE: + if (sTextArea && sTextArea->wclass && sTextArea->wclass->onKey) { + sTextArea->wclass->onKey(sTextArea, 22, 0); // Ctrl+V + } break; case CMD_SELALL: + if (sTextArea && sTextArea->wclass && sTextArea->wclass->onKey) { + sTextArea->wclass->onKey(sTextArea, 1, 0); // Ctrl+A + } break; } } diff --git a/apps/progman/progman.c b/apps/progman/progman.c index 1de94ec..26ccf92 100644 --- a/apps/progman/progman.c +++ b/apps/progman/progman.c @@ -326,7 +326,7 @@ static void onPmMenu(WindowT *win, int32_t menuId) { static void scanAppsDir(void) { sAppCount = 0; scanAppsDirRecurse("apps"); - shellLog("Progman: found %ld app(s)", (long)sAppCount); + dvxLog("Progman: found %ld app(s)", (long)sAppCount); } @@ -339,7 +339,7 @@ static void scanAppsDirRecurse(const char *dirPath) { if (!dir) { if (sAppCount == 0) { - shellLog("Progman: %s directory not found", dirPath); + dvxLog("Progman: %s directory not found", dirPath); } return; diff --git a/config/dvx.ini b/config/dvx.ini index 0a47a5e..e674901 100644 --- a/config/dvx.ini +++ b/config/dvx.ini @@ -18,3 +18,9 @@ bpp = 16 wheel = normal doubleclick = 500 acceleration = medium + +; Shell settings. +; desktop: path to the desktop app loaded at startup + +[shell] +desktop = apps/progman/progman.app diff --git a/core/dvxApp.c b/core/dvxApp.c index 06e0828..0c9b0cd 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -136,6 +136,7 @@ static void updateTooltip(AppContextT *ctx); // click callback fires. This gives the user visual feedback that the // keyboard activation was registered, matching Win3.x/Motif behavior. WidgetT *sKeyPressedBtn = NULL; +void (*sCursorBlinkFn)(void) = NULL; // 4x4 Bayer dithering offsets for ordered dithering when converting // 24-bit wallpaper images to 15/16-bit pixel formats. @@ -1040,6 +1041,11 @@ static void dispatchEvents(AppContextT *ctx) { return; } + // Block all input while busy + if (ctx->busy) { + return; + } + // Handle left button press if ((buttons & MOUSE_LEFT) && !(prevBtn & MOUSE_LEFT)) { handleMouseButton(ctx, mx, my, buttons); @@ -1404,6 +1410,11 @@ WindowT *dvxCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t WindowT *win = wmCreateWindow(&ctx->stack, &ctx->display, title, x, y, w, h, resizable); if (win) { + // Stamp window with the current app ID (set by the shell before + // calling app entry points). Enables per-app window tracking for + // cleanup on crash/termination. + win->appId = ctx->currentAppId; + // Raise and focus int32_t idx = ctx->stack.count - 1; wmSetFocus(&ctx->stack, &ctx->dirty, idx); @@ -2301,12 +2312,15 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) { strncpy(ctx->wallpaperPath, path, sizeof(ctx->wallpaperPath) - 1); ctx->wallpaperPath[sizeof(ctx->wallpaperPath) - 1] = '\0'; + dvxSetBusy(ctx, true); + int32_t imgW; int32_t imgH; int32_t channels; uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3); if (!rgb) { + dvxSetBusy(ctx, false); return false; } @@ -2314,6 +2328,7 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) { ctx->wallpaperBuf = buildWallpaperBuf(ctx, rgb, imgW, imgH, ctx->wallpaperMode); ctx->wallpaperPitch = pitch; + dvxSetBusy(ctx, false); stbi_image_free(rgb); @@ -2541,6 +2556,40 @@ void dvxQuit(AppContextT *ctx) { } +// ============================================================ +// dvxSetBusy +// ============================================================ + +void dvxSetBusy(AppContextT *ctx, bool busy) { + ctx->busy = busy; + + // Dirty the cursor area so the shape change is visible + dirtyListAdd(&ctx->dirty, + ctx->mouseX - ctx->cursors[ctx->cursorId].hotX, + ctx->mouseY - ctx->cursors[ctx->cursorId].hotY, + CURSOR_DIRTY_SIZE, CURSOR_DIRTY_SIZE); + + if (busy) { + ctx->cursorId = CURSOR_BUSY; + } else { + ctx->cursorId = CURSOR_ARROW; + } + + // Dirty again for the new shape (different hotspot) + dirtyListAdd(&ctx->dirty, + ctx->mouseX - ctx->cursors[ctx->cursorId].hotX, + ctx->mouseY - ctx->cursors[ctx->cursorId].hotY, + CURSOR_DIRTY_SIZE, CURSOR_DIRTY_SIZE); + + // Flush immediately so the cursor change is visible before + // the blocking operation starts (or after it ends). + // Skip if display isn't initialized yet (startup wallpaper load). + if (ctx->display.backBuf) { + compositeAndFlush(ctx); + } +} + + // ============================================================ // dvxRun // ============================================================ @@ -2583,7 +2632,10 @@ bool dvxUpdate(AppContextT *ctx) { dispatchEvents(ctx); updateTooltip(ctx); pollWidgets(ctx); - wgtUpdateCursorBlink(); + if (sCursorBlinkFn) { + sCursorBlinkFn(); + } + wgtUpdateTimers(); ctx->frameCount++; @@ -3097,8 +3149,17 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t } } - // Check for click on minimized icon first - int32_t iconIdx = wmMinimizedIconHit(&ctx->stack, &ctx->display, mx, my); + // Check for click on minimized icon, but only if no window covers the point. + // Icons are drawn below windows, so a window at this position takes priority. + int32_t iconIdx = -1; + { + int32_t hitPart; + int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart); + + if (hitIdx < 0) { + iconIdx = wmMinimizedIconHit(&ctx->stack, &ctx->display, mx, my); + } + } if (iconIdx >= 0) { WindowT *iconWin = ctx->stack.windows[iconIdx]; @@ -4432,6 +4493,15 @@ static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t // 7. Default arrow cursor static void updateCursorShape(AppContextT *ctx) { + // Busy state overrides all cursor logic + if (ctx->busy) { + if (ctx->cursorId != CURSOR_BUSY) { + ctx->cursorId = CURSOR_BUSY; + } + + return; + } + int32_t newCursor = CURSOR_ARROW; int32_t mx = ctx->mouseX; int32_t my = ctx->mouseY; @@ -4569,9 +4639,17 @@ static void updateTooltip(AppContextT *ctx) { return; } - // Check minimized icons first (they sit outside any window) + // Check minimized icons, but only if no window covers the point const char *tipText = NULL; - int32_t iconIdx = wmMinimizedIconHit(&ctx->stack, &ctx->display, mx, my); + int32_t iconIdx = -1; + { + int32_t hitPart; + int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart); + + if (hitIdx < 0) { + iconIdx = wmMinimizedIconHit(&ctx->stack, &ctx->display, mx, my); + } + } if (iconIdx >= 0) { tipText = ctx->stack.windows[iconIdx]->title; diff --git a/core/dvxApp.h b/core/dvxApp.h index cfe5e6e..de770de 100644 --- a/core/dvxApp.h +++ b/core/dvxApp.h @@ -42,7 +42,7 @@ typedef struct AppContextT { PopupStateT popup; SysMenuStateT sysMenu; KbMoveResizeT kbMoveResize; - CursorT cursors[5]; // indexed by CURSOR_xxx + CursorT cursors[6]; // indexed by CURSOR_xxx int32_t cursorId; // active cursor shape uint32_t cursorFg; // pre-packed cursor colors uint32_t cursorBg; @@ -82,6 +82,8 @@ typedef struct AppContextT { void *ctrlEscCtx; void (*onTitleChange)(void *ctx); // called when any window title changes void *titleChangeCtx; + int32_t currentAppId; // set by shell before calling app code (0 = shell) + bool busy; // when true, show hourglass and block input // Tooltip state -- tooltip appears after the mouse hovers over a widget // with a tooltip string for a brief delay. Pre-computing W/H avoids // re-measuring on every paint frame. @@ -223,6 +225,9 @@ void dvxMaximizeWindow(AppContextT *ctx, WindowT *win); // Request exit from main loop void dvxQuit(AppContextT *ctx); +// Set/clear busy state. While busy, shows hourglass cursor and blocks input. +void dvxSetBusy(AppContextT *ctx, bool busy); + // Save the entire screen (backbuffer contents) to a PNG file. Converts // from native pixel format to RGB for the PNG encoder. Returns 0 on // success, -1 on failure. diff --git a/core/dvxCursor.h b/core/dvxCursor.h index 0635549..68a2660 100644 --- a/core/dvxCursor.h +++ b/core/dvxCursor.h @@ -28,7 +28,8 @@ #define CURSOR_RESIZE_V 2 // up/down (vertical) #define CURSOR_RESIZE_DIAG_NWSE 3 // NW-SE diagonal (top-left / bottom-right) #define CURSOR_RESIZE_DIAG_NESW 4 // NE-SW diagonal (top-right / bottom-left) -#define CURSOR_COUNT 5 +#define CURSOR_BUSY 5 // hourglass (wait) +#define CURSOR_COUNT 6 // ============================================================ // Standard arrow cursor, 16x16 @@ -305,6 +306,54 @@ static const uint16_t cursorResizeDiagNESWXor[16] = { 0x0000 // 0000000000000000 row 15 }; +// ============================================================ +// Hourglass (busy/wait) cursor, 16x16 +// ============================================================ +// +// Classic hourglass: black outline with white glass interior, +// black sand accumulated in the bottom half, horizontal bars +// at top and bottom. Modeled after the Windows 3.1 wait cursor. + +static const uint16_t cursorBusyAnd[16] = { + // .BBBBBBBBBBBBBB. + 0x8001, // row 0 top bar + 0x8001, // row 1 .BWWWWWWWWWWWWB. + 0xC003, // row 2 ..BWWWWWWWWWWB.. + 0xE007, // row 3 ...BWWWWWWWWB... + 0xF00F, // row 4 ....BWWWWWWB.... + 0xF81F, // row 5 .....BWWWWB..... + 0xFC3F, // row 6 ......BWWB...... + 0xFE7F, // row 7 .......BB....... waist + 0xFC3F, // row 8 ......BWWB...... + 0xF81F, // row 9 .....BBBBBB..... sand + 0xF00F, // row 10 ....BBBBBBBB.... sand + 0xE007, // row 11 ...BWBBBBBBWB... sand + glass + 0xC003, // row 12 ..BWWBBBBBBWWB.. sand + glass + 0x8001, // row 13 .BWWWWWWWWWWWWB. + 0x8001, // row 14 .BBBBBBBBBBBBBB. bottom bar + 0xFFFF // row 15 ................ transparent +}; + +static const uint16_t cursorBusyXor[16] = { + 0x0000, // row 0 top bar (black) + 0x3FFC, // row 1 white interior + 0x1FF8, // row 2 white interior + 0x0FF0, // row 3 white interior + 0x07E0, // row 4 white interior + 0x03C0, // row 5 white interior + 0x0180, // row 6 white interior + 0x0000, // row 7 waist (black) + 0x0180, // row 8 white interior + 0x0000, // row 9 sand (black) + 0x0000, // row 10 sand (black) + 0x0810, // row 11 glass edges white, sand black + 0x1818, // row 12 glass edges white, sand black + 0x3FFC, // row 13 white interior + 0x0000, // row 14 bottom bar (black) + 0x0000 // row 15 transparent +}; + + // ============================================================ // Cursor table // ============================================================ @@ -315,6 +364,7 @@ static const CursorT dvxCursors[CURSOR_COUNT] = { { 16, 16, 7, 7, cursorResizeVAnd, cursorResizeVXor }, // CURSOR_RESIZE_V { 16, 16, 7, 7, cursorResizeDiagNWSEAnd, cursorResizeDiagNWSEXor }, // CURSOR_RESIZE_DIAG_NWSE { 16, 16, 7, 7, cursorResizeDiagNESWAnd, cursorResizeDiagNESWXor }, // CURSOR_RESIZE_DIAG_NESW + { 16, 16, 7, 7, cursorBusyAnd, cursorBusyXor }, // CURSOR_BUSY }; // Legacy alias -- kept for backward compatibility with code that predates diff --git a/core/dvxWidgetPlugin.h b/core/dvxWidgetPlugin.h index eb703dc..f1c3926 100644 --- a/core/dvxWidgetPlugin.h +++ b/core/dvxWidgetPlugin.h @@ -49,6 +49,7 @@ extern const WidgetClassT **widgetClassTable; #define CURSOR_RESIZE_V 2 #define CURSOR_RESIZE_DIAG_NWSE 3 #define CURSOR_RESIZE_DIAG_NESW 4 +#define CURSOR_BUSY 5 // ============================================================ // Keyboard modifier flags @@ -112,6 +113,7 @@ extern WidgetT *sDragReorder; extern WidgetT *sDragScrollbar; extern int32_t sDragScrollbarOff; extern int32_t sDragScrollbarOrient; +extern void (*sCursorBlinkFn)(void); // ============================================================ // Core widget functions (widgetCore.c) diff --git a/core/platform/dvxPlatform.h b/core/platform/dvxPlatform.h index 3885992..7b64793 100644 --- a/core/platform/dvxPlatform.h +++ b/core/platform/dvxPlatform.h @@ -39,6 +39,13 @@ typedef struct { int32_t scancode; // PC scan code (0x48=Up, 0x50=Down, etc.) } PlatformKeyEventT; +// ============================================================ +// Logging +// ============================================================ + +// Append a line to dvx.log. Lives in dvx.exe, exported to all modules. +void dvxLog(const char *fmt, ...); + // ============================================================ // System lifecycle // ============================================================ diff --git a/core/platform/dvxPlatformDos.c b/core/platform/dvxPlatformDos.c index 86ba5dc..3167a8a 100644 --- a/core/platform/dvxPlatformDos.c +++ b/core/platform/dvxPlatformDos.c @@ -54,6 +54,7 @@ #include #include #include +#include // VESA mode scoring weights: higher score = preferred mode #define MODE_SCORE_16BPP 100 // 16bpp: fastest span fill (half the bytes of 32bpp) @@ -87,16 +88,21 @@ static void sysInfoAppend(const char *fmt, ...); static bool sHasMouseWheel = false; static int32_t sLastWheelDelta = 0; -// Software cursor tracking. Many real-hardware mouse drivers fail to -// honour INT 33h functions 07h/08h (set coordinate range) in VESA modes -// because they don't recognise non-standard video modes. We bypass the -// driver's position entirely: platformMousePoll reads raw mickey deltas -// via function 0Bh, accumulates them into sCurX/sCurY, and clamps to -// the screen bounds. Function 03h is still used for button state. -static int32_t sMouseRangeW = 0; -static int32_t sMouseRangeH = 0; -static int32_t sCurX = 0; -static int32_t sCurY = 0; +// Software cursor tracking. Two modes detected at runtime: +// Mickey mode (default): function 0Bh raw deltas accumulated into +// sCurX/sCurY. Works on 86Box and real hardware where function +// 03h coordinates may be wrong in VESA modes. +// Absolute mode: function 03h coordinates used directly. Auto- +// activated when function 0Bh returns zero deltas but function +// 03h position is changing (DOSBox-X seamless mouse). +static bool sAbsMouseMode = false; +static bool sDetecting = true; +static int32_t sPrevAbsX = -1; +static int32_t sPrevAbsY = -1; +static int32_t sMouseRangeW = 0; +static int32_t sMouseRangeH = 0; +static int32_t sCurX = 0; +static int32_t sCurY = 0; // Alt+key scan code to ASCII lookup table (indexed by BIOS scan code). // INT 16h returns these scan codes with ascii=0 for Alt+key combos. @@ -764,8 +770,17 @@ const char *platformGetSystemInfo(const DisplayT *display) { // ---- CPU ---- sysInfoAppend("=== CPU ==="); + // Get CPU type from DPMI (works even without CPUID) + { + __dpmi_version_ret dpmiVer; + + if (__dpmi_get_version(&dpmiVer) == 0) { + sysInfoAppend("CPU Type: %d86", dpmiVer.cpu); + } + } + if (!hasCpuid()) { - sysInfoAppend("Processor: 386/486 (no CPUID support)"); + sysInfoAppend("(no CPUID support)"); } else { // Vendor string (CPUID leaf 0) uint32_t maxFunc; @@ -943,7 +958,6 @@ const char *platformGetSystemInfo(const DisplayT *display) { (ver.flags & 0x01) ? "32-bit " : "16-bit ", (ver.flags & 0x02) ? "V86 " : "", (ver.flags & 0x04) ? "VirtMem " : ""); - sysInfoAppend("CPU Type: %d86", ver.cpu); } const char *tmpDir = getenv("TEMP"); @@ -1275,6 +1289,8 @@ int32_t platformStripLineEndings(char *buf, int32_t len) { // software cursor on top of the backbuffer. We only use INT 33h // for position/button state via polling (function 03h). + + void platformMouseInit(int32_t screenW, int32_t screenH) { __dpmi_regs r; @@ -1288,8 +1304,30 @@ void platformMouseInit(int32_t screenW, int32_t screenH) { r.x.ax = 0x0000; __dpmi_int(0x33, &r); + // Set coordinate range for function 03h. Always do this so that + // absolute mode works if we switch to it during detection. + // Function 07h: set horizontal range + memset(&r, 0, sizeof(r)); + r.x.ax = 0x0007; + r.x.cx = 0; + r.x.dx = screenW - 1; + __dpmi_int(0x33, &r); + + // Function 08h: set vertical range + memset(&r, 0, sizeof(r)); + r.x.ax = 0x0008; + r.x.cx = 0; + r.x.dx = screenH - 1; + __dpmi_int(0x33, &r); + + // Function 04h: warp cursor to center + memset(&r, 0, sizeof(r)); + r.x.ax = 0x0004; + r.x.cx = screenW / 2; + r.x.dx = screenH / 2; + __dpmi_int(0x33, &r); + // Flush any stale mickey counters so the first poll starts clean. - // Function 0Bh returns and resets the accumulated motion counters. memset(&r, 0, sizeof(r)); r.x.ax = 0x000B; __dpmi_int(0x33, &r); @@ -1329,7 +1367,8 @@ void platformMouseSetAccel(int32_t threshold) { void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) { __dpmi_regs r; - // Function 03h: read button state only + // Function 03h: read button state and absolute position. + // Coordinate range was set by functions 07h/08h in platformMouseInit. memset(&r, 0, sizeof(r)); r.x.ax = 0x0003; __dpmi_int(0x33, &r); @@ -1341,14 +1380,45 @@ void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) { sLastWheelDelta = (int32_t)(int8_t)(r.x.bx >> 8); } - // Function 0Bh: read mickey motion counters (signed 16-bit deltas, - // cleared on read). Accumulate into software cursor position. + int32_t absX = (int16_t)r.x.cx; + int32_t absY = (int16_t)r.x.dx; + + // Function 0Bh: read mickey motion counters memset(&r, 0, sizeof(r)); r.x.ax = 0x000B; __dpmi_int(0x33, &r); - sCurX += (int16_t)r.x.cx; - sCurY += (int16_t)r.x.dx; + int16_t mickeyDx = (int16_t)r.x.cx; + int16_t mickeyDy = (int16_t)r.x.dx; + + // Runtime detection: if mickeys are zero but the driver's own + // position changed, function 0Bh isn't generating deltas. + // Switch to absolute mode (DOSBox-X seamless mouse, etc.). + if (sDetecting) { + if (mickeyDx == 0 && mickeyDy == 0 && + sPrevAbsX >= 0 && (absX != sPrevAbsX || absY != sPrevAbsY)) { + sAbsMouseMode = true; + sDetecting = false; + dvxLog("Mouse: absolute mode (no mickeys detected)"); + } + + // Once we get real mickeys, stop checking + if (mickeyDx != 0 || mickeyDy != 0) { + sDetecting = false; + dvxLog("Mouse: mickey mode (raw deltas detected)"); + } + + sPrevAbsX = absX; + sPrevAbsY = absY; + } + + if (sAbsMouseMode) { + sCurX = absX; + sCurY = absY; + } else { + sCurX += mickeyDx; + sCurY += mickeyDy; + } if (sCurX < 0) { sCurX = 0; @@ -1419,8 +1489,16 @@ void platformMouseWarp(int32_t x, int32_t y) { sCurX = x; sCurY = y; - // Flush any pending mickeys so the next poll doesn't undo the warp __dpmi_regs r; + + // Tell the driver the new position (function 04h) and flush + // pending mickeys (function 0Bh) so neither mode gets stale data. + memset(&r, 0, sizeof(r)); + r.x.ax = 0x0004; + r.x.cx = x; + r.x.dx = y; + __dpmi_int(0x33, &r); + memset(&r, 0, sizeof(r)); r.x.ax = 0x000B; __dpmi_int(0x33, &r); @@ -2061,6 +2139,9 @@ extern unsigned char __dj_ctype_tolower[]; extern void *__emutls_get_address(void *); // stb_ds internals (implementation compiled into dvx.exe via loaderMain.c) +// dvxLog lives in dvx.exe (loaderMain.c) +extern void dvxLog(const char *fmt, ...); + extern void *stbds_arrgrowf(void *a, size_t elemsize, size_t addlen, size_t min_cap); extern void stbds_arrfreef(void *a); extern void stbds_hmfree_func(void *a, size_t elemsize); @@ -2115,6 +2196,9 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(platformVideoShutdown) DXE_EXPORT(platformYield) + // --- dvx logging (lives in dvx.exe, used by all modules) --- + DXE_EXPORT(dvxLog) + // --- memory --- DXE_EXPORT(calloc) DXE_EXPORT(free) diff --git a/core/widgetCore.c b/core/widgetCore.c index 80bf702..79673a2 100644 --- a/core/widgetCore.c +++ b/core/widgetCore.c @@ -119,6 +119,13 @@ int32_t clipboardMaxLen(void) { int32_t multiClickDetect(int32_t vx, int32_t vy) { clock_t now = clock(); + // Guard against multiple calls in the same frame (e.g. widget onMouse + // calls it, then widgetEvent.c calls it again). If coords and time + // are identical to the last call, return the cached count. + if (now == sLastClickTime && vx == sLastClickX && vy == sLastClickY) { + return sClickCount; + } + if ((now - sLastClickTime) < sDblClickTicks && abs(vx - sLastClickX) < 4 && abs(vy - sLastClickY) < 4) { sClickCount++; diff --git a/loader/loaderMain.c b/loader/loaderMain.c index 73dcc0e..5e3e473 100644 --- a/loader/loaderMain.c +++ b/loader/loaderMain.c @@ -33,10 +33,13 @@ #define LOG_PATH "dvx.log" // ============================================================ -// loaderLog -- write to dvx.log (before shell is loaded) +// dvxLog -- append a line to dvx.log // ============================================================ +// +// Global logging function exported to all DXE modules. +// Opens/closes the file per-write so it's never held open. -static void loaderLog(const char *fmt, ...) { +void dvxLog(const char *fmt, ...) { FILE *f = fopen(LOG_PATH, "a"); if (!f) { @@ -269,24 +272,21 @@ static void loadInOrder(ModuleT *mods) { continue; } - loaderLog("Loading: %s", mods[i].path); + dvxLog("Loading: %s", mods[i].path); mods[i].handle = dlopen(mods[i].path, RTLD_GLOBAL); if (!mods[i].handle) { const char *err = dlerror(); - loaderLog(" FAILED: %s", err ? err : "(unknown)"); + dvxLog(" FAILED: %s", err ? err : "(unknown)"); mods[i].loaded = true; loaded++; progress = true; continue; } - loaderLog(" OK"); - RegFnT regFn = (RegFnT)dlsym(mods[i].handle, "_wgtRegister"); if (regFn) { - loaderLog(" Calling wgtRegister()"); regFn(); } @@ -315,20 +315,20 @@ static void loadInOrder(ModuleT *mods) { // Returns a stb_ds dynamic array of dlopen handles. static void logAndReadDeps(ModuleT *mods) { - loaderLog("Discovered %d modules:", arrlen(mods)); + dvxLog("Discovered %d modules:", arrlen(mods)); for (int32_t i = 0; i < arrlen(mods); i++) { - loaderLog(" [%d] %s (base: %s)", i, mods[i].path, mods[i].baseName); + dvxLog(" [%d] %s (base: %s)", i, mods[i].path, mods[i].baseName); } for (int32_t i = 0; i < arrlen(mods); i++) { readDeps(&mods[i]); if (arrlen(mods[i].deps) > 0) { - loaderLog(" %s deps:", mods[i].baseName); + dvxLog(" %s deps:", mods[i].baseName); for (int32_t d = 0; d < arrlen(mods[i].deps); d++) { - loaderLog(" %s", mods[i].deps[d]); + dvxLog(" %s", mods[i].deps[d]); } } } @@ -425,11 +425,14 @@ int main(int argc, char *argv[]) { fclose(logInit); } - loaderLog("DVX Loader starting..."); + // Suppress Ctrl+C before anything else + platformInit(); + + dvxLog("DVX Loader starting..."); // Register platform + libc/libm/runtime symbols for DXE resolution platformRegisterDxeExports(); - loaderLog("Platform exports registered."); + dvxLog("Platform exports registered."); // Load all modules from libs/ and widgets/ in dependency order. // Each module may have a .dep file specifying load-before deps. @@ -447,7 +450,7 @@ int main(int argc, char *argv[]) { ShellMainFnT shellMain = (ShellMainFnT)findSymbol(handles, "_shellMain"); if (!shellMain) { - loaderLog("ERROR: No module exports shellMain"); + dvxLog("ERROR: No module exports shellMain"); for (int32_t i = arrlen(handles) - 1; i >= 0; i--) { dlclose(handles[i]); diff --git a/shell/Makefile b/shell/Makefile index 60f042d..2c76bda 100644 --- a/shell/Makefile +++ b/shell/Makefile @@ -13,7 +13,7 @@ CONFIGDIR = ../bin/config THEMEDIR = ../bin/config/themes WPAPERDIR = ../bin/config/wpaper -SRCS = shellMain.c shellApp.c shellExport.c shellInfo.c shellTaskMgr.c +SRCS = shellMain.c shellApp.c shellInfo.c shellTaskMgr.c OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) TARGET = $(LIBSDIR)/dvxshell.lib diff --git a/shell/shellApp.c b/shell/shellApp.c index fae39d7..44a369b 100644 --- a/shell/shellApp.c +++ b/shell/shellApp.c @@ -1,7 +1,7 @@ // shellApp.c -- DVX Shell application loading, lifecycle, and reaping // // Manages DXE app loading via dlopen/dlsym, resource tracking through -// sCurrentAppId, and clean teardown of both callback-only and main-loop apps. +// ctx->currentAppId, and clean teardown of both callback-only and main-loop apps. #include "shellApp.h" #include "dvxDialog.h" @@ -24,7 +24,6 @@ // New slots are appended as needed; freed slots are recycled. static ShellAppT *sApps = NULL; static int32_t sAppsCap = 0; // number of slots allocated (including slot 0) -int32_t sCurrentAppId = 0; // ============================================================ // Prototypes @@ -70,7 +69,7 @@ static int32_t allocSlot(void) { // Task entry point for main-loop apps. Runs in its own cooperative task. -// Sets sCurrentAppId before calling the app's entry point so that any +// Sets ctx->currentAppId before calling the app's entry point so that any // GUI resources created during the app's lifetime are tagged with the // correct owner. When the app's main loop returns (normal exit), the // state is set to Terminating so the shell's main loop will reap it. @@ -81,9 +80,9 @@ static int32_t allocSlot(void) { static void appTaskWrapper(void *arg) { ShellAppT *app = (ShellAppT *)arg; - sCurrentAppId = app->appId; + app->dxeCtx.shellCtx->currentAppId = app->appId; app->entryFn(&app->dxeCtx); - sCurrentAppId = 0; + app->dxeCtx.shellCtx->currentAppId = 0; // App returned from its main loop -- mark for reaping app->state = AppStateTerminatingE; @@ -115,14 +114,14 @@ static int32_t copyFile(const char *src, const char *dst) { FILE *in = fopen(src, "rb"); if (!in) { - shellLog("copyFile: failed to open source '%s' (errno=%d)", src, errno); + dvxLog("copyFile: failed to open source '%s' (errno=%d)", src, errno); return -1; } FILE *out = fopen(dst, "wb"); if (!out) { - shellLog("copyFile: failed to create dest '%s' (errno=%d)", dst, errno); + dvxLog("copyFile: failed to create dest '%s' (errno=%d)", dst, errno); fclose(in); return -1; } @@ -132,7 +131,7 @@ static int32_t copyFile(const char *src, const char *dst) { while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { if (fwrite(buf, 1, n, out) != n) { - shellLog("copyFile: write failed '%s' -> '%s' (errno=%d)", src, dst, errno); + dvxLog("copyFile: write failed '%s' -> '%s' (errno=%d)", src, dst, errno); fclose(in); fclose(out); remove(dst); @@ -259,7 +258,7 @@ void shellForceKillApp(AppContextT *ctx, ShellAppT *app) { cleanupTempFile(app); app->state = AppStateFreeE; - shellLog("Shell: force-killed app '%s'", app->name); + dvxLog("Shell: force-killed app '%s'", app->name); } @@ -429,10 +428,9 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) { snprintf(app->dxeCtx.configDir, sizeof(app->dxeCtx.configDir), "CONFIG/%s", desc->name); } - // Launch. Set sCurrentAppId before any app code runs so that window - // creation wrappers stamp the correct owner. Reset to 0 afterward so - // shell-initiated operations (e.g., message boxes) aren't misattributed. - sCurrentAppId = id; + // Launch. Set currentAppId before any app code runs so that + // dvxCreateWindow stamps the correct owner on new windows. + ctx->currentAppId = id; if (desc->hasMainLoop) { uint32_t stackSize = desc->stackSize > 0 ? (uint32_t)desc->stackSize : TS_DEFAULT_STACK_SIZE; @@ -441,7 +439,7 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) { int32_t taskId = tsCreate(desc->name, appTaskWrapper, app, stackSize, priority); if (taskId < 0) { - sCurrentAppId = 0; + ctx->currentAppId = 0; dvxMessageBox(ctx, "Error", "Failed to create task for application.", MB_OK | MB_ICONERROR); dlclose(handle); app->state = AppStateFreeE; @@ -457,10 +455,10 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) { app->entryFn(&app->dxeCtx); } - sCurrentAppId = 0; + ctx->currentAppId = 0; app->state = AppStateRunningE; - shellLog("Shell: loaded '%s' (id=%ld, mainLoop=%s, entry=0x%08lx, desc=0x%08lx)", app->name, (long)id, app->hasMainLoop ? "yes" : "no", (unsigned long)entry, (unsigned long)desc); + dvxLog("Shell: loaded '%s' (id=%ld, mainLoop=%s, entry=0x%08lx, desc=0x%08lx)", app->name, (long)id, app->hasMainLoop ? "yes" : "no", (unsigned long)entry, (unsigned long)desc); shellDesktopUpdate(); return id; } @@ -469,7 +467,7 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) { // Graceful reap -- called from shellReapApps when an app has reached // the Terminating state. Unlike forceKill, this calls the app's // shutdown hook (if provided) giving it a chance to save state, close -// files, etc. The sCurrentAppId is set during the shutdown call so +// files, etc. The ctx->currentAppId is set during the shutdown call so // any final resource operations are attributed correctly. void shellReapApp(AppContextT *ctx, ShellAppT *app) { if (!app || app->state == AppStateFreeE) { @@ -478,9 +476,10 @@ void shellReapApp(AppContextT *ctx, ShellAppT *app) { // Call shutdown hook if present if (app->shutdownFn) { - sCurrentAppId = app->appId; + ctx->currentAppId = app->appId; app->shutdownFn(); - sCurrentAppId = 0; + ctx->currentAppId = 0; + ctx->currentAppId = 0; } // Destroy all windows belonging to this app @@ -504,7 +503,7 @@ void shellReapApp(AppContextT *ctx, ShellAppT *app) { } cleanupTempFile(app); - shellLog("Shell: reaped app '%s'", app->name); + dvxLog("Shell: reaped app '%s'", app->name); app->state = AppStateFreeE; } diff --git a/shell/shellApp.h b/shell/shellApp.h index d674a98..de416d5 100644 --- a/shell/shellApp.h +++ b/shell/shellApp.h @@ -100,14 +100,6 @@ typedef struct { // Shell global state // ============================================================ -// Current app ID for resource tracking (0 = shell). -// Set before calling any app code (entry, shutdown, callbacks) so that -// wrapper functions (shellWrapCreateWindow, etc.) can stamp newly created -// resources with the correct owner. This is the mechanism that lets the -// shell know which windows belong to which app, enabling per-app cleanup -// on crash or termination. It's a global rather than a thread-local -// because cooperative multitasking means only one task runs at a time. -extern int32_t sCurrentAppId; // ============================================================ // App lifecycle API @@ -150,13 +142,6 @@ int32_t shellAppSlotCount(void); // Count running apps (not counting the shell itself) int32_t shellRunningAppCount(void); -// ============================================================ -// Logging -// ============================================================ - -// Write a printf-style message to DVX.LOG -void shellLog(const char *fmt, ...); - // Ensure an app's config directory exists (creates all parent dirs). // Returns 0 on success, -1 on failure. int32_t shellEnsureConfigDir(const DxeAppContextT *ctx); @@ -166,12 +151,6 @@ int32_t shellEnsureConfigDir(const DxeAppContextT *ctx); // -> "CONFIG/PROGMAN/settings.ini" void shellConfigPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize); -// ============================================================ -// DXE export table -// ============================================================ - -// Register the DXE symbol export table (call before any dlopen) -void shellExportInit(void); // ============================================================ // Desktop callback diff --git a/shell/shellExport.c b/shell/shellExport.c deleted file mode 100644 index 000e5ce..0000000 --- a/shell/shellExport.c +++ /dev/null @@ -1,119 +0,0 @@ -// shellExport.c -- DXE wrapper overrides for resource tracking -// -// Registers three wrapper functions that override the real -// dvxCreateWindow, dvxCreateWindowCentered, and dvxDestroyWindow -// for all subsequently loaded app modules. -// -// The key mechanic: symbol overrides registered via -// platformRegisterSymOverrides take precedence over previously loaded -// symbols. Since libdvx (which has the real functions) was loaded -// before shellExportInit() registers these wrappers, libdvx keeps the -// real implementations. But any app module loaded afterward gets the -// wrappers, which add resource tracking (appId stamping, last-window -// reaping) transparently. -// -// All other symbol exports (dvx*, wgt*, platform*, libc) are handled -// by the loaded modules and the platform layer's export table -- they -// no longer need to be listed here. - -#include "shellApp.h" -#include "dvxApp.h" -#include "dvxPlatform.h" -#include "dvxWm.h" - -// ============================================================ -// Prototypes -// ============================================================ - -static WindowT *shellWrapCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable); -static WindowT *shellWrapCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w, int32_t h, bool resizable); -static void shellWrapDestroyWindow(AppContextT *ctx, WindowT *win); - -// ============================================================ -// Wrapper: dvxCreateWindow -- stamps win->appId -// ============================================================ - -// The wrapper calls the real dvxCreateWindow (resolved from libdvx -// at shellcore load time), then tags the result with sCurrentAppId. -// This is how the shell knows which app owns which window, enabling -// per-app window cleanup on crash/termination. -static WindowT *shellWrapCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) { - WindowT *win = dvxCreateWindow(ctx, title, x, y, w, h, resizable); - - if (win) { - win->appId = sCurrentAppId; - } - - return win; -} - - -// ============================================================ -// Wrapper: dvxCreateWindowCentered -- stamps win->appId -// ============================================================ - -static WindowT *shellWrapCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w, int32_t h, bool resizable) { - WindowT *win = dvxCreateWindowCentered(ctx, title, w, h, resizable); - - if (win) { - win->appId = sCurrentAppId; - } - - return win; -} - - -// ============================================================ -// Wrapper: dvxDestroyWindow -- checks for last-window reap -// ============================================================ - -// Beyond just destroying the window, this wrapper implements the lifecycle -// rule for callback-only apps: when their last window closes, they're done. -// Main-loop apps manage their own lifetime (their task returns from -// appMain), so this check only applies to callback-only apps. -// The appId is captured before destruction because the window struct is -// freed by dvxDestroyWindow. -static void shellWrapDestroyWindow(AppContextT *ctx, WindowT *win) { - int32_t appId = win->appId; - - dvxDestroyWindow(ctx, win); - - // If this was a callback-only app's last window, mark for reaping - if (appId > 0) { - ShellAppT *app = shellGetApp(appId); - - if (app && !app->hasMainLoop && app->state == AppStateRunningE) { - // Check if app still has any windows - bool hasWindows = false; - - for (int32_t i = 0; i < ctx->stack.count; i++) { - if (ctx->stack.windows[i]->appId == appId) { - hasWindows = true; - break; - } - } - - if (!hasWindows) { - app->state = AppStateTerminatingE; - } - } - } -} - - -// ============================================================ -// shellExportInit -// ============================================================ - -// Register the wrapper overrides. Must be called before any -// shellLoadApp() -- wrappers only affect subsequently loaded modules. -void shellExportInit(void) { - static const PlatformSymOverrideT sOverrides[] = { - {"_dvxCreateWindow", (void *)shellWrapCreateWindow}, - {"_dvxCreateWindowCentered", (void *)shellWrapCreateWindowCentered}, - {"_dvxDestroyWindow", (void *)shellWrapDestroyWindow}, - {NULL, NULL} - }; - - platformRegisterSymOverrides(sOverrides); -} diff --git a/shell/shellInfo.c b/shell/shellInfo.c index 92a1bbb..aadca24 100644 --- a/shell/shellInfo.c +++ b/shell/shellInfo.c @@ -34,7 +34,7 @@ void shellInfoInit(AppContextT *ctx) { sCachedInfo = platformGetSystemInfo(&ctx->display); // Log each line individually so the log file is readable - shellLog("=== System Information ==="); + dvxLog("=== System Information ==="); const char *line = sCachedInfo; @@ -42,7 +42,7 @@ void shellInfoInit(AppContextT *ctx) { const char *eol = strchr(line, '\n'); if (!eol) { - shellLog("%s", line); + dvxLog("%s", line); break; } @@ -55,9 +55,9 @@ void shellInfoInit(AppContextT *ctx) { memcpy(tmp, line, len); tmp[len] = '\0'; - shellLog("%s", tmp); + dvxLog("%s", tmp); line = eol + 1; } - shellLog("=== End System Information ==="); + dvxLog("=== End System Information ==="); } diff --git a/shell/shellMain.c b/shell/shellMain.c index 0aaf2bd..002d8b4 100644 --- a/shell/shellMain.c +++ b/shell/shellMain.c @@ -50,7 +50,6 @@ static jmp_buf sCrashJmp; // Volatile because it's written from a signal handler context. Tells // the recovery code which signal fired (for logging/diagnostics). static volatile int sCrashSignal = 0; -static const char *sLogPath = NULL; // Desktop update callback list (dynamic, managed via stb_ds arrput/arrdel) typedef void (*DesktopUpdateFnT)(void); static DesktopUpdateFnT *sDesktopUpdateFns = NULL; @@ -113,32 +112,7 @@ static void idleYield(void *ctx) { static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData) { (void)userData; - shellLog(" %ldx%ld %ldbpp", (long)w, (long)h, (long)bpp); -} - - -// ============================================================ -// shellLog -- append a line to DVX.LOG -// ============================================================ - -void shellLog(const char *fmt, ...) { - if (!sLogPath) { - return; - } - - FILE *f = fopen(sLogPath, "a"); - - if (!f) { - return; - } - - va_list ap; - va_start(ap, fmt); - vfprintf(f, fmt, ap); - va_end(ap); - - fprintf(f, "\n"); - fclose(f); + dvxLog(" %ldx%ld %ldbpp", (long)w, (long)h, (long)bpp); } @@ -173,12 +147,7 @@ int shellMain(int argc, char *argv[]) { (void)argc; (void)argv; - // The loader already truncates and writes boot info to dvx.log. - // We just append from here on, opening/closing per-write so the - // file isn't held open (allows Notepad to read it while the shell runs). - sLogPath = "dvx.log"; - - shellLog("DVX Shell starting..."); + dvxLog("DVX Shell starting..."); // Load preferences (missing file or keys silently use defaults) prefsLoad("CONFIG/DVX.INI"); @@ -186,7 +155,7 @@ int shellMain(int argc, char *argv[]) { int32_t videoW = prefsGetInt("video", "width", 640); int32_t videoH = prefsGetInt("video", "height", 480); int32_t videoBpp = prefsGetInt("video", "bpp", 16); - shellLog("Preferences: video %ldx%ld %ldbpp", (long)videoW, (long)videoH, (long)videoBpp); + dvxLog("Preferences: video %ldx%ld %ldbpp", (long)videoW, (long)videoH, (long)videoBpp); // Initialize GUI int32_t result = dvxInit(&sCtx, videoW, videoH, videoBpp); @@ -213,7 +182,7 @@ int shellMain(int argc, char *argv[]) { } dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal); - shellLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s", wheelStr, (long)dblClick, accelStr); + dvxLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s", wheelStr, (long)dblClick, accelStr); // Apply saved color scheme from INI bool colorsLoaded = false; @@ -237,7 +206,7 @@ int shellMain(int argc, char *argv[]) { if (colorsLoaded) { dvxApplyColorScheme(&sCtx); - shellLog("Preferences: loaded custom color scheme"); + dvxLog("Preferences: loaded custom color scheme"); } // Apply saved wallpaper mode and image @@ -255,25 +224,25 @@ int shellMain(int argc, char *argv[]) { if (wpPath) { if (dvxSetWallpaper(&sCtx, wpPath)) { - shellLog("Preferences: loaded wallpaper %s (%s)", wpPath, wpMode); + dvxLog("Preferences: loaded wallpaper %s (%s)", wpPath, wpMode); } else { - shellLog("Preferences: failed to load wallpaper %s", wpPath); + dvxLog("Preferences: failed to load wallpaper %s", wpPath); } } } if (result != 0) { - shellLog("Failed to initialize DVX GUI (error %ld)", (long)result); + dvxLog("Failed to initialize DVX GUI (error %ld)", (long)result); return 1; } - shellLog("Available video modes:"); + dvxLog("Available video modes:"); platformVideoEnumModes(logVideoMode, NULL); - shellLog("Selected: %ldx%ld %ldbpp (pitch %ld)", (long)sCtx.display.width, (long)sCtx.display.height, (long)sCtx.display.format.bitsPerPixel, (long)sCtx.display.pitch); + dvxLog("Selected: %ldx%ld %ldbpp (pitch %ld)", (long)sCtx.display.width, (long)sCtx.display.height, (long)sCtx.display.format.bitsPerPixel, (long)sCtx.display.pitch); // Initialize task system if (tsInit() != TS_OK) { - shellLog("Failed to initialize task system"); + dvxLog("Failed to initialize task system"); dvxShutdown(&sCtx); return 1; } @@ -287,9 +256,6 @@ int shellMain(int argc, char *argv[]) { // Gather system information (CPU, memory, drives, etc.) shellInfoInit(&sCtx); - // Register DXE export table - shellExportInit(); - // Initialize app slot table shellAppInit(); @@ -309,7 +275,7 @@ int shellMain(int argc, char *argv[]) { int32_t desktopId = shellLoadApp(&sCtx, desktopApp); if (desktopId < 0) { - shellLog("Failed to load desktop app '%s'", desktopApp); + dvxLog("Failed to load desktop app '%s'", desktopApp); tsShutdown(); dvxShutdown(&sCtx); return 1; @@ -319,9 +285,9 @@ int shellMain(int argc, char *argv[]) { // initialization itself crashes, we want the default behavior // (abort with register dump) rather than our recovery path, because // the system isn't in a recoverable state yet. - platformInstallCrashHandler(&sCrashJmp, &sCrashSignal, shellLog); + platformInstallCrashHandler(&sCrashJmp, &sCrashSignal, dvxLog); - shellLog("DVX Shell ready."); + dvxLog("DVX Shell ready."); // Set recovery point for crash handler. setjmp returns 0 on initial // call (falls through to the main loop). On a crash, longjmp makes @@ -336,36 +302,38 @@ int shellMain(int argc, char *argv[]) { // Platform handler already logged signal name and register dump. // Log app-specific info here. - shellLog("Current app ID: %ld", (long)sCurrentAppId); + dvxLog("Current app ID: %ld", (long)sCtx.currentAppId); - if (sCurrentAppId > 0) { - ShellAppT *crashedApp = shellGetApp(sCurrentAppId); + if (sCtx.currentAppId > 0) { + ShellAppT *crashedApp = shellGetApp(sCtx.currentAppId); if (crashedApp) { - shellLog("App name: %s", crashedApp->name); - shellLog("App path: %s", crashedApp->path); - shellLog("Has main loop: %s", crashedApp->hasMainLoop ? "yes" : "no"); - shellLog("Task ID: %lu", (unsigned long)crashedApp->mainTaskId); + dvxLog("App name: %s", crashedApp->name); + dvxLog("App path: %s", crashedApp->path); + dvxLog("Has main loop: %s", crashedApp->hasMainLoop ? "yes" : "no"); + dvxLog("Task ID: %lu", (unsigned long)crashedApp->mainTaskId); } } else { - shellLog("Crashed in shell (task 0)"); + dvxLog("Crashed in shell (task 0)"); } - shellLog("Recovering from crash, killing app %ld", (long)sCurrentAppId); + dvxLog("Recovering from crash, killing app %ld", (long)sCtx.currentAppId); - if (sCurrentAppId > 0) { - ShellAppT *app = shellGetApp(sCurrentAppId); + if (sCtx.currentAppId > 0) { + ShellAppT *app = shellGetApp(sCtx.currentAppId); if (app) { char msg[256]; snprintf(msg, sizeof(msg), "'%s' has caused a fault and will be terminated.", app->name); shellForceKillApp(&sCtx, app); - sCurrentAppId = 0; + sCtx.currentAppId = 0; + sCtx.currentAppId = 0; dvxMessageBox(&sCtx, "Application Error", msg, MB_OK | MB_ICONERROR); } } - sCurrentAppId = 0; + sCtx.currentAppId = 0; + sCtx.currentAppId = 0; sCrashSignal = 0; shellDesktopUpdate(); } @@ -392,7 +360,7 @@ int shellMain(int argc, char *argv[]) { } } - shellLog("DVX Shell shutting down..."); + dvxLog("DVX Shell shutting down..."); // Clean shutdown: terminate all apps first (destroys windows, kills // tasks, closes DXE handles), then tear down the task system and GUI @@ -403,6 +371,6 @@ int shellMain(int argc, char *argv[]) { dvxShutdown(&sCtx); prefsFree(); - shellLog("DVX Shell exited."); + dvxLog("DVX Shell exited."); return 0; } diff --git a/texthelp/textHelp.c b/texthelp/textHelp.c index d061365..1fbbd64 100644 --- a/texthelp/textHelp.c +++ b/texthelp/textHelp.c @@ -14,6 +14,10 @@ // Static state // ============================================================ +// Cursor blink state +#define CURSOR_BLINK_MS 250 +static clock_t sCursorBlinkTime = 0; + // Track the widget that last had an active selection so we can // clear it in O(1) instead of walking every widget in every window. static WidgetT *sLastSelectedWidget = NULL; @@ -29,6 +33,31 @@ static int32_t wordBoundaryLeft(const char *buf, int32_t pos); static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos); +// ============================================================ +// wgtUpdateCursorBlink +// ============================================================ + +static void textHelpInit(void) __attribute__((constructor)); +static void textHelpInit(void) { + sCursorBlinkFn = wgtUpdateCursorBlink; +} + + +void wgtUpdateCursorBlink(void) { + clock_t now = clock(); + clock_t interval = (clock_t)CURSOR_BLINK_MS * CLOCKS_PER_SEC / 1000; + + if ((now - sCursorBlinkTime) >= interval) { + sCursorBlinkTime = now; + sCursorBlinkOn = !sCursorBlinkOn; + + if (sFocusedWidget) { + wgtInvalidatePaint(sFocusedWidget); + } + } +} + + // ============================================================ // clearOtherSelections // ============================================================ diff --git a/texthelp/textHelp.h b/texthelp/textHelp.h index ca9bfa8..6ac490f 100644 --- a/texthelp/textHelp.h +++ b/texthelp/textHelp.h @@ -12,6 +12,12 @@ #define TEXT_INPUT_PAD 3 +// ============================================================ +// Cursor blink +// ============================================================ + +void wgtUpdateCursorBlink(void); + // ============================================================ // Selection management // ============================================================ diff --git a/widgets/widgetAnsiTerm.c b/widgets/widgetAnsiTerm.c index 32e4157..199864c 100644 --- a/widgets/widgetAnsiTerm.c +++ b/widgets/widgetAnsiTerm.c @@ -908,6 +908,57 @@ void widgetAnsiTermPollVtable(WidgetT *w, WindowT *win) { } +static bool widgetAnsiTermClearSelection(WidgetT *w) { + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + + if (at->selStartLine >= 0) { + at->selStartLine = -1; + at->selStartCol = -1; + at->selEndLine = -1; + at->selEndCol = -1; + at->selecting = false; + at->dirtyRows = 0xFFFFFFFF; + return true; + } + + return false; +} + + +static void widgetAnsiTermDragSelect(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { + (void)root; + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + const BitmapFontT *font = &ctx->font; + + int32_t innerX = w->x + ANSI_BORDER; + int32_t innerY = w->y + ANSI_BORDER; + int32_t col = (vx - innerX) / font->charWidth; + int32_t row = (vy - innerY) / font->charHeight; + + if (col < 0) { + col = 0; + } + + if (col >= at->cols) { + col = at->cols - 1; + } + + if (row < 0) { + row = 0; + } + + if (row >= at->rows) { + row = at->rows - 1; + } + + int32_t lineIndex = at->scrollPos + row; + at->selEndLine = lineIndex; + at->selEndCol = col; + at->dirtyRows = 0xFFFFFFFF; +} + + static const WidgetClassT sClassAnsiTerm = { .flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE | WCLASS_NEEDS_POLL | WCLASS_SWALLOWS_TAB, .paint = widgetAnsiTermPaint, @@ -919,6 +970,8 @@ static const WidgetClassT sClassAnsiTerm = { .destroy = widgetAnsiTermDestroy, .getText = NULL, .setText = NULL, + .clearSelection = widgetAnsiTermClearSelection, + .dragSelect = widgetAnsiTermDragSelect, .poll = widgetAnsiTermPollVtable, .quickRepaint = wgtAnsiTermRepaint }; diff --git a/widgets/widgetComboBox.c b/widgets/widgetComboBox.c index a44b418..3df8e30 100644 --- a/widgets/widgetComboBox.c +++ b/widgets/widgetComboBox.c @@ -202,19 +202,57 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { w->focused = true; ComboBoxDataT *d = (ComboBoxDataT *)w->data; + // If popup is open, this click is on a popup item -- select it + if (d->open) { + AppContextT *ctx = wgtGetContext(w); + const BitmapFontT *font = &ctx->font; + int32_t popX; + int32_t popY; + int32_t popW; + int32_t popH; + + widgetDropdownPopupRect(w, font, w->window->contentH, &popX, &popY, &popW, &popH); + + int32_t itemIdx = d->listScrollPos + (vy - popY - 2) / font->charHeight; + + if (itemIdx >= 0 && itemIdx < d->itemCount) { + d->selectedIdx = itemIdx; + + int32_t slen = (int32_t)strlen(d->items[itemIdx]); + + if (slen >= d->bufSize) { + slen = d->bufSize - 1; + } + + memcpy(d->buf, d->items[itemIdx], slen); + d->buf[slen] = '\0'; + d->len = slen; + d->cursorPos = slen; + d->scrollOff = 0; + d->selStart = -1; + d->selEnd = -1; + + if (w->onChange) { + w->onChange(w); + } + } + + d->open = false; + sOpenPopup = NULL; + return; + } + // Check if click is on the button area int32_t textAreaW = w->w - DROPDOWN_BTN_WIDTH; if (vx >= w->x + textAreaW) { - // If this combobox's popup was just closed by click-outside, don't re-open if (w == sClosedPopup) { return; } - // Button click -- toggle popup - d->open = !d->open; + d->open = true; d->hoverIdx = d->selectedIdx; - sOpenPopup = d->open ? w : NULL; + sOpenPopup = w; } else { // Text area click -- focus for editing clearOtherSelections(w); @@ -372,6 +410,31 @@ static void widgetComboBoxGetPopupRect(const WidgetT *w, const BitmapFontT *font } +static bool widgetComboBoxClearSelection(WidgetT *w) { + ComboBoxDataT *d = (ComboBoxDataT *)w->data; + + if (d->selStart >= 0 && d->selEnd >= 0 && d->selStart != d->selEnd) { + d->selStart = -1; + d->selEnd = -1; + return true; + } + + return false; +} + + +static void widgetComboBoxDragSelect(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { + (void)root; + (void)vy; + ComboBoxDataT *d = (ComboBoxDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + int32_t fieldW = w->w - DROPDOWN_BTN_WIDTH; + int32_t maxChars = (fieldW - TEXT_INPUT_PAD * 2) / ctx->font.charWidth; + + widgetTextEditDragUpdateLine(vx, w->x + TEXT_INPUT_PAD, maxChars, &ctx->font, d->len, &d->cursorPos, &d->scrollOff, &d->selEnd); +} + + static const WidgetClassT sClassComboBox = { .flags = WCLASS_FOCUSABLE | WCLASS_HAS_POPUP | WCLASS_SCROLLABLE, .paint = widgetComboBoxPaint, @@ -387,6 +450,8 @@ static const WidgetClassT sClassComboBox = { .openPopup = widgetComboBoxOpenPopup, .closePopup = widgetComboBoxClosePopup, .getPopupItemCount = widgetComboBoxGetPopupItemCount, + .clearSelection = widgetComboBoxClearSelection, + .dragSelect = widgetComboBoxDragSelect, .getPopupRect = widgetComboBoxGetPopupRect }; diff --git a/widgets/widgetDropdown.c b/widgets/widgetDropdown.c index ab4ae98..4e535e9 100644 --- a/widgets/widgetDropdown.c +++ b/widgets/widgetDropdown.c @@ -161,19 +161,43 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) { // check. The sClosedPopup reference is cleared on the next event cycle. void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { (void)root; - (void)vx; - (void)vy; w->focused = true; + DropdownDataT *d = (DropdownDataT *)w->data; + + // If popup is open, this click is on a popup item -- select it + if (d->open) { + AppContextT *ctx = wgtGetContext(w); + const BitmapFontT *font = &ctx->font; + int32_t popX; + int32_t popY; + int32_t popW; + int32_t popH; + + widgetDropdownPopupRect(w, font, w->window->contentH, &popX, &popY, &popW, &popH); + + int32_t itemIdx = d->scrollPos + (vy - popY - 2) / font->charHeight; + + if (itemIdx >= 0 && itemIdx < d->itemCount) { + d->selectedIdx = itemIdx; + + if (w->onChange) { + w->onChange(w); + } + } + + d->open = false; + sOpenPopup = NULL; + return; + } // If this dropdown's popup was just closed by click-outside, don't re-open if (w == sClosedPopup) { return; } - DropdownDataT *d = (DropdownDataT *)w->data; - d->open = !d->open; + d->open = true; d->hoverIdx = d->selectedIdx; - sOpenPopup = d->open ? w : NULL; + sOpenPopup = w; } diff --git a/widgets/widgetListBox.c b/widgets/widgetListBox.c index de981d5..0b90da6 100644 --- a/widgets/widgetListBox.c +++ b/widgets/widgetListBox.c @@ -62,6 +62,7 @@ typedef struct { static void ensureScrollVisible(WidgetT *w, int32_t idx); static void selectRange(WidgetT *w, int32_t from, int32_t to); +static void widgetListBoxScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY); // ============================================================ @@ -230,6 +231,8 @@ void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) { } d->selectedIdx = newSel; + d->dragIdx = -1; + d->dropIdx = -1; // Update selection if (multi && d->selBits) { @@ -469,11 +472,120 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm } +// ============================================================ +// widgetListBoxScrollDragUpdate +// ============================================================ + +// Handle scrollbar thumb drag for vertical scrollbar. +static void widgetListBoxScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) { + (void)mouseX; + ListBoxDataT *d = (ListBoxDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + const BitmapFontT *font = &ctx->font; + + if (orient != 0) { + return; + } + + int32_t innerH = w->h - LISTBOX_BORDER * 2; + int32_t visibleRows = innerH / font->charHeight; + int32_t maxScroll = d->itemCount - visibleRows; + + if (maxScroll <= 0) { + return; + } + + int32_t trackLen = innerH - WGT_SB_W * 2; + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, d->itemCount, visibleRows, d->scrollPos, &thumbPos, &thumbSize); + + int32_t sbY = w->y + LISTBOX_BORDER; + int32_t relMouse = mouseY - sbY - WGT_SB_W - dragOff; + int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0; + d->scrollPos = clampInt(newScroll, 0, maxScroll); +} + + // ============================================================ // DXE registration // ============================================================ +static void widgetListBoxReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { + (void)root; + (void)x; + ListBoxDataT *d = (ListBoxDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + const BitmapFontT *font = &ctx->font; + int32_t innerY = w->y + LISTBOX_BORDER; + int32_t relY = y - innerY; + int32_t dropIdx = d->scrollPos + relY / font->charHeight; + + if (dropIdx < 0) { + dropIdx = 0; + } + + if (dropIdx > d->itemCount) { + dropIdx = d->itemCount; + } + + d->dropIdx = dropIdx; + + // Auto-scroll when dragging near edges + int32_t visibleRows = (w->h - LISTBOX_BORDER * 2) / font->charHeight; + + if (relY < font->charHeight && d->scrollPos > 0) { + d->scrollPos--; + } else if (relY >= (visibleRows - 1) * font->charHeight && d->scrollPos < d->itemCount - visibleRows) { + d->scrollPos++; + } +} + + +static void widgetListBoxReorderDrop(WidgetT *w) { + ListBoxDataT *d = (ListBoxDataT *)w->data; + int32_t from = d->dragIdx; + int32_t to = d->dropIdx; + + d->dragIdx = -1; + d->dropIdx = -1; + + if (from < 0 || to < 0 || from == to || from == to - 1) { + return; + } + + if (from < 0 || from >= d->itemCount) { + return; + } + + // Move the item by shifting the pointer array + const char *moving = d->items[from]; + + if (to > from) { + // Moving down: shift items up + for (int32_t i = from; i < to - 1; i++) { + d->items[i] = d->items[i + 1]; + } + + d->items[to - 1] = moving; + d->selectedIdx = to - 1; + } else { + // Moving up: shift items down + for (int32_t i = from; i > to; i--) { + d->items[i] = d->items[i - 1]; + } + + d->items[to] = moving; + d->selectedIdx = to; + } + + if (w->onChange) { + w->onChange(w); + } +} + + static const WidgetClassT sClassListBox = { .flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE, .paint = widgetListBoxPaint, @@ -482,9 +594,12 @@ static const WidgetClassT sClassListBox = { .layout = NULL, .onMouse = widgetListBoxOnMouse, .onKey = widgetListBoxOnKey, - .destroy = widgetListBoxDestroy, - .getText = NULL, - .setText = NULL + .destroy = widgetListBoxDestroy, + .getText = NULL, + .setText = NULL, + .reorderDrop = widgetListBoxReorderDrop, + .reorderUpdate = widgetListBoxReorderUpdate, + .scrollDragUpdate = widgetListBoxScrollDragUpdate }; // ============================================================ diff --git a/widgets/widgetListView.c b/widgets/widgetListView.c index 9de6771..4bdaa04 100644 --- a/widgets/widgetListView.c +++ b/widgets/widgetListView.c @@ -1279,8 +1279,6 @@ void wgtListViewSetSort(WidgetT *w, int32_t col, ListViewSortE dir) { // the initial click position. This prevents accidental resizes from stray // clicks on column borders. static void widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) { - (void)dragOff; - (void)mouseY; ListViewDataT *lv = (ListViewDataT *)w->data; // orient == -1 means column resize drag @@ -1303,14 +1301,80 @@ static void widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t d lv->resolvedColW[lv->resizeCol] = newW; int32_t total = 0; + for (int32_t c = 0; c < lv->colCount; c++) { total += lv->resolvedColW[c]; } + lv->totalColW = total; } return; } + + AppContextT *ctx = wgtGetContext(w); + const BitmapFontT *font = &ctx->font; + + int32_t headerH = font->charHeight + 4; + int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH; + int32_t innerW = w->w - LISTVIEW_BORDER * 2; + int32_t visibleRows = innerH / font->charHeight; + int32_t totalColW = lv->totalColW; + bool needVSb = (lv->rowCount > visibleRows); + + if (needVSb) { + innerW -= WGT_SB_W; + } + + if (totalColW > innerW) { + innerH -= WGT_SB_W; + visibleRows = innerH / font->charHeight; + + if (!needVSb && lv->rowCount > visibleRows) { + needVSb = true; + innerW -= WGT_SB_W; + } + } + + if (visibleRows < 1) { + visibleRows = 1; + } + + if (orient == 0) { + // Vertical scrollbar drag + int32_t maxScroll = lv->rowCount - visibleRows; + + if (maxScroll <= 0) { + return; + } + + int32_t trackLen = innerH - WGT_SB_W * 2; + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, lv->rowCount, visibleRows, lv->scrollPos, &thumbPos, &thumbSize); + + int32_t sbY = w->y + LISTVIEW_BORDER + headerH; + int32_t relMouse = mouseY - sbY - WGT_SB_W - dragOff; + int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0; + lv->scrollPos = clampInt(newScroll, 0, maxScroll); + } else if (orient == 1) { + // Horizontal scrollbar drag + int32_t maxScroll = totalColW - innerW; + + if (maxScroll <= 0) { + return; + } + + int32_t trackLen = innerW - WGT_SB_W * 2; + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, totalColW, innerW, lv->scrollPosH, &thumbPos, &thumbSize); + + int32_t sbX = w->x + LISTVIEW_BORDER; + int32_t relMouse = mouseX - sbX - WGT_SB_W - dragOff; + int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0; + lv->scrollPosH = clampInt(newScroll, 0, maxScroll); + } } @@ -1331,6 +1395,105 @@ int32_t widgetListViewGetCursorShape(const WidgetT *w, int32_t vx, int32_t vy) { } +static void widgetListViewReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { + (void)root; + (void)x; + ListViewDataT *lv = (ListViewDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + const BitmapFontT *font = &ctx->font; + int32_t headerH = font->charHeight + 4; + int32_t dataY = w->y + LISTVIEW_BORDER + headerH; + int32_t relY = y - dataY; + int32_t dropIdx = lv->scrollPos + relY / font->charHeight; + + if (dropIdx < 0) { + dropIdx = 0; + } + + if (dropIdx > lv->rowCount) { + dropIdx = lv->rowCount; + } + + lv->dropIdx = dropIdx; + + // Auto-scroll near edges + int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH; + int32_t visibleRows = innerH / font->charHeight; + + if (relY < font->charHeight && lv->scrollPos > 0) { + lv->scrollPos--; + } else if (relY >= (visibleRows - 1) * font->charHeight && lv->scrollPos < lv->rowCount - visibleRows) { + lv->scrollPos++; + } +} + + +static void widgetListViewReorderDrop(WidgetT *w) { + ListViewDataT *lv = (ListViewDataT *)w->data; + int32_t from = lv->dragIdx; + int32_t to = lv->dropIdx; + + lv->dragIdx = -1; + lv->dropIdx = -1; + + if (from < 0 || to < 0 || from == to || from == to - 1) { + return; + } + + if (from < 0 || from >= lv->rowCount) { + return; + } + + // Move row data by shifting the cellData pointers. + // cellData is row-major: cellData[row * colCount + col]. + int32_t cols = lv->colCount; + const char **moving = (const char **)malloc(cols * sizeof(const char *)); + + if (!moving) { + return; + } + + // Save the moving row + for (int32_t c = 0; c < cols; c++) { + moving[c] = lv->cellData[from * cols + c]; + } + + if (to > from) { + // Moving down: shift rows up + for (int32_t r = from; r < to - 1; r++) { + for (int32_t c = 0; c < cols; c++) { + ((const char **)lv->cellData)[r * cols + c] = lv->cellData[(r + 1) * cols + c]; + } + } + + for (int32_t c = 0; c < cols; c++) { + ((const char **)lv->cellData)[(to - 1) * cols + c] = moving[c]; + } + + lv->selectedIdx = to - 1; + } else { + // Moving up: shift rows down + for (int32_t r = from; r > to; r--) { + for (int32_t c = 0; c < cols; c++) { + ((const char **)lv->cellData)[r * cols + c] = lv->cellData[(r - 1) * cols + c]; + } + } + + for (int32_t c = 0; c < cols; c++) { + ((const char **)lv->cellData)[to * cols + c] = moving[c]; + } + + lv->selectedIdx = to; + } + + free(moving); + + if (w->onChange) { + w->onChange(w); + } +} + + static const WidgetClassT sClassListView = { .flags = WCLASS_FOCUSABLE | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLLABLE, .paint = widgetListViewPaint, @@ -1343,6 +1506,8 @@ static const WidgetClassT sClassListView = { .getText = NULL, .setText = NULL, .getCursorShape = widgetListViewGetCursorShape, + .reorderDrop = widgetListViewReorderDrop, + .reorderUpdate = widgetListViewReorderUpdate, .scrollDragUpdate = widgetListViewScrollDragUpdate }; diff --git a/widgets/widgetScrollPane.c b/widgets/widgetScrollPane.c index f54fe4b..74bcc4e 100644 --- a/widgets/widgetScrollPane.c +++ b/widgets/widgetScrollPane.c @@ -50,6 +50,7 @@ static void drawSPHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const static void drawSPVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbH, int32_t totalH, int32_t visibleH); static void spCalcNeeds(WidgetT *w, const BitmapFontT *font, int32_t *contentMinW, int32_t *contentMinH, int32_t *innerW, int32_t *innerH, bool *needVSb, bool *needHSb); static void widgetScrollPaneDestroy(WidgetT *w); +static void widgetScrollPaneScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY); // ============================================================ @@ -623,6 +624,64 @@ void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy } +// ============================================================ +// widgetScrollPaneScrollDragUpdate +// ============================================================ + +// Handle scrollbar thumb drag for vertical and horizontal scrollbars. +// Uses spCalcNeeds to determine content and viewport dimensions. +static void widgetScrollPaneScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) { + ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + const BitmapFontT *font = &ctx->font; + + int32_t contentMinW; + int32_t contentMinH; + int32_t innerW; + int32_t innerH; + bool needVSb; + bool needHSb; + + spCalcNeeds(w, font, &contentMinW, &contentMinH, &innerW, &innerH, &needVSb, &needHSb); + + if (orient == 0) { + // Vertical scrollbar drag + int32_t maxScroll = contentMinH - innerH; + + if (maxScroll <= 0) { + return; + } + + int32_t trackLen = innerH - SP_SB_W * 2; + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, contentMinH, innerH, sp->scrollPosV, &thumbPos, &thumbSize); + + int32_t sbY = w->y + SP_BORDER; + int32_t relMouse = mouseY - sbY - SP_SB_W - dragOff; + int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0; + sp->scrollPosV = clampInt(newScroll, 0, maxScroll); + } else if (orient == 1) { + // Horizontal scrollbar drag + int32_t maxScroll = contentMinW - innerW; + + if (maxScroll <= 0) { + return; + } + + int32_t trackLen = innerW - SP_SB_W * 2; + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, contentMinW, innerW, sp->scrollPosH, &thumbPos, &thumbSize); + + int32_t sbX = w->x + SP_BORDER; + int32_t relMouse = mouseX - sbX - SP_SB_W - dragOff; + int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0; + sp->scrollPosH = clampInt(newScroll, 0, maxScroll); + } +} + + // ============================================================ // widgetScrollPanePaint // ============================================================ @@ -711,16 +770,17 @@ void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B // ============================================================ static const WidgetClassT sClassScrollPane = { - .flags = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLL_CONTAINER | WCLASS_RELAYOUT_ON_SCROLL, - .paint = widgetScrollPanePaint, - .paintOverlay = NULL, - .calcMinSize = widgetScrollPaneCalcMinSize, - .layout = widgetScrollPaneLayout, - .onMouse = widgetScrollPaneOnMouse, - .onKey = widgetScrollPaneOnKey, - .destroy = widgetScrollPaneDestroy, - .getText = NULL, - .setText = NULL + .flags = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLL_CONTAINER | WCLASS_RELAYOUT_ON_SCROLL, + .paint = widgetScrollPanePaint, + .paintOverlay = NULL, + .calcMinSize = widgetScrollPaneCalcMinSize, + .layout = widgetScrollPaneLayout, + .onMouse = widgetScrollPaneOnMouse, + .onKey = widgetScrollPaneOnKey, + .destroy = widgetScrollPaneDestroy, + .getText = NULL, + .setText = NULL, + .scrollDragUpdate = widgetScrollPaneScrollDragUpdate }; diff --git a/widgets/widgetSplitter.c b/widgets/widgetSplitter.c index 54b8d6b..b683d5d 100644 --- a/widgets/widgetSplitter.c +++ b/widgets/widgetSplitter.c @@ -400,6 +400,20 @@ int32_t widgetSplitterGetCursorShape(const WidgetT *w, int32_t vx, int32_t vy) { } } + // Not on our divider -- check children (handles nested splitters + // and other widgets with cursor shapes like ListView column borders) + for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { + WidgetT *child = widgetHitTest(c, vx, vy); + + if (child && child->wclass && child->wclass->getCursorShape) { + int32_t shape = child->wclass->getCursorShape(child, vx, vy); + + if (shape > 0) { + return shape; + } + } + } + return 0; } diff --git a/widgets/widgetTextInput.c b/widgets/widgetTextInput.c index 3d46ae0..b42706a 100644 --- a/widgets/widgetTextInput.c +++ b/widgets/widgetTextInput.c @@ -133,6 +133,7 @@ static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row); static int32_t textAreaMaxLineLen(const char *buf, int32_t len); static void textAreaOffToRowCol(const char *buf, int32_t off, int32_t *row, int32_t *col); static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize); +static void widgetTextAreaScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY); static int32_t wordBoundaryLeft(const char *buf, int32_t pos); static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos); WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen); @@ -2020,6 +2021,30 @@ int32_t widgetTextInputGetFieldWidth(const WidgetT *w) { // ============================================================ +static bool widgetTextInputClearSelection(WidgetT *w) { + TextInputDataT *ti = (TextInputDataT *)w->data; + + if (ti->selStart >= 0 && ti->selEnd >= 0 && ti->selStart != ti->selEnd) { + ti->selStart = -1; + ti->selEnd = -1; + return true; + } + + return false; +} + + +static void widgetTextInputDragSelect(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { + (void)root; + (void)vy; + TextInputDataT *ti = (TextInputDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2) / ctx->font.charWidth; + + widgetTextEditDragUpdateLine(vx, w->x + TEXT_INPUT_PAD, maxChars, &ctx->font, ti->len, &ti->cursorPos, &ti->scrollOff, &ti->selEnd); +} + + static const WidgetClassT sClassTextInput = { .flags = WCLASS_FOCUSABLE, .paint = widgetTextInputPaint, @@ -2031,9 +2056,141 @@ static const WidgetClassT sClassTextInput = { .destroy = widgetTextInputDestroy, .getText = widgetTextInputGetText, .setText = widgetTextInputSetText, + .clearSelection = widgetTextInputClearSelection, + .dragSelect = widgetTextInputDragSelect, .getTextFieldWidth = widgetTextInputGetFieldWidth }; +// ============================================================ +// widgetTextAreaScrollDragUpdate +// ============================================================ + +// Handle scrollbar thumb drag for TextArea vertical and horizontal scrollbars. +// The TextArea always reserves space for the V scrollbar on the right. +// The H scrollbar appears at the bottom only when the longest line exceeds +// visible columns. +static void widgetTextAreaScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) { + TextAreaDataT *ta = (TextAreaDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + const BitmapFontT *font = &ctx->font; + + int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W; + int32_t visCols = innerW / font->charWidth; + int32_t maxLL = textAreaGetMaxLineLen(w); + bool needHSb = (maxLL > visCols); + int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0); + int32_t visRows = innerH / font->charHeight; + + if (visRows < 1) { + visRows = 1; + } + + if (visCols < 1) { + visCols = 1; + } + + if (orient == 0) { + // Vertical scrollbar drag + int32_t totalLines = textAreaGetLineCount(w); + int32_t maxScroll = totalLines - visRows; + + if (maxScroll <= 0) { + return; + } + + int32_t trackLen = innerH - TEXTAREA_SB_W * 2; + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, totalLines, visRows, ta->scrollRow, &thumbPos, &thumbSize); + + int32_t sbY = w->y + TEXTAREA_BORDER; + int32_t relMouse = mouseY - sbY - TEXTAREA_SB_W - dragOff; + int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0; + ta->scrollRow = clampInt(newScroll, 0, maxScroll); + } else if (orient == 1) { + // Horizontal scrollbar drag + int32_t maxHScroll = maxLL - visCols; + + if (maxHScroll <= 0) { + return; + } + + int32_t hsbW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_SB_W; + int32_t trackLen = hsbW - TEXTAREA_SB_W * 2; + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, maxLL, visCols, ta->scrollCol, &thumbPos, &thumbSize); + + int32_t sbX = w->x + TEXTAREA_BORDER; + int32_t relMouse = mouseX - sbX - TEXTAREA_SB_W - dragOff; + int32_t newScroll = (trackLen > thumbSize) ? (maxHScroll * relMouse) / (trackLen - thumbSize) : 0; + ta->scrollCol = clampInt(newScroll, 0, maxHScroll); + } +} + + +static bool widgetTextAreaClearSelection(WidgetT *w) { + TextAreaDataT *ta = (TextAreaDataT *)w->data; + + if (ta->selAnchor >= 0 && ta->selCursor >= 0 && ta->selAnchor != ta->selCursor) { + ta->selAnchor = -1; + ta->selCursor = -1; + return true; + } + + return false; +} + + +static void widgetTextAreaDragSelect(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { + (void)root; + TextAreaDataT *ta = (TextAreaDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + const BitmapFontT *font = &ctx->font; + + int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD; + int32_t innerY = w->y + TEXTAREA_BORDER; + int32_t relX = vx - innerX; + int32_t relY = vy - innerY; + + int32_t totalLines = textAreaGetLineCount(w); + int32_t visRows = (w->h - TEXTAREA_BORDER * 2) / font->charHeight; + + // Auto-scroll when dragging past edges + if (relY < 0 && ta->scrollRow > 0) { + ta->scrollRow--; + } else if (relY >= visRows * font->charHeight && ta->scrollRow < totalLines - visRows) { + ta->scrollRow++; + } + + int32_t clickRow = ta->scrollRow + relY / font->charHeight; + int32_t clickCol = ta->scrollCol + relX / font->charWidth; + + if (clickRow < 0) { + clickRow = 0; + } + + if (clickRow >= totalLines) { + clickRow = totalLines - 1; + } + + if (clickCol < 0) { + clickCol = 0; + } + + int32_t lineL = textAreaLineLen(ta->buf, ta->len, clickRow); + + if (clickCol > lineL) { + clickCol = lineL; + } + + ta->cursorRow = clickRow; + ta->cursorCol = clickCol; + ta->desiredCol = clickCol; + ta->selCursor = textAreaCursorToOff(ta->buf, ta->len, clickRow, clickCol); +} + + static const WidgetClassT sClassTextArea = { .flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE, .paint = widgetTextAreaPaint, @@ -2044,7 +2201,10 @@ static const WidgetClassT sClassTextArea = { .onKey = widgetTextAreaOnKey, .destroy = widgetTextAreaDestroy, .getText = widgetTextAreaGetText, - .setText = widgetTextAreaSetText + .setText = widgetTextAreaSetText, + .clearSelection = widgetTextAreaClearSelection, + .dragSelect = widgetTextAreaDragSelect, + .scrollDragUpdate = widgetTextAreaScrollDragUpdate }; // ============================================================ diff --git a/widgets/widgetTimer.c b/widgets/widgetTimer.c index 8dc099d..88c575a 100644 --- a/widgets/widgetTimer.c +++ b/widgets/widgetTimer.c @@ -202,29 +202,6 @@ void wgtUpdateTimers(void) { } -// ============================================================ -// wgtUpdateCursorBlink -// ============================================================ - -#define CURSOR_BLINK_MS 250 - -static clock_t sCursorBlinkTime = 0; - -void wgtUpdateCursorBlink(void) { - clock_t now = clock(); - clock_t interval = (clock_t)CURSOR_BLINK_MS * CLOCKS_PER_SEC / 1000; - - if ((now - sCursorBlinkTime) >= interval) { - sCursorBlinkTime = now; - sCursorBlinkOn = !sCursorBlinkOn; - - if (sFocusedWidget) { - wgtInvalidatePaint(sFocusedWidget); - } - } -} - - // ============================================================ // DXE registration // ============================================================ @@ -237,15 +214,13 @@ static const struct { void (*setInterval)(WidgetT *w, int32_t intervalMs); bool (*isRunning)(const WidgetT *w); void (*updateTimers)(void); - void (*updateCursorBlink)(void); } sApi = { .create = wgtTimer, .start = wgtTimerStart, .stop = wgtTimerStop, .setInterval = wgtTimerSetInterval, .isRunning = wgtTimerIsRunning, - .updateTimers = wgtUpdateTimers, - .updateCursorBlink = wgtUpdateCursorBlink + .updateTimers = wgtUpdateTimers }; void wgtRegister(void) { diff --git a/widgets/widgetTimer.h b/widgets/widgetTimer.h index a6d971a..cd76af8 100644 --- a/widgets/widgetTimer.h +++ b/widgets/widgetTimer.h @@ -11,7 +11,6 @@ typedef struct { void (*setInterval)(WidgetT *w, int32_t intervalMs); bool (*isRunning)(const WidgetT *w); void (*updateTimers)(void); - void (*updateCursorBlink)(void); } TimerApiT; static inline const TimerApiT *dvxTimerApi(void) { @@ -25,8 +24,6 @@ static inline const TimerApiT *dvxTimerApi(void) { #define wgtTimerStop(w) dvxTimerApi()->stop(w) #define wgtTimerSetInterval(w, intervalMs) dvxTimerApi()->setInterval(w, intervalMs) #define wgtTimerIsRunning(w) dvxTimerApi()->isRunning(w) - #define wgtUpdateTimers() do { const TimerApiT *_ta = dvxTimerApi(); if (_ta) { _ta->updateTimers(); } } while(0) -#define wgtUpdateCursorBlink() do { const TimerApiT *_ta = dvxTimerApi(); if (_ta) { _ta->updateCursorBlink(); } } while(0) #endif // WIDGET_TIMER_H diff --git a/widgets/widgetTreeView.c b/widgets/widgetTreeView.c index 2604f2d..04d8112 100644 --- a/widgets/widgetTreeView.c +++ b/widgets/widgetTreeView.c @@ -99,6 +99,7 @@ static void treeCalcScrollbarNeeds(WidgetT *w, const BitmapFontT *font, int32_t static WidgetT *treeItemAtY(WidgetT *parent, int32_t targetY, int32_t *curY, const BitmapFontT *font); static int32_t treeItemYPos(WidgetT *treeView, WidgetT *target, const BitmapFontT *font); static int32_t treeItemYPosHelper(WidgetT *parent, WidgetT *target, int32_t *curY, const BitmapFontT *font); +static void widgetTreeViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY); // ============================================================ // calcTreeItemsHeight @@ -1299,6 +1300,63 @@ void widgetTreeViewOnChildChanged(WidgetT *parent, WidgetT *child) { } +// ============================================================ +// widgetTreeViewScrollDragUpdate +// ============================================================ + +// Handle scrollbar thumb drag for vertical and horizontal scrollbars. +static void widgetTreeViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) { + TreeViewDataT *tv = (TreeViewDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + const BitmapFontT *font = &ctx->font; + + int32_t totalH; + int32_t totalW; + int32_t innerH; + int32_t innerW; + bool needVSb; + bool needHSb; + + treeCalcScrollbarNeeds(w, font, &totalH, &totalW, &innerH, &innerW, &needVSb, &needHSb); + + if (orient == 0) { + // Vertical scrollbar drag + int32_t maxScroll = totalH - innerH; + + if (maxScroll <= 0) { + return; + } + + int32_t trackLen = innerH - WGT_SB_W * 2; + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, totalH, innerH, tv->scrollPos, &thumbPos, &thumbSize); + + int32_t sbY = w->y + TREE_BORDER; + int32_t relMouse = mouseY - sbY - WGT_SB_W - dragOff; + int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0; + tv->scrollPos = clampInt(newScroll, 0, maxScroll); + } else if (orient == 1) { + // Horizontal scrollbar drag + int32_t maxScroll = totalW - innerW; + + if (maxScroll <= 0) { + return; + } + + int32_t trackLen = innerW - WGT_SB_W * 2; + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, totalW, innerW, tv->scrollPosH, &thumbPos, &thumbSize); + + int32_t sbX = w->x + TREE_BORDER; + int32_t relMouse = mouseX - sbX - WGT_SB_W - dragOff; + int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0; + tv->scrollPosH = clampInt(newScroll, 0, maxScroll); + } +} + + // ============================================================ // widgetTreeViewDestroy // ============================================================ @@ -1319,6 +1377,112 @@ static void widgetTreeItemDestroy(WidgetT *w) { } +static void widgetTreeViewReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { + (void)root; + (void)x; + TreeViewDataT *tv = (TreeViewDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + const BitmapFontT *font = &ctx->font; + + int32_t innerY = w->y + TREE_BORDER; + int32_t relY = y - innerY + tv->scrollPos; + + // Find which item the mouse is over + int32_t curY = 0; + WidgetT *target = treeItemAtY(w, relY, &curY, font); + + if (target) { + tv->dropTarget = target; + // Drop after if mouse is in the bottom half of the item + int32_t itemY = treeItemYPos(w, target, font); + tv->dropAfter = (relY > itemY + font->charHeight / 2); + } else { + // Below all items -- drop after the last item + tv->dropTarget = w->lastChild; + tv->dropAfter = true; + } + + // Auto-scroll when dragging near edges + int32_t innerH = w->h - TREE_BORDER * 2; + int32_t visRows = innerH / font->charHeight; + int32_t mouseRelY = y - innerY; + + if (mouseRelY < font->charHeight && tv->scrollPos > 0) { + tv->scrollPos--; + } else if (mouseRelY >= (visRows - 1) * font->charHeight) { + tv->scrollPos++; + } +} + + +static void widgetTreeViewReorderDrop(WidgetT *w) { + TreeViewDataT *tv = (TreeViewDataT *)w->data; + WidgetT *dragItem = tv->dragItem; + WidgetT *dropTarget = tv->dropTarget; + bool dropAfter = tv->dropAfter; + + tv->dragItem = NULL; + tv->dropTarget = NULL; + + if (!dragItem || !dropTarget || dragItem == dropTarget) { + return; + } + + // Remove dragged item from its current parent + WidgetT *oldParent = dragItem->parent; + + if (oldParent) { + widgetRemoveChild(oldParent, dragItem); + } + + // Insert at the drop position + WidgetT *newParent = dropTarget->parent; + + if (!newParent) { + return; + } + + // Re-insert: walk newParent's children to find dropTarget, insert before/after + dragItem->nextSibling = NULL; + dragItem->parent = newParent; + + if (!dropAfter) { + // Insert before dropTarget + WidgetT *prev = NULL; + + for (WidgetT *c = newParent->firstChild; c; c = c->nextSibling) { + if (c == dropTarget) { + dragItem->nextSibling = dropTarget; + + if (prev) { + prev->nextSibling = dragItem; + } else { + newParent->firstChild = dragItem; + } + + break; + } + + prev = c; + } + } else { + // Insert after dropTarget + dragItem->nextSibling = dropTarget->nextSibling; + dropTarget->nextSibling = dragItem; + + if (newParent->lastChild == dropTarget) { + newParent->lastChild = dragItem; + } + } + + tv->dimsValid = false; + + if (w->onChange) { + w->onChange(w); + } +} + + static const WidgetClassT sClassTreeView = { .flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLLABLE, .paint = widgetTreeViewPaint, @@ -1327,10 +1491,13 @@ static const WidgetClassT sClassTreeView = { .layout = widgetTreeViewLayout, .onMouse = widgetTreeViewOnMouse, .onKey = widgetTreeViewOnKey, - .destroy = widgetTreeViewDestroy, - .getText = NULL, - .setText = NULL, - .onChildChanged = widgetTreeViewOnChildChanged + .destroy = widgetTreeViewDestroy, + .getText = NULL, + .setText = NULL, + .onChildChanged = widgetTreeViewOnChildChanged, + .reorderDrop = widgetTreeViewReorderDrop, + .reorderUpdate = widgetTreeViewReorderUpdate, + .scrollDragUpdate = widgetTreeViewScrollDragUpdate }; static const WidgetClassT sClassTreeItem = {