diff --git a/Makefile b/Makefile index b9af839..763ce07 100644 --- a/Makefile +++ b/Makefile @@ -24,4 +24,5 @@ clean: $(MAKE) -C dvxshell 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 - -rmdir bin/config bin/apps/progman bin/apps/notepad bin/apps/clock bin/apps/dvxdemo bin/apps bin lib 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 diff --git a/apps/Makefile b/apps/Makefile index f0953ae..bbc2214 100644 --- a/apps/Makefile +++ b/apps/Makefile @@ -10,17 +10,21 @@ OBJDIR = ../obj/apps BINDIR = ../bin/apps # App definitions: each is a subdir with a single .c file -APPS = progman notepad clock dvxdemo +APPS = progman notepad clock dvxdemo ctrlpanel .PHONY: all clean $(APPS) all: $(APPS) +ctrlpanel: $(BINDIR)/ctrlpanel/ctrlpanel.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 + $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< + $(BINDIR)/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/progman $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< @@ -36,6 +40,9 @@ $(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) + $(CC) $(CFLAGS) -c -o $@ $< + $(OBJDIR)/progman.o: progman/progman.c | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< @@ -51,6 +58,9 @@ $(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c | $(OBJDIR) $(OBJDIR): mkdir -p $(OBJDIR) +$(BINDIR)/ctrlpanel: + mkdir -p $(BINDIR)/ctrlpanel + $(BINDIR)/progman: mkdir -p $(BINDIR)/progman @@ -64,13 +74,15 @@ $(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)/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)/progman.o $(OBJDIR)/notepad.o $(OBJDIR)/clock.o $(OBJDIR)/dvxdemo.o + 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 $(BINDIR)/progman/progman.app rm -f $(BINDIR)/notepad/notepad.app rm -f $(BINDIR)/clock/clock.app diff --git a/apps/clock/clock.c b/apps/clock/clock.c index c634cdc..53b4b4b 100644 --- a/apps/clock/clock.c +++ b/apps/clock/clock.c @@ -144,7 +144,14 @@ static void updateTime(void) { return; } - snprintf(sState.timeStr, sizeof(sState.timeStr), "%02d:%02d:%02d", tm->tm_hour, tm->tm_min, tm->tm_sec); + int32_t hour12 = tm->tm_hour % 12; + const char *ampm = (tm->tm_hour < 12) ? "AM" : "PM"; + + if (hour12 == 0) { + hour12 = 12; + } + + snprintf(sState.timeStr, sizeof(sState.timeStr), "%d:%02d:%02d %s", hour12, tm->tm_min, tm->tm_sec, ampm); snprintf(sState.dateStr, sizeof(sState.dateStr), "%04d-%02d-%02d", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday); sState.lastUpdate = now; } diff --git a/apps/ctrlpanel/ctrlpanel.c b/apps/ctrlpanel/ctrlpanel.c new file mode 100644 index 0000000..58d6687 --- /dev/null +++ b/apps/ctrlpanel/ctrlpanel.c @@ -0,0 +1,806 @@ +// ctrlpanel.c — DVX Control Panel +// +// System configuration app with four tabs: +// Mouse — scroll direction, double-click speed, acceleration +// Colors — all 18 system colors, theme load/save +// Desktop — wallpaper image (stretch mode) +// Video — resolution and color depth +// +// Changes preview live. OK saves to DVX.INI, Cancel reverts to the +// state captured when the control panel was opened. + +#include "dvxApp.h" +#include "dvxDialog.h" +#include "dvxPrefs.h" +#include "dvxWidget.h" +#include "dvxWm.h" +#include "platform/dvxPlatform.h" +#include "shellApp.h" + +#include +#include +#include +#include +#include +#include + +// ============================================================ +// Constants +// ============================================================ + +#define CP_WIN_W 460 +#define CP_WIN_H 340 +#define MAX_VIDEO_MODES 64 +#define MAX_THEME_FILES 32 +#define THEME_DIR "CONFIG/THEMES" +#define THEME_EXT ".THM" + +// ============================================================ +// App descriptor +// ============================================================ + +AppDescriptorT appDescriptor = { + .name = "Control Panel", + .hasMainLoop = false, + .multiInstance = false, + .stackSize = SHELL_STACK_DEFAULT, + .priority = TS_PRIORITY_NORMAL +}; + +// ============================================================ +// Video mode entry +// ============================================================ + +typedef struct { + int32_t w; + int32_t h; + int32_t bpp; + char label[32]; +} VideoModeEntryT; + +// ============================================================ +// Module state +// ============================================================ + +static DxeAppContextT *sCtx = NULL; +static AppContextT *sAc = NULL; +static WindowT *sWin = NULL; + +// Saved state for Cancel +static uint8_t sSavedColorRgb[ColorCountE][3]; +static int32_t sSavedWheelDir; +static int32_t sSavedDblClick; +static int32_t sSavedAccel; +static int32_t sSavedVideoW; +static int32_t sSavedVideoH; +static int32_t sSavedVideoBpp; +static bool sSavedHadWallpaper; + +// Mouse tab widgets +static WidgetT *sWheelDrop = NULL; +static WidgetT *sDblClickSldr = NULL; +static WidgetT *sDblClickLbl = NULL; +static WidgetT *sAccelDrop = NULL; + +// Colors tab widgets +static WidgetT *sColorList = NULL; +static WidgetT *sRedSldr = NULL; +static WidgetT *sGreenSldr = NULL; +static WidgetT *sBlueSldr = NULL; +static WidgetT *sRedLbl = NULL; +static WidgetT *sGreenLbl = NULL; +static WidgetT *sBlueLbl = NULL; + +// Desktop tab widgets +static WidgetT *sWallpaperLbl = NULL; +static char sWallpaperPath[260]; + +// Video tab +static VideoModeEntryT sVideoModes[MAX_VIDEO_MODES]; +static const char *sVideoLabels[MAX_VIDEO_MODES]; +static int32_t sVideoModeCount = 0; +static WidgetT *sVideoList = NULL; + +// Theme list +static char sThemeNames[MAX_THEME_FILES][64]; +static const char *sThemeLabels[MAX_THEME_FILES]; +static char sThemePaths[MAX_THEME_FILES][260]; +static int32_t sThemeCount = 0; +static WidgetT *sThemeList = NULL; + +// ============================================================ +// Prototypes +// ============================================================ + +int32_t appMain(DxeAppContextT *ctx); +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 onResetColors(WidgetT *w); +static void onCancel(WidgetT *w); +static void onChooseWallpaper(WidgetT *w); +static void onClearWallpaper(WidgetT *w); +static void onClose(WindowT *win); +static void onColorSelect(WidgetT *w); +static void onColorSlider(WidgetT *w); +static void onDblClickSlider(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 scanThemes(void); +static void updateColorSliders(void); +static void updateDblClickLabel(void); + + +// ============================================================ +// buildColorsTab +// ============================================================ + +static void buildColorsTab(WidgetT *page) { + // Color list on the left, sliders on the right + WidgetT *hbox = wgtHBox(page); + hbox->weight = 100; + hbox->spacing = wgtPixels(8); + + // Left side: color name list + theme controls + WidgetT *leftVbox = wgtVBox(hbox); + leftVbox->weight = 50; + leftVbox->spacing = wgtPixels(4); + + wgtLabel(leftVbox, "System Colors:"); + + // Build color name list + static const char *colorNames[ColorCountE]; + + for (int32_t i = 0; i < ColorCountE; i++) { + colorNames[i] = dvxColorName((ColorIdE)i); + } + + sColorList = wgtListBox(leftVbox); + sColorList->weight = 100; + sColorList->onChange = onColorSelect; + wgtListBoxSetItems(sColorList, colorNames, ColorCountE); + wgtListBoxSetSelected(sColorList, 0); + + // Theme row + WidgetT *themeRow = wgtHBox(leftVbox); + themeRow->spacing = wgtPixels(4); + + wgtLabel(themeRow, "Theme:"); + + scanThemes(); + sThemeList = wgtDropdown(themeRow); + sThemeList->weight = 100; + wgtDropdownSetItems(sThemeList, sThemeLabels, sThemeCount); + + WidgetT *loadBtn = wgtButton(themeRow, "Apply"); + loadBtn->onClick = onApplyTheme; + loadBtn->prefW = wgtPixels(60); + + WidgetT *saveBtn = wgtButton(themeRow, "Save..."); + saveBtn->onClick = onSaveTheme; + saveBtn->prefW = wgtPixels(60); + + WidgetT *resetBtn = wgtButton(themeRow, "Reset"); + resetBtn->onClick = onResetColors; + resetBtn->prefW = wgtPixels(60); + + // Right side: RGB sliders + WidgetT *rightVbox = wgtVBox(hbox); + rightVbox->weight = 50; + rightVbox->spacing = wgtPixels(4); + + wgtLabel(rightVbox, "Red:"); + sRedSldr = wgtSlider(rightVbox, 0, 255); + sRedSldr->onChange = onColorSlider; + sRedLbl = wgtLabel(rightVbox, "0"); + + wgtLabel(rightVbox, "Green:"); + sGreenSldr = wgtSlider(rightVbox, 0, 255); + sGreenSldr->onChange = onColorSlider; + sGreenLbl = wgtLabel(rightVbox, "0"); + + wgtLabel(rightVbox, "Blue:"); + sBlueSldr = wgtSlider(rightVbox, 0, 255); + sBlueSldr->onChange = onColorSlider; + sBlueLbl = wgtLabel(rightVbox, "0"); + + updateColorSliders(); +} + + +// ============================================================ +// buildDesktopTab +// ============================================================ + +static void buildDesktopTab(WidgetT *page) { + wgtLabel(page, "Wallpaper:"); + + sWallpaperLbl = wgtLabel(page, sWallpaperPath[0] ? sWallpaperPath : "(none)"); + + WidgetT *btnRow = wgtHBox(page); + btnRow->spacing = wgtPixels(8); + + WidgetT *chooseBtn = wgtButton(btnRow, "Browse..."); + chooseBtn->onClick = onChooseWallpaper; + chooseBtn->prefW = wgtPixels(90); + + WidgetT *clearBtn = wgtButton(btnRow, "Clear"); + clearBtn->onClick = onClearWallpaper; + clearBtn->prefW = wgtPixels(90); +} + + +// ============================================================ +// buildMouseTab +// ============================================================ + +static void buildMouseTab(WidgetT *page) { + // Scroll direction + WidgetT *wheelRow = wgtHBox(page); + wheelRow->spacing = wgtPixels(8); + wgtLabel(wheelRow, "Scroll Wheel:"); + + static const char *wheelItems[] = {"Normal", "Reversed"}; + sWheelDrop = wgtDropdown(wheelRow); + sWheelDrop->weight = 100; + sWheelDrop->onChange = onWheelChange; + wgtDropdownSetItems(sWheelDrop, wheelItems, 2); + wgtDropdownSetSelected(sWheelDrop, sAc->wheelDirection < 0 ? 1 : 0); + + // Double-click speed + wgtLabel(page, "Double-Click Speed:"); + + WidgetT *dblRow = wgtHBox(page); + dblRow->spacing = wgtPixels(8); + + wgtLabel(dblRow, "Fast"); + sDblClickSldr = wgtSlider(dblRow, 200, 900); + sDblClickSldr->weight = 100; + sDblClickSldr->onChange = onDblClickSlider; + + int32_t dblMs = prefsGetInt("mouse", "doubleclick", 500); + wgtSliderSetValue(sDblClickSldr, dblMs); + wgtLabel(dblRow, "Slow"); + + sDblClickLbl = wgtLabel(page, ""); + updateDblClickLabel(); + + // Acceleration + WidgetT *accelRow = wgtHBox(page); + accelRow->spacing = wgtPixels(8); + wgtLabel(accelRow, "Acceleration:"); + + static const char *accelItems[] = {"Off", "Low", "Medium", "High"}; + sAccelDrop = wgtDropdown(accelRow); + sAccelDrop->weight = 100; + sAccelDrop->onChange = onAccelChange; + wgtDropdownSetItems(sAccelDrop, accelItems, 4); + + const char *accelStr = prefsGetString("mouse", "acceleration", "medium"); + + if (strcmp(accelStr, "off") == 0) { + wgtDropdownSetSelected(sAccelDrop, 0); + } else if (strcmp(accelStr, "low") == 0) { + wgtDropdownSetSelected(sAccelDrop, 1); + } else if (strcmp(accelStr, "high") == 0) { + wgtDropdownSetSelected(sAccelDrop, 3); + } else { + wgtDropdownSetSelected(sAccelDrop, 2); + } +} + + +// ============================================================ +// buildVideoTab +// ============================================================ + +static void buildVideoTab(WidgetT *page) { + wgtLabel(page, "Available Video Modes:"); + + sVideoModeCount = 0; + platformVideoEnumModes(enumVideoModesCb, NULL); + + sVideoList = wgtListBox(page); + sVideoList->weight = 100; + wgtListBoxSetItems(sVideoList, sVideoLabels, sVideoModeCount); + + // Select the current mode + for (int32_t i = 0; i < sVideoModeCount; i++) { + if (sVideoModes[i].w == sAc->display.width && + sVideoModes[i].h == sAc->display.height && + sVideoModes[i].bpp == sAc->display.format.bitsPerPixel) { + wgtListBoxSetSelected(sVideoList, i); + break; + } + } + + WidgetT *applyBtn = wgtButton(page, "Apply Mode"); + applyBtn->onClick = onVideoApply; + applyBtn->prefW = wgtPixels(100); +} + + +// ============================================================ +// 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 +// ============================================================ + +static int32_t mapAccelName(const char *name) { + if (strcmp(name, "off") == 0) return 10000; + if (strcmp(name, "low") == 0) return 100; + if (strcmp(name, "medium") == 0) return 64; + if (strcmp(name, "high") == 0) return 32; + return 64; +} + + +static const char *mapAccelValue(int32_t idx) { + switch (idx) { + case 0: return "off"; + case 1: return "low"; + case 2: return "medium"; + case 3: return "high"; + default: return "medium"; + } +} + + +// ============================================================ +// Callbacks — Mouse tab +// ============================================================ + +static void onWheelChange(WidgetT *w) { + int32_t sel = wgtDropdownGetSelected(w); + int32_t dir = (sel == 1) ? -1 : 1; + int32_t dbl = wgtSliderGetValue(sDblClickSldr); + + const char *accelName = mapAccelValue(wgtDropdownGetSelected(sAccelDrop)); + int32_t accelVal = mapAccelName(accelName); + + dvxSetMouseConfig(sAc, dir, dbl, accelVal); +} + + +static void onDblClickSlider(WidgetT *w) { + (void)w; + updateDblClickLabel(); + + int32_t dir = (wgtDropdownGetSelected(sWheelDrop) == 1) ? -1 : 1; + int32_t dbl = wgtSliderGetValue(sDblClickSldr); + + const char *accelName = mapAccelValue(wgtDropdownGetSelected(sAccelDrop)); + int32_t accelVal = mapAccelName(accelName); + + dvxSetMouseConfig(sAc, dir, dbl, accelVal); +} + + +static void onAccelChange(WidgetT *w) { + int32_t dir = (wgtDropdownGetSelected(sWheelDrop) == 1) ? -1 : 1; + int32_t dbl = wgtSliderGetValue(sDblClickSldr); + + const char *accelName = mapAccelValue(wgtDropdownGetSelected(w)); + int32_t accelVal = mapAccelName(accelName); + + dvxSetMouseConfig(sAc, dir, dbl, accelVal); +} + + +// ============================================================ +// Callbacks — Colors tab +// ============================================================ + +static void onColorSelect(WidgetT *w) { + (void)w; + updateColorSliders(); +} + + +static void onColorSlider(WidgetT *w) { + (void)w; + + int32_t sel = wgtListBoxGetSelected(sColorList); + + if (sel < 0 || sel >= ColorCountE) { + return; + } + + uint8_t r = (uint8_t)wgtSliderGetValue(sRedSldr); + uint8_t g = (uint8_t)wgtSliderGetValue(sGreenSldr); + uint8_t b = (uint8_t)wgtSliderGetValue(sBlueSldr); + + // Update label text + static char rBuf[8]; + static char gBuf[8]; + static char bBuf[8]; + snprintf(rBuf, sizeof(rBuf), "%d", r); + snprintf(gBuf, sizeof(gBuf), "%d", g); + snprintf(bBuf, sizeof(bBuf), "%d", b); + wgtSetText(sRedLbl, rBuf); + wgtSetText(sGreenLbl, gBuf); + wgtSetText(sBlueLbl, bBuf); + + dvxSetColor(sAc, (ColorIdE)sel, r, g, b); +} + + +static void onApplyTheme(WidgetT *w) { + (void)w; + + int32_t sel = wgtDropdownGetSelected(sThemeList); + + if (sel < 0 || sel >= sThemeCount) { + return; + } + + dvxLoadTheme(sAc, sThemePaths[sel]); + updateColorSliders(); +} + + +static void onSaveTheme(WidgetT *w) { + (void)w; + + FileFilterT filters[] = { + { "Theme Files (*.thm)", "*.thm" }, + { "All Files (*.*)", "*.*" } + }; + char path[260]; + + if (dvxFileDialog(sAc, "Save Theme", FD_SAVE, THEME_DIR, filters, 2, path, sizeof(path))) { + dvxSaveTheme(sAc, path); + scanThemes(); + wgtDropdownSetItems(sThemeList, sThemeLabels, sThemeCount); + } +} + + +static void onResetColors(WidgetT *w) { + (void)w; + dvxResetColorScheme(sAc); + updateColorSliders(); +} + + +// ============================================================ +// Callbacks — Desktop tab +// ============================================================ + +static void onChooseWallpaper(WidgetT *w) { + (void)w; + + FileFilterT filters[] = { + { "Images (*.bmp;*.png;*.jpg)", "*.bmp" }, + { "All Files (*.*)", "*.*" } + }; + char path[260]; + + if (dvxFileDialog(sAc, "Choose Wallpaper", FD_OPEN, NULL, filters, 2, path, sizeof(path))) { + if (dvxSetWallpaper(sAc, path)) { + strncpy(sWallpaperPath, path, sizeof(sWallpaperPath) - 1); + sWallpaperPath[sizeof(sWallpaperPath) - 1] = '\0'; + wgtSetText(sWallpaperLbl, sWallpaperPath); + } else { + dvxMessageBox(sAc, "Error", "Could not load wallpaper image.", MB_OK | MB_ICONERROR); + } + } +} + + +static void onClearWallpaper(WidgetT *w) { + (void)w; + dvxSetWallpaper(sAc, NULL); + sWallpaperPath[0] = '\0'; + wgtSetText(sWallpaperLbl, "(none)"); +} + + +// ============================================================ +// Callbacks — Video tab +// ============================================================ + +static void onVideoApply(WidgetT *w) { + (void)w; + + int32_t sel = wgtListBoxGetSelected(sVideoList); + + if (sel < 0 || sel >= sVideoModeCount) { + return; + } + + VideoModeEntryT *m = &sVideoModes[sel]; + + if (m->w == sAc->display.width && + m->h == sAc->display.height && + m->bpp == sAc->display.format.bitsPerPixel) { + return; + } + + if (dvxChangeVideoMode(sAc, m->w, m->h, m->bpp) != 0) { + dvxMessageBox(sAc, "Error", "Failed to change video mode.", MB_OK | MB_ICONERROR); + } +} + + +// ============================================================ +// Callbacks — OK / Cancel / Close +// ============================================================ + +static void onOk(WidgetT *w) { + (void)w; + + // Save mouse settings + int32_t wheelSel = wgtDropdownGetSelected(sWheelDrop); + prefsSetString("mouse", "wheel", wheelSel == 1 ? "reversed" : "normal"); + prefsSetInt("mouse", "doubleclick", wgtSliderGetValue(sDblClickSldr)); + prefsSetString("mouse", "acceleration", mapAccelValue(wgtDropdownGetSelected(sAccelDrop))); + + // Save colors to INI + for (int32_t i = 0; i < ColorCountE; i++) { + uint8_t r; + uint8_t g; + uint8_t b; + dvxGetColor(sAc, (ColorIdE)i, &r, &g, &b); + + char val[16]; + snprintf(val, sizeof(val), "%d,%d,%d", r, g, b); + prefsSetString("colors", dvxColorName((ColorIdE)i), val); + } + + // Save desktop settings + if (sWallpaperPath[0]) { + prefsSetString("desktop", "wallpaper", sWallpaperPath); + } else { + prefsRemove("desktop", "wallpaper"); + } + + // Save video settings + prefsSetInt("video", "width", sAc->display.width); + prefsSetInt("video", "height", sAc->display.height); + prefsSetInt("video", "bpp", sAc->display.format.bitsPerPixel); + + prefsSave(); + dvxDestroyWindow(sAc, sWin); + sWin = NULL; +} + + +static void onCancel(WidgetT *w) { + (void)w; + restoreSnapshot(); + dvxDestroyWindow(sAc, sWin); + sWin = NULL; +} + + +static void onClose(WindowT *win) { + restoreSnapshot(); + dvxDestroyWindow(sAc, win); + sWin = NULL; +} + + +// ============================================================ +// saveSnapshot / restoreSnapshot +// ============================================================ + +static void saveSnapshot(void) { + memcpy(sSavedColorRgb, sAc->colorRgb, sizeof(sSavedColorRgb)); + sSavedWheelDir = sAc->wheelDirection; + sSavedDblClick = (int32_t)(sAc->dblClickTicks * 1000 / CLOCKS_PER_SEC); + sSavedAccel = 0; + sSavedVideoW = sAc->display.width; + sSavedVideoH = sAc->display.height; + sSavedVideoBpp = sAc->display.format.bitsPerPixel; + sSavedHadWallpaper = (sAc->wallpaperBuf != NULL); + + const char *wp = prefsGetString("desktop", "wallpaper", NULL); + + if (wp) { + strncpy(sWallpaperPath, wp, sizeof(sWallpaperPath) - 1); + sWallpaperPath[sizeof(sWallpaperPath) - 1] = '\0'; + } else { + sWallpaperPath[0] = '\0'; + } +} + + +static void restoreSnapshot(void) { + // Restore colors + memcpy(sAc->colorRgb, sSavedColorRgb, sizeof(sSavedColorRgb)); + dvxApplyColorScheme(sAc); + + // Restore mouse + const char *accelStr = prefsGetString("mouse", "acceleration", "medium"); + int32_t accelVal = mapAccelName(accelStr); + dvxSetMouseConfig(sAc, sSavedWheelDir, sSavedDblClick, accelVal); + + // Restore video mode if changed + if (sAc->display.width != sSavedVideoW || + sAc->display.height != sSavedVideoH || + sAc->display.format.bitsPerPixel != sSavedVideoBpp) { + dvxChangeVideoMode(sAc, sSavedVideoW, sSavedVideoH, sSavedVideoBpp); + } + + // Restore wallpaper + if (sSavedHadWallpaper && sWallpaperPath[0]) { + dvxSetWallpaper(sAc, sWallpaperPath); + } else { + dvxSetWallpaper(sAc, NULL); + } +} + + +// ============================================================ +// scanThemes +// ============================================================ + +static void scanThemes(void) { + sThemeCount = 0; + + // Scan THEME_DIR for .THM files + DIR *dir = opendir(THEME_DIR); + + if (!dir) { + return; + } + + struct dirent *ent; + + while ((ent = readdir(dir)) != NULL && sThemeCount < MAX_THEME_FILES) { + char *dot = strrchr(ent->d_name, '.'); + + if (!dot) { + continue; + } + + // Case-insensitive extension check + if (strcasecmp(dot, THEME_EXT) != 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++; + } + + closedir(dir); +} + + +// ============================================================ +// updateColorSliders +// ============================================================ + +static void updateColorSliders(void) { + int32_t sel = wgtListBoxGetSelected(sColorList); + + if (sel < 0 || sel >= ColorCountE) { + return; + } + + uint8_t r; + uint8_t g; + uint8_t b; + dvxGetColor(sAc, (ColorIdE)sel, &r, &g, &b); + + wgtSliderSetValue(sRedSldr, r); + wgtSliderSetValue(sGreenSldr, g); + wgtSliderSetValue(sBlueSldr, b); + + static char rBuf[8]; + static char gBuf[8]; + static char bBuf[8]; + snprintf(rBuf, sizeof(rBuf), "%d", r); + snprintf(gBuf, sizeof(gBuf), "%d", g); + snprintf(bBuf, sizeof(bBuf), "%d", b); + wgtSetText(sRedLbl, rBuf); + wgtSetText(sGreenLbl, gBuf); + wgtSetText(sBlueLbl, bBuf); +} + + +// ============================================================ +// updateDblClickLabel +// ============================================================ + +static void updateDblClickLabel(void) { + static char buf[32]; + snprintf(buf, sizeof(buf), "%ld ms", (long)wgtSliderGetValue(sDblClickSldr)); + wgtSetText(sDblClickLbl, buf); +} + + +// ============================================================ +// appMain +// ============================================================ + +int32_t appMain(DxeAppContextT *ctx) { + sCtx = ctx; + sAc = ctx->shellCtx; + + int32_t winX = (sAc->display.width - CP_WIN_W) / 2; + int32_t winY = (sAc->display.height - CP_WIN_H) / 2; + + sWin = dvxCreateWindow(sAc, "Control Panel", winX, winY, CP_WIN_W, CP_WIN_H, true); + + if (!sWin) { + return -1; + } + + sWin->onClose = onClose; + + saveSnapshot(); + + WidgetT *root = wgtInitWindow(sAc, sWin); + + // Tab control fills the window + WidgetT *tabs = wgtTabControl(root); + tabs->weight = 100; + + WidgetT *mouseTab = wgtTabPage(tabs, "Mouse"); + WidgetT *colorsTab = wgtTabPage(tabs, "Colors"); + WidgetT *desktopTab = wgtTabPage(tabs, "Desktop"); + WidgetT *videoTab = wgtTabPage(tabs, "Video"); + + buildMouseTab(mouseTab); + buildColorsTab(colorsTab); + buildDesktopTab(desktopTab); + buildVideoTab(videoTab); + + // OK / Cancel buttons at bottom + WidgetT *btnRow = wgtHBox(root); + btnRow->align = AlignEndE; + btnRow->spacing = wgtPixels(8); + + WidgetT *okBtn = wgtButton(btnRow, "OK"); + okBtn->onClick = onOk; + okBtn->prefW = wgtPixels(80); + + WidgetT *cancelBtn = wgtButton(btnRow, "Cancel"); + cancelBtn->onClick = onCancel; + cancelBtn->prefW = wgtPixels(80); + + dvxFitWindow(sAc, sWin); + return 0; +} diff --git a/apps/notepad/notepad.c b/apps/notepad/notepad.c index b068535..799a584 100644 --- a/apps/notepad/notepad.c +++ b/apps/notepad/notepad.c @@ -14,6 +14,7 @@ #include "dvxDialog.h" #include "dvxWidget.h" #include "dvxWm.h" +#include "platform/dvxPlatform.h" #include "shellApp.h" #include @@ -75,10 +76,11 @@ static void onMenu(WindowT *win, int32_t menuId); // Callback-only: the TextArea widget handles all interactive editing // entirely within the shell's event dispatch, so no dedicated task needed. AppDescriptorT appDescriptor = { - .name = "Notepad", - .hasMainLoop = false, - .stackSize = SHELL_STACK_DEFAULT, - .priority = TS_PRIORITY_NORMAL + .name = "Notepad", + .hasMainLoop = false, + .multiInstance = true, + .stackSize = SHELL_STACK_DEFAULT, + .priority = TS_PRIORITY_NORMAL }; // ============================================================ @@ -194,6 +196,7 @@ static void doOpen(void) { if (buf) { fread(buf, 1, size, f); buf[size] = '\0'; + platformStripLineEndings(buf, (int32_t)size); wgtSetText(sTextArea, buf); free(buf); } @@ -228,7 +231,16 @@ static void doSave(void) { return; } - fwrite(text, 1, strlen(text), f); + const char *eol = platformLineEnding(); + + for (const char *p = text; *p; p++) { + if (*p == '\n') { + fputs(eol, f); + } else { + fputc(*p, f); + } + } + fclose(f); markClean(); } diff --git a/apps/progman/progman.c b/apps/progman/progman.c index 14969da..8ef8f4a 100644 --- a/apps/progman/progman.c +++ b/apps/progman/progman.c @@ -23,6 +23,7 @@ #include "dvxWidget.h" #include "dvxWm.h" #include "shellApp.h" +#include "shellTaskMgr.h" #include "shellInfo.h" #include @@ -58,9 +59,6 @@ #define CMD_TASK_MGR 301 #define CMD_SYSINFO 302 -// Task Manager column count -#define TM_COL_COUNT 4 - // ============================================================ // Module state // ============================================================ @@ -76,7 +74,6 @@ typedef struct { // of collision between apps even though the names look global. static DxeAppContextT *sCtx = NULL; static AppContextT *sAc = NULL; -static int32_t sMyAppId = 0; static WindowT *sPmWindow = NULL; static WidgetT *sStatusLabel = NULL; static bool sMinOnRun = false; @@ -99,14 +96,6 @@ static void showAboutDialog(void); static void showSystemInfo(void); static void updateStatusText(void); -// Task Manager -static WindowT *sTmWindow = NULL; -static WidgetT *sTmListView = NULL; -static void buildTaskManager(void); -static void onTmClose(WindowT *win); -static void onTmEndTask(WidgetT *w); -static void onTmSwitchTo(WidgetT *w); -static void refreshTaskList(void); // ============================================================ // App descriptor @@ -220,86 +209,11 @@ static void buildPmWindow(void) { } -// Build or raise the Task Manager window. Singleton pattern: if sTmWindow is -// already live, we just raise it to the top of the Z-order instead of -// creating a duplicate, mimicking Windows 3.x Task Manager behavior. -static void buildTaskManager(void) { - if (sTmWindow) { - // Already open — find it in the window stack and bring to front - for (int32_t i = 0; i < sAc->stack.count; i++) { - if (sAc->stack.windows[i] == sTmWindow) { - wmRaiseWindow(&sAc->stack, &sAc->dirty, i); - wmSetFocus(&sAc->stack, &sAc->dirty, sAc->stack.count - 1); - break; - } - } - return; - } - - int32_t screenW = sAc->display.width; - int32_t screenH = sAc->display.height; - int32_t winW = 420; - int32_t winH = 280; - int32_t winX = (screenW - winW) / 2; - int32_t winY = (screenH - winH) / 3; - - sTmWindow = dvxCreateWindow(sAc, "Task Manager", winX, winY, winW, winH, true); - - if (!sTmWindow) { - return; - } - - sTmWindow->onClose = onTmClose; - - WidgetT *root = wgtInitWindow(sAc, sTmWindow); - - // ListView with Name (descriptor), File (basename), Type, Status columns. - // Static: wgtListViewSetColumns stores a pointer, not a copy. - static ListViewColT tmCols[TM_COL_COUNT]; - tmCols[0].title = "Name"; - tmCols[0].width = wgtPercent(35); - tmCols[0].align = ListViewAlignLeftE; - tmCols[1].title = "File"; - tmCols[1].width = wgtPercent(30); - tmCols[1].align = ListViewAlignLeftE; - tmCols[2].title = "Type"; - tmCols[2].width = wgtPercent(17); - tmCols[2].align = ListViewAlignLeftE; - tmCols[3].title = "Status"; - tmCols[3].width = wgtPercent(18); - tmCols[3].align = ListViewAlignLeftE; - - sTmListView = wgtListView(root); - sTmListView->weight = 100; - sTmListView->prefH = wgtPixels(160); - wgtListViewSetColumns(sTmListView, tmCols, TM_COL_COUNT); - - // Button row right-aligned (AlignEndE) to follow Windows UI convention - WidgetT *btnRow = wgtHBox(root); - btnRow->align = AlignEndE; - btnRow->spacing = wgtPixels(8); - - WidgetT *switchBtn = wgtButton(btnRow, "Switch To"); - switchBtn->onClick = onTmSwitchTo; - switchBtn->prefW = wgtPixels(90); - - WidgetT *endBtn = wgtButton(btnRow, "End Task"); - endBtn->onClick = onTmEndTask; - endBtn->prefW = wgtPixels(90); - - refreshTaskList(); - dvxFitWindow(sAc, sTmWindow); -} - - // Shell calls this via shellRegisterDesktopUpdate whenever an app is loaded, -// reaped, or crashes. We refresh the running count and Task Manager list. +// reaped, or crashes. We refresh the running count in the status bar. +// (Task Manager refresh is handled by the shell's shellDesktopUpdate.) static void desktopUpdate(void) { updateStatusText(); - - if (sTmWindow) { - refreshTaskList(); - } } @@ -390,165 +304,12 @@ static void onPmMenu(WindowT *win, int32_t menuId) { break; case CMD_TASK_MGR: - buildTaskManager(); + shellTaskMgrOpen(sAc); break; } } -// Null the static pointers before destroying so buildTaskManager() knows -// the window is gone and will create a fresh one next time. -static void onTmClose(WindowT *win) { - sTmListView = NULL; - sTmWindow = NULL; - dvxDestroyWindow(sAc, win); -} - - -static void onTmEndTask(WidgetT *w) { - (void)w; - - if (!sTmListView) { - return; - } - - int32_t sel = wgtListViewGetSelected(sTmListView); - - if (sel < 0) { - return; - } - - // The list view rows don't carry app IDs directly, so we re-walk the - // app slot table in the same order as refreshTaskList() to map the - // selected row index back to the correct ShellAppT. We skip our own - // appId so progman can't kill itself. - int32_t idx = 0; - - for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { - if (i == sMyAppId) { - continue; - } - - ShellAppT *app = shellGetApp(i); - - if (app && app->state == AppStateRunningE) { - if (idx == sel) { - shellForceKillApp(sAc, app); - refreshTaskList(); - updateStatusText(); - return; - } - - idx++; - } - } -} - - -static void onTmSwitchTo(WidgetT *w) { - (void)w; - - if (!sTmListView) { - return; - } - - int32_t sel = wgtListViewGetSelected(sTmListView); - - if (sel < 0) { - return; - } - - // Same index-to-appId mapping as onTmEndTask. We scan the window - // stack top-down (highest Z first) to find the app's topmost window, - // restore it if minimized, then raise and focus it. - int32_t idx = 0; - - for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { - if (i == sMyAppId) { - continue; - } - - ShellAppT *app = shellGetApp(i); - - if (app && app->state == AppStateRunningE) { - if (idx == sel) { - // Find the topmost window for this app - for (int32_t j = sAc->stack.count - 1; j >= 0; j--) { - WindowT *win = sAc->stack.windows[j]; - - if (win->appId == i) { - if (win->minimized) { - wmRestoreMinimized(&sAc->stack, &sAc->dirty, win); - } - - wmRaiseWindow(&sAc->stack, &sAc->dirty, j); - wmSetFocus(&sAc->stack, &sAc->dirty, sAc->stack.count - 1); - return; - } - } - - return; - } - - idx++; - } - } -} - - -// Rebuild the Task Manager list view from the shell's app slot table. -// Uses static arrays because the list view data pointers must remain valid -// until the next call to wgtListViewSetData (the widget doesn't copy strings). -static void refreshTaskList(void) { - if (!sTmListView) { - return; - } - - // Flat array of cell strings: [row0_col0..col3, row1_col0..col3, ...] - static const char *cells[SHELL_MAX_APPS * TM_COL_COUNT]; - static char typeStrs[SHELL_MAX_APPS][12]; - static char fileStrs[SHELL_MAX_APPS][64]; - int32_t rowCount = 0; - - for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { - if (i == sMyAppId) { - continue; - } - - ShellAppT *app = shellGetApp(i); - - if (app && app->state == AppStateRunningE) { - int32_t base = rowCount * TM_COL_COUNT; - - // Column 0: Name (from appDescriptor) - cells[base] = app->name; - - // Column 1: Filename (basename of .app path) - const char *slash = strrchr(app->path, '/'); - const char *back = strrchr(app->path, '\\'); - - if (back > slash) { - slash = back; - } - - const char *fname = slash ? slash + 1 : app->path; - snprintf(fileStrs[rowCount], sizeof(fileStrs[rowCount]), "%.63s", fname); - cells[base + 1] = fileStrs[rowCount]; - - // Column 2: Type (main-loop task vs callback-only) - snprintf(typeStrs[rowCount], sizeof(typeStrs[rowCount]), "%s", app->hasMainLoop ? "Task" : "Callback"); - cells[base + 2] = typeStrs[rowCount]; - - // Column 3: Status - cells[base + 3] = "Running"; - rowCount++; - } - } - - wgtListViewSetData(sTmListView, cells, rowCount); -} - - // Top-level scan entry point. Recursively walks apps/ looking for .app files. // The apps/ path is relative to the working directory, which the shell sets // to the root of the DVX install before loading any apps. @@ -681,14 +442,7 @@ static void updateStatusText(void) { } static char buf[64]; - - // shellRunningAppCount() includes us. Subtract 1 so the user sees - // only the apps they launched, not the Program Manager itself. - int32_t count = shellRunningAppCount() - 1; - - if (count < 0) { - count = 0; - } + int32_t count = shellRunningAppCount(); if (count == 0) { snprintf(buf, sizeof(buf), "No applications running"); @@ -712,7 +466,6 @@ static void updateStatusText(void) { int32_t appMain(DxeAppContextT *ctx) { sCtx = ctx; sAc = ctx->shellCtx; - sMyAppId = ctx->appId; scanAppsDir(); buildPmWindow(); @@ -721,5 +474,7 @@ int32_t appMain(DxeAppContextT *ctx) { // bar and Task Manager stay current without polling shellRegisterDesktopUpdate(desktopUpdate); + + return 0; } diff --git a/dvx/Makefile b/dvx/Makefile index 544eb7a..54a929f 100644 --- a/dvx/Makefile +++ b/dvx/Makefile @@ -5,19 +5,17 @@ CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib AR = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ar RANLIB = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ranlib -CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 +CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../tasks OBJDIR = ../obj/dvx WOBJDIR = ../obj/dvx/widgets POBJDIR = ../obj/dvx/platform -TOBJDIR = ../obj/dvx/thirdparty LIBDIR = ../lib SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c dvxApp.c dvxDialog.c dvxPrefs.c PSRCS = platform/dvxPlatformDos.c -TSRCS = thirdparty/ini/src/ini.c WSRCS = widgets/widgetAnsiTerm.c \ widgets/widgetClass.c \ @@ -54,15 +52,14 @@ WSRCS = widgets/widgetAnsiTerm.c \ OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) POBJS = $(patsubst platform/%.c,$(POBJDIR)/%.o,$(PSRCS)) WOBJS = $(patsubst widgets/%.c,$(WOBJDIR)/%.o,$(WSRCS)) -TOBJS = $(TOBJDIR)/ini.o TARGET = $(LIBDIR)/libdvx.a .PHONY: all clean all: $(TARGET) -$(TARGET): $(OBJS) $(POBJS) $(WOBJS) $(TOBJS) | $(LIBDIR) - $(AR) rcs $@ $(OBJS) $(POBJS) $(WOBJS) $(TOBJS) +$(TARGET): $(OBJS) $(POBJS) $(WOBJS) | $(LIBDIR) + $(AR) rcs $@ $(OBJS) $(POBJS) $(WOBJS) $(RANLIB) $@ $(OBJDIR)/%.o: %.c | $(OBJDIR) @@ -74,9 +71,6 @@ $(POBJDIR)/%.o: platform/%.c | $(POBJDIR) $(WOBJDIR)/%.o: widgets/%.c | $(WOBJDIR) $(CC) $(CFLAGS) -c -o $@ $< -$(TOBJDIR)/ini.o: thirdparty/ini/src/ini.c | $(TOBJDIR) - $(CC) $(CFLAGS) -c -o $@ $< - $(OBJDIR): mkdir -p $(OBJDIR) @@ -86,9 +80,6 @@ $(POBJDIR): $(WOBJDIR): mkdir -p $(WOBJDIR) -$(TOBJDIR): - mkdir -p $(TOBJDIR) - $(LIBDIR): mkdir -p $(LIBDIR) @@ -102,13 +93,12 @@ $(OBJDIR)/dvxImageWrite.o: dvxImageWrite.c thirdparty/stb_image_write.h $(OBJDIR)/dvxApp.o: dvxApp.c dvxApp.h platform/dvxPlatform.h dvxTypes.h dvxVideo.h dvxDraw.h dvxComp.h dvxWm.h dvxFont.h dvxCursor.h $(OBJDIR)/dvxDialog.o: dvxDialog.c dvxDialog.h platform/dvxPlatform.h dvxApp.h dvxWidget.h widgets/widgetInternal.h dvxTypes.h dvxDraw.h -$(OBJDIR)/dvxPrefs.o: dvxPrefs.c dvxPrefs.h thirdparty/ini/src/ini.h +$(OBJDIR)/dvxPrefs.o: dvxPrefs.c dvxPrefs.h # Platform file dependencies $(POBJDIR)/dvxPlatformDos.o: platform/dvxPlatformDos.c platform/dvxPlatform.h dvxTypes.h dvxPalette.h # Thirdparty file dependencies -$(TOBJDIR)/ini.o: thirdparty/ini/src/ini.c thirdparty/ini/src/ini.h # Widget file dependencies WIDGET_DEPS = widgets/widgetInternal.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h @@ -145,4 +135,4 @@ $(WOBJDIR)/widgetScrollbar.o: widgets/widgetScrollbar.c $(WIDGET_DEPS) $(WOBJDIR)/widgetTreeView.o: widgets/widgetTreeView.c $(WIDGET_DEPS) clean: - rm -f $(OBJS) $(POBJS) $(WOBJS) $(TOBJS) $(TARGET) + rm -f $(OBJS) $(POBJS) $(WOBJS) $(TARGET) diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index b705cea..474405c 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -461,8 +461,18 @@ static void compositeAndFlush(AppContextT *ctx) { // Set clip rect to this dirty rect setClipRect(d, dr->x, dr->y, dr->w, dr->h); - // 1. Draw desktop background - rectFill(d, ops, dr->x, dr->y, dr->w, dr->h, ctx->colors.desktop); + // 1. Draw desktop background (wallpaper or solid color) + if (ctx->wallpaperBuf) { + int32_t bytesPerPx = d->format.bitsPerPixel / 8; + + 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); + } + } else { + rectFill(d, ops, dr->x, dr->y, dr->w, dr->h, ctx->colors.desktop); + } // 2. Draw minimized window icons (under all windows) wmDrawMinimizedIcons(d, ops, &ctx->colors, ws, dr); @@ -1373,6 +1383,86 @@ const char *dvxClipboardGet(int32_t *outLen) { } +// ============================================================ +// Color scheme — name table and indexed access +// ============================================================ + +static const char *sColorNames[ColorCountE] = { + "desktop", "windowFace", "windowHighlight", + "windowShadow", "activeTitleBg", "activeTitleFg", + "inactiveTitleBg", "inactiveTitleFg", "contentBg", + "contentFg", "menuBg", "menuFg", + "menuHighlightBg", "menuHighlightFg", "buttonFace", + "scrollbarBg", "scrollbarFg", "scrollbarTrough" +}; + +// Default GEOS Ensemble Motif-style colors (RGB triplets) +static const uint8_t sDefaultColors[ColorCountE][3] = { + { 0, 128, 128}, // desktop — GEOS teal + {192, 192, 192}, // windowFace + {255, 255, 255}, // windowHighlight + {128, 128, 128}, // windowShadow + { 48, 48, 48}, // activeTitleBg — dark charcoal + {255, 255, 255}, // activeTitleFg + {160, 160, 160}, // inactiveTitleBg + { 64, 64, 64}, // inactiveTitleFg + {192, 192, 192}, // contentBg + { 0, 0, 0}, // contentFg + {192, 192, 192}, // menuBg + { 0, 0, 0}, // menuFg + { 48, 48, 48}, // menuHighlightBg + {255, 255, 255}, // menuHighlightFg + {192, 192, 192}, // buttonFace + {192, 192, 192}, // scrollbarBg + {128, 128, 128}, // scrollbarFg + {160, 160, 160}, // scrollbarTrough +}; + +// Access the packed color value in ColorSchemeT by index. +static uint32_t *colorSlot(ColorSchemeT *cs, ColorIdE id) { + return ((uint32_t *)cs) + (int32_t)id; +} + + +// ============================================================ +// dvxColorName +// ============================================================ + +const char *dvxColorName(ColorIdE id) { + if (id < 0 || id >= ColorCountE) { + return "unknown"; + } + + return sColorNames[id]; +} + + +// ============================================================ +// dvxApplyColorScheme +// ============================================================ + +void dvxApplyColorScheme(AppContextT *ctx) { + DisplayT *d = &ctx->display; + + for (int32_t i = 0; i < ColorCountE; i++) { + *colorSlot(&ctx->colors, (ColorIdE)i) = packColor(d, ctx->colorRgb[i][0], ctx->colorRgb[i][1], ctx->colorRgb[i][2]); + } + + // Repaint everything + dirtyListAdd(&ctx->dirty, 0, 0, d->width, d->height); +} + + +// ============================================================ +// dvxResetColorScheme +// ============================================================ + +void dvxResetColorScheme(AppContextT *ctx) { + memcpy(ctx->colorRgb, sDefaultColors, sizeof(sDefaultColors)); + dvxApplyColorScheme(ctx); +} + + // ============================================================ // dvxCascadeWindows // ============================================================ @@ -1423,6 +1513,124 @@ void dvxCascadeWindows(AppContextT *ctx) { } +// ============================================================ +// dvxChangeVideoMode +// ============================================================ +// +// Live video mode switch. Saves the old display state, attempts to +// set the new mode, and if successful, reinitializes all dependent +// subsystems: blit ops, colors, cursors, mouse range, wallpaper, +// and all window content buffers. Windows larger than the new +// screen are clamped. On failure, the old mode is restored. + +int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) { + // Save old state for rollback + DisplayT oldDisplay = ctx->display; + + // Stash old wallpaper (don't free — we may need it for rollback) + uint8_t *oldWpBuf = ctx->wallpaperBuf; + int32_t oldWpPitch = ctx->wallpaperPitch; + ctx->wallpaperBuf = NULL; + ctx->wallpaperPitch = 0; + + // Free old video buffers (no text mode restore) + platformVideoFreeBuffers(&ctx->display); + + // Try the new mode + if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) { + // Restore old mode + ctx->display = oldDisplay; + ctx->display.backBuf = NULL; + ctx->display.palette = NULL; + + if (videoInit(&ctx->display, oldDisplay.width, oldDisplay.height, oldDisplay.format.bitsPerPixel) != 0) { + // Both failed — catastrophic + return -1; + } + + // Restore wallpaper + ctx->wallpaperBuf = oldWpBuf; + ctx->wallpaperPitch = oldWpPitch; + drawInit(&ctx->blitOps, &ctx->display); + dvxApplyColorScheme(ctx); + return -1; + } + + // New mode succeeded — free old wallpaper + free(oldWpBuf); + + // Reinit blit ops for new pixel format + drawInit(&ctx->blitOps, &ctx->display); + + // Repack all colors for new pixel format + dvxApplyColorScheme(ctx); + + // Repack cursor colors + ctx->cursorFg = packColor(&ctx->display, 255, 255, 255); + ctx->cursorBg = packColor(&ctx->display, 0, 0, 0); + + // Reinit mouse range + platformMouseInit(ctx->display.width, ctx->display.height); + ctx->hasMouseWheel = platformMouseWheelInit(); + + // Clamp mouse position to new screen + if (ctx->mouseX >= ctx->display.width) { + ctx->mouseX = ctx->display.width - 1; + } + + if (ctx->mouseY >= ctx->display.height) { + ctx->mouseY = ctx->display.height - 1; + } + + // Clamp and reallocate all window content buffers + for (int32_t i = 0; i < ctx->stack.count; i++) { + WindowT *win = ctx->stack.windows[i]; + + // Clamp window position to new screen bounds + if (win->x + win->w > ctx->display.width) { + win->x = ctx->display.width - win->w; + } + + if (win->x < 0) { + win->x = 0; + win->w = ctx->display.width; + } + + if (win->y + win->h > ctx->display.height) { + win->y = ctx->display.height - win->h; + } + + if (win->y < 0) { + win->y = 0; + win->h = ctx->display.height; + } + + // Clear maximized flag since screen size changed + win->maximized = false; + + wmUpdateContentRect(win); + wmReallocContentBuf(win, &ctx->display); + + if (win->onResize) { + win->onResize(win, win->contentW, win->contentH); + } + + if (win->onPaint) { + RectT fullRect = {0, 0, win->contentW, win->contentH}; + win->onPaint(win, &fullRect); + win->contentDirty = true; + } + } + + // Reset clip and dirty the full screen + resetClipRect(&ctx->display); + dirtyListInit(&ctx->dirty); + dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height); + + return 0; +} + + // ============================================================ // dvxCreateAccelTable // ============================================================ @@ -1548,6 +1756,244 @@ const BitmapFontT *dvxGetFont(const AppContextT *ctx) { } +// ============================================================ +// dvxGetColor +// ============================================================ + +void dvxGetColor(const AppContextT *ctx, ColorIdE id, uint8_t *r, uint8_t *g, uint8_t *b) { + if (id < 0 || id >= ColorCountE) { + *r = *g = *b = 0; + return; + } + + *r = ctx->colorRgb[id][0]; + *g = ctx->colorRgb[id][1]; + *b = ctx->colorRgb[id][2]; +} + + +// ============================================================ +// dvxLoadTheme +// ============================================================ + +bool dvxLoadTheme(AppContextT *ctx, const char *filename) { + FILE *fp = fopen(filename, "rb"); + + if (!fp) { + return false; + } + + char line[256]; + + while (fgets(line, sizeof(line), fp)) { + // Strip trailing whitespace + char *end = line + strlen(line) - 1; + + while (end >= line && (*end == '\r' || *end == '\n' || *end == ' ')) { + *end-- = '\0'; + } + + char *p = line; + + while (*p == ' ' || *p == '\t') { + p++; + } + + // Skip comments, blank lines, section headers + if (*p == '\0' || *p == ';' || *p == '#' || *p == '[') { + continue; + } + + // Parse key = r,g,b + char *eq = strchr(p, '='); + + if (!eq) { + continue; + } + + *eq = '\0'; + + // Trim key + char *key = p; + char *keyEnd = eq - 1; + + while (keyEnd >= key && (*keyEnd == ' ' || *keyEnd == '\t')) { + *keyEnd-- = '\0'; + } + + // Parse r,g,b + char *val = eq + 1; + + while (*val == ' ' || *val == '\t') { + val++; + } + + int32_t r; + int32_t g; + int32_t b; + + if (sscanf(val, "%d,%d,%d", &r, &g, &b) != 3) { + continue; + } + + // Find matching color name + for (int32_t i = 0; i < ColorCountE; i++) { + if (strcmp(key, sColorNames[i]) == 0) { + ctx->colorRgb[i][0] = (uint8_t)r; + ctx->colorRgb[i][1] = (uint8_t)g; + ctx->colorRgb[i][2] = (uint8_t)b; + break; + } + } + } + + fclose(fp); + dvxApplyColorScheme(ctx); + return true; +} + + +// ============================================================ +// dvxSaveTheme +// ============================================================ + +bool dvxSaveTheme(const AppContextT *ctx, const char *filename) { + FILE *fp = fopen(filename, "wb"); + + if (!fp) { + return false; + } + + fprintf(fp, "; DVX Color Theme\r\n\r\n[colors]\r\n"); + + for (int32_t i = 0; i < ColorCountE; i++) { + fprintf(fp, "%-20s = %d,%d,%d\r\n", sColorNames[i], ctx->colorRgb[i][0], ctx->colorRgb[i][1], ctx->colorRgb[i][2]); + } + + fclose(fp); + return true; +} + + +// ============================================================ +// dvxSetColor +// ============================================================ + +void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b) { + if (id < 0 || id >= ColorCountE) { + return; + } + + ctx->colorRgb[id][0] = r; + ctx->colorRgb[id][1] = g; + ctx->colorRgb[id][2] = b; + + *colorSlot(&ctx->colors, id) = packColor(&ctx->display, r, g, b); + dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height); +} + + +// ============================================================ +// dvxSetWallpaper +// ============================================================ + +bool dvxSetWallpaper(AppContextT *ctx, const char *path) { + // Free existing wallpaper + free(ctx->wallpaperBuf); + ctx->wallpaperBuf = NULL; + ctx->wallpaperPitch = 0; + + if (!path) { + dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height); + return true; + } + + int32_t imgW; + int32_t imgH; + int32_t channels; + uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3); + + if (!rgb) { + return false; + } + + // Pre-scale to screen dimensions using bilinear interpolation and + // convert to native pixel format. Bilinear samples the 4 nearest + // source pixels and blends by fractional distance, producing smooth + // gradients instead of blocky nearest-neighbor artifacts. Uses + // 8-bit fixed-point weights (256 = 1.0) to avoid floating point. + int32_t screenW = ctx->display.width; + int32_t screenH = ctx->display.height; + int32_t bpp = ctx->display.format.bitsPerPixel; + int32_t pitch = screenW * (bpp / 8); + uint8_t *buf = (uint8_t *)malloc(pitch * screenH); + + if (!buf) { + stbi_image_free(rgb); + return false; + } + + int32_t srcStride = imgW * 3; + + for (int32_t y = 0; y < screenH; y++) { + // Fixed-point source Y: 16.16 + int32_t srcYfp = (int32_t)((int64_t)y * imgH * 65536 / screenH); + int32_t sy0 = srcYfp >> 16; + int32_t sy1 = sy0 + 1; + int32_t fy = (srcYfp >> 8) & 0xFF; // fractional Y (0-255) + int32_t ify = 256 - fy; + uint8_t *dst = buf + y * pitch; + + if (sy1 >= imgH) { + sy1 = imgH - 1; + } + + uint8_t *row0 = rgb + sy0 * srcStride; + uint8_t *row1 = rgb + sy1 * srcStride; + + for (int32_t x = 0; x < screenW; x++) { + int32_t srcXfp = (int32_t)((int64_t)x * imgW * 65536 / screenW); + int32_t sx0 = srcXfp >> 16; + int32_t sx1 = sx0 + 1; + int32_t fx = (srcXfp >> 8) & 0xFF; + int32_t ifx = 256 - fx; + + if (sx1 >= imgW) { + sx1 = imgW - 1; + } + + // Sample 4 source pixels + uint8_t *p00 = row0 + sx0 * 3; + uint8_t *p10 = row0 + sx1 * 3; + uint8_t *p01 = row1 + sx0 * 3; + uint8_t *p11 = row1 + sx1 * 3; + + // Bilinear blend (8-bit fixed-point, 256 = 1.0) + 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(&ctx->display, (uint8_t)r, (uint8_t)g, (uint8_t)b); + + if (bpp == 8) { + dst[x] = (uint8_t)px; + } else if (bpp == 15 || bpp == 16) { + ((uint16_t *)dst)[x] = (uint16_t)px; + } else { + ((uint32_t *)dst)[x] = px; + } + } + } + + stbi_image_free(rgb); + + ctx->wallpaperBuf = buf; + ctx->wallpaperPitch = pitch; + dirtyListAdd(&ctx->dirty, 0, 0, screenW, screenH); + return true; +} + + // ============================================================ // dvxInit // ============================================================ @@ -1912,6 +2358,8 @@ void dvxShutdown(AppContextT *ctx) { wmDestroyWindow(&ctx->stack, ctx->stack.windows[ctx->stack.count - 1]); } + free(ctx->wallpaperBuf); + ctx->wallpaperBuf = NULL; videoShutdown(&ctx->display); } @@ -1922,6 +2370,10 @@ void dvxShutdown(AppContextT *ctx) { void dvxSetTitle(AppContextT *ctx, WindowT *win, const char *title) { wmSetTitle(win, &ctx->dirty, title); + + if (ctx->onTitleChange) { + ctx->onTitleChange(ctx->titleChangeCtx); + } } @@ -2182,7 +2634,11 @@ static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd) { case SysMenuMinimizeE: if (ctx->modalWindow != win) { wmMinimize(&ctx->stack, &ctx->dirty, win); - dirtyListAdd(&ctx->dirty, 0, ctx->display.height - ICON_TOTAL_SIZE - ICON_SPACING, ctx->display.width, ICON_TOTAL_SIZE + ICON_SPACING); + + int32_t iconY; + int32_t iconH; + wmMinimizedIconRect(&ctx->stack, &ctx->display, &iconY, &iconH); + dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH); } break; @@ -2259,10 +2715,11 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t if (ctx->lastIconClickId == iconWin->id && (now - ctx->lastIconClickTime) < ctx->dblClickTicks) { // Double-click: restore minimized window - // Dirty the entire icon strip area - dirtyListAdd(&ctx->dirty, 0, - ctx->display.height - ICON_TOTAL_SIZE - ICON_SPACING, - ctx->display.width, ICON_TOTAL_SIZE + ICON_SPACING); + // Dirty the entire icon area (may span multiple rows) + int32_t iconY; + int32_t iconH; + wmMinimizedIconRect(&ctx->stack, &ctx->display, &iconY, &iconH); + dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH); wmRestoreMinimized(&ctx->stack, &ctx->dirty, iconWin); ctx->lastIconClickId = -1; } else { @@ -2382,10 +2839,11 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t case HIT_MINIMIZE: if (ctx->modalWindow != win) { wmMinimize(&ctx->stack, &ctx->dirty, win); - // Dirty the icon strip area so the new icon gets drawn - dirtyListAdd(&ctx->dirty, 0, - ctx->display.height - ICON_TOTAL_SIZE - ICON_SPACING, - ctx->display.width, ICON_TOTAL_SIZE + ICON_SPACING); + + int32_t iconY; + int32_t iconH; + wmMinimizedIconRect(&ctx->stack, &ctx->display, &iconY, &iconH); + dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH); } break; @@ -2412,27 +2870,9 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t // GEOS's blue, giving DV/X its own identity. static void initColorScheme(AppContextT *ctx) { - DisplayT *d = &ctx->display; - - // GEOS Ensemble Motif-style color scheme - ctx->colors.desktop = packColor(d, 0, 128, 128); // GEOS teal desktop - ctx->colors.windowFace = packColor(d, 192, 192, 192); // standard Motif grey - ctx->colors.windowHighlight = packColor(d, 255, 255, 255); - ctx->colors.windowShadow = packColor(d, 128, 128, 128); - ctx->colors.activeTitleBg = packColor(d, 48, 48, 48); // GEOS dark charcoal - ctx->colors.activeTitleFg = packColor(d, 255, 255, 255); - ctx->colors.inactiveTitleBg = packColor(d, 160, 160, 160); // lighter grey - ctx->colors.inactiveTitleFg = packColor(d, 64, 64, 64); - ctx->colors.contentBg = packColor(d, 192, 192, 192); - ctx->colors.contentFg = packColor(d, 0, 0, 0); - ctx->colors.menuBg = packColor(d, 192, 192, 192); - ctx->colors.menuFg = packColor(d, 0, 0, 0); - ctx->colors.menuHighlightBg = packColor(d, 48, 48, 48); - ctx->colors.menuHighlightFg = packColor(d, 255, 255, 255); - ctx->colors.buttonFace = packColor(d, 192, 192, 192); - ctx->colors.scrollbarBg = packColor(d, 192, 192, 192); - ctx->colors.scrollbarFg = packColor(d, 128, 128, 128); - ctx->colors.scrollbarTrough = packColor(d, 160, 160, 160); // GEOS lighter trough + // Load defaults into the RGB source array, then pack all at once + memcpy(ctx->colorRgb, sDefaultColors, sizeof(sDefaultColors)); + dvxApplyColorScheme(ctx); } @@ -2748,7 +3188,8 @@ 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. F10 — activate/toggle menu bar +// 3. Ctrl+Esc — system-wide hotkey (task manager) +// 4. 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) @@ -2822,6 +3263,15 @@ static void pollKeyboard(AppContextT *ctx) { continue; } + // Ctrl+Esc — system-wide hotkey (e.g. task manager) + if (scancode == 0x01 && ascii == 0x1B && (shiftFlags & KEY_MOD_CTRL)) { + if (ctx->onCtrlEsc) { + ctx->onCtrlEsc(ctx->ctrlEscCtx); + } + + continue; + } + // F10 — activate menu bar if (ascii == 0 && scancode == 0x44) { if (ctx->stack.focusedIdx >= 0) { @@ -3481,8 +3931,9 @@ static void refreshMinimizedIcons(AppContextT *ctx) { if (!win->iconData && win->contentDirty) { if (count >= ctx->iconRefreshIdx) { - int32_t ix = ICON_SPACING + iconIdx * (ICON_TOTAL_SIZE + ICON_SPACING); - int32_t iy = d->height - ICON_TOTAL_SIZE - ICON_SPACING; + int32_t ix; + int32_t iy; + wmMinimizedIconPos(d, iconIdx, &ix, &iy); dirtyListAdd(&ctx->dirty, ix, iy, ICON_TOTAL_SIZE, ICON_TOTAL_SIZE); win->contentDirty = false; ctx->iconRefreshIdx = count + 1; @@ -3721,66 +4172,77 @@ static void updateTooltip(AppContextT *ctx) { return; } - // Find the widget under the cursor - int32_t hitPart; - int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart); + // Check minimized icons first (they sit outside any window) + const char *tipText = NULL; + int32_t iconIdx = wmMinimizedIconHit(&ctx->stack, &ctx->display, mx, my); - if (hitIdx < 0 || hitPart != 0) { - return; + if (iconIdx >= 0) { + tipText = ctx->stack.windows[iconIdx]->title; } - WindowT *win = ctx->stack.windows[hitIdx]; + // Otherwise check widgets in the content area of a window + if (!tipText) { + int32_t hitPart; + int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart); - if (!win->widgetRoot) { - return; - } + if (hitIdx < 0 || hitPart != 0) { + return; + } - int32_t cx = mx - win->x - win->contentX; - int32_t cy = my - win->y - win->contentY; - int32_t scrollX = win->hScroll ? win->hScroll->value : 0; - int32_t scrollY = win->vScroll ? win->vScroll->value : 0; - int32_t vx = cx + scrollX; - int32_t vy = cy + scrollY; + WindowT *win = ctx->stack.windows[hitIdx]; - WidgetT *hit = widgetHitTest(win->widgetRoot, vx, vy); + if (!win->widgetRoot) { + return; + } - // Walk into NO_HIT_RECURSE containers to find deepest child - while (hit && hit->wclass && (hit->wclass->flags & WCLASS_NO_HIT_RECURSE)) { - WidgetT *inner = NULL; + int32_t cx = mx - win->x - win->contentX; + int32_t cy = my - win->y - win->contentY; + int32_t scrollX = win->hScroll ? win->hScroll->value : 0; + int32_t scrollY = win->vScroll ? win->vScroll->value : 0; + int32_t vx = cx + scrollX; + int32_t vy = cy + scrollY; - for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) { - if (c->visible && vx >= c->x && vx < c->x + c->w && vy >= c->y && vy < c->y + c->h) { - inner = c; + WidgetT *hit = widgetHitTest(win->widgetRoot, vx, vy); + + // Walk into NO_HIT_RECURSE containers to find deepest child + while (hit && hit->wclass && (hit->wclass->flags & WCLASS_NO_HIT_RECURSE)) { + WidgetT *inner = NULL; + + for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) { + if (c->visible && vx >= c->x && vx < c->x + c->w && vy >= c->y && vy < c->y + c->h) { + inner = c; + } + } + + if (!inner) { + break; + } + + if (inner->tooltip) { + hit = inner; + break; + } + + if (inner->wclass && (inner->wclass->flags & WCLASS_NO_HIT_RECURSE)) { + hit = inner; + } else { + WidgetT *deep = widgetHitTest(inner, vx, vy); + hit = deep ? deep : inner; + break; } } - if (!inner) { - break; + if (!hit || !hit->tooltip) { + return; } - // If the inner child has a tooltip, use it; otherwise check if it's another container - if (inner->tooltip) { - hit = inner; - break; - } - - if (inner->wclass && (inner->wclass->flags & WCLASS_NO_HIT_RECURSE)) { - hit = inner; - } else { - WidgetT *deep = widgetHitTest(inner, vx, vy); - hit = deep ? deep : inner; - break; - } - } - - if (!hit || !hit->tooltip) { - return; + tipText = hit->tooltip; } // Show the tooltip - ctx->tooltipText = hit->tooltip; + ctx->tooltipText = tipText; - int32_t tw = textWidth(&ctx->font, hit->tooltip) + TOOLTIP_PAD * 2; + int32_t tw = textWidth(&ctx->font, tipText) + TOOLTIP_PAD * 2; int32_t th = ctx->font.charHeight + TOOLTIP_PAD * 2; // Position below and right of cursor diff --git a/dvx/dvxApp.h b/dvx/dvxApp.h index 8efb386..743c8bf 100644 --- a/dvx/dvxApp.h +++ b/dvx/dvxApp.h @@ -78,6 +78,10 @@ typedef struct AppContextT { void (*idleCallback)(void *ctx); // called instead of yield when non-NULL void *idleCtx; WindowT *modalWindow; // if non-NULL, only this window receives input + void (*onCtrlEsc)(void *ctx); // system-wide Ctrl+Esc handler (e.g. task manager) + void *ctrlEscCtx; + void (*onTitleChange)(void *ctx); // called when any window title changes + void *titleChangeCtx; // Tooltip state — tooltip appears after the mouse hovers over a widget // with a tooltip string for a brief delay. Pre-computing W/H avoids // re-measuring on every paint frame. @@ -95,6 +99,12 @@ typedef struct AppContextT { // Mouse configuration (loaded from preferences) int32_t wheelDirection; // 1 = normal, -1 = reversed clock_t dblClickTicks; // double-click speed in clock() ticks + // Color scheme source RGB values (unpacked, for theme save/get) + uint8_t colorRgb[ColorCountE][3]; + // Wallpaper — pre-scaled to screen dimensions in native pixel format. + // NULL means no wallpaper (solid desktop color). + uint8_t *wallpaperBuf; // pixel data (screen width * height * bpp/8) + int32_t wallpaperPitch; // bytes per row } AppContextT; // Initialize the entire GUI stack: video mode, input devices, font, @@ -102,11 +112,51 @@ typedef struct AppContextT { // entry point for starting a DVX application. Returns 0 on success. int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp); +// Switch to a new video mode live. Reallocates the backbuffer, all +// window content buffers, repacks colors, rescales wallpaper, and +// repositions windows that would be off-screen. Returns 0 on success +// or -1 on failure (old mode is restored on failure). +int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp); + // Configure mouse behaviour. wheelDir: 1 = normal, -1 = reversed. // dblClickMs: double-click speed in milliseconds (e.g. 500). // accelThreshold: double-speed threshold in mickeys/sec (0 = don't change). void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold); +// ============================================================ +// Color scheme +// ============================================================ + +// Set a single color by ID. Repacks to native pixel format and +// invalidates the entire screen. +void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b); + +// Get a color's RGB values by ID. +void dvxGetColor(const AppContextT *ctx, ColorIdE id, uint8_t *r, uint8_t *g, uint8_t *b); + +// Apply all colors from ctx->colorRgb[] at once (repack + full repaint). +void dvxApplyColorScheme(AppContextT *ctx); + +// Reset all colors to the built-in defaults and repaint. +void dvxResetColorScheme(AppContextT *ctx); + +// Load a theme file (INI format with [colors] section) and apply it. +bool dvxLoadTheme(AppContextT *ctx, const char *filename); + +// Save the current color scheme to a theme file. +bool dvxSaveTheme(const AppContextT *ctx, const char *filename); + +// Return the INI key name for a color ID (e.g. "desktop", "windowFace"). +const char *dvxColorName(ColorIdE id); + +// ============================================================ +// Wallpaper +// ============================================================ + +// Load and apply a wallpaper image (stretched to screen). Pass NULL +// to clear the wallpaper and revert to the solid desktop color. +bool dvxSetWallpaper(AppContextT *ctx, const char *path); + // Tear down the GUI stack in reverse order: destroy all windows, restore // text mode, release input devices. Safe to call after a failed dvxInit(). void dvxShutdown(AppContextT *ctx); diff --git a/dvx/dvxPrefs.c b/dvx/dvxPrefs.c index 28cbdb4..46d9907 100644 --- a/dvx/dvxPrefs.c +++ b/dvx/dvxPrefs.c @@ -1,16 +1,128 @@ -// dvxPrefs.c — INI-based preferences system +// dvxPrefs.c — INI-based preferences system (read/write) // -// Thin wrapper around rxi/ini that adds typed accessors with defaults. - -#include -#include -#include +// Custom INI parser and writer. Stores entries as a dynamic array of +// section/key/value triples using stb_ds. Preserves insertion order +// on save so the file remains human-readable. #include "dvxPrefs.h" -#include "thirdparty/ini/src/ini.h" + +#include +#include +#include +#include + +// stb_ds dynamic arrays (implementation lives in libtasks.a) +#include "thirdparty/stb_ds.h" -static ini_t *sIni = NULL; +// ============================================================ +// Internal types +// ============================================================ + +typedef struct { + char *section; + char *key; + char *value; +} PrefsEntryT; + +// Comment lines are stored to preserve them on save. A comment has +// key=NULL and value=the full line text (including the ; prefix). +// Section headers have key=NULL and value=NULL. + +static PrefsEntryT *sEntries = NULL; // stb_ds dynamic array +static char *sFilePath = NULL; // path used by prefsLoad (for prefsSave) + + +// ============================================================ +// Helpers +// ============================================================ + +static char *dupStr(const char *s) { + if (!s) { + return NULL; + } + + size_t len = strlen(s); + char *d = (char *)malloc(len + 1); + + if (d) { + memcpy(d, s, len + 1); + } + + return d; +} + + +static void freeEntry(PrefsEntryT *e) { + free(e->section); + free(e->key); + free(e->value); + e->section = NULL; + e->key = NULL; + e->value = NULL; +} + + +// Case-insensitive string compare +static int strcmpci(const char *a, const char *b) { + for (;;) { + int d = tolower((unsigned char)*a) - tolower((unsigned char)*b); + + if (d != 0 || !*a) { + return d; + } + + a++; + b++; + } +} + + +// Find an entry by section+key (case-insensitive). Returns index or -1. +static int32_t findEntry(const char *section, const char *key) { + for (int32_t i = 0; i < arrlen(sEntries); i++) { + PrefsEntryT *e = &sEntries[i]; + + if (e->key && e->section && + strcmpci(e->section, section) == 0 && + strcmpci(e->key, key) == 0) { + return i; + } + } + + return -1; +} + + +// Find the index of a section header entry. Returns -1 if not found. +static int32_t findSection(const char *section) { + for (int32_t i = 0; i < arrlen(sEntries); i++) { + PrefsEntryT *e = &sEntries[i]; + + if (!e->key && !e->value && e->section && + strcmpci(e->section, section) == 0) { + return i; + } + } + + return -1; +} + + +// Trim leading/trailing whitespace in place. Returns pointer into buf. +static char *trimInPlace(char *buf) { + while (*buf == ' ' || *buf == '\t') { + buf++; + } + + char *end = buf + strlen(buf) - 1; + + while (end >= buf && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) { + *end-- = '\0'; + } + + return buf; +} // ============================================================ @@ -18,10 +130,14 @@ static ini_t *sIni = NULL; // ============================================================ void prefsFree(void) { - if (sIni) { - ini_free(sIni); - sIni = NULL; + for (int32_t i = 0; i < arrlen(sEntries); i++) { + freeEntry(&sEntries[i]); } + + arrfree(sEntries); + sEntries = NULL; + free(sFilePath); + sFilePath = NULL; } @@ -36,7 +152,6 @@ bool prefsGetBool(const char *section, const char *key, bool defaultVal) { return defaultVal; } - // Case-insensitive first character check covers true/yes/1 and false/no/0 char c = (char)tolower((unsigned char)val[0]); if (c == 't' || c == 'y' || c == '1') { @@ -78,13 +193,13 @@ int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal) { // ============================================================ const char *prefsGetString(const char *section, const char *key, const char *defaultVal) { - if (!sIni) { + int32_t idx = findEntry(section, key); + + if (idx < 0) { return defaultVal; } - const char *val = ini_get(sIni, section, key); - - return val ? val : defaultVal; + return sEntries[idx].value; } @@ -95,7 +210,231 @@ const char *prefsGetString(const char *section, const char *key, const char *def bool prefsLoad(const char *filename) { prefsFree(); - sIni = ini_load(filename); + FILE *fp = fopen(filename, "rb"); - return sIni != NULL; + if (!fp) { + return false; + } + + sFilePath = dupStr(filename); + + char line[512]; + char *currentSection = dupStr(""); + + while (fgets(line, sizeof(line), fp)) { + // Strip trailing whitespace/newline + char *end = line + strlen(line) - 1; + + while (end >= line && (*end == '\r' || *end == '\n' || *end == ' ' || *end == '\t')) { + *end-- = '\0'; + } + + char *p = line; + + // Skip leading whitespace + while (*p == ' ' || *p == '\t') { + p++; + } + + // Blank line — store as comment to preserve formatting + if (*p == '\0') { + PrefsEntryT e = {0}; + e.section = dupStr(currentSection); + e.value = dupStr(""); + arrput(sEntries, e); + continue; + } + + // Comment line + if (*p == ';' || *p == '#') { + PrefsEntryT e = {0}; + e.section = dupStr(currentSection); + e.value = dupStr(line); + arrput(sEntries, e); + continue; + } + + // Section header + if (*p == '[') { + char *close = strchr(p, ']'); + + if (close) { + *close = '\0'; + free(currentSection); + currentSection = dupStr(trimInPlace(p + 1)); + + PrefsEntryT e = {0}; + e.section = dupStr(currentSection); + arrput(sEntries, e); + } + + continue; + } + + // Key=value + char *eq = strchr(p, '='); + + if (eq) { + *eq = '\0'; + + PrefsEntryT e = {0}; + e.section = dupStr(currentSection); + e.key = dupStr(trimInPlace(p)); + e.value = dupStr(trimInPlace(eq + 1)); + arrput(sEntries, e); + } + } + + free(currentSection); + fclose(fp); + return true; +} + + +// ============================================================ +// prefsRemove +// ============================================================ + +void prefsRemove(const char *section, const char *key) { + int32_t idx = findEntry(section, key); + + if (idx >= 0) { + freeEntry(&sEntries[idx]); + arrdel(sEntries, idx); + } +} + + +// ============================================================ +// prefsSave +// ============================================================ + +bool prefsSave(void) { + if (!sFilePath) { + return false; + } + + return prefsSaveAs(sFilePath); +} + + +// ============================================================ +// prefsSaveAs +// ============================================================ + +bool prefsSaveAs(const char *filename) { + FILE *fp = fopen(filename, "wb"); + + if (!fp) { + return false; + } + + const char *lastSection = ""; + + for (int32_t i = 0; i < arrlen(sEntries); i++) { + PrefsEntryT *e = &sEntries[i]; + + // Comment or blank line (key=NULL, value=text or empty) + if (!e->key && e->value) { + fprintf(fp, "%s\r\n", e->value); + continue; + } + + // Section header (key=NULL, value=NULL) + if (!e->key && !e->value) { + fprintf(fp, "[%s]\r\n", e->section); + lastSection = e->section; + continue; + } + + // Key=value + if (e->key && e->value) { + fprintf(fp, "%s = %s\r\n", e->key, e->value); + } + } + + fclose(fp); + return true; +} + + +// ============================================================ +// prefsSetBool +// ============================================================ + +void prefsSetBool(const char *section, const char *key, bool value) { + prefsSetString(section, key, value ? "true" : "false"); +} + + +// ============================================================ +// prefsSetInt +// ============================================================ + +void prefsSetInt(const char *section, const char *key, int32_t value) { + char buf[32]; + snprintf(buf, sizeof(buf), "%ld", (long)value); + prefsSetString(section, key, buf); +} + + +// ============================================================ +// prefsSetString +// ============================================================ + +void prefsSetString(const char *section, const char *key, const char *value) { + int32_t idx = findEntry(section, key); + + if (idx >= 0) { + // Update existing entry + free(sEntries[idx].value); + sEntries[idx].value = dupStr(value); + return; + } + + // Find or create section header + int32_t secIdx = findSection(section); + + if (secIdx < 0) { + // Add blank line before new section (unless file is empty) + if (arrlen(sEntries) > 0) { + PrefsEntryT blank = {0}; + blank.section = dupStr(section); + blank.value = dupStr(""); + arrput(sEntries, blank); + } + + // Add section header + PrefsEntryT secEntry = {0}; + secEntry.section = dupStr(section); + arrput(sEntries, secEntry); + secIdx = arrlen(sEntries) - 1; + } + + // Find insertion point: after last entry in this section + int32_t insertAt = secIdx + 1; + + while (insertAt < arrlen(sEntries)) { + PrefsEntryT *e = &sEntries[insertAt]; + + // Stop if we've hit a different section header + if (!e->key && !e->value && e->section && + strcmpci(e->section, section) != 0) { + break; + } + + // Stop if we've hit an entry from a different section + if (e->section && strcmpci(e->section, section) != 0) { + break; + } + + insertAt++; + } + + // Insert new entry + PrefsEntryT newEntry = {0}; + newEntry.section = dupStr(section); + newEntry.key = dupStr(key); + newEntry.value = dupStr(value); + arrins(sEntries, insertAt, newEntry); } diff --git a/dvx/dvxPrefs.h b/dvx/dvxPrefs.h index c0ba614..5766cc8 100644 --- a/dvx/dvxPrefs.h +++ b/dvx/dvxPrefs.h @@ -1,11 +1,9 @@ -// dvxPrefs.h — INI-based preferences system +// dvxPrefs.h — INI-based preferences system (read/write) // // Loads a configuration file at startup and provides typed accessors -// with caller-supplied defaults. The INI file is read-only at runtime; -// values are queried by section + key. If the file is missing or a key -// is absent, the default is returned silently. -// -// The underlying parser is rxi/ini (thirdparty/ini/src). +// with caller-supplied defaults. Values can be modified at runtime +// and saved back to disk. If the file is missing or a key is absent, +// getters return the default silently. #ifndef DVX_PREFS_H #define DVX_PREFS_H @@ -18,20 +16,36 @@ // Only one file may be loaded at a time; calling again frees the previous. bool prefsLoad(const char *filename); -// Release all memory held by the loaded INI file. +// Save the current in-memory state back to the file that was loaded. +// Returns true on success. +bool prefsSave(void); + +// Save the current in-memory state to a specific file. +bool prefsSaveAs(const char *filename); + +// Release all memory held by the preferences. void prefsFree(void); -// Retrieve a string value. Returns defaultVal if the section/key pair -// is not present. The returned pointer is valid until prefsFree(). +// Retrieve a string value. Returns defaultVal if the key is not present. +// The returned pointer is valid until the key is modified or prefsFree(). const char *prefsGetString(const char *section, const char *key, const char *defaultVal); -// Retrieve an integer value. Returns defaultVal if the section/key pair -// is not present or cannot be parsed. +// Retrieve an integer value. int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal); -// Retrieve a boolean value. Recognises "true", "yes", "1" as true and -// "false", "no", "0" as false (case-insensitive). Returns defaultVal -// for anything else or if the key is missing. +// Retrieve a boolean value. Recognises "true"/"yes"/"1" and "false"/"no"/"0". bool prefsGetBool(const char *section, const char *key, bool defaultVal); +// Set a string value. Creates the section and key if they don't exist. +void prefsSetString(const char *section, const char *key, const char *value); + +// Set an integer value. +void prefsSetInt(const char *section, const char *key, int32_t value); + +// Set a boolean value (stored as "true"/"false"). +void prefsSetBool(const char *section, const char *key, bool value); + +// Remove a key from a section. No-op if not found. +void prefsRemove(const char *section, const char *key); + #endif diff --git a/dvx/dvxTypes.h b/dvx/dvxTypes.h index 5ed7cee..9a61e1b 100644 --- a/dvx/dvxTypes.h +++ b/dvx/dvxTypes.h @@ -206,6 +206,30 @@ typedef struct { uint32_t scrollbarTrough; } ColorSchemeT; +// Color IDs for addressing individual colors in ColorSchemeT. +// Order matches the struct field order. +typedef enum { + ColorDesktopE, + ColorWindowFaceE, + ColorWindowHighlightE, + ColorWindowShadowE, + ColorActiveTitleBgE, + ColorActiveTitleFgE, + ColorInactiveTitleBgE, + ColorInactiveTitleFgE, + ColorContentBgE, + ColorContentFgE, + ColorMenuBgE, + ColorMenuFgE, + ColorMenuHighlightBgE, + ColorMenuHighlightFgE, + ColorButtonFaceE, + ColorScrollbarBgE, + ColorScrollbarFgE, + ColorScrollbarTroughE, + ColorCountE +} ColorIdE; + // ============================================================ // Dirty rectangle list // ============================================================ diff --git a/dvx/dvxWm.c b/dvx/dvxWm.c index e8268e1..67e9115 100644 --- a/dvx/dvxWm.c +++ b/dvx/dvxWm.c @@ -97,7 +97,7 @@ static void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT * static void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, int32_t dir); static void drawTitleBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win); static void drawTitleGadget(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size); -static void minimizedIconPos(const DisplayT *d, int32_t index, int32_t *x, int32_t *y); +// wmMinimizedIconPos declared in dvxWm.h static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32_t *thumbSize); static void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH); @@ -720,17 +720,61 @@ static void freeMenuRecursive(MenuT *menu) { // ============================================================ // // Computes the screen position of a minimized window icon. Icons are laid -// out in a horizontal strip along the bottom of the screen, left to right. -// This mirrors the DESQview/X and Windows 3.x convention of showing minimized -// windows as icons at the bottom of the desktop. +// out left to right along the bottom of the screen, wrapping to the next +// row upward when the current row is full. This mirrors the Windows 3.x +// convention. If rows wrap past the top of the screen the icons are simply +// clipped by the compositor. // // The index is the ordinal among minimized windows (not the stack index), // so icon positions stay packed when non-minimized windows exist between // minimized ones in the stack. This avoids gaps in the icon strip. -static void minimizedIconPos(const DisplayT *d, int32_t index, int32_t *x, int32_t *y) { - *x = ICON_SPACING + index * (ICON_TOTAL_SIZE + ICON_SPACING); - *y = d->height - ICON_TOTAL_SIZE - ICON_SPACING; +void wmMinimizedIconPos(const DisplayT *d, int32_t index, int32_t *x, int32_t *y) { + int32_t cellW = ICON_TOTAL_SIZE + ICON_SPACING; + int32_t perRow = (d->width - ICON_SPACING) / cellW; + + if (perRow < 1) { + perRow = 1; + } + + int32_t col = index % perRow; + int32_t row = index / perRow; + + *x = ICON_SPACING + col * cellW; + *y = d->height - (row + 1) * (ICON_TOTAL_SIZE + ICON_SPACING); +} + + +// ============================================================ +// wmMinimizedIconRect +// ============================================================ + +void wmMinimizedIconRect(const WindowStackT *stack, const DisplayT *d, int32_t *ry, int32_t *rh) { + int32_t count = 0; + + for (int32_t i = 0; i < stack->count; i++) { + if (stack->windows[i]->visible && stack->windows[i]->minimized) { + count++; + } + } + + if (count == 0) { + *ry = d->height; + *rh = 0; + return; + } + + // Find the topmost icon position (last icon has highest row index) + int32_t topX; + int32_t topY; + wmMinimizedIconPos(d, count - 1, &topX, &topY); + + if (topY < 0) { + topY = 0; + } + + *ry = topY; + *rh = d->height - topY; } @@ -1384,7 +1428,7 @@ void wmDrawMinimizedIcons(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT * int32_t ix; int32_t iy; - minimizedIconPos(d, iconIdx, &ix, &iy); + wmMinimizedIconPos(d, iconIdx, &ix, &iy); iconIdx++; // Check if icon intersects clip rect @@ -1713,7 +1757,7 @@ int32_t wmMinimizedIconHit(const WindowStackT *stack, const DisplayT *d, int32_t int32_t ix; int32_t iy; - minimizedIconPos(d, iconIdx, &ix, &iy); + wmMinimizedIconPos(d, iconIdx, &ix, &iy); iconIdx++; if (mx >= ix && mx < ix + ICON_TOTAL_SIZE && diff --git a/dvx/dvxWm.h b/dvx/dvxWm.h index 1a34c42..f709be1 100644 --- a/dvx/dvxWm.h +++ b/dvx/dvxWm.h @@ -179,6 +179,14 @@ void wmMinimize(WindowStackT *stack, DirtyListT *dl, WindowT *win); // Returns stack index of the minimized window, or -1 int32_t wmMinimizedIconHit(const WindowStackT *stack, const DisplayT *d, int32_t mx, int32_t my); +// Compute screen position of a minimized icon by ordinal index. +// Icons wrap into rows from bottom to top when the screen is full. +void wmMinimizedIconPos(const DisplayT *d, int32_t index, int32_t *x, int32_t *y); + +// Compute the screen rect covering all minimized icon rows. +// Used to dirty the icon area when windows are minimized or restored. +void wmMinimizedIconRect(const WindowStackT *stack, const DisplayT *d, int32_t *y, int32_t *h); + // Restore a maximized window to its pre-maximize geometry void wmRestore(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT *win); diff --git a/dvx/platform/dvxPlatform.h b/dvx/platform/dvxPlatform.h index 24425a8..503ad7f 100644 --- a/dvx/platform/dvxPlatform.h +++ b/dvx/platform/dvxPlatform.h @@ -200,14 +200,30 @@ const char *platformGetSystemInfo(const DisplayT *display); // describing why it's invalid. Used by the file dialog's save-as validation. const char *platformValidateFilename(const char *name); +// Query current system memory. Sets totalKb and freeKb to the total +// and free physical memory in kilobytes. Returns false if unavailable. +bool platformGetMemoryInfo(uint32_t *totalKb, uint32_t *freeKb); + // Change the working directory, including drive letter on DOS. Standard // chdir() does not switch drives under DJGPP; this wrapper calls setdisk() // first when the path contains a drive prefix (e.g. "A:\DVX"). void platformChdir(const char *path); +// Free the backbuffer and palette without restoring text mode. Used +// when switching between graphics modes live. +void platformVideoFreeBuffers(DisplayT *d); + // Return a pointer to the last directory separator in path, or NULL if // none is found. On DOS this checks both '/' and '\\' since DJGPP // accepts either. On other platforms only '/' is recognised. char *platformPathDirEnd(const char *path); +// Return the platform's native line ending string. +// "\r\n" on DOS/Windows, "\n" on Unix/Linux. +const char *platformLineEnding(void); + +// Strip platform-specific line ending characters from a buffer in place. +// On DOS this removes '\r' from CR+LF pairs. Returns the new length. +int32_t platformStripLineEndings(char *buf, int32_t len); + #endif // DVX_PLATFORM_H diff --git a/dvx/platform/dvxPlatformDos.c b/dvx/platform/dvxPlatformDos.c index 3d291c8..ec605d2 100644 --- a/dvx/platform/dvxPlatformDos.c +++ b/dvx/platform/dvxPlatformDos.c @@ -1032,6 +1032,27 @@ const char *platformGetSystemInfo(const DisplayT *display) { } +// ============================================================ +// platformGetMemoryInfo +// ============================================================ + +bool platformGetMemoryInfo(uint32_t *totalKb, uint32_t *freeKb) { + __dpmi_free_mem_info memInfo; + + if (__dpmi_get_free_memory_information(&memInfo) != 0) { + *totalKb = 0; + *freeKb = 0; + return false; + } + + *totalKb = (memInfo.total_number_of_physical_pages != 0xFFFFFFFFUL) + ? memInfo.total_number_of_physical_pages * 4 : 0; + *freeKb = (memInfo.total_number_of_free_pages != 0xFFFFFFFFUL) + ? memInfo.total_number_of_free_pages * 4 : 0; + return true; +} + + // ============================================================ // platformInit // ============================================================ @@ -1114,6 +1135,33 @@ bool platformKeyboardRead(PlatformKeyEventT *evt) { } +// ============================================================ +// platformLineEnding +// ============================================================ + +const char *platformLineEnding(void) { + return "\r\n"; +} + + +// ============================================================ +// platformStripLineEndings +// ============================================================ + +int32_t platformStripLineEndings(char *buf, int32_t len) { + int32_t dst = 0; + + for (int32_t src = 0; src < len; src++) { + if (buf[src] != '\r') { + buf[dst++] = buf[src]; + } + } + + buf[dst] = '\0'; + return dst; +} + + // ============================================================ // platformMouseInit // ============================================================ @@ -1705,6 +1753,21 @@ void platformVideoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t cou // Also frees the backbuffer, palette, and disables near pointers // (re-enables DJGPP's segment limit checking for safety). +void platformVideoFreeBuffers(DisplayT *d) { + if (d->backBuf) { + free(d->backBuf); + d->backBuf = NULL; + } + + if (d->palette) { + free(d->palette); + d->palette = NULL; + } + + d->lfb = NULL; +} + + void platformVideoShutdown(DisplayT *d) { // INT 10h function 00h, mode 03h = 80x25 color text __dpmi_regs r; diff --git a/dvx/thirdparty/ini/LICENSE b/dvx/thirdparty/ini/LICENSE deleted file mode 100644 index 5818e8d..0000000 --- a/dvx/thirdparty/ini/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2016 rxi - - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/dvx/thirdparty/ini/README.md b/dvx/thirdparty/ini/README.md deleted file mode 100644 index e0f330d..0000000 --- a/dvx/thirdparty/ini/README.md +++ /dev/null @@ -1,63 +0,0 @@ - -# ini -A *tiny* ANSI C library for loading .ini config files - -## Usage -The files **[ini.c](src/ini.c?raw=1)** and **[ini.h](src/ini.h?raw=1)** should -be dropped into an existing project. - -The library has support for sections, comment lines and quoted string values -(with escapes). Unquoted values and keys are trimmed of whitespace when loaded. - -```ini -; last modified 1 April 2001 by John Doe -[owner] -name = John Doe -organization = Acme Widgets Inc. - -[database] -; use IP address in case network name resolution is not working -server = 192.0.2.62 -port = 143 -file = "payroll.dat" -``` - -An ini file can be loaded into memory by using the `ini_load()` function. -`NULL` is returned if the file cannot be loaded. -```c -ini_t *config = ini_load("config.ini"); -``` - -The library provides two functions for retrieving values: the first is -`ini_get()`. Given a section and a key the corresponding value is returned if -it exists. If the `section` argument is `NULL` then all sections are searched. -```c -const char *name = ini_get(config, "owner", "name"); -if (name) { - printf("name: %s\n", name); -} -``` - -The second, `ini_sget()`, takes the same arguments as `ini_get()` with the -addition of a scanf format string and a pointer for where to store the value. -```c -const char *server = "default"; -int port = 80; - -ini_sget(config, "database", "server", NULL, &server); -ini_sget(config, "database", "port", "%d", &port); - -printf("server: %s:%d\n", server, port); -``` - -The `ini_free()` function is used to free the memory used by the `ini_t*` -object when we are done with it. Calling this function invalidates all string -pointers returned by the library. -```c -ini_free(config); -``` - - -## License -This library is free software; you can redistribute it and/or modify it under -the terms of the MIT license. See [LICENSE](LICENSE) for details. diff --git a/dvx/thirdparty/ini/src/ini.c b/dvx/thirdparty/ini/src/ini.c deleted file mode 100644 index ab5f11d..0000000 --- a/dvx/thirdparty/ini/src/ini.c +++ /dev/null @@ -1,274 +0,0 @@ -/** - * Copyright (c) 2016 rxi - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -#include -#include -#include -#include - -#include "ini.h" - -struct ini_t { - char *data; - char *end; -}; - - -/* Case insensitive string compare */ -static int strcmpci(const char *a, const char *b) { - for (;;) { - int d = tolower(*a) - tolower(*b); - if (d != 0 || !*a) { - return d; - } - a++, b++; - } -} - -/* Returns the next string in the split data */ -static char* next(ini_t *ini, char *p) { - p += strlen(p); - while (p < ini->end && *p == '\0') { - p++; - } - return p; -} - -static void trim_back(ini_t *ini, char *p) { - while (p >= ini->data && (*p == ' ' || *p == '\t' || *p == '\r')) { - *p-- = '\0'; - } -} - -static char* discard_line(ini_t *ini, char *p) { - while (p < ini->end && *p != '\n') { - *p++ = '\0'; - } - return p; -} - - -static char *unescape_quoted_value(ini_t *ini, char *p) { - /* Use `q` as write-head and `p` as read-head, `p` is always ahead of `q` - * as escape sequences are always larger than their resultant data */ - char *q = p; - p++; - while (p < ini->end && *p != '"' && *p != '\r' && *p != '\n') { - if (*p == '\\') { - /* Handle escaped char */ - p++; - switch (*p) { - default : *q = *p; break; - case 'r' : *q = '\r'; break; - case 'n' : *q = '\n'; break; - case 't' : *q = '\t'; break; - case '\r' : - case '\n' : - case '\0' : goto end; - } - - } else { - /* Handle normal char */ - *q = *p; - } - q++, p++; - } -end: - return q; -} - - -/* Splits data in place into strings containing section-headers, keys and - * values using one or more '\0' as a delimiter. Unescapes quoted values */ -static void split_data(ini_t *ini) { - char *value_start, *line_start; - char *p = ini->data; - - while (p < ini->end) { - switch (*p) { - case '\r': - case '\n': - case '\t': - case ' ': - *p = '\0'; - /* Fall through */ - - case '\0': - p++; - break; - - case '[': - p += strcspn(p, "]\n"); - *p = '\0'; - break; - - case ';': - p = discard_line(ini, p); - break; - - default: - line_start = p; - p += strcspn(p, "=\n"); - - /* Is line missing a '='? */ - if (*p != '=') { - p = discard_line(ini, line_start); - break; - } - trim_back(ini, p - 1); - - /* Replace '=' and whitespace after it with '\0' */ - do { - *p++ = '\0'; - } while (*p == ' ' || *p == '\r' || *p == '\t'); - - /* Is a value after '=' missing? */ - if (*p == '\n' || *p == '\0') { - p = discard_line(ini, line_start); - break; - } - - if (*p == '"') { - /* Handle quoted string value */ - value_start = p; - p = unescape_quoted_value(ini, p); - - /* Was the string empty? */ - if (p == value_start) { - p = discard_line(ini, line_start); - break; - } - - /* Discard the rest of the line after the string value */ - p = discard_line(ini, p); - - } else { - /* Handle normal value */ - p += strcspn(p, "\n"); - trim_back(ini, p - 1); - } - break; - } - } -} - - - -ini_t* ini_load(const char *filename) { - ini_t *ini = NULL; - FILE *fp = NULL; - int n, sz; - - /* Init ini struct */ - ini = malloc(sizeof(*ini)); - if (!ini) { - goto fail; - } - memset(ini, 0, sizeof(*ini)); - - /* Open file */ - fp = fopen(filename, "rb"); - if (!fp) { - goto fail; - } - - /* Get file size */ - fseek(fp, 0, SEEK_END); - sz = ftell(fp); - rewind(fp); - - /* Load file content into memory, null terminate, init end var */ - ini->data = malloc(sz + 1); - ini->data[sz] = '\0'; - ini->end = ini->data + sz; - n = fread(ini->data, 1, sz, fp); - if (n != sz) { - goto fail; - } - - /* Prepare data */ - split_data(ini); - - /* Clean up and return */ - fclose(fp); - return ini; - -fail: - if (fp) fclose(fp); - if (ini) ini_free(ini); - return NULL; -} - - -void ini_free(ini_t *ini) { - free(ini->data); - free(ini); -} - - -const char* ini_get(ini_t *ini, const char *section, const char *key) { - char *current_section = ""; - char *val; - char *p = ini->data; - - if (*p == '\0') { - p = next(ini, p); - } - - while (p < ini->end) { - if (*p == '[') { - /* Handle section */ - current_section = p + 1; - - } else { - /* Handle key */ - val = next(ini, p); - if (!section || !strcmpci(section, current_section)) { - if (!strcmpci(p, key)) { - return val; - } - } - p = val; - } - - p = next(ini, p); - } - - return NULL; -} - - -int ini_sget( - ini_t *ini, const char *section, const char *key, - const char *scanfmt, void *dst -) { - const char *val = ini_get(ini, section, key); - if (!val) { - return 0; - } - if (scanfmt) { - sscanf(val, scanfmt, dst); - } else { - *((const char**) dst) = val; - } - return 1; -} diff --git a/dvx/thirdparty/ini/src/ini.h b/dvx/thirdparty/ini/src/ini.h deleted file mode 100644 index cd6af9f..0000000 --- a/dvx/thirdparty/ini/src/ini.h +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2016 rxi - * - * This library is free software; you can redistribute it and/or modify it - * under the terms of the MIT license. See `ini.c` for details. - */ - -#ifndef INI_H -#define INI_H - -#define INI_VERSION "0.1.1" - -typedef struct ini_t ini_t; - -ini_t* ini_load(const char *filename); -void ini_free(ini_t *ini); -const char* ini_get(ini_t *ini, const char *section, const char *key); -int ini_sget(ini_t *ini, const char *section, const char *key, const char *scanfmt, void *dst); - -#endif diff --git a/dvxshell/Makefile b/dvxshell/Makefile index d055ce1..143b7de 100644 --- a/dvxshell/Makefile +++ b/dvxshell/Makefile @@ -11,15 +11,18 @@ LDFLAGS = -L../lib -ldvx -ltasks -lm OBJDIR = ../obj/dvxshell BINDIR = ../bin CONFIGDIR = ../bin/config +THEMEDIR = ../bin/config/themes LIBDIR = ../lib -SRCS = shellMain.c shellApp.c shellExport.c shellInfo.c +SRCS = shellMain.c shellApp.c shellExport.c shellInfo.c shellTaskMgr.c OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) TARGET = $(BINDIR)/dvx.exe .PHONY: all clean libs -all: libs $(TARGET) $(CONFIGDIR)/dvx.ini +THEMES = $(THEMEDIR)/geos.thm $(THEMEDIR)/win31.thm $(THEMEDIR)/cde.thm + +all: libs $(TARGET) $(CONFIGDIR)/dvx.ini $(THEMES) libs: $(MAKE) -C ../dvx @@ -46,11 +49,18 @@ $(BINDIR): $(CONFIGDIR): mkdir -p $(CONFIGDIR) +$(THEMEDIR): + mkdir -p $(THEMEDIR) + +$(THEMEDIR)/%.thm: ../themes/%.thm | $(THEMEDIR) + sed 's/$$/\r/' $< > $@ + # 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 $(OBJDIR)/shellExport.o: shellExport.c shellApp.h shellInfo.h ../dvx/dvxApp.h ../dvx/dvxDialog.h ../dvx/dvxWidget.h ../dvx/dvxDraw.h ../dvx/dvxVideo.h ../dvx/dvxWm.h ../tasks/taskswitch.h $(OBJDIR)/shellInfo.o: shellInfo.c shellInfo.h shellApp.h ../dvx/dvxApp.h ../dvx/platform/dvxPlatform.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 $(CONFIGDIR) + rm -rf $(THEMEDIR) $(CONFIGDIR) diff --git a/dvxshell/shellApp.h b/dvxshell/shellApp.h index 37ac185..96f9150 100644 --- a/dvxshell/shellApp.h +++ b/dvxshell/shellApp.h @@ -172,12 +172,13 @@ void shellExportInit(void); #define SHELL_DESKTOP_APP "apps/progman/progman.app" // Register a callback for app state changes (load, reap, crash). -// The desktop app (Program Manager) calls this during appMain to receive -// notifications so it can refresh its task list / window list display. -// Only one callback is supported — the desktop is always loaded first -// and is the only consumer. +// Apps call this during appMain to receive notifications when app state +// changes (load, reap, crash). Multiple callbacks are supported. void shellRegisterDesktopUpdate(void (*updateFn)(void)); +// Remove a previously registered callback (call before app shutdown). +void shellUnregisterDesktopUpdate(void (*updateFn)(void)); + // Notify the desktop app that app state has changed (load, reap, crash). void shellDesktopUpdate(void); diff --git a/dvxshell/shellExport.c b/dvxshell/shellExport.c index 783389e..85b37b6 100644 --- a/dvxshell/shellExport.c +++ b/dvxshell/shellExport.c @@ -29,23 +29,43 @@ #include "shellApp.h" #include "shellInfo.h" +#include "shellTaskMgr.h" #include "dvxApp.h" #include "dvxDialog.h" #include "dvxWidget.h" #include "dvxDraw.h" #include "dvxPrefs.h" +#include "platform/dvxPlatform.h" #include "dvxVideo.h" #include "dvxWm.h" #include "taskswitch.h" #include #include +#include #include +#include +#include +#include +#include #include #include -#include -#include +#include #include +#include +#include + +// stb headers (no IMPLEMENTATION — symbols are in libdvx.a / libtasks.a) +#include "thirdparty/stb_image.h" +#include "thirdparty/stb_image_write.h" +#include "thirdparty/stb_ds.h" + +// DJGPP stdio internals — the stdin/stdout/stderr macros dereference +// these pointers. Without exporting them, any DXE that uses printf +// or fprintf gets an unresolved symbol. +extern FILE __dj_stdin; +extern FILE __dj_stdout; +extern FILE __dj_stderr; // ============================================================ // Prototypes @@ -144,14 +164,41 @@ DXE_EXPORT_TABLE(shellExportTable) { "_dvxCreateWindow", (void *)shellWrapCreateWindow }, { "_dvxDestroyWindow", (void *)shellWrapDestroyWindow }, + // dvxPlatform.h — platform abstraction + DXE_EXPORT(platformLineEnding) + DXE_EXPORT(platformChdir) + DXE_EXPORT(platformGetMemoryInfo) + DXE_EXPORT(platformMouseSetAccel) + DXE_EXPORT(platformMouseWarp) + DXE_EXPORT(platformPathDirEnd) + DXE_EXPORT(platformStripLineEndings) + DXE_EXPORT(platformValidateFilename) + DXE_EXPORT(platformVideoEnumModes) + // dvxPrefs.h — preferences DXE_EXPORT(prefsGetBool) DXE_EXPORT(prefsGetInt) DXE_EXPORT(prefsGetString) + DXE_EXPORT(prefsLoad) + DXE_EXPORT(prefsRemove) + DXE_EXPORT(prefsSave) + DXE_EXPORT(prefsSaveAs) + DXE_EXPORT(prefsSetBool) + DXE_EXPORT(prefsSetInt) + DXE_EXPORT(prefsSetString) // dvxApp.h — direct exports + DXE_EXPORT(dvxApplyColorScheme) + DXE_EXPORT(dvxChangeVideoMode) + DXE_EXPORT(dvxColorName) + DXE_EXPORT(dvxGetColor) DXE_EXPORT(dvxInit) + DXE_EXPORT(dvxLoadTheme) + DXE_EXPORT(dvxResetColorScheme) + DXE_EXPORT(dvxSaveTheme) + DXE_EXPORT(dvxSetColor) DXE_EXPORT(dvxSetMouseConfig) + DXE_EXPORT(dvxSetWallpaper) DXE_EXPORT(dvxShutdown) DXE_EXPORT(dvxUpdate) { "_dvxCreateWindowCentered", (void *)shellWrapCreateWindowCentered }, @@ -202,6 +249,8 @@ DXE_EXPORT_TABLE(shellExportTable) // dvxVideo.h DXE_EXPORT(packColor) + DXE_EXPORT(resetClipRect) + DXE_EXPORT(setClipRect) // dvxWm.h DXE_EXPORT(wmAddMenuBar) @@ -213,6 +262,8 @@ DXE_EXPORT_TABLE(shellExportTable) DXE_EXPORT(wmAddSubMenu) DXE_EXPORT(wmAddVScrollbar) DXE_EXPORT(wmAddHScrollbar) + DXE_EXPORT(wmMinimizedIconPos) + DXE_EXPORT(wmMinimizedIconRect) DXE_EXPORT(wmSetTitle) DXE_EXPORT(wmSetIcon) DXE_EXPORT(wmCreateMenu) @@ -389,9 +440,17 @@ DXE_EXPORT_TABLE(shellExportTable) // tsCreate/tsKill/etc. are NOT exported because apps should not // manipulate the task system directly — the shell manages task // lifecycle through shellLoadApp/shellForceKillApp. - DXE_EXPORT(tsYield) - DXE_EXPORT(tsCurrentId) DXE_EXPORT(tsActiveCount) + DXE_EXPORT(tsCreate) + DXE_EXPORT(tsCurrentId) + DXE_EXPORT(tsGetName) + DXE_EXPORT(tsGetPriority) + DXE_EXPORT(tsGetState) + DXE_EXPORT(tsKill) + DXE_EXPORT(tsPause) + DXE_EXPORT(tsResume) + DXE_EXPORT(tsSetPriority) + DXE_EXPORT(tsYield) // dvxWm.h — direct window management DXE_EXPORT(wmRaiseWindow) @@ -402,80 +461,222 @@ DXE_EXPORT_TABLE(shellExportTable) DXE_EXPORT(shellLog) DXE_EXPORT(shellLoadApp) DXE_EXPORT(shellGetApp) + DXE_EXPORT(shellTaskMgrOpen) DXE_EXPORT(shellForceKillApp) DXE_EXPORT(shellRunningAppCount) DXE_EXPORT(shellRegisterDesktopUpdate) + DXE_EXPORT(shellUnregisterDesktopUpdate) DXE_EXPORT(shellGetSystemInfo) - // libc exports below. DXE3 modules are compiled as relocatable objects, - // not fully linked executables. Any libc function the DXE calls must be - // re-exported here so the DXE3 loader can resolve the reference at - // dlopen time. Forgetting an entry produces a cryptic "unresolved - // symbol" error at load time — no lazy binding fallback exists. + // ================================================================ + // libc / libm exports. DXE3 modules are relocatable objects, not + // fully linked executables. Every C library function a DXE calls + // must appear here so the loader can resolve it at dlopen time. + // This is intentionally comprehensive to avoid "unresolved symbol" + // surprises when apps use standard functions. + // ================================================================ - // libc — memory - DXE_EXPORT(malloc) - DXE_EXPORT(free) + // --- memory --- DXE_EXPORT(calloc) + DXE_EXPORT(free) + DXE_EXPORT(malloc) DXE_EXPORT(realloc) - // libc — string - DXE_EXPORT(memcpy) - DXE_EXPORT(memset) - DXE_EXPORT(memmove) + // --- string / memory ops --- + DXE_EXPORT(memchr) DXE_EXPORT(memcmp) - DXE_EXPORT(strlen) - DXE_EXPORT(strcmp) - DXE_EXPORT(strncmp) - DXE_EXPORT(strcpy) - DXE_EXPORT(strncpy) + DXE_EXPORT(memcpy) + DXE_EXPORT(memmove) + DXE_EXPORT(memset) + DXE_EXPORT(strcasecmp) DXE_EXPORT(strcat) - DXE_EXPORT(strncat) DXE_EXPORT(strchr) + DXE_EXPORT(strcmp) + DXE_EXPORT(strcpy) + DXE_EXPORT(strcspn) + DXE_EXPORT(strdup) + DXE_EXPORT(strerror) + DXE_EXPORT(strlen) + DXE_EXPORT(strncasecmp) + DXE_EXPORT(strncat) + DXE_EXPORT(strncmp) + DXE_EXPORT(strncpy) + DXE_EXPORT(strpbrk) DXE_EXPORT(strrchr) + DXE_EXPORT(strspn) DXE_EXPORT(strstr) - DXE_EXPORT(strtol) + DXE_EXPORT(strtok) - // libc — I/O - DXE_EXPORT(printf) + // --- ctype --- + DXE_EXPORT(isalnum) + DXE_EXPORT(isalpha) + DXE_EXPORT(isdigit) + DXE_EXPORT(islower) + DXE_EXPORT(isprint) + DXE_EXPORT(ispunct) + DXE_EXPORT(isspace) + DXE_EXPORT(isupper) + DXE_EXPORT(isxdigit) + DXE_EXPORT(tolower) + DXE_EXPORT(toupper) + + // --- conversion --- + DXE_EXPORT(abs) + DXE_EXPORT(atof) + DXE_EXPORT(atoi) + DXE_EXPORT(atol) + DXE_EXPORT(labs) + DXE_EXPORT(strtod) + DXE_EXPORT(strtol) + DXE_EXPORT(strtoul) + + // --- formatted I/O --- DXE_EXPORT(fprintf) - DXE_EXPORT(sprintf) - DXE_EXPORT(snprintf) + DXE_EXPORT(fputs) + DXE_EXPORT(fscanf) + DXE_EXPORT(printf) DXE_EXPORT(puts) - DXE_EXPORT(fopen) - DXE_EXPORT(fclose) - DXE_EXPORT(fread) - DXE_EXPORT(fwrite) + DXE_EXPORT(snprintf) + DXE_EXPORT(sprintf) + DXE_EXPORT(sscanf) + DXE_EXPORT(vfprintf) + DXE_EXPORT(vprintf) + DXE_EXPORT(vsnprintf) + DXE_EXPORT(vsprintf) + + // --- character I/O --- + DXE_EXPORT(fgetc) DXE_EXPORT(fgets) - DXE_EXPORT(fseek) - DXE_EXPORT(ftell) + DXE_EXPORT(fputc) + DXE_EXPORT(getc) + DXE_EXPORT(putc) + DXE_EXPORT(putchar) + DXE_EXPORT(ungetc) + + // --- file I/O --- + DXE_EXPORT(fclose) DXE_EXPORT(feof) DXE_EXPORT(ferror) + DXE_EXPORT(fflush) + DXE_EXPORT(fopen) + DXE_EXPORT(fread) + DXE_EXPORT(freopen) + DXE_EXPORT(fseek) + DXE_EXPORT(ftell) + DXE_EXPORT(fwrite) + DXE_EXPORT(remove) + DXE_EXPORT(rename) + DXE_EXPORT(rewind) + DXE_EXPORT(tmpfile) + DXE_EXPORT(tmpnam) - // libc — math - DXE_EXPORT(sin) - DXE_EXPORT(cos) - DXE_EXPORT(sqrt) - - // libc — time - DXE_EXPORT(clock) - DXE_EXPORT(time) - DXE_EXPORT(localtime) - - // libc — directory + // --- directory --- + DXE_EXPORT(closedir) + DXE_EXPORT(mkdir) DXE_EXPORT(opendir) DXE_EXPORT(readdir) - DXE_EXPORT(closedir) + DXE_EXPORT(rmdir) - // libc — filesystem + // --- filesystem --- + DXE_EXPORT(access) + DXE_EXPORT(chdir) + DXE_EXPORT(getcwd) DXE_EXPORT(stat) + DXE_EXPORT(unlink) - // libc — misc + // --- time --- + DXE_EXPORT(clock) + DXE_EXPORT(difftime) + DXE_EXPORT(gmtime) + DXE_EXPORT(localtime) + DXE_EXPORT(mktime) + DXE_EXPORT(strftime) + DXE_EXPORT(time) + + // --- process / environment --- + DXE_EXPORT(abort) + DXE_EXPORT(atexit) + DXE_EXPORT(exit) + DXE_EXPORT(getenv) + DXE_EXPORT(system) + + // --- sorting / searching --- + DXE_EXPORT(bsearch) DXE_EXPORT(qsort) + + // --- random --- DXE_EXPORT(rand) DXE_EXPORT(srand) - DXE_EXPORT(abs) - DXE_EXPORT(atoi) + + // --- setjmp / signal --- + DXE_EXPORT(longjmp) + DXE_EXPORT(setjmp) + DXE_EXPORT(signal) + + // --- libm --- + DXE_EXPORT(acos) + DXE_EXPORT(asin) + DXE_EXPORT(atan) + DXE_EXPORT(atan2) + DXE_EXPORT(ceil) + DXE_EXPORT(cos) + DXE_EXPORT(exp) + DXE_EXPORT(fabs) + DXE_EXPORT(floor) + DXE_EXPORT(fmod) + DXE_EXPORT(frexp) + DXE_EXPORT(ldexp) + DXE_EXPORT(log) + DXE_EXPORT(log10) + DXE_EXPORT(modf) + DXE_EXPORT(pow) + DXE_EXPORT(sin) + DXE_EXPORT(sqrt) + DXE_EXPORT(tan) + + // --- errno --- + DXE_EXPORT(errno) + + // --- DJGPP stdio internals --- + // The stdin/stdout/stderr macros in DJGPP expand to pointers to + // these FILE structs. Without them, any DXE that does printf() + // or fprintf(stderr, ...) gets an unresolved symbol at load time. + DXE_EXPORT(__dj_stdin) + DXE_EXPORT(__dj_stdout) + DXE_EXPORT(__dj_stderr) + + // --- stb_ds (dynamic arrays / hashmaps) --- + // Internal functions called by the arrput/arrfree/hm* macros. + // Implementation lives in libtasks.a. + DXE_EXPORT(stbds_arrfreef) + DXE_EXPORT(stbds_arrgrowf) + DXE_EXPORT(stbds_hash_bytes) + DXE_EXPORT(stbds_hash_string) + DXE_EXPORT(stbds_hmdel_key) + DXE_EXPORT(stbds_hmfree_func) + DXE_EXPORT(stbds_hmget_key) + DXE_EXPORT(stbds_hmget_key_ts) + DXE_EXPORT(stbds_hmput_default) + DXE_EXPORT(stbds_hmput_key) + DXE_EXPORT(stbds_rand_seed) + DXE_EXPORT(stbds_shmode_func) + DXE_EXPORT(stbds_stralloc) + DXE_EXPORT(stbds_strreset) + + // --- stb_image (image loading) --- + DXE_EXPORT(stbi_failure_reason) + DXE_EXPORT(stbi_image_free) + DXE_EXPORT(stbi_info) + DXE_EXPORT(stbi_info_from_memory) + DXE_EXPORT(stbi_load) + DXE_EXPORT(stbi_load_from_memory) + DXE_EXPORT(stbi_set_flip_vertically_on_load) + + // --- stb_image_write --- + DXE_EXPORT(stbi_write_bmp) + DXE_EXPORT(stbi_write_png) + DXE_EXPORT(stbi_write_jpg) + DXE_EXPORT(stbi_write_tga) DXE_EXPORT_END diff --git a/dvxshell/shellMain.c b/dvxshell/shellMain.c index a2a9003..e8e3631 100644 --- a/dvxshell/shellMain.c +++ b/dvxshell/shellMain.c @@ -26,9 +26,11 @@ #include "shellApp.h" #include "shellInfo.h" +#include "shellTaskMgr.h" #include "dvxDialog.h" #include "dvxPrefs.h" #include "platform/dvxPlatform.h" +#include "thirdparty/stb_ds.h" #include #include @@ -51,7 +53,9 @@ static jmp_buf sCrashJmp; // the recovery code which signal fired (for logging/diagnostics). static volatile int sCrashSignal = 0; static FILE *sLogFile = NULL; -static void (*sDesktopUpdateFn)(void) = NULL; +// Desktop update callback list (dynamic, managed via stb_ds arrput/arrdel) +typedef void (*DesktopUpdateFnT)(void); +static DesktopUpdateFnT *sDesktopUpdateFns = NULL; // ============================================================ // Prototypes @@ -93,12 +97,31 @@ static void crashHandler(int sig) { // ============================================================ void shellDesktopUpdate(void) { - if (sDesktopUpdateFn) { - sDesktopUpdateFn(); + for (int32_t i = 0; i < arrlen(sDesktopUpdateFns); i++) { + sDesktopUpdateFns[i](); } } +// ============================================================ +// ctrlEscHandler — system-wide Ctrl+Esc callback +// ============================================================ + +static void ctrlEscHandler(void *ctx) { + shellTaskMgrOpen((AppContextT *)ctx); +} + + +// ============================================================ +// titleChangeHandler — refresh listeners when a window title changes +// ============================================================ + +static void titleChangeHandler(void *ctx) { + (void)ctx; + shellDesktopUpdate(); +} + + // ============================================================ // idleYield — called when no dirty rects need compositing // ============================================================ @@ -213,7 +236,21 @@ void shellLog(const char *fmt, ...) { // ============================================================ void shellRegisterDesktopUpdate(void (*updateFn)(void)) { - sDesktopUpdateFn = updateFn; + arrput(sDesktopUpdateFns, updateFn); +} + + +// ============================================================ +// shellUnregisterDesktopUpdate +// ============================================================ + +void shellUnregisterDesktopUpdate(void (*updateFn)(void)) { + for (int32_t i = 0; i < arrlen(sDesktopUpdateFns); i++) { + if (sDesktopUpdateFns[i] == updateFn) { + arrdel(sDesktopUpdateFns, i); + return; + } + } } @@ -275,6 +312,42 @@ int main(int argc, char *argv[]) { dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal); shellLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s", wheelStr, (long)dblClick, accelStr); + + // Apply saved color scheme from INI + bool colorsLoaded = false; + + for (int32_t i = 0; i < ColorCountE; i++) { + const char *val = prefsGetString("colors", dvxColorName((ColorIdE)i), NULL); + + if (val) { + int32_t r; + int32_t g; + int32_t b; + + if (sscanf(val, "%d,%d,%d", &r, &g, &b) == 3) { + sCtx.colorRgb[i][0] = (uint8_t)r; + sCtx.colorRgb[i][1] = (uint8_t)g; + sCtx.colorRgb[i][2] = (uint8_t)b; + colorsLoaded = true; + } + } + } + + if (colorsLoaded) { + dvxApplyColorScheme(&sCtx); + shellLog("Preferences: loaded custom color scheme"); + } + + // Apply saved wallpaper + const char *wpPath = prefsGetString("desktop", "wallpaper", NULL); + + if (wpPath) { + if (dvxSetWallpaper(&sCtx, wpPath)) { + shellLog("Preferences: loaded wallpaper %s", wpPath); + } else { + shellLog("Preferences: failed to load wallpaper %s", wpPath); + } + } } if (result != 0) { @@ -324,6 +397,10 @@ int main(int argc, char *argv[]) { // app tasks CPU time during quiet periods. sCtx.idleCallback = idleYield; sCtx.idleCtx = &sCtx; + sCtx.onCtrlEsc = ctrlEscHandler; + sCtx.ctrlEscCtx = &sCtx; + sCtx.onTitleChange = titleChangeHandler; + sCtx.titleChangeCtx = &sCtx; // Load the desktop app int32_t desktopId = shellLoadApp(&sCtx, SHELL_DESKTOP_APP); diff --git a/dvxshell/shellTaskMgr.c b/dvxshell/shellTaskMgr.c new file mode 100644 index 0000000..0a8b2a4 --- /dev/null +++ b/dvxshell/shellTaskMgr.c @@ -0,0 +1,360 @@ +// shellTaskMgr.c — System Task Manager +// +// Shell-level Task Manager window. Accessible via Ctrl+Esc regardless +// of which app is focused or whether the desktop app is running. Lists +// all running apps with Switch To, End Task, and Run buttons. + +#include "shellTaskMgr.h" +#include "shellApp.h" +#include "dvxDialog.h" +#include "dvxWidget.h" +#include "dvxWm.h" +#include "platform/dvxPlatform.h" +#include "thirdparty/stb_ds.h" + +#include +#include + +// ============================================================ +// Constants +// ============================================================ + +#define TM_COL_COUNT 5 +#define TM_MAX_PATH 260 + +// ============================================================ +// Prototypes +// ============================================================ + +static void onTmClose(WindowT *win); +static void onTmEndTask(WidgetT *w); +static void onTmRun(WidgetT *w); +static void onTmSwitchTo(WidgetT *w); +static void refreshTaskList(void); +static void updateStatusText(void); + +// ============================================================ +// Module state +// ============================================================ + +// Per-row string storage for the list view (must outlive each refresh cycle) +typedef struct { + char title[MAX_TITLE_LEN]; + char file[64]; + char type[12]; +} TmRowStringsT; + +static AppContextT *sCtx = NULL; +static WindowT *sTmWindow = NULL; +static WidgetT *sTmListView = NULL; +static WidgetT *sTmStatusLbl = NULL; +static const char **sCells = NULL; // dynamic array of cell pointers +static TmRowStringsT *sRowStrs = NULL; // dynamic array of per-row strings + + +// ============================================================ +// onTmClose +// ============================================================ + +static void onTmClose(WindowT *win) { + shellUnregisterDesktopUpdate(shellTaskMgrRefresh); + arrfree(sCells); + arrfree(sRowStrs); + sCells = NULL; + sRowStrs = NULL; + sTmListView = NULL; + sTmStatusLbl = NULL; + sTmWindow = NULL; + dvxDestroyWindow(sCtx, win); +} + + +// ============================================================ +// onTmEndTask +// ============================================================ + +static void onTmEndTask(WidgetT *w) { + (void)w; + + if (!sTmListView) { + return; + } + + int32_t sel = wgtListViewGetSelected(sTmListView); + + if (sel < 0) { + return; + } + + // Re-walk the app slot table in the same order as refreshTaskList() + // to map the selected row index back to the correct ShellAppT. + int32_t idx = 0; + + for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + ShellAppT *app = shellGetApp(i); + + if (app && app->state == AppStateRunningE) { + if (idx == sel) { + shellForceKillApp(sCtx, app); + refreshTaskList(); + return; + } + + idx++; + } + } +} + + +// ============================================================ +// onTmRun +// ============================================================ + +static void onTmRun(WidgetT *w) { + (void)w; + + FileFilterT filters[] = { + { "Applications (*.app)", "*.app" }, + { "All Files (*.*)", "*.*" } + }; + char path[TM_MAX_PATH]; + + if (dvxFileDialog(sCtx, "Run Application", FD_OPEN, "apps", filters, 2, path, sizeof(path))) { + shellLoadApp(sCtx, path); + } +} + + +// ============================================================ +// onTmSwitchTo +// ============================================================ + +static void onTmSwitchTo(WidgetT *w) { + (void)w; + + if (!sTmListView) { + return; + } + + int32_t sel = wgtListViewGetSelected(sTmListView); + + if (sel < 0) { + return; + } + + // Same index-to-appId mapping as refreshTaskList. Scan the window + // stack top-down (highest Z first) to find the app's topmost window, + // restore it if minimized, then raise and focus it. + int32_t idx = 0; + + for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + ShellAppT *app = shellGetApp(i); + + if (app && app->state == AppStateRunningE) { + if (idx == sel) { + for (int32_t j = sCtx->stack.count - 1; j >= 0; j--) { + WindowT *win = sCtx->stack.windows[j]; + + if (win->appId == i) { + if (win->minimized) { + wmRestoreMinimized(&sCtx->stack, &sCtx->dirty, win); + } + + wmRaiseWindow(&sCtx->stack, &sCtx->dirty, j); + wmSetFocus(&sCtx->stack, &sCtx->dirty, sCtx->stack.count - 1); + return; + } + } + + return; + } + + idx++; + } + } +} + + +// ============================================================ +// refreshTaskList +// ============================================================ + +static void refreshTaskList(void) { + if (!sTmListView) { + return; + } + + // Reset dynamic arrays (keep allocations for reuse) + arrsetlen(sCells, 0); + arrsetlen(sRowStrs, 0); + + int32_t rowCount = 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]; + + if (win->appId == app->appId && win->visible) { + snprintf(row.title, sizeof(row.title), "%s", win->title); + break; + } + } + + // 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++; + } + } + + wgtListViewSetData(sTmListView, sCells, rowCount); +} + + +// ============================================================ +// updateStatusText +// ============================================================ + +static void updateStatusText(void) { + if (!sTmStatusLbl) { + return; + } + + static char buf[128]; + int32_t count = shellRunningAppCount(); + uint32_t totalKb; + uint32_t freeKb; + + 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); + } + + 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)); + } + + wgtSetText(sTmStatusLbl, buf); +} + + +// ============================================================ +// shellTaskMgrOpen +// ============================================================ + +void shellTaskMgrOpen(AppContextT *ctx) { + sCtx = ctx; + + if (sTmWindow) { + // Already open — raise and focus + for (int32_t i = 0; i < ctx->stack.count; i++) { + if (ctx->stack.windows[i] == sTmWindow) { + wmRaiseWindow(&ctx->stack, &ctx->dirty, i); + wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1); + break; + } + } + + return; + } + + int32_t winW = 520; + int32_t winH = 280; + int32_t winX = (ctx->display.width - winW) / 2; + int32_t winY = (ctx->display.height - winH) / 3; + + sTmWindow = dvxCreateWindow(ctx, "Task Manager", winX, winY, winW, winH, true); + + if (!sTmWindow) { + return; + } + + sTmWindow->onClose = onTmClose; + sTmWindow->appId = 0; // owned by the shell, not any app + + WidgetT *root = wgtInitWindow(ctx, sTmWindow); + + static ListViewColT tmCols[TM_COL_COUNT]; + tmCols[0].title = "Name"; + tmCols[0].width = wgtPercent(20); + tmCols[0].align = ListViewAlignLeftE; + tmCols[1].title = "Title"; + tmCols[1].width = wgtPercent(30); + tmCols[1].align = ListViewAlignLeftE; + tmCols[2].title = "File"; + tmCols[2].width = wgtPercent(22); + tmCols[2].align = ListViewAlignLeftE; + tmCols[3].title = "Type"; + tmCols[3].width = wgtPercent(14); + tmCols[3].align = ListViewAlignLeftE; + tmCols[4].title = "Status"; + tmCols[4].width = wgtPercent(14); + tmCols[4].align = ListViewAlignLeftE; + + sTmListView = wgtListView(root); + sTmListView->weight = 100; + sTmListView->prefH = wgtPixels(160); + wgtListViewSetColumns(sTmListView, tmCols, TM_COL_COUNT); + + WidgetT *btnRow = wgtHBox(root); + btnRow->spacing = wgtPixels(8); + + sTmStatusLbl = wgtLabel(btnRow, ""); + sTmStatusLbl->weight = 100; + + WidgetT *switchBtn = wgtButton(btnRow, "Switch To"); + switchBtn->onClick = onTmSwitchTo; + switchBtn->prefW = wgtPixels(90); + + WidgetT *endBtn = wgtButton(btnRow, "End Task"); + endBtn->onClick = onTmEndTask; + endBtn->prefW = wgtPixels(90); + + WidgetT *runBtn = wgtButton(btnRow, "Run..."); + runBtn->onClick = onTmRun; + runBtn->prefW = wgtPixels(90); + + shellRegisterDesktopUpdate(shellTaskMgrRefresh); + refreshTaskList(); + updateStatusText(); + dvxFitWindow(ctx, sTmWindow); +} + + +// ============================================================ +// shellTaskMgrRefresh +// ============================================================ + +void shellTaskMgrRefresh(void) { + if (sTmWindow) { + refreshTaskList(); + updateStatusText(); + } +} diff --git a/dvxshell/shellTaskMgr.h b/dvxshell/shellTaskMgr.h new file mode 100644 index 0000000..be2b41b --- /dev/null +++ b/dvxshell/shellTaskMgr.h @@ -0,0 +1,18 @@ +// shellTaskMgr.h — System Task Manager +// +// The Task Manager is a shell-level component, not tied to any app. +// It is always available via Ctrl+Esc and persists even if the desktop +// app (Program Manager) is terminated. + +#ifndef SHELL_TASKMGR_H +#define SHELL_TASKMGR_H + +#include "dvxApp.h" + +// Open or raise the Task Manager window. +void shellTaskMgrOpen(AppContextT *ctx); + +// Refresh the task list (called by desktop update notification). +void shellTaskMgrRefresh(void); + +#endif diff --git a/mkcd.sh b/mkcd.sh new file mode 100755 index 0000000..a077598 --- /dev/null +++ b/mkcd.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# mkcd.sh — Build DVX and create a CD-ROM ISO image for 86Box +# +# Usage: ./mkcd.sh +# +# Builds the full DVX stack (dvx, tasks, shell, apps), then creates +# an ISO 9660 image from the bin/ directory. The ISO uses short +# 8.3 filenames (-iso-level 1) for DOS compatibility. +# +# The ISO is placed in 86Box's data directory so it can be mounted +# as a CD-ROM drive. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ISO_DIR="$HOME/.var/app/net._86box._86Box/data/86Box" +ISO_PATH="$ISO_DIR/dvx.iso" + +# Build everything +echo "Building DVX..." +make -C "$SCRIPT_DIR" all + +# Verify build output exists +if [ ! -f "$SCRIPT_DIR/bin/dvx.exe" ]; then + echo "ERROR: bin/dvx.exe not found — build failed?" + exit 1 +fi + +# Create the ISO image +# -iso-level 1: strict 8.3 filenames (DOS compatibility) +# -J: Joliet extensions (long names for Windows/Linux) +# -R: Rock Ridge (long names for Linux) +# -V: volume label +echo "Creating ISO image..." +mkisofs \ + -iso-level 1 \ + -J \ + -R \ + -V "DVX" \ + -o "$ISO_PATH" \ + "$SCRIPT_DIR/bin/" + +echo "ISO created: $ISO_PATH" +echo "Size: $(du -h "$ISO_PATH" | cut -f1)" +echo "" +echo "In 86Box, mount $ISO_PATH as a CD-ROM drive." +echo "Then from DOS: D:\\DVX.EXE (or whatever drive letter)" diff --git a/themes/cde.thm b/themes/cde.thm new file mode 100644 index 0000000..0d3942a --- /dev/null +++ b/themes/cde.thm @@ -0,0 +1,22 @@ +; DVX Color Theme — CDE (Common Desktop Environment) +; Warm tan/brown palette inspired by Solaris/HP-UX CDE. + +[colors] +desktop = 115,130,143 +windowFace = 174,178,195 +windowHighlight = 234,234,234 +windowShadow = 100,100,120 +activeTitleBg = 88,107,136 +activeTitleFg = 255,255,255 +inactiveTitleBg = 174,178,195 +inactiveTitleFg = 0,0,0 +contentBg = 174,178,195 +contentFg = 0,0,0 +menuBg = 174,178,195 +menuFg = 0,0,0 +menuHighlightBg = 88,107,136 +menuHighlightFg = 255,255,255 +buttonFace = 174,178,195 +scrollbarBg = 174,178,195 +scrollbarFg = 100,100,120 +scrollbarTrough = 140,150,165 diff --git a/themes/geos.thm b/themes/geos.thm new file mode 100644 index 0000000..69648fa --- /dev/null +++ b/themes/geos.thm @@ -0,0 +1,22 @@ +; DVX Color Theme — GEOS Ensemble +; Motif-style 3D bevels with teal desktop and dark charcoal title bars. + +[colors] +desktop = 0,128,128 +windowFace = 192,192,192 +windowHighlight = 255,255,255 +windowShadow = 128,128,128 +activeTitleBg = 48,48,48 +activeTitleFg = 255,255,255 +inactiveTitleBg = 160,160,160 +inactiveTitleFg = 64,64,64 +contentBg = 192,192,192 +contentFg = 0,0,0 +menuBg = 192,192,192 +menuFg = 0,0,0 +menuHighlightBg = 48,48,48 +menuHighlightFg = 255,255,255 +buttonFace = 192,192,192 +scrollbarBg = 192,192,192 +scrollbarFg = 128,128,128 +scrollbarTrough = 160,160,160 diff --git a/themes/win31.thm b/themes/win31.thm new file mode 100644 index 0000000..0a01a87 --- /dev/null +++ b/themes/win31.thm @@ -0,0 +1,22 @@ +; DVX Color Theme — Windows 3.1 +; Classic teal desktop with navy blue title bars. + +[colors] +desktop = 0,128,128 +windowFace = 192,192,192 +windowHighlight = 255,255,255 +windowShadow = 128,128,128 +activeTitleBg = 0,0,128 +activeTitleFg = 255,255,255 +inactiveTitleBg = 192,192,192 +inactiveTitleFg = 0,0,0 +contentBg = 255,255,255 +contentFg = 0,0,0 +menuBg = 192,192,192 +menuFg = 0,0,0 +menuHighlightBg = 0,0,128 +menuHighlightFg = 255,255,255 +buttonFace = 192,192,192 +scrollbarBg = 192,192,192 +scrollbarFg = 0,0,0 +scrollbarTrough = 192,192,192