Many many bugs fixed. We have a control panel!

This commit is contained in:
Scott Duensing 2026-03-19 22:35:00 -05:00
parent fc9fc46c79
commit 70459616cc
30 changed files with 1494 additions and 218 deletions

2
.gitattributes vendored
View file

@ -1,4 +1,6 @@
*.bmp filter=lfs diff=lfs merge=lfs -text
*.BMP filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.JPG filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.ZIP filter=lfs diff=lfs merge=lfs -text

2
.gitignore vendored
View file

@ -4,5 +4,5 @@ obj/
lib/
*.~
.gitignore~
DVX_GUI_DESIGN.md
.gitattributes~
*.SWP

View file

@ -25,4 +25,4 @@ clean:
$(MAKE) -C apps clean
-rmdir obj/dvx/widgets obj/dvx/platform obj/dvx/thirdparty obj/dvx obj/tasks obj/dvxshell obj/apps obj 2>/dev/null
-rm -rf bin/config
-rmdir bin/apps/ctrlpanel bin/apps/progman bin/apps/notepad bin/apps/clock bin/apps/dvxdemo bin/apps bin lib 2>/dev/null
-rmdir bin/apps/cpanel bin/apps/imgview bin/apps/progman bin/apps/notepad bin/apps/clock bin/apps/dvxdemo bin/apps bin lib 2>/dev/null

View file

