First pass of major debugging. At a glance, everything is working again.

This commit is contained in:
Scott Duensing 2026-03-25 21:43:41 -05:00
parent 9ee73c8806
commit 67872c6b98
32 changed files with 1246 additions and 328 deletions

View file

@ -28,6 +28,7 @@
#include "widgetImageButton.h" #include "widgetImageButton.h"
#include "widgetLabel.h" #include "widgetLabel.h"
#include "widgetListBox.h" #include "widgetListBox.h"
#include "widgetListView.h"
#include "widgetProgressBar.h" #include "widgetProgressBar.h"
#include "widgetRadio.h" #include "widgetRadio.h"
#include "widgetScrollPane.h" #include "widgetScrollPane.h"

View file

@ -178,10 +178,13 @@ static void loadAndDisplay(const char *path) {
stbi_image_free(sImgRgb); stbi_image_free(sImgRgb);
sImgRgb = NULL; sImgRgb = NULL;
dvxSetBusy(sAc, true);
int32_t channels; int32_t channels;
sImgRgb = stbi_load(path, &sImgW, &sImgH, &channels, 3); sImgRgb = stbi_load(path, &sImgW, &sImgH, &channels, 3);
if (!sImgRgb) { if (!sImgRgb) {
dvxSetBusy(sAc, false);
dvxMessageBox(sAc, "Error", "Could not load image.", MB_OK | MB_ICONERROR); dvxMessageBox(sAc, "Error", "Could not load image.", MB_OK | MB_ICONERROR);
return; return;
} }
@ -202,6 +205,7 @@ static void loadAndDisplay(const char *path) {
// Scale and repaint // Scale and repaint
buildScaled(sWin->contentW, sWin->contentH); buildScaled(sWin->contentW, sWin->contentH);
dvxSetBusy(sAc, false);
RectT fullRect = {0, 0, sWin->contentW, sWin->contentH}; RectT fullRect = {0, 0, sWin->contentW, sWin->contentH};
sWin->onPaint(sWin, &fullRect); 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. // scaled image (or dark background) to avoid expensive per-frame scaling.
if (sImgRgb && sAc->stack.resizeWindow < 0) { if (sImgRgb && sAc->stack.resizeWindow < 0) {
if (sLastFitW != win->contentW || sLastFitH != win->contentH) { if (sLastFitW != win->contentW || sLastFitH != win->contentH) {
dvxSetBusy(sAc, true);
buildScaled(win->contentW, win->contentH); buildScaled(win->contentW, win->contentH);
dvxSetBusy(sAc, false);
} }
} }

View file

@ -313,15 +313,27 @@ static void onMenu(WindowT *win, int32_t menuId) {
break; break;
case CMD_CUT: case CMD_CUT:
if (sTextArea && sTextArea->wclass && sTextArea->wclass->onKey) {
sTextArea->wclass->onKey(sTextArea, 24, 0); // Ctrl+X
}
break; break;
case CMD_COPY: case CMD_COPY:
if (sTextArea && sTextArea->wclass && sTextArea->wclass->onKey) {
sTextArea->wclass->onKey(sTextArea, 3, 0); // Ctrl+C
}
break; break;
case CMD_PASTE: case CMD_PASTE:
if (sTextArea && sTextArea->wclass && sTextArea->wclass->onKey) {
sTextArea->wclass->onKey(sTextArea, 22, 0); // Ctrl+V
}
break; break;
case CMD_SELALL: case CMD_SELALL:
if (sTextArea && sTextArea->wclass && sTextArea->wclass->onKey) {
sTextArea->wclass->onKey(sTextArea, 1, 0); // Ctrl+A
}
break; break;
} }
} }

View file

@ -326,7 +326,7 @@ static void onPmMenu(WindowT *win, int32_t menuId) {
static void scanAppsDir(void) { static void scanAppsDir(void) {
sAppCount = 0; sAppCount = 0;
scanAppsDirRecurse("apps"); 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 (!dir) {
if (sAppCount == 0) { if (sAppCount == 0) {
shellLog("Progman: %s directory not found", dirPath); dvxLog("Progman: %s directory not found", dirPath);
} }
return; return;

View file

@ -18,3 +18,9 @@ bpp = 16
wheel = normal wheel = normal
doubleclick = 500 doubleclick = 500
acceleration = medium acceleration = medium
; Shell settings.
; desktop: path to the desktop app loaded at startup
[shell]
desktop = apps/progman/progman.app

View file

@ -136,6 +136,7 @@ static void updateTooltip(AppContextT *ctx);
// click callback fires. This gives the user visual feedback that the // click callback fires. This gives the user visual feedback that the
// keyboard activation was registered, matching Win3.x/Motif behavior. // keyboard activation was registered, matching Win3.x/Motif behavior.
WidgetT *sKeyPressedBtn = NULL; WidgetT *sKeyPressedBtn = NULL;
void (*sCursorBlinkFn)(void) = NULL;
// 4x4 Bayer dithering offsets for ordered dithering when converting // 4x4 Bayer dithering offsets for ordered dithering when converting
// 24-bit wallpaper images to 15/16-bit pixel formats. // 24-bit wallpaper images to 15/16-bit pixel formats.
@ -1040,6 +1041,11 @@ static void dispatchEvents(AppContextT *ctx) {
return; return;
} }
// Block all input while busy
if (ctx->busy) {
return;
}
// Handle left button press // Handle left button press
if ((buttons & MOUSE_LEFT) && !(prevBtn & MOUSE_LEFT)) { if ((buttons & MOUSE_LEFT) && !(prevBtn & MOUSE_LEFT)) {
handleMouseButton(ctx, mx, my, buttons); 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); WindowT *win = wmCreateWindow(&ctx->stack, &ctx->display, title, x, y, w, h, resizable);
if (win) { 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 // Raise and focus
int32_t idx = ctx->stack.count - 1; int32_t idx = ctx->stack.count - 1;
wmSetFocus(&ctx->stack, &ctx->dirty, idx); 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); strncpy(ctx->wallpaperPath, path, sizeof(ctx->wallpaperPath) - 1);
ctx->wallpaperPath[sizeof(ctx->wallpaperPath) - 1] = '\0'; ctx->wallpaperPath[sizeof(ctx->wallpaperPath) - 1] = '\0';
dvxSetBusy(ctx, true);
int32_t imgW; int32_t imgW;
int32_t imgH; int32_t imgH;
int32_t channels; int32_t channels;
uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3); uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3);
if (!rgb) { if (!rgb) {
dvxSetBusy(ctx, false);
return false; return false;
} }
@ -2314,6 +2328,7 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) {
ctx->wallpaperBuf = buildWallpaperBuf(ctx, rgb, imgW, imgH, ctx->wallpaperMode); ctx->wallpaperBuf = buildWallpaperBuf(ctx, rgb, imgW, imgH, ctx->wallpaperMode);
ctx->wallpaperPitch = pitch; ctx->wallpaperPitch = pitch;
dvxSetBusy(ctx, false);
stbi_image_free(rgb); 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 // dvxRun
// ============================================================ // ============================================================
@ -2583,7 +2632,10 @@ bool dvxUpdate(AppContextT *ctx) {
dispatchEvents(ctx); dispatchEvents(ctx);
updateTooltip(ctx); updateTooltip(ctx);
pollWidgets(ctx); pollWidgets(ctx);
wgtUpdateCursorBlink(); if (sCursorBlinkFn) {
sCursorBlinkFn();
}
wgtUpdateTimers(); wgtUpdateTimers();
ctx->frameCount++; 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 // Check for click on minimized icon, but only if no window covers the point.
int32_t iconIdx = wmMinimizedIconHit(&ctx->stack, &ctx->display, mx, my); // 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) { if (iconIdx >= 0) {
WindowT *iconWin = ctx->stack.windows[iconIdx]; 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 // 7. Default arrow cursor
static void updateCursorShape(AppContextT *ctx) { 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 newCursor = CURSOR_ARROW;
int32_t mx = ctx->mouseX; int32_t mx = ctx->mouseX;
int32_t my = ctx->mouseY; int32_t my = ctx->mouseY;
@ -4569,9 +4639,17 @@ static void updateTooltip(AppContextT *ctx) {
return; 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; 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) { if (iconIdx >= 0) {
tipText = ctx->stack.windows[iconIdx]->title; tipText = ctx->stack.windows[iconIdx]->title;

View file

@ -42,7 +42,7 @@ typedef struct AppContextT {
PopupStateT popup; PopupStateT popup;
SysMenuStateT sysMenu; SysMenuStateT sysMenu;
KbMoveResizeT kbMoveResize; KbMoveResizeT kbMoveResize;
CursorT cursors[5]; // indexed by CURSOR_xxx CursorT cursors[6]; // indexed by CURSOR_xxx
int32_t cursorId; // active cursor shape int32_t cursorId; // active cursor shape
uint32_t cursorFg; // pre-packed cursor colors uint32_t cursorFg; // pre-packed cursor colors
uint32_t cursorBg; uint32_t cursorBg;
@ -82,6 +82,8 @@ typedef struct AppContextT {
void *ctrlEscCtx; void *ctrlEscCtx;
void (*onTitleChange)(void *ctx); // called when any window title changes void (*onTitleChange)(void *ctx); // called when any window title changes
void *titleChangeCtx; 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 // Tooltip state -- tooltip appears after the mouse hovers over a widget
// with a tooltip string for a brief delay. Pre-computing W/H avoids // with a tooltip string for a brief delay. Pre-computing W/H avoids
// re-measuring on every paint frame. // re-measuring on every paint frame.
@ -223,6 +225,9 @@ void dvxMaximizeWindow(AppContextT *ctx, WindowT *win);
// Request exit from main loop // Request exit from main loop
void dvxQuit(AppContextT *ctx); 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 // Save the entire screen (backbuffer contents) to a PNG file. Converts
// from native pixel format to RGB for the PNG encoder. Returns 0 on // from native pixel format to RGB for the PNG encoder. Returns 0 on
// success, -1 on failure. // success, -1 on failure.

View file

@ -28,7 +28,8 @@
#define CURSOR_RESIZE_V 2 // up/down (vertical) #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_NWSE 3 // NW-SE diagonal (top-left / bottom-right)
#define CURSOR_RESIZE_DIAG_NESW 4 // NE-SW diagonal (top-right / bottom-left) #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 // Standard arrow cursor, 16x16
@ -305,6 +306,54 @@ static const uint16_t cursorResizeDiagNESWXor[16] = {
0x0000 // 0000000000000000 row 15 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 // 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, cursorResizeVAnd, cursorResizeVXor }, // CURSOR_RESIZE_V
{ 16, 16, 7, 7, cursorResizeDiagNWSEAnd, cursorResizeDiagNWSEXor }, // CURSOR_RESIZE_DIAG_NWSE { 16, 16, 7, 7, cursorResizeDiagNWSEAnd, cursorResizeDiagNWSEXor }, // CURSOR_RESIZE_DIAG_NWSE
{ 16, 16, 7, 7, cursorResizeDiagNESWAnd, cursorResizeDiagNESWXor }, // CURSOR_RESIZE_DIAG_NESW { 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 // Legacy alias -- kept for backward compatibility with code that predates

View file

@ -49,6 +49,7 @@ extern const WidgetClassT **widgetClassTable;
#define CURSOR_RESIZE_V 2 #define CURSOR_RESIZE_V 2
#define CURSOR_RESIZE_DIAG_NWSE 3 #define CURSOR_RESIZE_DIAG_NWSE 3
#define CURSOR_RESIZE_DIAG_NESW 4 #define CURSOR_RESIZE_DIAG_NESW 4
#define CURSOR_BUSY 5
// ============================================================ // ============================================================
// Keyboard modifier flags // Keyboard modifier flags
@ -112,6 +113,7 @@ extern WidgetT *sDragReorder;
extern WidgetT *sDragScrollbar; extern WidgetT *sDragScrollbar;
extern int32_t sDragScrollbarOff; extern int32_t sDragScrollbarOff;
extern int32_t sDragScrollbarOrient; extern int32_t sDragScrollbarOrient;
extern void (*sCursorBlinkFn)(void);
// ============================================================ // ============================================================
// Core widget functions (widgetCore.c) // Core widget functions (widgetCore.c)

View file

@ -39,6 +39,13 @@ typedef struct {
int32_t scancode; // PC scan code (0x48=Up, 0x50=Down, etc.) int32_t scancode; // PC scan code (0x48=Up, 0x50=Down, etc.)
} PlatformKeyEventT; } PlatformKeyEventT;
// ============================================================
// Logging
// ============================================================
// Append a line to dvx.log. Lives in dvx.exe, exported to all modules.
void dvxLog(const char *fmt, ...);
// ============================================================ // ============================================================
// System lifecycle // System lifecycle
// ============================================================ // ============================================================

View file

@ -54,6 +54,7 @@
#include <sys/exceptn.h> #include <sys/exceptn.h>
#include <sys/nearptr.h> #include <sys/nearptr.h>
#include <sys/farptr.h> #include <sys/farptr.h>
#include <sys/movedata.h>
// VESA mode scoring weights: higher score = preferred mode // VESA mode scoring weights: higher score = preferred mode
#define MODE_SCORE_16BPP 100 // 16bpp: fastest span fill (half the bytes of 32bpp) #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 bool sHasMouseWheel = false;
static int32_t sLastWheelDelta = 0; static int32_t sLastWheelDelta = 0;
// Software cursor tracking. Many real-hardware mouse drivers fail to // Software cursor tracking. Two modes detected at runtime:
// honour INT 33h functions 07h/08h (set coordinate range) in VESA modes // Mickey mode (default): function 0Bh raw deltas accumulated into
// because they don't recognise non-standard video modes. We bypass the // sCurX/sCurY. Works on 86Box and real hardware where function
// driver's position entirely: platformMousePoll reads raw mickey deltas // 03h coordinates may be wrong in VESA modes.
// via function 0Bh, accumulates them into sCurX/sCurY, and clamps to // Absolute mode: function 03h coordinates used directly. Auto-
// the screen bounds. Function 03h is still used for button state. // activated when function 0Bh returns zero deltas but function
static int32_t sMouseRangeW = 0; // 03h position is changing (DOSBox-X seamless mouse).
static int32_t sMouseRangeH = 0; static bool sAbsMouseMode = false;
static int32_t sCurX = 0; static bool sDetecting = true;
static int32_t sCurY = 0; 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). // 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. // INT 16h returns these scan codes with ascii=0 for Alt+key combos.
@ -764,8 +770,17 @@ const char *platformGetSystemInfo(const DisplayT *display) {
// ---- CPU ---- // ---- CPU ----
sysInfoAppend("=== 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()) { if (!hasCpuid()) {
sysInfoAppend("Processor: 386/486 (no CPUID support)"); sysInfoAppend("(no CPUID support)");
} else { } else {
// Vendor string (CPUID leaf 0) // Vendor string (CPUID leaf 0)
uint32_t maxFunc; uint32_t maxFunc;
@ -943,7 +958,6 @@ const char *platformGetSystemInfo(const DisplayT *display) {
(ver.flags & 0x01) ? "32-bit " : "16-bit ", (ver.flags & 0x01) ? "32-bit " : "16-bit ",
(ver.flags & 0x02) ? "V86 " : "", (ver.flags & 0x02) ? "V86 " : "",
(ver.flags & 0x04) ? "VirtMem " : ""); (ver.flags & 0x04) ? "VirtMem " : "");
sysInfoAppend("CPU Type: %d86", ver.cpu);
} }
const char *tmpDir = getenv("TEMP"); 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 // software cursor on top of the backbuffer. We only use INT 33h
// for position/button state via polling (function 03h). // for position/button state via polling (function 03h).
void platformMouseInit(int32_t screenW, int32_t screenH) { void platformMouseInit(int32_t screenW, int32_t screenH) {
__dpmi_regs r; __dpmi_regs r;
@ -1288,8 +1304,30 @@ void platformMouseInit(int32_t screenW, int32_t screenH) {
r.x.ax = 0x0000; r.x.ax = 0x0000;
__dpmi_int(0x33, &r); __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. // 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)); memset(&r, 0, sizeof(r));
r.x.ax = 0x000B; r.x.ax = 0x000B;
__dpmi_int(0x33, &r); __dpmi_int(0x33, &r);
@ -1329,7 +1367,8 @@ void platformMouseSetAccel(int32_t threshold) {
void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) { void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) {
__dpmi_regs r; __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)); memset(&r, 0, sizeof(r));
r.x.ax = 0x0003; r.x.ax = 0x0003;
__dpmi_int(0x33, &r); __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); sLastWheelDelta = (int32_t)(int8_t)(r.x.bx >> 8);
} }
// Function 0Bh: read mickey motion counters (signed 16-bit deltas, int32_t absX = (int16_t)r.x.cx;
// cleared on read). Accumulate into software cursor position. int32_t absY = (int16_t)r.x.dx;
// Function 0Bh: read mickey motion counters
memset(&r, 0, sizeof(r)); memset(&r, 0, sizeof(r));
r.x.ax = 0x000B; r.x.ax = 0x000B;
__dpmi_int(0x33, &r); __dpmi_int(0x33, &r);
sCurX += (int16_t)r.x.cx; int16_t mickeyDx = (int16_t)r.x.cx;
sCurY += (int16_t)r.x.dx; 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) { if (sCurX < 0) {
sCurX = 0; sCurX = 0;
@ -1419,8 +1489,16 @@ void platformMouseWarp(int32_t x, int32_t y) {
sCurX = x; sCurX = x;
sCurY = y; sCurY = y;
// Flush any pending mickeys so the next poll doesn't undo the warp
__dpmi_regs r; __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)); memset(&r, 0, sizeof(r));
r.x.ax = 0x000B; r.x.ax = 0x000B;
__dpmi_int(0x33, &r); __dpmi_int(0x33, &r);
@ -2061,6 +2139,9 @@ extern unsigned char __dj_ctype_tolower[];
extern void *__emutls_get_address(void *); extern void *__emutls_get_address(void *);
// stb_ds internals (implementation compiled into dvx.exe via loaderMain.c) // 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_arrgrowf(void *a, size_t elemsize, size_t addlen, size_t min_cap);
extern void stbds_arrfreef(void *a); extern void stbds_arrfreef(void *a);
extern void stbds_hmfree_func(void *a, size_t elemsize); extern void stbds_hmfree_func(void *a, size_t elemsize);
@ -2115,6 +2196,9 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformVideoShutdown) DXE_EXPORT(platformVideoShutdown)
DXE_EXPORT(platformYield) DXE_EXPORT(platformYield)
// --- dvx logging (lives in dvx.exe, used by all modules) ---
DXE_EXPORT(dvxLog)
// --- memory --- // --- memory ---
DXE_EXPORT(calloc) DXE_EXPORT(calloc)
DXE_EXPORT(free) DXE_EXPORT(free)

View file

@ -119,6 +119,13 @@ int32_t clipboardMaxLen(void) {
int32_t multiClickDetect(int32_t vx, int32_t vy) { int32_t multiClickDetect(int32_t vx, int32_t vy) {
clock_t now = clock(); 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 && if ((now - sLastClickTime) < sDblClickTicks &&
abs(vx - sLastClickX) < 4 && abs(vy - sLastClickY) < 4) { abs(vx - sLastClickX) < 4 && abs(vy - sLastClickY) < 4) {
sClickCount++; sClickCount++;

View file

@ -33,10 +33,13 @@
#define LOG_PATH "dvx.log" #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"); FILE *f = fopen(LOG_PATH, "a");
if (!f) { if (!f) {
@ -269,24 +272,21 @@ static void loadInOrder(ModuleT *mods) {
continue; continue;
} }
loaderLog("Loading: %s", mods[i].path); dvxLog("Loading: %s", mods[i].path);
mods[i].handle = dlopen(mods[i].path, RTLD_GLOBAL); mods[i].handle = dlopen(mods[i].path, RTLD_GLOBAL);
if (!mods[i].handle) { if (!mods[i].handle) {
const char *err = dlerror(); const char *err = dlerror();
loaderLog(" FAILED: %s", err ? err : "(unknown)"); dvxLog(" FAILED: %s", err ? err : "(unknown)");
mods[i].loaded = true; mods[i].loaded = true;
loaded++; loaded++;
progress = true; progress = true;
continue; continue;
} }
loaderLog(" OK");
RegFnT regFn = (RegFnT)dlsym(mods[i].handle, "_wgtRegister"); RegFnT regFn = (RegFnT)dlsym(mods[i].handle, "_wgtRegister");
if (regFn) { if (regFn) {
loaderLog(" Calling wgtRegister()");
regFn(); regFn();
} }
@ -315,20 +315,20 @@ static void loadInOrder(ModuleT *mods) {
// Returns a stb_ds dynamic array of dlopen handles. // Returns a stb_ds dynamic array of dlopen handles.
static void logAndReadDeps(ModuleT *mods) { 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++) { 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++) { for (int32_t i = 0; i < arrlen(mods); i++) {
readDeps(&mods[i]); readDeps(&mods[i]);
if (arrlen(mods[i].deps) > 0) { 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++) { 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); 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 // Register platform + libc/libm/runtime symbols for DXE resolution
platformRegisterDxeExports(); platformRegisterDxeExports();
loaderLog("Platform exports registered."); dvxLog("Platform exports registered.");
// Load all modules from libs/ and widgets/ in dependency order. // Load all modules from libs/ and widgets/ in dependency order.
// Each module may have a .dep file specifying load-before deps. // 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"); ShellMainFnT shellMain = (ShellMainFnT)findSymbol(handles, "_shellMain");
if (!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--) { for (int32_t i = arrlen(handles) - 1; i >= 0; i--) {
dlclose(handles[i]); dlclose(handles[i]);

View file

@ -13,7 +13,7 @@ CONFIGDIR = ../bin/config
THEMEDIR = ../bin/config/themes THEMEDIR = ../bin/config/themes
WPAPERDIR = ../bin/config/wpaper 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)) OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
TARGET = $(LIBSDIR)/dvxshell.lib TARGET = $(LIBSDIR)/dvxshell.lib

View file

@ -1,7 +1,7 @@
// shellApp.c -- DVX Shell application loading, lifecycle, and reaping // shellApp.c -- DVX Shell application loading, lifecycle, and reaping
// //
// Manages DXE app loading via dlopen/dlsym, resource tracking through // 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 "shellApp.h"
#include "dvxDialog.h" #include "dvxDialog.h"
@ -24,7 +24,6 @@
// New slots are appended as needed; freed slots are recycled. // New slots are appended as needed; freed slots are recycled.
static ShellAppT *sApps = NULL; static ShellAppT *sApps = NULL;
static int32_t sAppsCap = 0; // number of slots allocated (including slot 0) static int32_t sAppsCap = 0; // number of slots allocated (including slot 0)
int32_t sCurrentAppId = 0;
// ============================================================ // ============================================================
// Prototypes // Prototypes
@ -70,7 +69,7 @@ static int32_t allocSlot(void) {
// Task entry point for main-loop apps. Runs in its own cooperative task. // 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 // GUI resources created during the app's lifetime are tagged with the
// correct owner. When the app's main loop returns (normal exit), 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. // 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) { static void appTaskWrapper(void *arg) {
ShellAppT *app = (ShellAppT *)arg; ShellAppT *app = (ShellAppT *)arg;
sCurrentAppId = app->appId; app->dxeCtx.shellCtx->currentAppId = app->appId;
app->entryFn(&app->dxeCtx); app->entryFn(&app->dxeCtx);
sCurrentAppId = 0; app->dxeCtx.shellCtx->currentAppId = 0;
// App returned from its main loop -- mark for reaping // App returned from its main loop -- mark for reaping
app->state = AppStateTerminatingE; app->state = AppStateTerminatingE;
@ -115,14 +114,14 @@ static int32_t copyFile(const char *src, const char *dst) {
FILE *in = fopen(src, "rb"); FILE *in = fopen(src, "rb");
if (!in) { 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; return -1;
} }
FILE *out = fopen(dst, "wb"); FILE *out = fopen(dst, "wb");
if (!out) { 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); fclose(in);
return -1; 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) { while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
if (fwrite(buf, 1, n, out) != n) { 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(in);
fclose(out); fclose(out);
remove(dst); remove(dst);
@ -259,7 +258,7 @@ void shellForceKillApp(AppContextT *ctx, ShellAppT *app) {
cleanupTempFile(app); cleanupTempFile(app);
app->state = AppStateFreeE; 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); snprintf(app->dxeCtx.configDir, sizeof(app->dxeCtx.configDir), "CONFIG/%s", desc->name);
} }
// Launch. Set sCurrentAppId before any app code runs so that window // Launch. Set currentAppId before any app code runs so that
// creation wrappers stamp the correct owner. Reset to 0 afterward so // dvxCreateWindow stamps the correct owner on new windows.
// shell-initiated operations (e.g., message boxes) aren't misattributed. ctx->currentAppId = id;
sCurrentAppId = id;
if (desc->hasMainLoop) { if (desc->hasMainLoop) {
uint32_t stackSize = desc->stackSize > 0 ? (uint32_t)desc->stackSize : TS_DEFAULT_STACK_SIZE; 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); int32_t taskId = tsCreate(desc->name, appTaskWrapper, app, stackSize, priority);
if (taskId < 0) { if (taskId < 0) {
sCurrentAppId = 0; ctx->currentAppId = 0;
dvxMessageBox(ctx, "Error", "Failed to create task for application.", MB_OK | MB_ICONERROR); dvxMessageBox(ctx, "Error", "Failed to create task for application.", MB_OK | MB_ICONERROR);
dlclose(handle); dlclose(handle);
app->state = AppStateFreeE; app->state = AppStateFreeE;
@ -457,10 +455,10 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
app->entryFn(&app->dxeCtx); app->entryFn(&app->dxeCtx);
} }
sCurrentAppId = 0; ctx->currentAppId = 0;
app->state = AppStateRunningE; 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(); shellDesktopUpdate();
return id; return id;
} }
@ -469,7 +467,7 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
// Graceful reap -- called from shellReapApps when an app has reached // Graceful reap -- called from shellReapApps when an app has reached
// the Terminating state. Unlike forceKill, this calls the app's // the Terminating state. Unlike forceKill, this calls the app's
// shutdown hook (if provided) giving it a chance to save state, close // 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. // any final resource operations are attributed correctly.
void shellReapApp(AppContextT *ctx, ShellAppT *app) { void shellReapApp(AppContextT *ctx, ShellAppT *app) {
if (!app || app->state == AppStateFreeE) { if (!app || app->state == AppStateFreeE) {
@ -478,9 +476,10 @@ void shellReapApp(AppContextT *ctx, ShellAppT *app) {
// Call shutdown hook if present // Call shutdown hook if present
if (app->shutdownFn) { if (app->shutdownFn) {
sCurrentAppId = app->appId; ctx->currentAppId = app->appId;
app->shutdownFn(); app->shutdownFn();
sCurrentAppId = 0; ctx->currentAppId = 0;
ctx->currentAppId = 0;
} }
// Destroy all windows belonging to this app // Destroy all windows belonging to this app
@ -504,7 +503,7 @@ void shellReapApp(AppContextT *ctx, ShellAppT *app) {
} }
cleanupTempFile(app); cleanupTempFile(app);
shellLog("Shell: reaped app '%s'", app->name); dvxLog("Shell: reaped app '%s'", app->name);
app->state = AppStateFreeE; app->state = AppStateFreeE;
} }

View file

@ -100,14 +100,6 @@ typedef struct {
// Shell global state // 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 // App lifecycle API
@ -150,13 +142,6 @@ int32_t shellAppSlotCount(void);
// Count running apps (not counting the shell itself) // Count running apps (not counting the shell itself)
int32_t shellRunningAppCount(void); 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). // Ensure an app's config directory exists (creates all parent dirs).
// Returns 0 on success, -1 on failure. // Returns 0 on success, -1 on failure.
int32_t shellEnsureConfigDir(const DxeAppContextT *ctx); int32_t shellEnsureConfigDir(const DxeAppContextT *ctx);
@ -166,12 +151,6 @@ int32_t shellEnsureConfigDir(const DxeAppContextT *ctx);
// -> "CONFIG/PROGMAN/settings.ini" // -> "CONFIG/PROGMAN/settings.ini"
void shellConfigPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize); 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 // Desktop callback

View file

@ -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);
}

View file

@ -34,7 +34,7 @@ void shellInfoInit(AppContextT *ctx) {
sCachedInfo = platformGetSystemInfo(&ctx->display); sCachedInfo = platformGetSystemInfo(&ctx->display);
// Log each line individually so the log file is readable // Log each line individually so the log file is readable
shellLog("=== System Information ==="); dvxLog("=== System Information ===");
const char *line = sCachedInfo; const char *line = sCachedInfo;
@ -42,7 +42,7 @@ void shellInfoInit(AppContextT *ctx) {
const char *eol = strchr(line, '\n'); const char *eol = strchr(line, '\n');
if (!eol) { if (!eol) {
shellLog("%s", line); dvxLog("%s", line);
break; break;
} }
@ -55,9 +55,9 @@ void shellInfoInit(AppContextT *ctx) {
memcpy(tmp, line, len); memcpy(tmp, line, len);
tmp[len] = '\0'; tmp[len] = '\0';
shellLog("%s", tmp); dvxLog("%s", tmp);
line = eol + 1; line = eol + 1;
} }
shellLog("=== End System Information ==="); dvxLog("=== End System Information ===");
} }

View file

@ -50,7 +50,6 @@ static jmp_buf sCrashJmp;
// Volatile because it's written from a signal handler context. Tells // Volatile because it's written from a signal handler context. Tells
// the recovery code which signal fired (for logging/diagnostics). // the recovery code which signal fired (for logging/diagnostics).
static volatile int sCrashSignal = 0; static volatile int sCrashSignal = 0;
static const char *sLogPath = NULL;
// Desktop update callback list (dynamic, managed via stb_ds arrput/arrdel) // Desktop update callback list (dynamic, managed via stb_ds arrput/arrdel)
typedef void (*DesktopUpdateFnT)(void); typedef void (*DesktopUpdateFnT)(void);
static DesktopUpdateFnT *sDesktopUpdateFns = NULL; 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) { static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData) {
(void)userData; (void)userData;
shellLog(" %ldx%ld %ldbpp", (long)w, (long)h, (long)bpp); dvxLog(" %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);
} }
@ -173,12 +147,7 @@ int shellMain(int argc, char *argv[]) {
(void)argc; (void)argc;
(void)argv; (void)argv;
// The loader already truncates and writes boot info to dvx.log. dvxLog("DVX Shell starting...");
// 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...");
// Load preferences (missing file or keys silently use defaults) // Load preferences (missing file or keys silently use defaults)
prefsLoad("CONFIG/DVX.INI"); prefsLoad("CONFIG/DVX.INI");
@ -186,7 +155,7 @@ int shellMain(int argc, char *argv[]) {
int32_t videoW = prefsGetInt("video", "width", 640); int32_t videoW = prefsGetInt("video", "width", 640);
int32_t videoH = prefsGetInt("video", "height", 480); int32_t videoH = prefsGetInt("video", "height", 480);
int32_t videoBpp = prefsGetInt("video", "bpp", 16); 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 // Initialize GUI
int32_t result = dvxInit(&sCtx, videoW, videoH, videoBpp); int32_t result = dvxInit(&sCtx, videoW, videoH, videoBpp);
@ -213,7 +182,7 @@ int shellMain(int argc, char *argv[]) {
} }
dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal); 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 // Apply saved color scheme from INI
bool colorsLoaded = false; bool colorsLoaded = false;
@ -237,7 +206,7 @@ int shellMain(int argc, char *argv[]) {
if (colorsLoaded) { if (colorsLoaded) {
dvxApplyColorScheme(&sCtx); dvxApplyColorScheme(&sCtx);
shellLog("Preferences: loaded custom color scheme"); dvxLog("Preferences: loaded custom color scheme");
} }
// Apply saved wallpaper mode and image // Apply saved wallpaper mode and image
@ -255,25 +224,25 @@ int shellMain(int argc, char *argv[]) {
if (wpPath) { if (wpPath) {
if (dvxSetWallpaper(&sCtx, wpPath)) { if (dvxSetWallpaper(&sCtx, wpPath)) {
shellLog("Preferences: loaded wallpaper %s (%s)", wpPath, wpMode); dvxLog("Preferences: loaded wallpaper %s (%s)", wpPath, wpMode);
} else { } else {
shellLog("Preferences: failed to load wallpaper %s", wpPath); dvxLog("Preferences: failed to load wallpaper %s", wpPath);
} }
} }
} }
if (result != 0) { 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; return 1;
} }
shellLog("Available video modes:"); dvxLog("Available video modes:");
platformVideoEnumModes(logVideoMode, NULL); 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 // Initialize task system
if (tsInit() != TS_OK) { if (tsInit() != TS_OK) {
shellLog("Failed to initialize task system"); dvxLog("Failed to initialize task system");
dvxShutdown(&sCtx); dvxShutdown(&sCtx);
return 1; return 1;
} }
@ -287,9 +256,6 @@ int shellMain(int argc, char *argv[]) {
// Gather system information (CPU, memory, drives, etc.) // Gather system information (CPU, memory, drives, etc.)
shellInfoInit(&sCtx); shellInfoInit(&sCtx);
// Register DXE export table
shellExportInit();
// Initialize app slot table // Initialize app slot table
shellAppInit(); shellAppInit();
@ -309,7 +275,7 @@ int shellMain(int argc, char *argv[]) {
int32_t desktopId = shellLoadApp(&sCtx, desktopApp); int32_t desktopId = shellLoadApp(&sCtx, desktopApp);
if (desktopId < 0) { if (desktopId < 0) {
shellLog("Failed to load desktop app '%s'", desktopApp); dvxLog("Failed to load desktop app '%s'", desktopApp);
tsShutdown(); tsShutdown();
dvxShutdown(&sCtx); dvxShutdown(&sCtx);
return 1; return 1;
@ -319,9 +285,9 @@ int shellMain(int argc, char *argv[]) {
// initialization itself crashes, we want the default behavior // initialization itself crashes, we want the default behavior
// (abort with register dump) rather than our recovery path, because // (abort with register dump) rather than our recovery path, because
// the system isn't in a recoverable state yet. // 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 // Set recovery point for crash handler. setjmp returns 0 on initial
// call (falls through to the main loop). On a crash, longjmp makes // 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. // Platform handler already logged signal name and register dump.
// Log app-specific info here. // Log app-specific info here.
shellLog("Current app ID: %ld", (long)sCurrentAppId); dvxLog("Current app ID: %ld", (long)sCtx.currentAppId);
if (sCurrentAppId > 0) { if (sCtx.currentAppId > 0) {
ShellAppT *crashedApp = shellGetApp(sCurrentAppId); ShellAppT *crashedApp = shellGetApp(sCtx.currentAppId);
if (crashedApp) { if (crashedApp) {
shellLog("App name: %s", crashedApp->name); dvxLog("App name: %s", crashedApp->name);
shellLog("App path: %s", crashedApp->path); dvxLog("App path: %s", crashedApp->path);
shellLog("Has main loop: %s", crashedApp->hasMainLoop ? "yes" : "no"); dvxLog("Has main loop: %s", crashedApp->hasMainLoop ? "yes" : "no");
shellLog("Task ID: %lu", (unsigned long)crashedApp->mainTaskId); dvxLog("Task ID: %lu", (unsigned long)crashedApp->mainTaskId);
} }
} else { } 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) { if (sCtx.currentAppId > 0) {
ShellAppT *app = shellGetApp(sCurrentAppId); ShellAppT *app = shellGetApp(sCtx.currentAppId);
if (app) { if (app) {
char msg[256]; char msg[256];
snprintf(msg, sizeof(msg), "'%s' has caused a fault and will be terminated.", app->name); snprintf(msg, sizeof(msg), "'%s' has caused a fault and will be terminated.", app->name);
shellForceKillApp(&sCtx, app); shellForceKillApp(&sCtx, app);
sCurrentAppId = 0; sCtx.currentAppId = 0;
sCtx.currentAppId = 0;
dvxMessageBox(&sCtx, "Application Error", msg, MB_OK | MB_ICONERROR); dvxMessageBox(&sCtx, "Application Error", msg, MB_OK | MB_ICONERROR);
} }
} }
sCurrentAppId = 0; sCtx.currentAppId = 0;
sCtx.currentAppId = 0;
sCrashSignal = 0; sCrashSignal = 0;
shellDesktopUpdate(); 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 // Clean shutdown: terminate all apps first (destroys windows, kills
// tasks, closes DXE handles), then tear down the task system and GUI // tasks, closes DXE handles), then tear down the task system and GUI
@ -403,6 +371,6 @@ int shellMain(int argc, char *argv[]) {
dvxShutdown(&sCtx); dvxShutdown(&sCtx);
prefsFree(); prefsFree();
shellLog("DVX Shell exited."); dvxLog("DVX Shell exited.");
return 0; return 0;
} }

View file

@ -14,6 +14,10 @@
// Static state // 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 // 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. // clear it in O(1) instead of walking every widget in every window.
static WidgetT *sLastSelectedWidget = NULL; 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); 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 // clearOtherSelections
// ============================================================ // ============================================================

View file

@ -12,6 +12,12 @@
#define TEXT_INPUT_PAD 3 #define TEXT_INPUT_PAD 3
// ============================================================
// Cursor blink
// ============================================================
void wgtUpdateCursorBlink(void);
// ============================================================ // ============================================================
// Selection management // Selection management
// ============================================================ // ============================================================

View file

@ -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 = { static const WidgetClassT sClassAnsiTerm = {
.flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE | WCLASS_NEEDS_POLL | WCLASS_SWALLOWS_TAB, .flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE | WCLASS_NEEDS_POLL | WCLASS_SWALLOWS_TAB,
.paint = widgetAnsiTermPaint, .paint = widgetAnsiTermPaint,
@ -919,6 +970,8 @@ static const WidgetClassT sClassAnsiTerm = {
.destroy = widgetAnsiTermDestroy, .destroy = widgetAnsiTermDestroy,
.getText = NULL, .getText = NULL,
.setText = NULL, .setText = NULL,
.clearSelection = widgetAnsiTermClearSelection,
.dragSelect = widgetAnsiTermDragSelect,
.poll = widgetAnsiTermPollVtable, .poll = widgetAnsiTermPollVtable,
.quickRepaint = wgtAnsiTermRepaint .quickRepaint = wgtAnsiTermRepaint
}; };

View file

@ -202,19 +202,57 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
w->focused = true; w->focused = true;
ComboBoxDataT *d = (ComboBoxDataT *)w->data; 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 // Check if click is on the button area
int32_t textAreaW = w->w - DROPDOWN_BTN_WIDTH; int32_t textAreaW = w->w - DROPDOWN_BTN_WIDTH;
if (vx >= w->x + textAreaW) { if (vx >= w->x + textAreaW) {
// If this combobox's popup was just closed by click-outside, don't re-open
if (w == sClosedPopup) { if (w == sClosedPopup) {
return; return;
} }
// Button click -- toggle popup d->open = true;
d->open = !d->open;
d->hoverIdx = d->selectedIdx; d->hoverIdx = d->selectedIdx;
sOpenPopup = d->open ? w : NULL; sOpenPopup = w;
} else { } else {
// Text area click -- focus for editing // Text area click -- focus for editing
clearOtherSelections(w); 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 = { static const WidgetClassT sClassComboBox = {
.flags = WCLASS_FOCUSABLE | WCLASS_HAS_POPUP | WCLASS_SCROLLABLE, .flags = WCLASS_FOCUSABLE | WCLASS_HAS_POPUP | WCLASS_SCROLLABLE,
.paint = widgetComboBoxPaint, .paint = widgetComboBoxPaint,
@ -387,6 +450,8 @@ static const WidgetClassT sClassComboBox = {
.openPopup = widgetComboBoxOpenPopup, .openPopup = widgetComboBoxOpenPopup,
.closePopup = widgetComboBoxClosePopup, .closePopup = widgetComboBoxClosePopup,
.getPopupItemCount = widgetComboBoxGetPopupItemCount, .getPopupItemCount = widgetComboBoxGetPopupItemCount,
.clearSelection = widgetComboBoxClearSelection,
.dragSelect = widgetComboBoxDragSelect,
.getPopupRect = widgetComboBoxGetPopupRect .getPopupRect = widgetComboBoxGetPopupRect
}; };

View file

@ -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. // check. The sClosedPopup reference is cleared on the next event cycle.
void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)root; (void)root;
(void)vx;
(void)vy;
w->focused = true; 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 this dropdown's popup was just closed by click-outside, don't re-open
if (w == sClosedPopup) { if (w == sClosedPopup) {
return; return;
} }
DropdownDataT *d = (DropdownDataT *)w->data; d->open = true;
d->open = !d->open;
d->hoverIdx = d->selectedIdx; d->hoverIdx = d->selectedIdx;
sOpenPopup = d->open ? w : NULL; sOpenPopup = w;
} }

View file

@ -62,6 +62,7 @@ typedef struct {
static void ensureScrollVisible(WidgetT *w, int32_t idx); static void ensureScrollVisible(WidgetT *w, int32_t idx);
static void selectRange(WidgetT *w, int32_t from, int32_t to); 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->selectedIdx = newSel;
d->dragIdx = -1;
d->dropIdx = -1;
// Update selection // Update selection
if (multi && d->selBits) { 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 // 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 = { static const WidgetClassT sClassListBox = {
.flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE, .flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE,
.paint = widgetListBoxPaint, .paint = widgetListBoxPaint,
@ -482,9 +594,12 @@ static const WidgetClassT sClassListBox = {
.layout = NULL, .layout = NULL,
.onMouse = widgetListBoxOnMouse, .onMouse = widgetListBoxOnMouse,
.onKey = widgetListBoxOnKey, .onKey = widgetListBoxOnKey,
.destroy = widgetListBoxDestroy, .destroy = widgetListBoxDestroy,
.getText = NULL, .getText = NULL,
.setText = NULL .setText = NULL,
.reorderDrop = widgetListBoxReorderDrop,
.reorderUpdate = widgetListBoxReorderUpdate,
.scrollDragUpdate = widgetListBoxScrollDragUpdate
}; };
// ============================================================ // ============================================================

View file

@ -1279,8 +1279,6 @@ void wgtListViewSetSort(WidgetT *w, int32_t col, ListViewSortE dir) {
// the initial click position. This prevents accidental resizes from stray // the initial click position. This prevents accidental resizes from stray
// clicks on column borders. // clicks on column borders.
static void widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) { 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; ListViewDataT *lv = (ListViewDataT *)w->data;
// orient == -1 means column resize drag // 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; lv->resolvedColW[lv->resizeCol] = newW;
int32_t total = 0; int32_t total = 0;
for (int32_t c = 0; c < lv->colCount; c++) { for (int32_t c = 0; c < lv->colCount; c++) {
total += lv->resolvedColW[c]; total += lv->resolvedColW[c];
} }
lv->totalColW = total; lv->totalColW = total;
} }
return; 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 = { static const WidgetClassT sClassListView = {
.flags = WCLASS_FOCUSABLE | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLLABLE, .flags = WCLASS_FOCUSABLE | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLLABLE,
.paint = widgetListViewPaint, .paint = widgetListViewPaint,
@ -1343,6 +1506,8 @@ static const WidgetClassT sClassListView = {
.getText = NULL, .getText = NULL,
.setText = NULL, .setText = NULL,
.getCursorShape = widgetListViewGetCursorShape, .getCursorShape = widgetListViewGetCursorShape,
.reorderDrop = widgetListViewReorderDrop,
.reorderUpdate = widgetListViewReorderUpdate,
.scrollDragUpdate = widgetListViewScrollDragUpdate .scrollDragUpdate = widgetListViewScrollDragUpdate
}; };

View file

@ -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 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 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 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 // widgetScrollPanePaint
// ============================================================ // ============================================================
@ -711,16 +770,17 @@ void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
// ============================================================ // ============================================================
static const WidgetClassT sClassScrollPane = { static const WidgetClassT sClassScrollPane = {
.flags = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLL_CONTAINER | WCLASS_RELAYOUT_ON_SCROLL, .flags = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLL_CONTAINER | WCLASS_RELAYOUT_ON_SCROLL,
.paint = widgetScrollPanePaint, .paint = widgetScrollPanePaint,
.paintOverlay = NULL, .paintOverlay = NULL,
.calcMinSize = widgetScrollPaneCalcMinSize, .calcMinSize = widgetScrollPaneCalcMinSize,
.layout = widgetScrollPaneLayout, .layout = widgetScrollPaneLayout,
.onMouse = widgetScrollPaneOnMouse, .onMouse = widgetScrollPaneOnMouse,
.onKey = widgetScrollPaneOnKey, .onKey = widgetScrollPaneOnKey,
.destroy = widgetScrollPaneDestroy, .destroy = widgetScrollPaneDestroy,
.getText = NULL, .getText = NULL,
.setText = NULL .setText = NULL,
.scrollDragUpdate = widgetScrollPaneScrollDragUpdate
}; };

View file

@ -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; return 0;
} }

View file

@ -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 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 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 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 wordBoundaryLeft(const char *buf, int32_t pos);
static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos); static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos);
WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen); 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 = { static const WidgetClassT sClassTextInput = {
.flags = WCLASS_FOCUSABLE, .flags = WCLASS_FOCUSABLE,
.paint = widgetTextInputPaint, .paint = widgetTextInputPaint,
@ -2031,9 +2056,141 @@ static const WidgetClassT sClassTextInput = {
.destroy = widgetTextInputDestroy, .destroy = widgetTextInputDestroy,
.getText = widgetTextInputGetText, .getText = widgetTextInputGetText,
.setText = widgetTextInputSetText, .setText = widgetTextInputSetText,
.clearSelection = widgetTextInputClearSelection,
.dragSelect = widgetTextInputDragSelect,
.getTextFieldWidth = widgetTextInputGetFieldWidth .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 = { static const WidgetClassT sClassTextArea = {
.flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE, .flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE,
.paint = widgetTextAreaPaint, .paint = widgetTextAreaPaint,
@ -2044,7 +2201,10 @@ static const WidgetClassT sClassTextArea = {
.onKey = widgetTextAreaOnKey, .onKey = widgetTextAreaOnKey,
.destroy = widgetTextAreaDestroy, .destroy = widgetTextAreaDestroy,
.getText = widgetTextAreaGetText, .getText = widgetTextAreaGetText,
.setText = widgetTextAreaSetText .setText = widgetTextAreaSetText,
.clearSelection = widgetTextAreaClearSelection,
.dragSelect = widgetTextAreaDragSelect,
.scrollDragUpdate = widgetTextAreaScrollDragUpdate
}; };
// ============================================================ // ============================================================

View file

@ -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 // DXE registration
// ============================================================ // ============================================================
@ -237,15 +214,13 @@ static const struct {
void (*setInterval)(WidgetT *w, int32_t intervalMs); void (*setInterval)(WidgetT *w, int32_t intervalMs);
bool (*isRunning)(const WidgetT *w); bool (*isRunning)(const WidgetT *w);
void (*updateTimers)(void); void (*updateTimers)(void);
void (*updateCursorBlink)(void);
} sApi = { } sApi = {
.create = wgtTimer, .create = wgtTimer,
.start = wgtTimerStart, .start = wgtTimerStart,
.stop = wgtTimerStop, .stop = wgtTimerStop,
.setInterval = wgtTimerSetInterval, .setInterval = wgtTimerSetInterval,
.isRunning = wgtTimerIsRunning, .isRunning = wgtTimerIsRunning,
.updateTimers = wgtUpdateTimers, .updateTimers = wgtUpdateTimers
.updateCursorBlink = wgtUpdateCursorBlink
}; };
void wgtRegister(void) { void wgtRegister(void) {

View file

@ -11,7 +11,6 @@ typedef struct {
void (*setInterval)(WidgetT *w, int32_t intervalMs); void (*setInterval)(WidgetT *w, int32_t intervalMs);
bool (*isRunning)(const WidgetT *w); bool (*isRunning)(const WidgetT *w);
void (*updateTimers)(void); void (*updateTimers)(void);
void (*updateCursorBlink)(void);
} TimerApiT; } TimerApiT;
static inline const TimerApiT *dvxTimerApi(void) { static inline const TimerApiT *dvxTimerApi(void) {
@ -25,8 +24,6 @@ static inline const TimerApiT *dvxTimerApi(void) {
#define wgtTimerStop(w) dvxTimerApi()->stop(w) #define wgtTimerStop(w) dvxTimerApi()->stop(w)
#define wgtTimerSetInterval(w, intervalMs) dvxTimerApi()->setInterval(w, intervalMs) #define wgtTimerSetInterval(w, intervalMs) dvxTimerApi()->setInterval(w, intervalMs)
#define wgtTimerIsRunning(w) dvxTimerApi()->isRunning(w) #define wgtTimerIsRunning(w) dvxTimerApi()->isRunning(w)
#define wgtUpdateTimers() do { const TimerApiT *_ta = dvxTimerApi(); if (_ta) { _ta->updateTimers(); } } while(0) #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 #endif // WIDGET_TIMER_H

View file

@ -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 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 treeItemYPos(WidgetT *treeView, WidgetT *target, const BitmapFontT *font);
static int32_t treeItemYPosHelper(WidgetT *parent, WidgetT *target, int32_t *curY, 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 // 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 // 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 = { static const WidgetClassT sClassTreeView = {
.flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLLABLE, .flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLLABLE,
.paint = widgetTreeViewPaint, .paint = widgetTreeViewPaint,
@ -1327,10 +1491,13 @@ static const WidgetClassT sClassTreeView = {
.layout = widgetTreeViewLayout, .layout = widgetTreeViewLayout,
.onMouse = widgetTreeViewOnMouse, .onMouse = widgetTreeViewOnMouse,
.onKey = widgetTreeViewOnKey, .onKey = widgetTreeViewOnKey,
.destroy = widgetTreeViewDestroy, .destroy = widgetTreeViewDestroy,
.getText = NULL, .getText = NULL,
.setText = NULL, .setText = NULL,
.onChildChanged = widgetTreeViewOnChildChanged .onChildChanged = widgetTreeViewOnChildChanged,
.reorderDrop = widgetTreeViewReorderDrop,
.reorderUpdate = widgetTreeViewReorderUpdate,
.scrollDragUpdate = widgetTreeViewScrollDragUpdate
}; };
static const WidgetClassT sClassTreeItem = { static const WidgetClassT sClassTreeItem = {