From 70459616ccce6296918b1230ffc05305a54aa2c9 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Thu, 19 Mar 2026 22:35:00 -0500 Subject: [PATCH] Many many bugs fixed. We have a control panel! --- .gitattributes | 2 + .gitignore | 2 +- Makefile | 2 +- apps/Makefile | 30 +- .../ctrlpanel.c => cpanel/cpanel.c} | 411 ++++++++++++++---- apps/imgview/imgview.c | 338 ++++++++++++++ dvx/Makefile | 2 + dvx/dvxApp.c | 268 +++++++++++- dvx/dvxApp.h | 11 +- dvx/dvxDialog.c | 97 +++-- dvx/dvxTypes.h | 13 +- dvx/dvxWidget.h | 44 +- dvx/platform/dvxPlatformDos.c | 38 +- dvx/widgets/widgetClass.c | 16 +- dvx/widgets/widgetEvent.c | 27 ++ dvx/widgets/widgetInternal.h | 4 + dvx/widgets/widgetListBox.c | 13 +- dvx/widgets/widgetListView.c | 13 +- dvx/widgets/widgetOps.c | 16 +- dvx/widgets/widgetSlider.c | 26 +- dvx/widgets/widgetTimer.c | 184 ++++++++ dvx/widgets/widgetTreeView.c | 4 +- dvxshell/Makefile | 12 +- dvxshell/shellApp.c | 11 +- dvxshell/shellExport.c | 26 ++ dvxshell/shellMain.c | 47 +- dvxshell/shellTaskMgr.c | 46 +- wpaper/blueglow.jpg | 3 + wpaper/swoop.jpg | 3 + wpaper/triangle.jpg | 3 + 30 files changed, 1494 insertions(+), 218 deletions(-) rename apps/{ctrlpanel/ctrlpanel.c => cpanel/cpanel.c} (66%) create mode 100644 apps/imgview/imgview.c create mode 100644 dvx/widgets/widgetTimer.c create mode 100644 wpaper/blueglow.jpg create mode 100644 wpaper/swoop.jpg create mode 100644 wpaper/triangle.jpg diff --git a/.gitattributes b/.gitattributes index 20f36a2..718e405 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore index b930465..4a62d30 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,5 @@ obj/ lib/ *.~ .gitignore~ -DVX_GUI_DESIGN.md +.gitattributes~ *.SWP diff --git a/Makefile b/Makefile index 763ce07..5ee9df6 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/apps/Makefile b/apps/Makefile index bbc2214..f8aefff 100644 --- a/apps/Makefile +++ b/apps/Makefile @@ -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 diff --git a/apps/ctrlpanel/ctrlpanel.c b/apps/cpanel/cpanel.c similarity index 66% rename from apps/ctrlpanel/ctrlpanel.c rename to apps/cpanel/cpanel.c index 58d6687..8464888 100644 --- a/apps/ctrlpanel/ctrlpanel.c +++ b/apps/cpanel/cpanel.c @@ -16,6 +16,7 @@ #include "dvxWm.h" #include "platform/dvxPlatform.h" #include "shellApp.h" +#include "thirdparty/stb_ds.h" #include #include @@ -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,23 +89,25 @@ 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 char sWallpaperPath[260]; +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 WidgetT *sVideoList = NULL; +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 WidgetT *sThemeList = NULL; +static FileEntryT *sThemeEntries = NULL; // stb_ds dynamic array +static const char **sThemeLabels = NULL; // stb_ds dynamic array +static WidgetT *sThemeList = NULL; // ============================================================ // Prototypes @@ -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->weight = 100; + 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 // ============================================================ diff --git a/apps/imgview/imgview.c b/apps/imgview/imgview.c new file mode 100644 index 0000000..da778a0 --- /dev/null +++ b/apps/imgview/imgview.c @@ -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 +#include +#include + +// ============================================================ +// 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; +} diff --git a/dvx/Makefile b/dvx/Makefile index 54a929f..156d3fa 100644 --- a/dvx/Makefile +++ b/dvx/Makefile @@ -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) diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index 474405c..b3ed9ca 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -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 #include @@ -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. - 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); + // + // 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 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) { diff --git a/dvx/dvxApp.h b/dvx/dvxApp.h index 743c8bf..651b896 100644 --- a/dvx/dvxApp.h +++ b/dvx/dvxApp.h @@ -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); diff --git a/dvx/dvxDialog.c b/dvx/dvxDialog.c index 2b33528..389c385 100644 --- a/dvx/dvxDialog.c +++ b/dvx/dvxDialog.c @@ -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,33 +1051,38 @@ static void fdOnListDblClick(WidgetT *w) { static void fdOnOk(WidgetT *w) { (void)w; - int32_t sel = wgtListBoxGetSelected(sFd.fileList); + const char *name = wgtGetText(sFd.nameInput); - 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]; + // 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 (display[0] == '[') { - strncpy(dirName, display + 1, sizeof(dirName) - 1); - dirName[sizeof(dirName) - 1] = '\0'; - char *bracket = strchr(dirName, ']'); + if (sel >= 0 && sel < sFd.entryCount && sFd.entryIsDir[sel]) { + const char *display = sFd.entryNames[sel]; + char dirName[FD_NAME_LEN]; - if (bracket) { - *bracket = '\0'; + if (display[0] == '[') { + strncpy(dirName, display + 1, sizeof(dirName) - 1); + dirName[sizeof(dirName) - 1] = '\0'; + char *bracket = strchr(dirName, ']'); + + if (bracket) { + *bracket = '\0'; + } + } else { + strncpy(dirName, display, sizeof(dirName) - 1); + dirName[sizeof(dirName) - 1] = '\0'; } - } else { - strncpy(dirName, display, sizeof(dirName) - 1); - dirName[sizeof(dirName) - 1] = '\0'; + + fdNavigate(dirName); + return; } - fdNavigate(dirName); - wgtSetText(sFd.nameInput, ""); return; } - const char *name = wgtGetText(sFd.nameInput); - if (!name || name[0] == '\0') { return; } diff --git a/dvx/dvxTypes.h b/dvx/dvxTypes.h index 9a61e1b..1d8a377 100644 --- a/dvx/dvxTypes.h +++ b/dvx/dvxTypes.h @@ -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]; diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index 656cac1..aba5fe3 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -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 // ============================================================ diff --git a/dvx/platform/dvxPlatformDos.c b/dvx/platform/dvxPlatformDos.c index ec605d2..d57a8f3 100644 --- a/dvx/platform/dvxPlatformDos.c +++ b/dvx/platform/dvxPlatformDos.c @@ -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 ==="); diff --git a/dvx/widgets/widgetClass.c b/dvx/widgets/widgetClass.c index 23db391..65b8ac6 100644 --- a/dvx/widgets/widgetClass.c +++ b/dvx/widgets/widgetClass.c @@ -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 }; diff --git a/dvx/widgets/widgetEvent.c b/dvx/widgets/widgetEvent.c index d35f901..67a2c0a 100644 --- a/dvx/widgets/widgetEvent.c +++ b/dvx/widgets/widgetEvent.c @@ -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); } diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index 299820f..107cb70 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -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 diff --git a/dvx/widgets/widgetListBox.c b/dvx/widgets/widgetListBox.c index 959f6c4..7749243 100644 --- a/dvx/widgets/widgetListBox.c +++ b/dvx/widgets/widgetListBox.c @@ -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) { diff --git a/dvx/widgets/widgetListView.c b/dvx/widgets/widgetListView.c index 07b5d4f..2410039 100644 --- a/dvx/widgets/widgetListView.c +++ b/dvx/widgets/widgetListView.c @@ -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) { diff --git a/dvx/widgets/widgetOps.c b/dvx/widgets/widgetOps.c index f3398dc..ccecfed 100644 --- a/dvx/widgets/widgetOps.c +++ b/dvx/widgets/widgetOps.c @@ -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); + } } diff --git a/dvx/widgets/widgetSlider.c b/dvx/widgets/widgetSlider.c index 1a4e169..ccbbf3e 100644 --- a/dvx/widgets/widgetSlider.c +++ b/dvx/widgets/widgetSlider.c @@ -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 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)) { diff --git a/dvx/widgets/widgetTimer.c b/dvx/widgets/widgetTimer.c new file mode 100644 index 0000000..03681ca --- /dev/null +++ b/dvx/widgets/widgetTimer.c @@ -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 + +#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); + } + } + } +} diff --git a/dvx/widgets/widgetTreeView.c b/dvx/widgets/widgetTreeView.c index 56ba52a..00d1b97 100644 --- a/dvx/widgets/widgetTreeView.c +++ b/dvx/widgets/widgetTreeView.c @@ -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); } } diff --git a/dvxshell/Makefile b/dvxshell/Makefile index 143b7de..5860725 100644 --- a/dvxshell/Makefile +++ b/dvxshell/Makefile @@ -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) diff --git a/dvxshell/shellApp.c b/dvxshell/shellApp.c index acdfbf6..820dd3a 100644 --- a/dvxshell/shellApp.c +++ b/dvxshell/shellApp.c @@ -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); diff --git a/dvxshell/shellExport.c b/dvxshell/shellExport.c index 85b37b6..a5a333b 100644 --- a/dvxshell/shellExport.c +++ b/dvxshell/shellExport.c @@ -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() diff --git a/dvxshell/shellMain.c b/dvxshell/shellMain.c index e8e3631..ffecff1 100644 --- a/dvxshell/shellMain.c +++ b/dvxshell/shellMain.c @@ -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; } diff --git a/dvxshell/shellTaskMgr.c b/dvxshell/shellTaskMgr.c index 0a8b2a4..3dcaf84 100644 --- a/dvxshell/shellTaskMgr.c +++ b/dvxshell/shellTaskMgr.c @@ -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); diff --git a/wpaper/blueglow.jpg b/wpaper/blueglow.jpg new file mode 100644 index 0000000..2defe4e --- /dev/null +++ b/wpaper/blueglow.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4dbd856c2e673ef8bdf909a90ed2b99696fe80720621089353afd241e0fbe492 +size 59382 diff --git a/wpaper/swoop.jpg b/wpaper/swoop.jpg new file mode 100644 index 0000000..e0849be --- /dev/null +++ b/wpaper/swoop.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d189059f7e327b579c418ac46c3e65081479becdae2b322466913ed7b572b765 +size 103733 diff --git a/wpaper/triangle.jpg b/wpaper/triangle.jpg new file mode 100644 index 0000000..5af759c --- /dev/null +++ b/wpaper/triangle.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac3529af5412dff8938cff32cba1d5a243590f86d4368a029d039586f5ffbf38 +size 135835