@ -10,19 +10,23 @@ OBJDIR = ../obj/apps
BINDIR = ../bin/apps
# App definitions: each is a subdir with a single .c file
APPS = progman notepad clock dvxdemo ctrlpanel
APPS = progman notepad clock dvxdemo cpanel imgview
.PHONY: all clean $(APPS)
all: $(APPS)
ctrlpanel: $(BINDIR)/ctrlpanel/ctrlpanel.app
cpanel: $(BINDIR)/cpanel/cpanel.app
imgview: $(BINDIR)/imgview/imgview.app
progman: $(BINDIR)/progman/progman.app
notepad: $(BINDIR)/notepad/notepad.app
clock: $(BINDIR)/clock/clock.app
dvxdemo: $(BINDIR)/dvxdemo/dvxdemo.app
$(BINDIR)/ctrlpanel/ctrlpanel.app: $(OBJDIR)/ctrlpanel.o | $(BINDIR)/ctrlpanel
$(BINDIR)/cpanel/cpanel.app: $(OBJDIR)/cpanel.o | $(BINDIR)/cpanel
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
$(BINDIR)/imgview/imgview.app: $(OBJDIR)/imgview.o | $(BINDIR)/imgview
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
$(BINDIR)/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/progman
@ -40,7 +44,10 @@ $(BINDIR)/dvxdemo/dvxdemo.app: $(OBJDIR)/dvxdemo.o $(addprefix dvxdemo/,$(DVXDEM
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/dvxdemo/
$(OBJDIR)/ctrlpanel.o: ctrlpanel/ctrlpanel.c | $(OBJDIR)
$(OBJDIR)/cpanel.o: cpanel/cpanel.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/imgview.o: imgview/imgview.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/progman.o: progman/progman.c | $(OBJDIR)
@ -58,8 +65,11 @@ $(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c | $(OBJDIR)
$(OBJDIR):
mkdir -p $(OBJDIR)
$(BINDIR)/ctrlpanel:
mkdir -p $(BINDIR)/ctrlpanel
$(BINDIR)/cpanel:
mkdir -p $(BINDIR)/cpanel
$(BINDIR)/imgview:
mkdir -p $(BINDIR)/imgview
$(BINDIR)/progman:
mkdir -p $(BINDIR)/progman
@ -74,15 +84,17 @@ $(BINDIR)/dvxdemo:
mkdir -p $(BINDIR)/dvxdemo
# Dependencies
$(OBJDIR)/ctrlpanel.o: ctrlpanel/ctrlpanel.c ../dvx/dvxApp.h ../dvx/dvxDialog.h ../dvx/dvxPrefs.h ../dvx/dvxWidget.h ../dvx/dvxWm.h ../dvx/platform/dvxPlatform.h ../dvxshell/shellApp.h
$(OBJDIR)/imgview.o: imgview/imgview.c ../dvx/dvxApp.h ../dvx/dvxDialog.h ../dvx/dvxWidget.h ../dvx/dvxWm.h ../dvx/dvxVideo.h ../dvxshell/shellApp.h
$(OBJDIR)/cpanel.o: cpanel/cpanel.c ../dvx/dvxApp.h ../dvx/dvxDialog.h ../dvx/dvxPrefs.h ../dvx/dvxWidget.h ../dvx/dvxWm.h ../dvx/platform/dvxPlatform.h ../dvxshell/shellApp.h
$(OBJDIR)/progman.o: progman/progman.c ../dvx/dvxApp.h ../dvx/dvxDialog.h ../dvx/dvxWidget.h ../dvx/dvxWm.h ../dvxshell/shellApp.h ../dvxshell/shellInfo.h
$(OBJDIR)/notepad.o: notepad/notepad.c ../dvx/dvxApp.h ../dvx/dvxDialog.h ../dvx/dvxWidget.h ../dvx/dvxWm.h ../dvxshell/shellApp.h
$(OBJDIR)/clock.o: clock/clock.c ../dvx/dvxApp.h ../dvx/dvxWidget.h ../dvx/dvxDraw.h ../dvx/dvxVideo.h ../dvxshell/shellApp.h ../tasks/taskswitch.h
$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c ../dvx/dvxApp.h ../dvx/dvxDialog.h ../dvx/dvxWidget.h ../dvx/dvxWm.h ../dvx/dvxVideo.h ../dvxshell/shellApp.h
clean:
rm -f $(OBJDIR)/ctrlpanel.o $(OBJDIR)/progman.o $(OBJDIR)/notepad.o $(OBJDIR)/clock.o $(OBJDIR)/dvxdemo.o
rm -f $(BINDIR)/ctrlpanel/ctrlpanel.app
rm -f $(OBJDIR)/cpanel.o $(OBJDIR)/imgview.o $(OBJDIR)/progman.o $(OBJDIR)/notepad.o $(OBJDIR)/clock.o $(OBJDIR)/dvxdemo.o
rm -f $(BINDIR)/cpanel/cpanel.app
rm -f $(BINDIR)/imgview/imgview.app
rm -f $(BINDIR)/progman/progman.app
rm -f $(BINDIR)/notepad/notepad.app
rm -f $(BINDIR)/clock/clock.app

View file

@ -16,6 +16,7 @@
#include "dvxWm.h"
#include "platform/dvxPlatform.h"
#include "shellApp.h"
#include "thirdparty/stb_ds.h"
#include <dirent.h>
#include <stdio.h>
@ -30,8 +31,7 @@
#define CP_WIN_W 460
#define CP_WIN_H 340
#define MAX_VIDEO_MODES 64
#define MAX_THEME_FILES 32
#define WPAPER_DIR "CONFIG/WPAPER"
#define THEME_DIR "CONFIG/THEMES"
#define THEME_EXT ".THM"
@ -48,15 +48,13 @@ AppDescriptorT appDescriptor = {
};
// ============================================================
// Video mode entry
// Entry types for dynamic lists
// ============================================================
typedef struct {
int32_t w;
int32_t h;
int32_t bpp;
char label[32];
} VideoModeEntryT;
char name[64];
char path[260];
} FileEntryT;
// ============================================================
// Module state
@ -81,6 +79,7 @@ static WidgetT *sWheelDrop = NULL;
static WidgetT *sDblClickSldr = NULL;
static WidgetT *sDblClickLbl = NULL;
static WidgetT *sAccelDrop = NULL;
static WidgetT *sDblTestLbl = NULL;
// Colors tab widgets
static WidgetT *sColorList = NULL;
@ -90,22 +89,24 @@ static WidgetT *sBlueSldr = NULL;
static WidgetT *sRedLbl = NULL;
static WidgetT *sGreenLbl = NULL;
static WidgetT *sBlueLbl = NULL;
static WidgetT *sColorSwatch = NULL;
// Desktop tab widgets
static WidgetT *sWallpaperLbl = NULL;
static WidgetT *sWpaperList = NULL;
static char sWallpaperPath[260];
static FileEntryT *sWpaperEntries = NULL; // stb_ds dynamic array
static const char **sWpaperLabels = NULL; // stb_ds dynamic array
// Video tab
static VideoModeEntryT sVideoModes[MAX_VIDEO_MODES];
static const char *sVideoLabels[MAX_VIDEO_MODES];
static int32_t sVideoModeCount = 0;
static const VideoModeInfoT *sVideoModes = NULL; // points into ctx, not owned
static int32_t sVideoCount = 0;
static const char **sVideoLabels = NULL; // stb_ds dynamic array
static WidgetT *sVideoList = NULL;
// Theme list
static char sThemeNames[MAX_THEME_FILES][64];
static const char *sThemeLabels[MAX_THEME_FILES];
static char sThemePaths[MAX_THEME_FILES][260];
static int32_t sThemeCount = 0;
static FileEntryT *sThemeEntries = NULL; // stb_ds dynamic array
static const char **sThemeLabels = NULL; // stb_ds dynamic array
static WidgetT *sThemeList = NULL;
// ============================================================
@ -117,12 +118,13 @@ static void buildMouseTab(WidgetT *page);
static void buildColorsTab(WidgetT *page);
static void buildDesktopTab(WidgetT *page);
static void buildVideoTab(WidgetT *page);
static void enumVideoModesCb(int32_t w, int32_t h, int32_t bpp, void *userData);
static int32_t mapAccelName(const char *name);
static const char *mapAccelValue(int32_t val);
static void onAccelChange(WidgetT *w);
static void onApplyTheme(WidgetT *w);
static void onBrowseTheme(WidgetT *w);
static void onResetColors(WidgetT *w);
static void onApplyWallpaper(WidgetT *w);
static void onCancel(WidgetT *w);
static void onChooseWallpaper(WidgetT *w);
static void onClearWallpaper(WidgetT *w);
@ -130,15 +132,18 @@ static void onClose(WindowT *win);
static void onColorSelect(WidgetT *w);
static void onColorSlider(WidgetT *w);
static void onDblClickSlider(WidgetT *w);
static void onDblClickTest(WidgetT *w);
static void onOk(WidgetT *w);
static void onSaveTheme(WidgetT *w);
static void onVideoApply(WidgetT *w);
static void onWheelChange(WidgetT *w);
static void saveSnapshot(void);
static void restoreSnapshot(void);
static void scanWallpapers(void);
static void scanThemes(void);
static void updateColorSliders(void);
static void updateDblClickLabel(void);
static void updateSwatch(void);
// ============================================================
@ -180,15 +185,19 @@ static void buildColorsTab(WidgetT *page) {
scanThemes();
sThemeList = wgtDropdown(themeRow);
sThemeList->weight = 100;
wgtDropdownSetItems(sThemeList, sThemeLabels, sThemeCount);
wgtDropdownSetItems(sThemeList, sThemeLabels, arrlen(sThemeEntries));
WidgetT *loadBtn = wgtButton(themeRow, "Apply");
loadBtn->onClick = onApplyTheme;
loadBtn->prefW = wgtPixels(60);
WidgetT *saveBtn = wgtButton(themeRow, "Save...");
WidgetT *browseBtn = wgtButton(themeRow, "Load...");
browseBtn->onClick = onBrowseTheme;
browseBtn->prefW = wgtPixels(60);
WidgetT *saveBtn = wgtButton(themeRow, "Save As...");
saveBtn->onClick = onSaveTheme;
saveBtn->prefW = wgtPixels(60);
saveBtn->prefW = wgtPixels(70);
WidgetT *resetBtn = wgtButton(themeRow, "Reset");
resetBtn->onClick = onResetColors;
@ -214,6 +223,9 @@ static void buildColorsTab(WidgetT *page) {
sBlueSldr->onChange = onColorSlider;
sBlueLbl = wgtLabel(rightVbox, "0");
wgtLabel(rightVbox, "Preview:");
sColorSwatch = wgtCanvas(rightVbox, 64, 24);
updateColorSliders();
}
@ -225,11 +237,21 @@ static void buildColorsTab(WidgetT *page) {
static void buildDesktopTab(WidgetT *page) {
wgtLabel(page, "Wallpaper:");
scanWallpapers();
sWpaperList = wgtListBox(page);
sWpaperList->weight = 100;
sWpaperList->onDblClick = onApplyWallpaper;
wgtListBoxSetItems(sWpaperList, sWpaperLabels, arrlen(sWpaperEntries));
sWallpaperLbl = wgtLabel(page, sWallpaperPath[0] ? sWallpaperPath : "(none)");
WidgetT *btnRow = wgtHBox(page);
btnRow->spacing = wgtPixels(8);
WidgetT *applyBtn = wgtButton(btnRow, "Apply");
applyBtn->onClick = onApplyWallpaper;
applyBtn->prefW = wgtPixels(90);
WidgetT *chooseBtn = wgtButton(btnRow, "Browse...");
chooseBtn->onClick = onChooseWallpaper;
chooseBtn->prefW = wgtPixels(90);
@ -245,6 +267,8 @@ static void buildDesktopTab(WidgetT *page) {
// ============================================================
static void buildMouseTab(WidgetT *page) {
page->spacing = wgtPixels(6);
// Scroll direction
WidgetT *wheelRow = wgtHBox(page);
wheelRow->spacing = wgtPixels(8);
@ -257,6 +281,8 @@ static void buildMouseTab(WidgetT *page) {
wgtDropdownSetItems(sWheelDrop, wheelItems, 2);
wgtDropdownSetSelected(sWheelDrop, sAc->wheelDirection < 0 ? 1 : 0);
wgtSpacer(page)->prefH = wgtPixels(4);
// Double-click speed
wgtLabel(page, "Double-Click Speed:");
@ -270,11 +296,13 @@ static void buildMouseTab(WidgetT *page) {
int32_t dblMs = prefsGetInt("mouse", "doubleclick", 500);
wgtSliderSetValue(sDblClickSldr, dblMs);
wgtLabel(dblRow, "Slow");
sDblClickLbl = wgtLabel(page, "");
wgtLabel(dblRow, "Slow ");
sDblClickLbl = wgtLabel(dblRow, "");
sDblClickLbl->prefW = wgtPixels(52);
updateDblClickLabel();
wgtSpacer(page)->prefH = wgtPixels(4);
// Acceleration
WidgetT *accelRow = wgtHBox(page);
accelRow->spacing = wgtPixels(8);
@ -297,6 +325,18 @@ static void buildMouseTab(WidgetT *page) {
} else {
wgtDropdownSetSelected(sAccelDrop, 2);
}
wgtSpacer(page)->prefH = wgtPixels(4);
// Double-click test area
WidgetT *testRow = wgtHBox(page);
testRow->spacing = wgtPixels(8);
WidgetT *testBtn = wgtButton(testRow, "Test Area");
testBtn->onDblClick = onDblClickTest;
testBtn->prefW = wgtPixels(100);
sDblTestLbl = wgtLabel(testRow, "Double-click the button to test");
}
@ -307,15 +347,39 @@ static void buildMouseTab(WidgetT *page) {
static void buildVideoTab(WidgetT *page) {
wgtLabel(page, "Available Video Modes:");
sVideoModeCount = 0;
platformVideoEnumModes(enumVideoModesCb, NULL);
// Read modes enumerated at init time
sVideoModes = dvxGetVideoModes(sAc, &sVideoCount);
// Build label strings
arrsetlen(sVideoLabels, 0);
static char (*labelBufs)[48] = NULL;
arrfree(labelBufs);
labelBufs = NULL;
arrsetlen(labelBufs, sVideoCount);
for (int32_t i = 0; i < sVideoCount; i++) {
const char *depthName;
switch (sVideoModes[i].bpp) {
case 8: depthName = "256 colors"; break;
case 15: depthName = "32 thousand colors"; break;
case 16: depthName = "65 thousand colors"; break;
case 24: depthName = "16 million colors"; break;
case 32: depthName = "16 million colors+"; break;
default: depthName = ""; break;
}
snprintf(labelBufs[i], 48, "%ldx%ld %s", (long)sVideoModes[i].w, (long)sVideoModes[i].h, depthName);
arrput(sVideoLabels, (const char *)labelBufs[i]);
}
sVideoList = wgtListBox(page);
sVideoList->weight = 100;
wgtListBoxSetItems(sVideoList, sVideoLabels, sVideoModeCount);
sVideoList->onDblClick = onVideoApply;
wgtListBoxSetItems(sVideoList, sVideoLabels, sVideoCount);
// Select the current mode
for (int32_t i = 0; i < sVideoModeCount; i++) {
for (int32_t i = 0; i < sVideoCount; i++) {
if (sVideoModes[i].w == sAc->display.width &&
sVideoModes[i].h == sAc->display.height &&
sVideoModes[i].bpp == sAc->display.format.bitsPerPixel) {
@ -330,26 +394,6 @@ static void buildVideoTab(WidgetT *page) {
}
// ============================================================
// enumVideoModesCb
// ============================================================
static void enumVideoModesCb(int32_t w, int32_t h, int32_t bpp, void *userData) {
(void)userData;
if (sVideoModeCount >= MAX_VIDEO_MODES) {
return;
}
VideoModeEntryT *m = &sVideoModes[sVideoModeCount];
m->w = w;
m->h = h;
m->bpp = bpp;
snprintf(m->label, sizeof(m->label), "%ldx%ld %ldbpp", (long)w, (long)h, (long)bpp);
sVideoLabels[sVideoModeCount] = m->label;
sVideoModeCount++;
}
// ============================================================
// mapAccelName / mapAccelValue
@ -405,6 +449,17 @@ static void onDblClickSlider(WidgetT *w) {
}
static void onDblClickTest(WidgetT *w) {
(void)w;
static int32_t count = 0;
count++;
static char buf[48];
snprintf(buf, sizeof(buf), "Double-click detected! (%ld)", (long)count);
wgtSetText(sDblTestLbl, buf);
}
static void onAccelChange(WidgetT *w) {
int32_t dir = (wgtDropdownGetSelected(sWheelDrop) == 1) ? -1 : 1;
int32_t dbl = wgtSliderGetValue(sDblClickSldr);
@ -451,6 +506,7 @@ static void onColorSlider(WidgetT *w) {
wgtSetText(sBlueLbl, bBuf);
dvxSetColor(sAc, (ColorIdE)sel, r, g, b);
updateSwatch();
}
@ -459,15 +515,31 @@ static void onApplyTheme(WidgetT *w) {
int32_t sel = wgtDropdownGetSelected(sThemeList);
if (sel < 0 || sel >= sThemeCount) {
if (sel < 0 || sel >= arrlen(sThemeEntries)) {
return;
}
dvxLoadTheme(sAc, sThemePaths[sel]);
dvxLoadTheme(sAc, sThemeEntries[sel].path);
updateColorSliders();
}
static void onBrowseTheme(WidgetT *w) {
(void)w;
FileFilterT filters[] = {
{ "Theme Files (*.thm)", "*.thm" },
{ "All Files (*.*)", "*.*" }
};
char path[260];
if (dvxFileDialog(sAc, "Load Theme", FD_OPEN, THEME_DIR, filters, 2, path, sizeof(path))) {
dvxLoadTheme(sAc, path);
updateColorSliders();
}
}
static void onSaveTheme(WidgetT *w) {
(void)w;
@ -480,7 +552,7 @@ static void onSaveTheme(WidgetT *w) {
if (dvxFileDialog(sAc, "Save Theme", FD_SAVE, THEME_DIR, filters, 2, path, sizeof(path))) {
dvxSaveTheme(sAc, path);
scanThemes();
wgtDropdownSetItems(sThemeList, sThemeLabels, sThemeCount);
wgtDropdownSetItems(sThemeList, sThemeLabels, arrlen(sThemeEntries));
}
}
@ -496,16 +568,34 @@ static void onResetColors(WidgetT *w) {
// Callbacks — Desktop tab
// ============================================================
static void onApplyWallpaper(WidgetT *w) {
(void)w;
int32_t sel = wgtListBoxGetSelected(sWpaperList);
if (sel < 0 || sel >= arrlen(sWpaperEntries)) {
return;
}
if (dvxSetWallpaper(sAc, sWpaperEntries[sel].path)) {
snprintf(sWallpaperPath, sizeof(sWallpaperPath), "%s", sWpaperEntries[sel].path);
wgtSetText(sWallpaperLbl, sWallpaperPath);
} else {
dvxMessageBox(sAc, "Error", "Could not load wallpaper image.", MB_OK | MB_ICONERROR);
}
}
static void onChooseWallpaper(WidgetT *w) {
(void)w;
FileFilterT filters[] = {
{ "Images (*.bmp;*.png;*.jpg)", "*.bmp" },
{ "Images (*.bmp;*.jpg;*.png)", "*.bmp;*.jpg;*.png" },
{ "All Files (*.*)", "*.*" }
};
char path[260];
if (dvxFileDialog(sAc, "Choose Wallpaper", FD_OPEN, NULL, filters, 2, path, sizeof(path))) {
if (dvxFileDialog(sAc, "Choose Wallpaper", FD_OPEN, "CONFIG/WPAPER", filters, 2, path, sizeof(path))) {
if (dvxSetWallpaper(sAc, path)) {
strncpy(sWallpaperPath, path, sizeof(sWallpaperPath) - 1);
sWallpaperPath[sizeof(sWallpaperPath) - 1] = '\0';
@ -529,26 +619,126 @@ static void onClearWallpaper(WidgetT *w) {
// Callbacks — Video tab
// ============================================================
static int32_t sVideoConfirmResult = -1;
static void onVideoConfirmYes(WidgetT *w) {
(void)w;
sVideoConfirmResult = 1;
}
static void onVideoConfirmNo(WidgetT *w) {
(void)w;
sVideoConfirmResult = 0;
}
static void onVideoConfirmClose(WindowT *win) {
(void)win;
sVideoConfirmResult = 0;
}
static void onVideoApply(WidgetT *w) {
(void)w;
int32_t sel = wgtListBoxGetSelected(sVideoList);
// Guard against re-entrancy: the confirmation dialog runs a nested
// dvxUpdate loop which can process pending events that call us again.
static bool sInProgress = false;
if (sel < 0 || sel >= sVideoModeCount) {
if (sInProgress) {
return;
}
VideoModeEntryT *m = &sVideoModes[sel];
sInProgress = true;
if (m->w == sAc->display.width &&
m->h == sAc->display.height &&
m->bpp == sAc->display.format.bitsPerPixel) {
int32_t sel = wgtListBoxGetSelected(sVideoList);
if (sel < 0 || sel >= sVideoCount) {
sInProgress = false;
return;
}
const VideoModeInfoT *m = &sVideoModes[sel];
int32_t oldW = sAc->display.width;
int32_t oldH = sAc->display.height;
int32_t oldBpp = sAc->display.format.bitsPerPixel;
if (m->w == oldW && m->h == oldH && m->bpp == oldBpp) {
sInProgress = false;
return;
}
if (dvxChangeVideoMode(sAc, m->w, m->h, m->bpp) != 0) {
dvxMessageBox(sAc, "Error", "Failed to change video mode.", MB_OK | MB_ICONERROR);
sInProgress = false;
return;
}
// Confirmation with 10-second auto-revert countdown. Run a nested
// event loop that updates the dialog title each second. If the user
// clicks Yes the mode is kept; No or timeout reverts to the old mode.
#define CONFIRM_SECONDS 10
WindowT *confirmWin = dvxCreateWindowCentered(sAc, "Keep this mode?", 300, 100, false);
if (!confirmWin) {
dvxChangeVideoMode(sAc, oldW, oldH, oldBpp);
sInProgress = false;
return;
}
confirmWin->modal = true;
confirmWin->onClose = onVideoConfirmClose;
sAc->modalWindow = confirmWin;
WidgetT *root = wgtInitWindow(sAc, confirmWin);
root->spacing = wgtPixels(8);
static char msgBuf[80];
snprintf(msgBuf, sizeof(msgBuf), "Reverting in %d seconds...", CONFIRM_SECONDS);
WidgetT *msgLbl = wgtLabel(root, msgBuf);
WidgetT *btnRow = wgtHBox(root);
btnRow->align = AlignEndE;
btnRow->spacing = wgtPixels(8);
WidgetT *yesBtn = wgtButton(btnRow, "Yes");
yesBtn->prefW = wgtPixels(70);
yesBtn->onClick = onVideoConfirmYes;
WidgetT *noBtn = wgtButton(btnRow, "No");
noBtn->prefW = wgtPixels(70);
noBtn->onClick = onVideoConfirmNo;
dvxFitWindow(sAc, confirmWin);
sVideoConfirmResult = -1;
clock_t lastSecond = clock();
int32_t secondsLeft = CONFIRM_SECONDS;
while (sAc->running && secondsLeft > 0 && sVideoConfirmResult < 0) {
dvxUpdate(sAc);
clock_t now = clock();
if ((now - lastSecond) >= CLOCKS_PER_SEC) {
lastSecond = now;
secondsLeft--;
snprintf(msgBuf, sizeof(msgBuf), "Reverting in %ld seconds...", (long)secondsLeft);
wgtSetText(msgLbl, msgBuf);
}
}
sAc->modalWindow = NULL;
dvxDestroyWindow(sAc, confirmWin);
if (sVideoConfirmResult != 1) {
dvxChangeVideoMode(sAc, oldW, oldH, oldBpp);
}
sInProgress = false;
}
@ -666,9 +856,9 @@ static void restoreSnapshot(void) {
// ============================================================
static void scanThemes(void) {
sThemeCount = 0;
arrsetlen(sThemeEntries, 0);
arrsetlen(sThemeLabels, 0);
// Scan THEME_DIR for .THM files
DIR *dir = opendir(THEME_DIR);
if (!dir) {
@ -677,34 +867,76 @@ static void scanThemes(void) {
struct dirent *ent;
while ((ent = readdir(dir)) != NULL && sThemeCount < MAX_THEME_FILES) {
while ((ent = readdir(dir)) != NULL) {
char *dot = strrchr(ent->d_name, '.');
if (!dot || strcasecmp(dot, THEME_EXT) != 0) {
continue;
}
FileEntryT entry = {0};
int32_t nameLen = (int32_t)(dot - ent->d_name);
if (nameLen >= (int32_t)sizeof(entry.name)) {
nameLen = (int32_t)sizeof(entry.name) - 1;
}
memcpy(entry.name, ent->d_name, nameLen);
entry.name[nameLen] = '\0';
snprintf(entry.path, sizeof(entry.path), "%s/%s", THEME_DIR, ent->d_name);
arrput(sThemeEntries, entry);
}
closedir(dir);
// Build label array now that sThemeEntries is stable
for (int32_t i = 0; i < arrlen(sThemeEntries); i++) {
arrput(sThemeLabels, sThemeEntries[i].name);
}
}
// ============================================================
// scanWallpapers
// ============================================================
static void scanWallpapers(void) {
arrsetlen(sWpaperEntries, 0);
arrsetlen(sWpaperLabels, 0);
DIR *dir = opendir(WPAPER_DIR);
if (!dir) {
return;
}
struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
char *dot = strrchr(ent->d_name, '.');
if (!dot) {
continue;
}
// Case-insensitive extension check
if (strcasecmp(dot, THEME_EXT) != 0) {
if (strcasecmp(dot, ".BMP") != 0 &&
strcasecmp(dot, ".JPG") != 0 &&
strcasecmp(dot, ".PNG") != 0) {
continue;
}
// Use filename without extension as display name
int32_t nameLen = (int32_t)(dot - ent->d_name);
if (nameLen >= 64) {
nameLen = 63;
}
memcpy(sThemeNames[sThemeCount], ent->d_name, nameLen);
sThemeNames[sThemeCount][nameLen] = '\0';
sThemeLabels[sThemeCount] = sThemeNames[sThemeCount];
snprintf(sThemePaths[sThemeCount], sizeof(sThemePaths[sThemeCount]), "%s/%s", THEME_DIR, ent->d_name);
sThemeCount++;
FileEntryT entry = {0};
snprintf(entry.name, sizeof(entry.name), "%s", ent->d_name);
snprintf(entry.path, sizeof(entry.path), "%s/%s", WPAPER_DIR, ent->d_name);
arrput(sWpaperEntries, entry);
}
closedir(dir);
// Build label array now that sWpaperEntries is stable
for (int32_t i = 0; i < arrlen(sWpaperEntries); i++) {
arrput(sWpaperLabels, sWpaperEntries[i].name);
}
}
@ -737,6 +969,7 @@ static void updateColorSliders(void) {
wgtSetText(sRedLbl, rBuf);
wgtSetText(sGreenLbl, gBuf);
wgtSetText(sBlueLbl, bBuf);
updateSwatch();
}
@ -751,6 +984,24 @@ static void updateDblClickLabel(void) {
}
// ============================================================
// updateSwatch
// ============================================================
static void updateSwatch(void) {
if (!sColorSwatch) {
return;
}
uint8_t r = (uint8_t)wgtSliderGetValue(sRedSldr);
uint8_t g = (uint8_t)wgtSliderGetValue(sGreenSldr);
uint8_t b = (uint8_t)wgtSliderGetValue(sBlueSldr);
uint32_t color = packColor(&sAc->display, r, g, b);
wgtCanvasClear(sColorSwatch, color);
}
// ============================================================
// appMain
// ============================================================

338
apps/imgview/imgview.c Normal file
View file

@ -0,0 +1,338 @@
// imgview.c — DVX Image Viewer
//
// Displays BMP, PNG, JPG, and GIF images. The image is scaled to fit
// the window while preserving aspect ratio. Resize the window to zoom.
// Open files via the File menu or by launching with Run in the Task Manager.
#include "dvxApp.h"
#include "dvxDialog.h"
#include "dvxWidget.h"
#include "dvxWm.h"
#include "shellApp.h"
#include "thirdparty/stb_image.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ============================================================
// App descriptor
// ============================================================
AppDescriptorT appDescriptor = {
.name = "Image Viewer",
.hasMainLoop = false,
.multiInstance = true,
.stackSize = SHELL_STACK_DEFAULT,
.priority = TS_PRIORITY_NORMAL
};
// ============================================================
// Constants
// ============================================================
#define IV_WIN_W 400
#define IV_WIN_H 320
#define CMD_OPEN 100
#define CMD_CLOSE 101
// ============================================================
// Prototypes
// ============================================================
int32_t appMain(DxeAppContextT *ctx);
static void loadAndDisplay(const char *path);
static void onMenu(WindowT *win, int32_t menuId);
static void onPaint(WindowT *win, RectT *dirty);
static void onResize(WindowT *win, int32_t contentW, int32_t contentH);
static void openFile(void);
// ============================================================
// Module state
// ============================================================
static DxeAppContextT *sCtx = NULL;
static AppContextT *sAc = NULL;
static WindowT *sWin = NULL;
// Source image (RGB, from stb_image)
static uint8_t *sImgRgb = NULL;
static int32_t sImgW = 0;
static int32_t sImgH = 0;
// Scaled image in native pixel format (for direct blit)
static uint8_t *sScaled = NULL;
static int32_t sScaledW = 0;
static int32_t sScaledH = 0;
static int32_t sScaledPitch = 0;
// ============================================================
// buildScaled — scale source image to fit window
// ============================================================
static void buildScaled(int32_t fitW, int32_t fitH) {
free(sScaled);
sScaled = NULL;
if (!sImgRgb || fitW < 1 || fitH < 1) {
return;
}
// Fit image into fitW x fitH preserving aspect ratio
int32_t dstW = fitW;
int32_t dstH = (sImgH * fitW) / sImgW;
if (dstH > fitH) {
dstH = fitH;
dstW = (sImgW * fitH) / sImgH;
}
if (dstW < 1) {
dstW = 1;
}
if (dstH < 1) {
dstH = 1;
}
int32_t bpp = sAc->display.format.bytesPerPixel;
int32_t pitch = dstW * bpp;
int32_t bitsPerPx = sAc->display.format.bitsPerPixel;
sScaled = (uint8_t *)malloc(pitch * dstH);
sScaledW = dstW;
sScaledH = dstH;
sScaledPitch = pitch;
if (!sScaled) {
return;
}
// Bilinear scale from sImgRgb to native pixel format
int32_t srcStride = sImgW * 3;
for (int32_t y = 0; y < dstH; y++) {
// Yield every 32 rows so the UI stays responsive
if ((y & 31) == 0 && y > 0) {
dvxUpdate(sAc);
}
int32_t srcYfp = (int32_t)((int64_t)y * sImgH * 65536 / dstH);
int32_t sy0 = srcYfp >> 16;
int32_t sy1 = sy0 + 1;
int32_t fy = (srcYfp >> 8) & 0xFF;
int32_t ify = 256 - fy;
uint8_t *dst = sScaled + y * pitch;
if (sy1 >= sImgH) {
sy1 = sImgH - 1;
}
uint8_t *row0 = sImgRgb + sy0 * srcStride;
uint8_t *row1 = sImgRgb + sy1 * srcStride;
for (int32_t x = 0; x < dstW; x++) {
int32_t srcXfp = (int32_t)((int64_t)x * sImgW * 65536 / dstW);
int32_t sx0 = srcXfp >> 16;
int32_t sx1 = sx0 + 1;
int32_t fx = (srcXfp >> 8) & 0xFF;
int32_t ifx = 256 - fx;
if (sx1 >= sImgW) {
sx1 = sImgW - 1;
}
uint8_t *p00 = row0 + sx0 * 3;
uint8_t *p10 = row0 + sx1 * 3;
uint8_t *p01 = row1 + sx0 * 3;
uint8_t *p11 = row1 + sx1 * 3;
int32_t r = (p00[0] * ifx * ify + p10[0] * fx * ify + p01[0] * ifx * fy + p11[0] * fx * fy) >> 16;
int32_t g = (p00[1] * ifx * ify + p10[1] * fx * ify + p01[1] * ifx * fy + p11[1] * fx * fy) >> 16;
int32_t b = (p00[2] * ifx * ify + p10[2] * fx * ify + p01[2] * ifx * fy + p11[2] * fx * fy) >> 16;
uint32_t px = packColor(&sAc->display, (uint8_t)r, (uint8_t)g, (uint8_t)b);
if (bitsPerPx == 8) {
dst[x] = (uint8_t)px;
} else if (bitsPerPx == 15 || bitsPerPx == 16) {
((uint16_t *)dst)[x] = (uint16_t)px;
} else {
((uint32_t *)dst)[x] = px;
}
}
}
}
// ============================================================
// loadAndDisplay
// ============================================================
static void loadAndDisplay(const char *path) {
// Free previous image
if (sImgRgb) {
stbi_image_free(sImgRgb);
sImgRgb = NULL;
}
int32_t channels;
sImgRgb = stbi_load(path, &sImgW, &sImgH, &channels, 3);
if (!sImgRgb) {
dvxMessageBox(sAc, "Error", "Could not load image.", MB_OK | MB_ICONERROR);
return;
}
// Update title bar
const char *fname = strrchr(path, '/');
const char *bslash = strrchr(path, '\\');
if (bslash > fname) {
fname = bslash;
}
fname = fname ? fname + 1 : path;
char title[128];
snprintf(title, sizeof(title), "%s - Image Viewer", fname);
dvxSetTitle(sAc, sWin, title);
// Scale and repaint
buildScaled(sWin->contentW, sWin->contentH);
RectT fullRect = {0, 0, sWin->contentW, sWin->contentH};
sWin->onPaint(sWin, &fullRect);
sWin->contentDirty = true;
dvxInvalidateWindow(sAc, sWin);
}
// ============================================================
// onMenu
// ============================================================
static void onMenu(WindowT *win, int32_t menuId) {
(void)win;
switch (menuId) {
case CMD_OPEN:
openFile();
break;
case CMD_CLOSE:
dvxDestroyWindow(sAc, sWin);
sWin = NULL;
break;
}
}
// ============================================================
// onPaint
// ============================================================
static void onPaint(WindowT *win, RectT *dirty) {
(void)dirty;
DisplayT cd = sAc->display;
cd.backBuf = win->contentBuf;
cd.width = win->contentW;
cd.height = win->contentH;
cd.pitch = win->contentPitch;
cd.clipX = 0;
cd.clipY = 0;
cd.clipW = win->contentW;
cd.clipH = win->contentH;
// Fill background
uint32_t bg = packColor(&sAc->display, 32, 32, 32);
rectFill(&cd, &sAc->blitOps, 0, 0, win->contentW, win->contentH, bg);
// Blit scaled image centered
if (sScaled) {
int32_t offX = (win->contentW - sScaledW) / 2;
int32_t offY = (win->contentH - sScaledH) / 2;
int32_t bpp = sAc->display.format.bytesPerPixel;
for (int32_t y = 0; y < sScaledH; y++) {
int32_t dstY = offY + y;
if (dstY < 0 || dstY >= win->contentH) {
continue;
}
uint8_t *src = sScaled + y * sScaledPitch;
uint8_t *dst = win->contentBuf + dstY * win->contentPitch + offX * bpp;
memcpy(dst, src, sScaledW * bpp);
}
}
}
// ============================================================
// onResize
// ============================================================
static void onResize(WindowT *win, int32_t contentW, int32_t contentH) {
(void)win;
buildScaled(contentW, contentH);
}
// ============================================================
// openFile
// ============================================================
static void openFile(void) {
FileFilterT filters[] = {
{ "Images (*.bmp;*.jpg;*.png;*.gif)", "*.bmp;*.jpg;*.png;*.gif" },
{ "All Files (*.*)", "*.*" }
};
char path[260];
if (dvxFileDialog(sAc, "Open Image", FD_OPEN, NULL, filters, 2, path, sizeof(path))) {
loadAndDisplay(path);
}
}
// ============================================================
// appMain
// ============================================================
int32_t appMain(DxeAppContextT *ctx) {
sCtx = ctx;
sAc = ctx->shellCtx;
int32_t winX = (sAc->display.width - IV_WIN_W) / 2;
int32_t winY = (sAc->display.height - IV_WIN_H) / 2;
sWin = dvxCreateWindow(sAc, "Image Viewer", winX, winY, IV_WIN_W, IV_WIN_H, true);
if (!sWin) {
return -1;
}
sWin->onPaint = onPaint;
sWin->onResize = onResize;
sWin->onMenu = onMenu;
MenuBarT *menuBar = wmAddMenuBar(sWin);
MenuT *fileMenu = wmAddMenu(menuBar, "&File");
wmAddMenuItem(fileMenu, "&Open...\tCtrl+O", CMD_OPEN);
wmAddMenuSeparator(fileMenu);
wmAddMenuItem(fileMenu, "&Close", CMD_CLOSE);
AccelTableT *accel = dvxCreateAccelTable();
dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_OPEN);
sWin->accelTable = accel;
// Initial paint (dark background)
RectT fullRect = {0, 0, sWin->contentW, sWin->contentH};
onPaint(sWin, &fullRect);
sWin->contentDirty = true;
return 0;
}

View file

@ -46,6 +46,7 @@ WSRCS = widgets/widgetAnsiTerm.c \
widgets/widgetStatusBar.c \
widgets/widgetTabControl.c \
widgets/widgetTextInput.c \
widgets/widgetTimer.c \
widgets/widgetToolbar.c \
widgets/widgetTreeView.c
@ -130,6 +131,7 @@ $(WOBJDIR)/widgetSpinner.o: widgets/widgetSpinner.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetStatusBar.o: widgets/widgetStatusBar.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetTabControl.o: widgets/widgetTabControl.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetTextInput.o: widgets/widgetTextInput.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetTimer.o: widgets/widgetTimer.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetToolbar.o: widgets/widgetToolbar.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetScrollbar.o: widgets/widgetScrollbar.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetTreeView.o: widgets/widgetTreeView.c $(WIDGET_DEPS)

View file

@ -29,12 +29,14 @@
// order of magnitude slower than system RAM writes on period hardware.
#include "dvxApp.h"
#include "dvxDialog.h"
#include "dvxWidget.h"
#include "widgets/widgetInternal.h"
#include "dvxFont.h"
#include "dvxCursor.h"
#include "platform/dvxPlatform.h"
#include "thirdparty/stb_ds.h"
#include <string.h>
#include <ctype.h>
@ -76,6 +78,8 @@ static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx);
static void closeAllPopups(AppContextT *ctx);
static void closePopupLevel(AppContextT *ctx);
static void closeSysMenu(AppContextT *ctx);
static void interactiveScreenshot(AppContextT *ctx);
static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win);
static void compositeAndFlush(AppContextT *ctx);
static void dirtyCursorArea(AppContextT *ctx, int32_t x, int32_t y);
static bool dispatchAccelKey(AppContextT *ctx, char key);
@ -85,6 +89,7 @@ static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, c
static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd);
static WindowT *findWindowById(AppContextT *ctx, int32_t id);
static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons);
static void enumModeCb(int32_t w, int32_t h, int32_t bpp, void *userData);
static void initColorScheme(AppContextT *ctx);
static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY);
static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx);
@ -463,12 +468,12 @@ static void compositeAndFlush(AppContextT *ctx) {
// 1. Draw desktop background (wallpaper or solid color)
if (ctx->wallpaperBuf) {
int32_t bytesPerPx = d->format.bitsPerPixel / 8;
int32_t bpp = d->format.bytesPerPixel;
for (int32_t row = dr->y; row < dr->y + dr->h; row++) {
uint8_t *src = ctx->wallpaperBuf + row * ctx->wallpaperPitch + dr->x * bytesPerPx;
uint8_t *dst = d->backBuf + row * d->pitch + dr->x * bytesPerPx;
memcpy(dst, src, dr->w * bytesPerPx);
uint8_t *src = ctx->wallpaperBuf + row * ctx->wallpaperPitch + dr->x * bpp;
uint8_t *dst = d->backBuf + row * d->pitch + dr->x * bpp;
memcpy(dst, src, dr->w * bpp);
}
} else {
rectFill(d, ops, dr->x, dr->y, dr->w, dr->h, ctx->colors.desktop);
@ -1308,6 +1313,38 @@ static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, c
// ============================================================
WindowT *dvxCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) {
// Auto-cascade: if another window already occupies this exact position,
// offset diagonally by the title bar height so the new window doesn't
// sit directly on top. Keeps offsetting while collisions exist, wrapping
// back to the origin if we'd go off screen.
int32_t step = CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT;
for (;;) {
bool collision = false;
for (int32_t i = 0; i < ctx->stack.count; i++) {
WindowT *other = ctx->stack.windows[i];
if (other->x == x && other->y == y) {
collision = true;
break;
}
}
if (!collision) {
break;
}
x += step;
y += step;
if (x + w > ctx->display.width || y + h > ctx->display.height) {
x = step;
y = step;
break;
}
}
WindowT *win = wmCreateWindow(&ctx->stack, &ctx->display, title, x, y, w, h, resizable);
if (win) {
@ -1556,7 +1593,7 @@ int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t request
return -1;
}
// New mode succeeded — free old wallpaper
// New mode succeeded — free old wallpaper buffer
free(oldWpBuf);
// Reinit blit ops for new pixel format
@ -1622,6 +1659,11 @@ int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t request
}
}
// Reload wallpaper at the new resolution/bpp
if (ctx->wallpaperPath[0]) {
dvxSetWallpaper(ctx, ctx->wallpaperPath);
}
// Reset clip and dirty the full screen
resetClipRect(&ctx->display);
dirtyListInit(&ctx->dirty);
@ -1647,8 +1689,26 @@ AccelTableT *dvxCreateAccelTable(void) {
void dvxDestroyWindow(AppContextT *ctx, WindowT *win) {
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
// If the window is minimized, dirty the icon strip so the icon
// disappears and remaining icons repack correctly.
if (win->minimized) {
int32_t iconY;
int32_t iconH;
wmMinimizedIconRect(&ctx->stack, &ctx->display, &iconY, &iconH);
dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH);
}
wmDestroyWindow(&ctx->stack, win);
// Dirty icon area again with the updated count (one fewer icon)
if (win->minimized) {
int32_t iconY;
int32_t iconH;
wmMinimizedIconRect(&ctx->stack, &ctx->display, &iconY, &iconH);
dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH);
}
// Focus the new top window
if (ctx->stack.count > 0) {
wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1);
@ -1756,6 +1816,35 @@ const BitmapFontT *dvxGetFont(const AppContextT *ctx) {
}
// ============================================================
// dvxGetVideoModes
// ============================================================
const VideoModeInfoT *dvxGetVideoModes(const AppContextT *ctx, int32_t *count) {
*count = ctx->videoModeCount;
return ctx->videoModes;
}
// ============================================================
// enumModeCb — used during dvxInit to capture available modes
// ============================================================
static void enumModeCb(int32_t w, int32_t h, int32_t bpp, void *userData) {
if (w < 640 || h < 480) {
return;
}
AppContextT *ctx = (AppContextT *)userData;
VideoModeInfoT m;
m.w = w;
m.h = h;
m.bpp = bpp;
arrput(ctx->videoModes, m);
ctx->videoModeCount++;
}
// ============================================================
// dvxGetColor
// ============================================================
@ -1904,10 +1993,15 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) {
ctx->wallpaperPitch = 0;
if (!path) {
ctx->wallpaperPath[0] = '\0';
dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height);
return true;
}
// Store path for reload after video mode change
strncpy(ctx->wallpaperPath, path, sizeof(ctx->wallpaperPath) - 1);
ctx->wallpaperPath[sizeof(ctx->wallpaperPath) - 1] = '\0';
int32_t imgW;
int32_t imgH;
int32_t channels;
@ -1922,10 +2016,23 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) {
// source pixels and blends by fractional distance, producing smooth
// gradients instead of blocky nearest-neighbor artifacts. Uses
// 8-bit fixed-point weights (256 = 1.0) to avoid floating point.
//
// For 15/16bpp modes, ordered dithering (4x4 Bayer matrix) breaks
// up color banding that occurs when quantizing 24-bit gradients to
// 5-6-5 or 5-5-5. The dither offset is added before packColor so
// the quantization error is distributed spatially.
static const int32_t bayerMatrix[4][4] = {
{ -7, 1, -5, 3},
{ 5, -3, 7, -1},
{ -4, 4, -6, 2},
{ 6, -2, 8, 0}
};
bool dither = (ctx->display.format.bitsPerPixel == 15 || ctx->display.format.bitsPerPixel == 16);
int32_t screenW = ctx->display.width;
int32_t screenH = ctx->display.height;
int32_t bpp = ctx->display.format.bitsPerPixel;
int32_t pitch = screenW * (bpp / 8);
int32_t bytesPerPx = ctx->display.format.bytesPerPixel;
int32_t pitch = screenW * bytesPerPx;
uint8_t *buf = (uint8_t *)malloc(pitch * screenH);
if (!buf) {
@ -1936,6 +2043,11 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) {
int32_t srcStride = imgW * 3;
for (int32_t y = 0; y < screenH; y++) {
// Yield every 32 rows so the UI stays responsive
if ((y & 31) == 0 && y > 0) {
dvxUpdate(ctx);
}
// Fixed-point source Y: 16.16
int32_t srcYfp = (int32_t)((int64_t)y * imgH * 65536 / screenH);
int32_t sy0 = srcYfp >> 16;
@ -1973,6 +2085,22 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) {
int32_t g = (p00[1] * ifx * ify + p10[1] * fx * ify + p01[1] * ifx * fy + p11[1] * fx * fy) >> 16;
int32_t b = (p00[2] * ifx * ify + p10[2] * fx * ify + p01[2] * ifx * fy + p11[2] * fx * fy) >> 16;
// Ordered dither for 15/16bpp to reduce color banding
if (dither) {
int32_t d = bayerMatrix[y & 3][x & 3];
r += d;
g += d;
b += d;
if (r < 0) r = 0;
if (r > 255) r = 255;
if (g < 0) g = 0;
if (g > 255) g = 255;
if (b < 0) b = 0;
if (b > 255) b = 255;
}
uint32_t px = packColor(&ctx->display, (uint8_t)r, (uint8_t)g, (uint8_t)b);
if (bpp == 8) {
@ -2008,6 +2136,13 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_
platformInit();
// Enumerate available video modes BEFORE setting one. Some VBE
// BIOSes return a stale or truncated mode list once a graphics
// mode is active, so we must query while still in text mode.
ctx->videoModes = NULL;
ctx->videoModeCount = 0;
platformVideoEnumModes(enumModeCb, ctx);
if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) {
return -1;
}
@ -2236,6 +2371,7 @@ bool dvxUpdate(AppContextT *ctx) {
updateTooltip(ctx);
pollAnsiTermWidgets(ctx);
wgtUpdateCursorBlink();
wgtUpdateTimers();
ctx->frameCount++;
@ -2360,6 +2496,9 @@ void dvxShutdown(AppContextT *ctx) {
free(ctx->wallpaperBuf);
ctx->wallpaperBuf = NULL;
arrfree(ctx->videoModes);
ctx->videoModes = NULL;
ctx->videoModeCount = 0;
videoShutdown(&ctx->display);
}
@ -2589,6 +2728,68 @@ void dvxTileWindowsV(AppContextT *ctx) {
}
// ============================================================
// interactiveScreenshot — snapshot screen, prompt for save path
// ============================================================
static void interactiveScreenshot(AppContextT *ctx) {
FileFilterT filters[] = {
{ "PNG Images (*.png)", "*.png" },
{ "BMP Images (*.bmp)", "*.bmp" }
};
char path[260];
int32_t scrW = ctx->display.width;
int32_t scrH = ctx->display.height;
int32_t scrPitch = ctx->display.pitch;
int32_t scrSize = scrPitch * scrH;
uint8_t *scrBuf = (uint8_t *)malloc(scrSize);
if (scrBuf) {
memcpy(scrBuf, ctx->display.backBuf, scrSize);
if (dvxFileDialog(ctx, "Save Screenshot", FD_SAVE, NULL, filters, 2, path, sizeof(path))) {
dvxSaveImage(ctx, scrBuf, scrW, scrH, scrPitch, path);
}
free(scrBuf);
}
}
// ============================================================
// interactiveWindowScreenshot — snapshot window content, prompt for save path
// ============================================================
static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win) {
if (!win || !win->contentBuf) {
return;
}
FileFilterT filters[] = {
{ "PNG Images (*.png)", "*.png" },
{ "BMP Images (*.bmp)", "*.bmp" }
};
char path[260];
int32_t capW = win->contentW;
int32_t capH = win->contentH;
int32_t capPitch = win->contentPitch;
int32_t capSize = capPitch * capH;
uint8_t *capBuf = (uint8_t *)malloc(capSize);
if (capBuf) {
memcpy(capBuf, win->contentBuf, capSize);
if (dvxFileDialog(ctx, "Save Window Screenshot", FD_SAVE, NULL, filters, 2, path, sizeof(path))) {
dvxSaveImage(ctx, capBuf, capW, capH, capPitch, path);
}
free(capBuf);
}
}
// ============================================================
// executeSysMenuCmd
// ============================================================
@ -2648,6 +2849,16 @@ static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd) {
}
break;
case SysMenuScreenshotE:
// Sys menu is already closed. Composite a clean frame first.
compositeAndFlush(ctx);
interactiveScreenshot(ctx);
break;
case SysMenuWinScreenshotE:
interactiveWindowScreenshot(ctx, win);
break;
case SysMenuCloseE:
if (win->onClose) {
win->onClose(win);
@ -3086,6 +3297,27 @@ static void openSysMenu(AppContextT *ctx, WindowT *win) {
memset(item, 0, sizeof(*item));
item->separator = true;
// Screenshot (full screen)
item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
strncpy(item->label, "Scree&nshot...", MAX_MENU_LABEL - 1);
item->cmd = SysMenuScreenshotE;
item->separator = false;
item->enabled = true;
item->accelKey = accelParse(item->label);
// Screenshot (this window)
item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
strncpy(item->label, "&Window Shot...", MAX_MENU_LABEL - 1);
item->cmd = SysMenuWinScreenshotE;
item->separator = false;
item->enabled = true;
item->accelKey = accelParse(item->label);
// Separator
item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
memset(item, 0, sizeof(*item));
item->separator = true;
// Close
item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
strncpy(item->label, "&Close", MAX_MENU_LABEL - 1);
@ -3188,8 +3420,9 @@ static void pollAnsiTermWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win)
//
// 1. Alt+Tab / Shift+Alt+Tab — window cycling (always works)
// 2. Alt+F4 — close focused window
// 3. Ctrl+Esc — system-wide hotkey (task manager)
// 4. F10 — activate/toggle menu bar
// 3. Ctrl+F12 / Ctrl+Shift+F12 — screenshot (full / window)
// 4. Ctrl+Esc — system-wide hotkey (task manager)
// 5. F10 — activate/toggle menu bar
// 4. Keyboard move/resize mode (arrow keys captured exclusively)
// 5. Alt+Space — system menu toggle
// 6. System menu keyboard navigation (arrows, enter, esc, accel)
@ -3263,6 +3496,19 @@ static void pollKeyboard(AppContextT *ctx) {
continue;
}
// Ctrl+F12 — save full screen screenshot
// Ctrl+Shift+F12 — save focused window screenshot
// BIOS returns scancode 0x58 for F12; Ctrl+F12 = scancode 0x8A.
if (ascii == 0 && scancode == 0x8A && (shiftFlags & KEY_MOD_CTRL)) {
if (shiftHeld && ctx->stack.focusedIdx >= 0) {
interactiveWindowScreenshot(ctx, ctx->stack.windows[ctx->stack.focusedIdx]);
} else {
interactiveScreenshot(ctx);
}
continue;
}
// Ctrl+Esc — system-wide hotkey (e.g. task manager)
if (scancode == 0x01 && ascii == 0x1B && (shiftFlags & KEY_MOD_CTRL)) {
if (ctx->onCtrlEsc) {

View file

@ -101,10 +101,15 @@ typedef struct AppContextT {
clock_t dblClickTicks; // double-click speed in clock() ticks
// Color scheme source RGB values (unpacked, for theme save/get)
uint8_t colorRgb[ColorCountE][3];
// Available video modes (enumerated once at init)
VideoModeInfoT *videoModes; // stb_ds dynamic array
int32_t videoModeCount;
// Wallpaper — pre-scaled to screen dimensions in native pixel format.
// NULL means no wallpaper (solid desktop color).
// NULL means no wallpaper (solid desktop color). wallpaperPath is
// kept so the image can be reloaded after a video mode change.
uint8_t *wallpaperBuf; // pixel data (screen width * height * bpp/8)
int32_t wallpaperPitch; // bytes per row
char wallpaperPath[260]; // source image path (empty = none)
} AppContextT;
// Initialize the entire GUI stack: video mode, input devices, font,
@ -157,6 +162,10 @@ const char *dvxColorName(ColorIdE id);
// to clear the wallpaper and revert to the solid desktop color.
bool dvxSetWallpaper(AppContextT *ctx, const char *path);
// Return the list of available video modes (enumerated at init).
// count receives the number of entries.
const VideoModeInfoT *dvxGetVideoModes(const AppContextT *ctx, int32_t *count);
// Tear down the GUI stack in reverse order: destroy all windows, restore
// text mode, release input devices. Safe to call after a failed dvxInit().
void dvxShutdown(AppContextT *ctx);

View file

@ -658,17 +658,13 @@ static FileDialogStateT sFd;
// historically only use extension-based filters. The case-insensitive
// extension compare handles DOS's case-insensitive filesystem behavior.
static bool fdFilterMatch(const char *name, const char *pattern) {
if (!pattern || pattern[0] == '\0') {
// Match a filename against a single *.ext pattern (case-insensitive).
static bool fdMatchSingle(const char *name, const char *pat) {
if (strcmp(pat, "*.*") == 0 || strcmp(pat, "*") == 0) {
return true;
}
if (strcmp(pattern, "*.*") == 0 || strcmp(pattern, "*") == 0) {
return true;
}
// Simple *.ext matching
if (pattern[0] == '*' && pattern[1] == '.') {
if (pat[0] == '*' && pat[1] == '.') {
const char *ext = strrchr(name, '.');
if (!ext) {
@ -676,9 +672,8 @@ static bool fdFilterMatch(const char *name, const char *pattern) {
}
ext++;
const char *patExt = pattern + 2;
const char *patExt = pat + 2;
// Case-insensitive extension compare
while (*patExt && *ext) {
if (tolower((unsigned char)*patExt) != tolower((unsigned char)*ext)) {
return false;
@ -694,6 +689,47 @@ static bool fdFilterMatch(const char *name, const char *pattern) {
return true;
}
// Match a filename against a pattern string that may contain multiple
// semicolon-delimited patterns (e.g. "*.bmp;*.jpg;*.png").
static bool fdFilterMatch(const char *name, const char *pattern) {
if (!pattern || pattern[0] == '\0') {
return true;
}
// Work on a copy so we can tokenize with NUL
char buf[128];
strncpy(buf, pattern, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char *p = buf;
while (*p) {
// Find the end of this token
char *semi = strchr(p, ';');
if (semi) {
*semi = '\0';
}
// Trim leading whitespace
while (*p == ' ') {
p++;
}
if (fdMatchSingle(name, p)) {
return true;
}
if (!semi) {
break;
}
p = semi + 1;
}
return false;
}
// ============================================================
// fdFreeEntries — free allocated entry name strings
@ -1015,10 +1051,15 @@ static void fdOnListDblClick(WidgetT *w) {
static void fdOnOk(WidgetT *w) {
(void)w;
const char *name = wgtGetText(sFd.nameInput);
// If the filename input is empty and a directory is selected in the
// list, navigate into it. But if the user has typed a filename,
// always accept it — don't let the listbox selection override.
if (!name || name[0] == '\0') {
int32_t sel = wgtListBoxGetSelected(sFd.fileList);
if (sel >= 0 && sel < sFd.entryCount && sFd.entryIsDir[sel]) {
// Extract directory name from "[name]"
const char *display = sFd.entryNames[sel];
char dirName[FD_NAME_LEN];
@ -1036,11 +1077,11 @@ static void fdOnOk(WidgetT *w) {
}
fdNavigate(dirName);
wgtSetText(sFd.nameInput, "");
return;
}
const char *name = wgtGetText(sFd.nameInput);
return;
}
if (!name || name[0] == '\0') {
return;

View file

@ -230,6 +230,13 @@ typedef enum {
ColorCountE
} ColorIdE;
// Video mode entry (enumerated at init, available to apps)
typedef struct {
int32_t w;
int32_t h;
int32_t bpp;
} VideoModeInfoT;
// ============================================================
// Dirty rectangle list
// ============================================================
@ -663,10 +670,12 @@ typedef enum {
SysMenuSizeE = 3,
SysMenuMinimizeE = 4,
SysMenuMaximizeE = 5,
SysMenuCloseE = 6
SysMenuCloseE = 6,
SysMenuScreenshotE = 7,
SysMenuWinScreenshotE = 8
} SysMenuCmdE;
#define SYS_MENU_MAX_ITEMS 8
#define SYS_MENU_MAX_ITEMS 10
typedef struct {
char label[MAX_MENU_LABEL];

View file

@ -103,7 +103,8 @@ typedef enum {
WidgetListViewE,
WidgetSpinnerE,
WidgetScrollPaneE,
WidgetSplitterE
WidgetSplitterE,
WidgetTimerE
} WidgetTypeE;
// ============================================================
@ -342,13 +343,18 @@ typedef struct WidgetT {
bool focused;
char accelKey; // lowercase accelerator character, 0 if none
// User data and callbacks
// User data and callbacks. These fire for ALL widget types from the
// central event dispatcher, not from individual widget handlers.
// Type-specific handlers (e.g. button press animation, listbox
// selection) run first, then these universal callbacks fire.
void *userData;
const char *tooltip; // tooltip text (NULL = none, caller owns string)
MenuT *contextMenu; // right-click context menu (NULL = none, caller owns)
void (*onClick)(struct WidgetT *w);
void (*onChange)(struct WidgetT *w);
void (*onDblClick)(struct WidgetT *w);
void (*onChange)(struct WidgetT *w);
void (*onFocus)(struct WidgetT *w);
void (*onBlur)(struct WidgetT *w);
// Type-specific data — tagged union keyed by the `type` field.
// Only the member corresponding to `type` is valid. This is the C
@ -581,6 +587,13 @@ typedef struct WidgetT {
int32_t dividerPos; // pixels from left/top edge
bool vertical; // true = vertical divider (left|right panes)
} splitter;
struct {
int32_t intervalMs; // timer period in milliseconds
bool repeat; // true = repeating, false = one-shot
bool running; // true = active
clock_t lastFire; // clock() value at last fire (or start)
} timer;
} as;
} WidgetT;
@ -759,6 +772,31 @@ WidgetT *wgtSplitter(WidgetT *parent, bool vertical);
void wgtSplitterSetPos(WidgetT *w, int32_t pos);
int32_t wgtSplitterGetPos(const WidgetT *w);
// ============================================================
// Timer (invisible, callback-driven)
// ============================================================
// Create a timer widget. The timer is invisible and takes no space in
// layout. When running, it calls onChange at the specified interval.
// intervalMs: period in milliseconds. repeat: true for repeating,
// false for one-shot (auto-stops after first fire).
WidgetT *wgtTimer(WidgetT *parent, int32_t intervalMs, bool repeat);
// Start/restart the timer. Resets the elapsed time.
void wgtTimerStart(WidgetT *w);
// Stop the timer.
void wgtTimerStop(WidgetT *w);
// Change the interval (takes effect on next fire).
void wgtTimerSetInterval(WidgetT *w, int32_t intervalMs);
// Check if the timer is currently running.
bool wgtTimerIsRunning(const WidgetT *w);
// Called once per frame by dvxUpdate. Checks all active timers.
void wgtUpdateTimers(void);
// ============================================================
// ImageButton
// ============================================================

View file

@ -182,13 +182,16 @@ static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t pref
// The mode list is a far pointer (seg:off) at offset 14 in the info
// block. It points to a null-terminated (0xFFFF) array of mode numbers
// in conventional memory.
// in conventional memory. Copy to a local buffer first because
// getModeInfo calls 4F01h which overwrites __tb, and some BIOSes
// store the mode list inside the __tb region.
uint16_t modeListOff = _farpeekw(_dos_ds, __tb + 14);
uint16_t modeListSeg = _farpeekw(_dos_ds, __tb + 16);
uint32_t modeListAddr = ((uint32_t)modeListSeg << 4) + modeListOff;
// Walk the mode list. Cap at 256 to prevent runaway on corrupt BIOS
// data (real hardware rarely has more than ~50 modes).
uint16_t modes[256];
int32_t modeCount = 0;
for (int32_t i = 0; i < 256; i++) {
uint16_t mode = _farpeekw(_dos_ds, modeListAddr + i * 2);
@ -196,15 +199,19 @@ static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t pref
break;
}
modes[modeCount++] = mode;
}
for (int32_t i = 0; i < modeCount; i++) {
DisplayT candidate;
int32_t score = 0;
memset(&candidate, 0, sizeof(candidate));
getModeInfo(mode, &candidate, &score, requestedW, requestedH, preferredBpp);
getModeInfo(modes[i], &candidate, &score, requestedW, requestedH, preferredBpp);
if (score > bestScore) {
bestScore = score;
bestMode = mode;
bestMode = modes[i];
bestDisplay = candidate;
}
}
@ -248,6 +255,12 @@ void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *
uint16_t modeListSeg = _farpeekw(_dos_ds, __tb + 16);
uint32_t modeListAddr = ((uint32_t)modeListSeg << 4) + modeListOff;
// Copy mode numbers to a local buffer BEFORE calling 4F01h.
// Some VBE BIOSes store the mode list inside the VBE info block
// at __tb, which 4F01h overwrites when querying mode details.
uint16_t modes[256];
int32_t modeCount = 0;
for (int32_t i = 0; i < 256; i++) {
uint16_t mode = _farpeekw(_dos_ds, modeListAddr + i * 2);
@ -255,9 +268,14 @@ void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *
break;
}
modes[modeCount++] = mode;
}
// Now safe to use __tb for 4F01h queries
for (int32_t i = 0; i < modeCount; i++) {
memset(&r, 0, sizeof(r));
r.x.ax = 0x4F01;
r.x.cx = mode;
r.x.cx = modes[i];
r.x.es = __tb >> 4;
r.x.di = __tb & 0x0F;
__dpmi_int(0x10, &r);
@ -909,6 +927,14 @@ const char *platformGetSystemInfo(const DisplayT *display) {
sysInfoAppend("CPU Type: %d86", ver.cpu);
}
const char *tmpDir = getenv("TEMP");
if (!tmpDir) {
tmpDir = getenv("TMP");
}
sysInfoAppend("Temp Path: %s", (tmpDir && tmpDir[0]) ? tmpDir : "(current directory)");
// ---- Video ----
sysInfoAppend("");
sysInfoAppend("=== Video ===");

View file

@ -458,6 +458,19 @@ static const WidgetClassT sClassSplitter = {
.setText = NULL
};
static const WidgetClassT sClassTimer = {
.flags = 0,
.paint = NULL,
.paintOverlay = NULL,
.calcMinSize = widgetTimerCalcMinSize,
.layout = NULL,
.onMouse = NULL,
.onKey = NULL,
.destroy = widgetTimerDestroy,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassSpinner = {
.flags = WCLASS_FOCUSABLE,
.paint = widgetSpinnerPaint,
@ -518,5 +531,6 @@ const WidgetClassT *widgetClassTable[] = {
[WidgetListViewE] = &sClassListView,
[WidgetSpinnerE] = &sClassSpinner,
[WidgetScrollPaneE] = &sClassScrollPane,
[WidgetSplitterE] = &sClassSplitter
[WidgetSplitterE] = &sClassSplitter,
[WidgetTimerE] = &sClassTimer
};

View file

@ -555,6 +555,8 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
// Clear focus from the previously focused widget. This is done via
// the cached sFocusedWidget pointer rather than walking the tree to
// find the focused widget — an O(1) operation vs O(n).
WidgetT *prevFocus = sFocusedWidget;
if (sFocusedWidget) {
sFocusedWidget->focused = false;
sFocusedWidget = NULL;
@ -566,11 +568,36 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
hit->wclass->onMouse(hit, root, vx, vy);
}
// Universal click/double-click callbacks — fire for ALL widget types
// after the type-specific handler has run. Buttons and image buttons
// are excluded from onClick because they use press-release semantics
// (onClick fires on button-up, not button-down) and already handle it
// in the release handler above. They still get onDblClick here.
if (hit->enabled) {
int32_t clicks = multiClickDetect(vx, vy);
bool isBtn = (hit->type == WidgetButtonE || hit->type == WidgetImageButtonE);
if (clicks >= 2 && hit->onDblClick) {
hit->onDblClick(hit);
} else if (!isBtn && hit->onClick) {
hit->onClick(hit);
}
}
// Update the cached focus pointer for O(1) access in widgetOnKey
if (hit->focused) {
sFocusedWidget = hit;
}
// Fire focus/blur callbacks on transitions
if (prevFocus && prevFocus != sFocusedWidget && prevFocus->onBlur) {
prevFocus->onBlur(prevFocus);
}
if (sFocusedWidget && sFocusedWidget != prevFocus && sFocusedWidget->onFocus) {
sFocusedWidget->onFocus(sFocusedWidget);
}
wgtInvalidate(root);
}

View file

@ -494,6 +494,10 @@ int32_t wordStart(const char *buf, int32_t pos);
void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod);
void widgetTreeViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
// Timer internals
void widgetTimerCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetTimerDestroy(WidgetT *w);
// Drag-reorder helpers — generic drag-and-drop reordering shared by
// ListBox, ListView, and TreeView. The update function tracks the mouse
// position and computes the drop target, the drop function commits the

View file

@ -383,6 +383,15 @@ void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
return;
}
// Enter — activate selected item (same as double-click)
if (key == '\r' || key == '\n') {
if (w->onDblClick && sel >= 0) {
w->onDblClick(w);
}
return;
}
AppContextT *ctx = wgtGetContext(w);
const BitmapFontT *font = &ctx->font;
int32_t visibleRows = (w->h - LISTBOX_BORDER * 2) / font->charHeight;
@ -530,9 +539,7 @@ void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
hit->onChange(hit);
}
if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) {
hit->onDblClick(hit);
}
// onDblClick is handled by the central dispatcher in widgetEvent.c
// Initiate drag-reorder if enabled (not from modifier clicks)
if (hit->as.listBox.reorderable && !shift && !ctrl) {

View file

@ -596,6 +596,15 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
int32_t rowCount = w->as.listView->rowCount;
int32_t *sortIdx = w->as.listView->sortIndex;
// Enter — activate selected item (same as double-click)
if (key == '\r' || key == '\n') {
if (w->onDblClick && w->as.listView->selectedIdx >= 0) {
w->onDblClick(w);
}
return;
}
// Ctrl+A — select all (multi-select only)
if (multi && ctrl && (key == 'a' || key == 'A' || key == 1)) {
wgtListViewSelectAll(w);
@ -1050,9 +1059,7 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
hit->onChange(hit);
}
if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) {
hit->onDblClick(hit);
}
// onDblClick is handled by the central dispatcher in widgetEvent.c
// Initiate drag-reorder if enabled (not from modifier clicks)
if (hit->as.listView->reorderable && !shift && !ctrl) {

View file

@ -484,14 +484,24 @@ void wgtSetFocused(WidgetT *w) {
return;
}
if (sFocusedWidget && sFocusedWidget != w) {
sFocusedWidget->focused = false;
wgtInvalidatePaint(sFocusedWidget);
WidgetT *prev = sFocusedWidget;
if (prev && prev != w) {
prev->focused = false;
wgtInvalidatePaint(prev);
}
w->focused = true;
sFocusedWidget = w;
wgtInvalidatePaint(w);
if (prev && prev != w && prev->onBlur) {
prev->onBlur(prev);
}
if (w->onFocus) {
w->onFocus(w);
}
}

View file

@ -101,18 +101,24 @@ void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font) {
void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod) {
(void)mod;
int32_t step = 1;
int32_t range = w->as.slider.maxValue - w->as.slider.minValue;
int32_t pageStep = range / 10;
if (range > 100) {
step = range / 100;
if (pageStep < 1) {
pageStep = 1;
}
// Arrow keys: step by 1. Page Up/Down: step by 10% of range.
// Home/End: jump to min/max.
if (w->as.slider.vertical) {
if (key == (0x48 | 0x100)) {
w->as.slider.value -= step;
w->as.slider.value -= 1;
} else if (key == (0x50 | 0x100)) {
w->as.slider.value += step;
w->as.slider.value += 1;
} else if (key == (0x49 | 0x100)) {
w->as.slider.value -= pageStep;
} else if (key == (0x51 | 0x100)) {
w->as.slider.value += pageStep;
} else if (key == (0x47 | 0x100)) {
w->as.slider.value = w->as.slider.minValue;
} else if (key == (0x4F | 0x100)) {
@ -122,9 +128,13 @@ void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
} else {
if (key == (0x4B | 0x100)) {
w->as.slider.value -= step;
w->as.slider.value -= 1;
} else if (key == (0x4D | 0x100)) {
w->as.slider.value += step;
w->as.slider.value += 1;
} else if (key == (0x49 | 0x100)) {
w->as.slider.value -= pageStep;
} else if (key == (0x51 | 0x100)) {
w->as.slider.value += pageStep;
} else if (key == (0x47 | 0x100)) {
w->as.slider.value = w->as.slider.minValue;
} else if (key == (0x4F | 0x100)) {

184
dvx/widgets/widgetTimer.c Normal file
View file

@ -0,0 +1,184 @@
// widgetTimer.c — Invisible timer widget
//
// Fires onChange callbacks at a configurable interval. Supports both
// one-shot and repeating modes. The timer is invisible — it takes no
// space in layout and produces no visual output.
//
// Active timers are tracked via a module-level linked list (using the
// stb_ds dynamic array) so wgtUpdateTimers can check only timers, not
// walk the entire widget tree each frame.
#include "widgetInternal.h"
#include <time.h>
#include "thirdparty/stb_ds.h"
// Active timer list — avoids walking the widget tree per frame
static WidgetT **sActiveTimers = NULL;
// ============================================================
// widgetTimerCalcMinSize
// ============================================================
void widgetTimerCalcMinSize(WidgetT *w, const BitmapFontT *font) {
(void)font;
w->calcMinW = 0;
w->calcMinH = 0;
}
// ============================================================
// widgetTimerDestroy
// ============================================================
void widgetTimerDestroy(WidgetT *w) {
// Remove from active list
for (int32_t i = 0; i < arrlen(sActiveTimers); i++) {
if (sActiveTimers[i] == w) {
arrdel(sActiveTimers, i);
break;
}
}
}
// ============================================================
// addToActiveList / removeFromActiveList
// ============================================================
static void addToActiveList(WidgetT *w) {
// Check if already present
for (int32_t i = 0; i < arrlen(sActiveTimers); i++) {
if (sActiveTimers[i] == w) {
return;
}
}
arrput(sActiveTimers, w);
}
static void removeFromActiveList(WidgetT *w) {
for (int32_t i = 0; i < arrlen(sActiveTimers); i++) {
if (sActiveTimers[i] == w) {
arrdel(sActiveTimers, i);
return;
}
}
}
// ============================================================
// wgtTimer
// ============================================================
WidgetT *wgtTimer(WidgetT *parent, int32_t intervalMs, bool repeat) {
WidgetT *w = widgetAlloc(parent, WidgetTimerE);
if (w) {
w->visible = false;
w->as.timer.intervalMs = intervalMs;
w->as.timer.repeat = repeat;
w->as.timer.running = false;
w->as.timer.lastFire = 0;
}
return w;
}
// ============================================================
// wgtTimerIsRunning
// ============================================================
bool wgtTimerIsRunning(const WidgetT *w) {
if (!w || w->type != WidgetTimerE) {
return false;
}
return w->as.timer.running;
}
// ============================================================
// wgtTimerSetInterval
// ============================================================
void wgtTimerSetInterval(WidgetT *w, int32_t intervalMs) {
if (!w || w->type != WidgetTimerE) {
return;
}
w->as.timer.intervalMs = intervalMs;
}
// ============================================================
// wgtTimerStart
// ============================================================
void wgtTimerStart(WidgetT *w) {
if (!w || w->type != WidgetTimerE) {
return;
}
w->as.timer.running = true;
w->as.timer.lastFire = clock();
addToActiveList(w);
}
// ============================================================
// wgtTimerStop
// ============================================================
void wgtTimerStop(WidgetT *w) {
if (!w || w->type != WidgetTimerE) {
return;
}
w->as.timer.running = false;
removeFromActiveList(w);
}
// ============================================================
// wgtUpdateTimers
// ============================================================
//
// Called once per frame from dvxUpdate. Iterates the active timer
// list (not the widget tree) so cost is proportional to timer count,
// not total widget count. One-shot timers are removed after firing.
void wgtUpdateTimers(void) {
clock_t now = clock();
// Iterate backwards so arrdel doesn't skip entries
for (int32_t i = arrlen(sActiveTimers) - 1; i >= 0; i--) {
WidgetT *w = sActiveTimers[i];
if (!w->as.timer.running) {
arrdel(sActiveTimers, i);
continue;
}
clock_t elapsed = now - w->as.timer.lastFire;
clock_t interval = (clock_t)w->as.timer.intervalMs * CLOCKS_PER_SEC / 1000;
if (elapsed >= interval) {
w->as.timer.lastFire = now;
if (w->onChange) {
w->onChange(w);
}
if (!w->as.timer.repeat) {
w->as.timer.running = false;
arrdel(sActiveTimers, i);
}
}
}
}

View file

@ -883,7 +883,9 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
sel->onChange(sel);
}
} else {
if (sel->onClick) {
if (sel->onDblClick) {
sel->onDblClick(sel);
} else if (sel->onClick) {
sel->onClick(sel);
}
}

View file

@ -12,6 +12,7 @@ OBJDIR = ../obj/dvxshell
BINDIR = ../bin
CONFIGDIR = ../bin/config
THEMEDIR = ../bin/config/themes
WPAPERDIR = ../bin/config/wpaper
LIBDIR = ../lib
SRCS = shellMain.c shellApp.c shellExport.c shellInfo.c shellTaskMgr.c
@ -21,8 +22,9 @@ TARGET = $(BINDIR)/dvx.exe
.PHONY: all clean libs
THEMES = $(THEMEDIR)/geos.thm $(THEMEDIR)/win31.thm $(THEMEDIR)/cde.thm
WPAPERS = $(WPAPERDIR)/blueglow.jpg $(WPAPERDIR)/swoop.jpg $(WPAPERDIR)/triangle.jpg
all: libs $(TARGET) $(CONFIGDIR)/dvx.ini $(THEMES)
all: libs $(TARGET) $(CONFIGDIR)/dvx.ini $(THEMES) $(WPAPERS)
libs:
$(MAKE) -C ../dvx
@ -55,6 +57,12 @@ $(THEMEDIR):
$(THEMEDIR)/%.thm: ../themes/%.thm | $(THEMEDIR)
sed 's/$$/\r/' $< > $@
$(WPAPERDIR):
mkdir -p $(WPAPERDIR)
$(WPAPERDIR)/%.jpg: ../wpaper/%.jpg | $(WPAPERDIR)
cp $< $@
# Dependencies
$(OBJDIR)/shellMain.o: shellMain.c shellApp.h ../dvx/dvxApp.h ../dvx/dvxDialog.h ../tasks/taskswitch.h
$(OBJDIR)/shellApp.o: shellApp.c shellApp.h ../dvx/dvxApp.h ../dvx/dvxDialog.h ../tasks/taskswitch.h
@ -63,4 +71,4 @@ $(OBJDIR)/shellInfo.o: shellInfo.c shellInfo.h shellApp.h ../dvx/dvxApp.h ../
$(OBJDIR)/shellTaskMgr.o: shellTaskMgr.c shellTaskMgr.h shellApp.h ../dvx/dvxApp.h ../dvx/dvxDialog.h ../dvx/dvxWidget.h ../dvx/dvxWm.h ../dvx/platform/dvxPlatform.h
clean:
rm -f $(OBJS) $(TARGET) $(BINDIR)/dvx.map $(BINDIR)/dvx.log
rm -rf $(THEMEDIR) $(CONFIGDIR)
rm -rf $(WPAPERDIR) $(THEMEDIR) $(CONFIGDIR)

View file

@ -32,7 +32,7 @@ static int32_t allocSlot(void);
static void appTaskWrapper(void *arg);
static const char *baseName(const char *path);
static void cleanupTempFile(ShellAppT *app);
static int32_t copyFile(const char *src, const char *dst);
static int32_t copyFile(AppContextT *ctx, const char *src, const char *dst);
static ShellAppT *findLoadedPath(const char *path);
static int32_t makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize);
void shellAppInit(void);
@ -104,7 +104,7 @@ static void cleanupTempFile(ShellAppT *app) {
// Binary file copy. Returns 0 on success, -1 on failure.
static int32_t copyFile(const char *src, const char *dst) {
static int32_t copyFile(AppContextT *ctx, const char *src, const char *dst) {
FILE *in = fopen(src, "rb");
if (!in) {
@ -118,7 +118,7 @@ static int32_t copyFile(const char *src, const char *dst) {
return -1;
}
char buf[4096];
char buf[32768];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
@ -128,6 +128,9 @@ static int32_t copyFile(const char *src, const char *dst) {
remove(dst);
return -1;
}
// Yield so the UI stays responsive during large copies
dvxUpdate(ctx);
}
fclose(in);
@ -288,7 +291,7 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
// an independent code+data image.
makeTempPath(path, id, tempPath, sizeof(tempPath));
if (copyFile(path, tempPath) != 0) {
if (copyFile(ctx, path, tempPath) != 0) {
char msg[320];
snprintf(msg, sizeof(msg), "Failed to create instance copy of %s.", baseName(path));
dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR);

View file

@ -67,6 +67,14 @@ extern FILE __dj_stdin;
extern FILE __dj_stdout;
extern FILE __dj_stderr;
// libgcc 64-bit integer math helpers (no header declares these)
extern long long __divdi3(long long, long long);
extern long long __moddi3(long long, long long);
extern long long __muldi3(long long, long long);
extern unsigned long long __udivdi3(unsigned long long, unsigned long long);
extern unsigned long long __udivmoddi4(unsigned long long, unsigned long long, unsigned long long *);
extern unsigned long long __umoddi3(unsigned long long, unsigned long long);
// ============================================================
// Prototypes
// ============================================================
@ -212,6 +220,7 @@ DXE_EXPORT_TABLE(shellExportTable)
DXE_EXPORT(dvxGetFont)
DXE_EXPORT(dvxGetColors)
DXE_EXPORT(dvxGetDisplay)
DXE_EXPORT(dvxGetVideoModes)
DXE_EXPORT(dvxGetBlitOps)
DXE_EXPORT(dvxSetWindowIcon)
DXE_EXPORT(dvxLoadImage)
@ -350,6 +359,13 @@ DXE_EXPORT_TABLE(shellExportTable)
DXE_EXPORT(wgtTreeItemIsSelected)
DXE_EXPORT(wgtTreeItemSetSelected)
// dvxWidget.h — timer
DXE_EXPORT(wgtTimer)
DXE_EXPORT(wgtTimerIsRunning)
DXE_EXPORT(wgtTimerSetInterval)
DXE_EXPORT(wgtTimerStart)
DXE_EXPORT(wgtTimerStop)
// dvxWidget.h — list view
DXE_EXPORT(wgtListView)
DXE_EXPORT(wgtListViewSetColumns)
@ -637,6 +653,16 @@ DXE_EXPORT_TABLE(shellExportTable)
// --- errno ---
DXE_EXPORT(errno)
// --- libgcc 64-bit integer math helpers ---
// GCC emits calls to these for int64_t division/modulo on 32-bit targets.
// Without them, any DXE using 64-bit arithmetic gets unresolved symbols.
DXE_EXPORT(__divdi3)
DXE_EXPORT(__moddi3)
DXE_EXPORT(__muldi3)
DXE_EXPORT(__udivdi3)
DXE_EXPORT(__udivmoddi4)
DXE_EXPORT(__umoddi3)
// --- DJGPP stdio internals ---
// The stdin/stdout/stderr macros in DJGPP expand to pointers to
// these FILE structs. Without them, any DXE that does printf()

View file

@ -52,7 +52,7 @@ 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 FILE *sLogFile = NULL;
static const char *sLogPath = NULL;
// Desktop update callback list (dynamic, managed via stb_ds arrput/arrdel)
typedef void (*DesktopUpdateFnT)(void);
static DesktopUpdateFnT *sDesktopUpdateFns = NULL;
@ -217,17 +217,23 @@ static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData) {
// ============================================================
void shellLog(const char *fmt, ...) {
if (!sLogFile) {
if (!sLogPath) {
return;
}
FILE *f = fopen(sLogPath, "a");
if (!f) {
return;
}
va_list ap;
va_start(ap, fmt);
vfprintf(sLogFile, fmt, ap);
vfprintf(f, fmt, ap);
va_end(ap);
fprintf(sLogFile, "\n");
fflush(sLogFile);
fprintf(f, "\n");
fclose(f);
}
@ -275,7 +281,15 @@ int main(int argc, char *argv[]) {
platformChdir(exeDir);
}
sLogFile = fopen("dvx.log", "w");
// Truncate the log file, then use append-per-write so the file
// isn't held open (allows Notepad to read it while the shell runs).
sLogPath = "dvx.log";
FILE *logInit = fopen(sLogPath, "w");
if (logInit) {
fclose(logInit);
}
shellLog("DVX Shell starting...");
// Load preferences (missing file or keys silently use defaults)
@ -352,11 +366,6 @@ int main(int argc, char *argv[]) {
if (result != 0) {
shellLog("Failed to initialize DVX GUI (error %ld)", (long)result);
if (sLogFile) {
fclose(sLogFile);
}
return 1;
}
@ -368,11 +377,6 @@ int main(int argc, char *argv[]) {
if (tsInit() != TS_OK) {
shellLog("Failed to initialize task system");
dvxShutdown(&sCtx);
if (sLogFile) {
fclose(sLogFile);
}
return 1;
}
@ -409,11 +413,6 @@ int main(int argc, char *argv[]) {
shellLog("Failed to load desktop app '%s'", SHELL_DESKTOP_APP);
tsShutdown();
dvxShutdown(&sCtx);
if (sLogFile) {
fclose(sLogFile);
}
return 1;
}
@ -489,11 +488,5 @@ int main(int argc, char *argv[]) {
prefsFree();
shellLog("DVX Shell exited.");
if (sLogFile) {
fclose(sLogFile);
sLogFile = NULL;
}
return 0;
}

View file

@ -96,7 +96,7 @@ static void onTmEndTask(WidgetT *w) {
if (app && app->state == AppStateRunningE) {
if (idx == sel) {
shellForceKillApp(sCtx, app);
refreshTaskList();
shellDesktopUpdate();
return;
}
@ -188,16 +188,18 @@ static void refreshTaskList(void) {
arrsetlen(sCells, 0);
arrsetlen(sRowStrs, 0);
int32_t rowCount = 0;
// Pass 1: collect all row data. Must finish before building cell
// pointers because arrput may reallocate, invalidating earlier
// pointers into the array.
static int32_t *appIds = NULL;
arrsetlen(appIds, 0);
for (int32_t i = 1; i < SHELL_MAX_APPS; i++) {
ShellAppT *app = shellGetApp(i);
if (app && app->state == AppStateRunningE) {
// Grow the per-row string storage
TmRowStringsT row = {0};
// Column 1: Title (from first visible window owned by this app)
for (int32_t w = 0; w < sCtx->stack.count; w++) {
WindowT *win = sCtx->stack.windows[w];
@ -207,27 +209,29 @@ static void refreshTaskList(void) {
}
}
// Column 2: Filename (basename of .app path)
char *sep = platformPathDirEnd(app->path);
const char *fname = sep ? sep + 1 : app->path;
snprintf(row.file, sizeof(row.file), "%.63s", fname);
// Column 3: Type
snprintf(row.type, sizeof(row.type), "%s", app->hasMainLoop ? "Task" : "Callback");
arrput(sRowStrs, row);
// Build cell pointers for this row
arrput(sCells, app->name);
arrput(sCells, sRowStrs[rowCount].title);
arrput(sCells, sRowStrs[rowCount].file);
arrput(sCells, sRowStrs[rowCount].type);
arrput(sCells, "Running");
rowCount++;
arrput(appIds, i);
}
}
// Pass 2: build cell pointer array. sRowStrs is stable now.
int32_t rowCount = arrlen(sRowStrs);
for (int32_t r = 0; r < rowCount; r++) {
ShellAppT *app = shellGetApp(appIds[r]);
arrput(sCells, app->name);
arrput(sCells, sRowStrs[r].title);
arrput(sCells, sRowStrs[r].file);
arrput(sCells, sRowStrs[r].type);
arrput(sCells, "Running");
}
wgtListViewSetData(sTmListView, sCells, rowCount);
}
@ -248,17 +252,11 @@ static void updateStatusText(void) {
bool hasMem = platformGetMemoryInfo(&totalKb, &freeKb);
int32_t pos = 0;
if (count == 1) {
pos = snprintf(buf, sizeof(buf), "1 app");
} else {
pos = snprintf(buf, sizeof(buf), "%ld apps", (long)count);
}
int32_t pos = snprintf(buf, sizeof(buf), "Applications: %ld", (long)count);
if (hasMem && totalKb > 0) {
uint32_t usedKb = totalKb - freeKb;
snprintf(buf + pos, sizeof(buf) - pos, " | Memory: %lu/%lu MB", (unsigned long)(usedKb / 1024), (unsigned long)(totalKb / 1024));
snprintf(buf + pos, sizeof(buf) - pos, " Memory: %lu/%lu MB", (unsigned long)(usedKb / 1024), (unsigned long)(totalKb / 1024));
}
wgtSetText(sTmStatusLbl, buf);

BIN
wpaper/blueglow.jpg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
wpaper/swoop.jpg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
wpaper/triangle.jpg (Stored with Git LFS) Normal file

Binary file not shown.