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