Many many bugs fixed. We have a control panel!
This commit is contained in:
parent
fc9fc46c79
commit
70459616cc
30 changed files with 1494 additions and 218 deletions
2
.gitattributes
vendored
2
.gitattributes
vendored
|
|
@ -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
2
.gitignore
vendored
|
|
@ -4,5 +4,5 @@ obj/
|
|||
lib/
|
||||
*.~
|
||||
.gitignore~
|
||||
DVX_GUI_DESIGN.md
|
||||
.gitattributes~
|
||||
*.SWP
|
||||
|
|
|
|||
2
Makefile
2
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:");
|
||||
|
||||
|
|
@ -271,10 +297,12 @@ static void buildMouseTab(WidgetT *page) {
|
|||
int32_t dblMs = prefsGetInt("mouse", "doubleclick", 500);
|
||||
wgtSliderSetValue(sDblClickSldr, dblMs);
|
||||
wgtLabel(dblRow, "Slow ");
|
||||
|
||||
sDblClickLbl = wgtLabel(page, "");
|
||||
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
338
apps/imgview/imgview.c
Normal 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;
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
262
dvx/dvxApp.c
262
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 <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) {
|
||||
|
|
|
|||
11
dvx/dvxApp.h
11
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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -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 ===");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
184
dvx/widgets/widgetTimer.c
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
BIN
wpaper/blueglow.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
wpaper/swoop.jpg
(Stored with Git LFS)
Normal file
BIN
wpaper/swoop.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
wpaper/triangle.jpg
(Stored with Git LFS)
Normal file
BIN
wpaper/triangle.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue