Compare commits
96 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36c6eeaf81 | |||
| d9889b2fbb | |||
| 60d24c8c33 | |||
| 4cdcfe6b8c | |||
| 4600f3e631 | |||
| 1affec7e8c | |||
| a8c38267bc | |||
| 05e69f60a7 | |||
| 217f138d07 | |||
| 48fb1c30ae | |||
| e6305db3b5 | |||
| dcd120d769 | |||
| de60200f23 | |||
| 7157f97c28 | |||
| 66952306df | |||
| 8abe947b8b | |||
| 3db721fdc0 | |||
| 7c1eb495e1 | |||
| 094b263c36 | |||
| 5f305dd14c | |||
| 454a3620f7 | |||
| 3c886a97f6 | |||
| fe68899020 | |||
| 5ece172ad0 | |||
| c43c586f2f | |||
| 10ba408465 | |||
| 2a641f42c3 | |||
| 0f5da09d1f | |||
| 7ec2aead8f | |||
| 2a2a386592 | |||
| 1923886d42 | |||
| af0ad3091f | |||
| 5f2358fcf2 | |||
| 35f877d3e1 | |||
| 85010d17dc | |||
| de7027c44e | |||
| 626befa664 | |||
| eb5e4e567e | |||
| 827d73fbd1 | |||
| d3898707f9 | |||
| 93b912d932 | |||
| 7cd7388607 | |||
| 7bc92549f7 | |||
| e36d4b9cec | |||
| bf5bf9bb1d | |||
| dd68a19b5b | |||
| 657b44eb25 | |||
| 0ef46ff6a0 | |||
| 289adb8c47 | |||
| 6d75e4996a | |||
| 88746ec2ba | |||
| 17fe1840e3 | |||
| 4d4aedbc43 | |||
| 1fb8e2a387 | |||
| 82939c3f27 | |||
| 5dd632a862 | |||
| 344ab4794d | |||
| d094205ed0 | |||
| 59bc2b5ed3 | |||
| 7d74d76985 | |||
| 5171753250 | |||
| 51ade9f119 | |||
| 0043e06c82 | |||
| 65d7a252ca | |||
| b3cc66be4b | |||
| f62b89fc02 | |||
| aa961425c9 | |||
| 89690ca97c | |||
| a630855b7e | |||
| 15019ef542 | |||
| bf610ba95b | |||
| be7473ff27 | |||
| fdc72f98a1 | |||
| 9b136995b7 | |||
| 09da5f3857 | |||
| 97503080a5 | |||
| a793941357 | |||
| 227b1179cc | |||
| 8886cee933 | |||
| 0ceae99da3 | |||
| f6354bef6f | |||
| 56816eedd8 | |||
| 67872c6b98 | |||
| 9ee73c8806 | |||
| 3b2d87845e | |||
| 163959a192 | |||
| 7ae7ea1a97 | |||
| b1040a655e | |||
| d3bafd26b4 | |||
| 0a36dbe09e | |||
| 26c3d7440d | |||
| 5a1332d024 | |||
| 70459616cc | |||
| fc9fc46c79 | |||
| 157d79f2d6 | |||
| a4727754e3 |
848 changed files with 345513 additions and 30301 deletions
8
.gitattributes
vendored
8
.gitattributes
vendored
|
|
@ -1,2 +1,10 @@
|
|||
*.bmp filter=lfs diff=lfs merge=lfs -text
|
||||
*.BMP filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
*.JPG filter=lfs diff=lfs merge=lfs -text
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.PNG filter=lfs diff=lfs merge=lfs -text
|
||||
*.xcf filter=lfs diff=lfs merge=lfs -text
|
||||
*.XCF filter=lfs diff=lfs merge=lfs -text
|
||||
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||
*.ZIP filter=lfs diff=lfs merge=lfs -text
|
||||
|
|
|
|||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -2,7 +2,11 @@ dosbench/
|
|||
bin/
|
||||
obj/
|
||||
lib/
|
||||
*~
|
||||
*.~
|
||||
.gitignore~
|
||||
DVX_GUI_DESIGN.md
|
||||
.gitattributes~
|
||||
*.SWP
|
||||
.claude/
|
||||
capture/
|
||||
just-stuff/
|
||||
|
|
|
|||
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright © 2026 Scott Duensing
|
||||
|
||||
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.
|
||||
206
Makefile
206
Makefile
|
|
@ -1,27 +1,197 @@
|
|||
# DVX GUI — Top-level Makefile
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Builds the full DVX stack: library, task switcher, shell, and apps.
|
||||
# Copyright (C) 2026 Scott Duensing
|
||||
#
|
||||
# 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.
|
||||
|
||||
.PHONY: all clean dvx tasks dvxshell apps
|
||||
# DVX GUI -- Top-level Makefile
|
||||
#
|
||||
# Builds the full DVX stack: core library, task switcher,
|
||||
# bootstrap loader, text help library, widgets, shell, and apps.
|
||||
#
|
||||
# Source tree mirrors bin/ layout:
|
||||
# src/libs/kpunch/<libname>/ -> bin/libs/kpunch/<libname>/<libname>.lib
|
||||
# src/widgets/kpunch/<widget>/ -> bin/widgets/kpunch/<widget>/<widget>.wgt
|
||||
# src/apps/kpunch/<app>/ -> bin/apps/kpunch/<app>/<app>.app
|
||||
# src/loader/ -> bin/DVX.EXE
|
||||
# src/tools/ -> bin/host/<tool>
|
||||
|
||||
all: dvx tasks dvxshell apps
|
||||
.PHONY: all clean libdvx libtasks loader texthelp listhelp widgets dvxshell taskmgr serial sql apps tools deploy-helpsrc compile-help deploy-sdk
|
||||
|
||||
dvx:
|
||||
$(MAKE) -C dvx
|
||||
all: libdvx libtasks loader texthelp listhelp tools widgets dvxshell taskmgr serial sql apps deploy-helpsrc compile-help deploy-sdk
|
||||
|
||||
tasks:
|
||||
$(MAKE) -C tasks
|
||||
libdvx:
|
||||
$(MAKE) -C src/libs/kpunch/libdvx
|
||||
|
||||
dvxshell: dvx tasks
|
||||
$(MAKE) -C dvxshell
|
||||
libtasks:
|
||||
$(MAKE) -C src/libs/kpunch/libtasks
|
||||
|
||||
apps: dvx tasks dvxshell
|
||||
$(MAKE) -C apps
|
||||
loader: libdvx libtasks
|
||||
$(MAKE) -C src/loader
|
||||
|
||||
texthelp: libdvx libtasks
|
||||
$(MAKE) -C src/libs/kpunch/texthelp
|
||||
|
||||
listhelp: libdvx libtasks
|
||||
$(MAKE) -C src/libs/kpunch/listhelp
|
||||
|
||||
widgets: libdvx libtasks texthelp listhelp
|
||||
$(MAKE) -C src/widgets/kpunch
|
||||
|
||||
dvxshell: libdvx libtasks
|
||||
$(MAKE) -C src/libs/kpunch/dvxshell
|
||||
|
||||
taskmgr: dvxshell
|
||||
$(MAKE) -C src/libs/kpunch/taskmgr
|
||||
|
||||
serial: libdvx libtasks
|
||||
$(MAKE) -C src/libs/kpunch/serial
|
||||
|
||||
sql: libdvx libtasks
|
||||
$(MAKE) -C src/libs/kpunch/sql
|
||||
|
||||
tools:
|
||||
$(MAKE) -C src/tools
|
||||
|
||||
apps: libdvx libtasks dvxshell tools
|
||||
$(MAKE) -C src/apps/kpunch
|
||||
|
||||
deploy-helpsrc:
|
||||
$(MAKE) -C src/tools deploy-helpsrc
|
||||
|
||||
HLPC = bin/host/dvxhlpc
|
||||
|
||||
SYSTEM_DHS = src/libs/kpunch/libdvx/sysdoc.dhs \
|
||||
src/libs/kpunch/libdvx/arch.dhs src/libs/kpunch/libdvx/apiref.dhs \
|
||||
src/libs/kpunch/libtasks/libtasks.dhs src/libs/kpunch/dvxshell/dvxshell.dhs src/libs/kpunch/sql/dvxsql.dhs \
|
||||
src/libs/kpunch/texthelp/texthelp.dhs src/libs/kpunch/listhelp/listhelp.dhs \
|
||||
src/libs/kpunch/taskmgr/taskmgr.dhs src/libs/kpunch/serial/serial.dhs \
|
||||
src/apps/kpunch/dvxbasic/basrt.dhs \
|
||||
src/widgets/kpunch/wgtsys.dhs
|
||||
|
||||
BASIC_DHS = src/apps/kpunch/dvxbasic/ideguide.dhs src/apps/kpunch/dvxbasic/langref.dhs \
|
||||
src/apps/kpunch/dvxbasic/ctrlover.dhs src/apps/kpunch/dvxbasic/form.dhs
|
||||
|
||||
compile-help:
|
||||
@mkdir -p docs
|
||||
$(HLPC) -o bin/apps/kpunch/dvxhelp/dvxhelp.hlp \
|
||||
--html docs/dvx_help_viewer.html \
|
||||
src/apps/kpunch/dvxhelp/help.dhs
|
||||
$(HLPC) -o bin/apps/kpunch/progman/dvxhelp.hlp \
|
||||
--html docs/dvx_system_reference.html \
|
||||
-i src/libs/kpunch/libdvx \
|
||||
$(SYSTEM_DHS) \
|
||||
$$(find src/widgets/kpunch -name "*.dhs" ! -path "*/wgtsys.dhs" | sort)
|
||||
$(HLPC) -o bin/apps/kpunch/dvxbasic/dvxbasic.hlp \
|
||||
--html docs/dvx_basic_reference.html \
|
||||
$(BASIC_DHS) \
|
||||
$$(find src/widgets/kpunch -name "*.bhs" | sort)
|
||||
|
||||
SDKDIR = bin/sdk
|
||||
|
||||
deploy-sdk:
|
||||
@echo "Building SDK..."
|
||||
@mkdir -p $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow
|
||||
@# Core headers (libdvx public API)
|
||||
@for f in src/libs/kpunch/libdvx/dvxApp.h src/libs/kpunch/libdvx/dvxTypes.h \
|
||||
src/libs/kpunch/libdvx/dvxWgt.h src/libs/kpunch/libdvx/dvxWgtP.h \
|
||||
src/libs/kpunch/libdvx/dvxWm.h src/libs/kpunch/libdvx/dvxDraw.h \
|
||||
src/libs/kpunch/libdvx/dvxVideo.h src/libs/kpunch/libdvx/dvxComp.h \
|
||||
src/libs/kpunch/libdvx/dvxPrefs.h src/libs/kpunch/libdvx/dvxDlg.h \
|
||||
src/libs/kpunch/libdvx/dvxRes.h src/libs/kpunch/libdvx/dvxFont.h \
|
||||
src/libs/kpunch/libdvx/dvxCur.h src/libs/kpunch/libdvx/dvxPal.h \
|
||||
src/libs/kpunch/libdvx/dvxMem.h src/libs/kpunch/libdvx/platform/dvxPlat.h; do \
|
||||
[ -f "$$f" ] && cp "$$f" $(SDKDIR)/include/core/; \
|
||||
done
|
||||
@# Shell header
|
||||
@cp src/libs/kpunch/dvxshell/shellApp.h $(SDKDIR)/include/shell/
|
||||
@cp src/libs/kpunch/dvxshell/shellInf.h $(SDKDIR)/include/shell/
|
||||
@# Tasks header
|
||||
@cp src/libs/kpunch/libtasks/taskSwch.h $(SDKDIR)/include/tasks/
|
||||
@# SQL header
|
||||
@cp src/libs/kpunch/sql/dvxSql.h $(SDKDIR)/include/sql/
|
||||
@# Widget headers -- one subdir per widget
|
||||
@for d in src/widgets/kpunch/*/; do \
|
||||
for h in "$$d"*.h; do \
|
||||
[ -f "$$h" ] || continue; \
|
||||
wgt=$$(basename "$$d"); \
|
||||
mkdir -p $(SDKDIR)/include/widget/"$$wgt"; \
|
||||
cp "$$h" $(SDKDIR)/include/widget/"$$wgt"/; \
|
||||
done; \
|
||||
done
|
||||
@# BASIC include files
|
||||
@cp src/include/basic/*.bas $(SDKDIR)/include/basic/
|
||||
@# BASIC sample: basdemo (project file + form + icon)
|
||||
@cp src/apps/kpunch/basdemo/basdemo.dbp $(SDKDIR)/samples/basic/basdemo/
|
||||
@cp src/apps/kpunch/basdemo/basdemo.frm $(SDKDIR)/samples/basic/basdemo/
|
||||
@cp src/apps/kpunch/basdemo/ICON32.BMP $(SDKDIR)/samples/basic/basdemo/
|
||||
@# BASIC sample: widshow (widget showcase)
|
||||
@cp src/apps/kpunch/widshow/widshow.dbp $(SDKDIR)/samples/basic/widshow/
|
||||
@cp src/apps/kpunch/widshow/widshow.frm $(SDKDIR)/samples/basic/widshow/
|
||||
@cp src/apps/kpunch/widshow/ICON32.BMP $(SDKDIR)/samples/basic/widshow/
|
||||
@# README
|
||||
@printf '%s\n' \
|
||||
'DVX SDK' \
|
||||
'=======' \
|
||||
'' \
|
||||
'Headers for developing applications and widgets against the DVX GUI.' \
|
||||
'' \
|
||||
'Directory Structure' \
|
||||
'-------------------' \
|
||||
'' \
|
||||
' include/' \
|
||||
' core/ DVX core headers (types, drawing, windows, widgets)' \
|
||||
' shell/ Shell/app loading API' \
|
||||
' tasks/ Cooperative task switching API' \
|
||||
' sql/ SQLite database wrapper API' \
|
||||
' widget/ Per-widget public API headers' \
|
||||
' basic/ BASIC include files (DECLARE LIBRARY modules)' \
|
||||
' samples/' \
|
||||
' basic/ Example BASIC projects (open in the DVX BASIC IDE)' \
|
||||
'' \
|
||||
'Requirements' \
|
||||
'------------' \
|
||||
'' \
|
||||
' - DJGPP cross-compiler (i586-pc-msdosdjgpp-gcc)' \
|
||||
' - dxe3gen (included with DJGPP)' \
|
||||
' - DVX resource compiler (SYSTEM/DVXRES.EXE on the target,' \
|
||||
' or bin/host/dvxres on the host)' \
|
||||
' - DVX help compiler (SYSTEM/DVXHLPC.EXE on the target,' \
|
||||
' or bin/host/dvxhlpc on the host)' \
|
||||
'' \
|
||||
'For complete working examples, see the app sources under' \
|
||||
'src/apps/kpunch/ in the DVX repository.' \
|
||||
> $(SDKDIR)/README.TXT
|
||||
|
||||
clean:
|
||||
$(MAKE) -C dvx clean
|
||||
$(MAKE) -C tasks 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
|
||||
$(MAKE) -C src/libs/kpunch/libdvx clean
|
||||
$(MAKE) -C src/libs/kpunch/libtasks clean
|
||||
$(MAKE) -C src/loader clean
|
||||
$(MAKE) -C src/libs/kpunch/texthelp clean
|
||||
$(MAKE) -C src/libs/kpunch/listhelp clean
|
||||
$(MAKE) -C src/widgets/kpunch clean
|
||||
$(MAKE) -C src/libs/kpunch/dvxshell clean
|
||||
$(MAKE) -C src/libs/kpunch/taskmgr clean
|
||||
$(MAKE) -C src/libs/kpunch/serial clean
|
||||
$(MAKE) -C src/libs/kpunch/sql clean
|
||||
$(MAKE) -C src/apps/kpunch clean
|
||||
$(MAKE) -C src/tools clean
|
||||
-rmdir obj 2>/dev/null
|
||||
-rm -rf bin/config bin/widgets bin/libs bin/sdk
|
||||
-rm -f docs/*.html
|
||||
|
|
|
|||
208
README.md
208
README.md
|
|
@ -1,122 +1,110 @@
|
|||
# DVX — DOS Visual eXecutive
|
||||
# DVX -- DOS Visual eXecutive
|
||||
|
||||
A Windows 3.x-style desktop shell for DOS, built with DJGPP/DPMI. Combines a
|
||||
windowed GUI compositor, cooperative task switcher, and DXE3 dynamic loading to
|
||||
create a multitasking desktop environment where applications are `.app` shared
|
||||
libraries loaded at runtime.
|
||||
DVX is a windowed GUI environment and application platform for DOS.
|
||||
It provides a Motif-inspired desktop, a cooperative multitasking kernel,
|
||||
a widget toolkit, a compiled BASIC language with a visual form designer,
|
||||
and a small set of bundled applications.
|
||||
|
||||
## Components
|
||||
The system is a compositor built on VESA VBE 2.0+ linear framebuffer
|
||||
video modes. A dirty-rectangle compositor keeps redraw cost proportional
|
||||
to what actually changes on screen, so the desktop is responsive even at
|
||||
1024x768 on modest hardware.
|
||||
|
||||
|
||||
## What's Included
|
||||
|
||||
* **DVX Shell** -- desktop, app lifecycle manager, Task Manager, crash
|
||||
recovery. The shell is the program the user interacts with and the
|
||||
host for all running applications.
|
||||
* **Widget toolkit** -- 31 widget types as plug-in modules: buttons,
|
||||
labels, text inputs, list boxes, tree views, tab controls, sliders,
|
||||
spinners, progress bars, canvases, scroll panes, splitters, an ANSI
|
||||
terminal, and more.
|
||||
* **DVX BASIC** -- a Visual Basic 3 compatible language with a visual
|
||||
form designer, per-procedure code editor, project management, and a
|
||||
bytecode compiler and VM. BASIC programs compile to standalone DVX
|
||||
applications.
|
||||
* **Help system** -- a full hypertext help viewer with table of contents,
|
||||
keyword index, full-text search, and inline images. Help files are
|
||||
compiled from a plain-text source format.
|
||||
* **Serial / networking stack** -- an ISR-driven UART driver, HDLC
|
||||
packet framing with CRC-16 and Go-Back-N reliable delivery, 1024-bit
|
||||
Diffie-Hellman key exchange, and XTEA-CTR encryption, plus a Linux
|
||||
bridge that connects a virtual serial port to a telnet BBS.
|
||||
* **Control Panel** -- themes, wallpapers, video mode, mouse
|
||||
configuration, all with live preview.
|
||||
* **Bundled applications** -- Program Manager (desktop launcher),
|
||||
Notepad, Clock, Image Viewer, DVX Demo, Control Panel, DVX BASIC
|
||||
IDE, Help Viewer, Resource Editor, Icon Editor, Help Editor, BASIC
|
||||
Demo.
|
||||
|
||||
|
||||
## Target Hardware
|
||||
|
||||
* DOS with a 386-class CPU or newer (486 recommended, Pentium for best
|
||||
performance)
|
||||
* VESA BIOS Extensions 2.0 or newer with linear framebuffer support
|
||||
* 4 MB of extended memory minimum; 8 MB or more recommended
|
||||
* Any resolution the video card reports at 8, 15, 16, 24, or 32 bits
|
||||
per pixel. 640x480, 800x600, and 1024x768 are the common cases.
|
||||
* Two-button mouse; scroll wheel supported if a compatible driver is
|
||||
loaded
|
||||
|
||||
|
||||
## Building and Deploying
|
||||
|
||||
```
|
||||
make # build everything
|
||||
make clean # remove all build artifacts
|
||||
./mkcd.sh # build and produce a CD-ROM ISO image
|
||||
```
|
||||
|
||||
`make` builds in dependency order: core, tasks, loader, helpers,
|
||||
host tools, widgets, shell, task manager, apps. Build output goes to
|
||||
`bin/` and intermediate objects to `obj/`.
|
||||
|
||||
`./mkcd.sh` builds everything, then wraps `bin/` into an ISO 9660
|
||||
image for use with an emulator or for burning to physical media.
|
||||
|
||||
|
||||
## Directory Layout
|
||||
|
||||
```
|
||||
dvxgui/
|
||||
dvx/ GUI compositor library -> lib/libdvx.a
|
||||
tasks/ Cooperative task switcher -> lib/libtasks.a
|
||||
dvxshell/ Desktop shell -> bin/dvx.exe
|
||||
apps/ DXE app modules (.app files) -> bin/apps/*/*.app
|
||||
rs232/ ISR-driven UART serial driver -> lib/librs232.a
|
||||
packet/ HDLC framing, CRC, Go-Back-N -> lib/libpacket.a
|
||||
security/ DH key exchange, XTEA-CTR cipher -> lib/libsecurity.a
|
||||
seclink/ Secure serial link wrapper -> lib/libseclink.a
|
||||
proxy/ Linux SecLink-to-telnet proxy -> bin/secproxy
|
||||
termdemo/ Encrypted ANSI terminal demo -> bin/termdemo.exe
|
||||
assets/ logo and splash artwork
|
||||
bin/ build output (runtime tree; mirrors what ships)
|
||||
config/ shipped INI, themes, wallpapers
|
||||
docs/ HTML exports of the built-in help content
|
||||
LICENSE.txt MIT license
|
||||
Makefile top-level build
|
||||
mkcd.sh build-and-package script
|
||||
src/
|
||||
apps/ bundled applications
|
||||
include/ shared include files (BASIC library declarations)
|
||||
libs/ core libraries (GUI, tasks, shell, helpers, serial, SQL)
|
||||
loader/ bootstrap executable
|
||||
tools/ host-native development tools and the BASIC compiler
|
||||
widgets/ widget plug-in modules
|
||||
```
|
||||
|
||||
## Building
|
||||
See `src/tools/README.md` for the host-side development tools
|
||||
(resource tool, icon generators, help compiler, proxy), and
|
||||
`src/apps/kpunch/README.md` for the SDK contract applications must
|
||||
follow.
|
||||
|
||||
Requires the DJGPP cross-compiler (`i586-pc-msdosdjgpp-gcc`).
|
||||
|
||||
```
|
||||
make -C dvx # builds lib/libdvx.a
|
||||
make -C tasks # builds lib/libtasks.a
|
||||
make -C dvxshell # builds bin/dvx.exe
|
||||
make -C apps # builds bin/apps/*/*.app
|
||||
```
|
||||
|
||||
Set `DJGPP_PREFIX` in the Makefiles if your toolchain is installed somewhere
|
||||
other than `~/djgpp/djgpp`.
|
||||
|
||||
## Architecture
|
||||
|
||||
The shell runs as a single DOS executable (`dvx.exe`) that loads
|
||||
applications dynamically via DJGPP's DXE3 shared library system. Each app
|
||||
is a `.app` file exporting an `appDescriptor` and `appMain` entry point.
|
||||
|
||||
```
|
||||
+-------------------------------------------------------------------+
|
||||
| dvx.exe (Task 0) |
|
||||
| +-------------+ +-----------+ +----------+ +----------------+ |
|
||||
| +-------------+ +-----------+ +------------+ |
|
||||
| | shellMain | | shellApp | | shellExport| |
|
||||
| | (event loop)| | (lifecycle| | (DXE symbol| |
|
||||
| | | | + reaper)| | export) | |
|
||||
| +-------------+ +-----------+ +------------+ |
|
||||
| | | | |
|
||||
| +------+-------+ +----+-----+ +----+----+ |
|
||||
| | libdvx.a | |libtasks.a| | libdxe | |
|
||||
| | (GUI/widgets)| |(scheduler)| | (DJGPP) | |
|
||||
| +--------------+ +----------+ +---------+ |
|
||||
+-------------------------------------------------------------------+
|
||||
| |
|
||||
+---------+ +---------+
|
||||
| app.app | | app.app |
|
||||
| (Task N)| | (Task M)|
|
||||
+---------+ +---------+
|
||||
```
|
||||
|
||||
### App Types
|
||||
|
||||
**Callback-only** (`hasMainLoop = false`): `appMain` creates windows, registers
|
||||
callbacks, and returns. The app lives through event callbacks in the shell's
|
||||
main loop. No dedicated task or stack needed.
|
||||
|
||||
**Main-loop** (`hasMainLoop = true`): A cooperative task is created for the app.
|
||||
`appMain` runs its own loop calling `tsYield()` to share CPU. Used for apps that
|
||||
need continuous processing (clocks, terminal emulators, games).
|
||||
|
||||
### Crash Recovery
|
||||
|
||||
The shell installs signal handlers for SIGSEGV, SIGFPE, and SIGILL. If an app
|
||||
crashes, the handler `longjmp`s back to the shell's main loop, the crashed app
|
||||
is force-killed, and the shell continues running. Diagnostic information
|
||||
(registers, faulting EIP) is logged to `dvx.log`.
|
||||
|
||||
## Sample Apps
|
||||
|
||||
| App | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `progman.app` | Callback | Program Manager: app launcher grid, Task Manager |
|
||||
| `notepad.app` | Callback | Text editor with file I/O |
|
||||
| `clock.app` | Main-loop | Digital clock (multi-instance capable) |
|
||||
| `dvxdemo.app` | Callback | Widget system showcase |
|
||||
|
||||
## Target Platform
|
||||
|
||||
- **CPU**: 486 baseline, Pentium-optimized paths where significant
|
||||
- **Video**: VESA VBE 2.0+ with linear framebuffer
|
||||
- **OS**: DOS with DPMI (CWSDPMI or equivalent)
|
||||
- **Test platform**: 86Box
|
||||
|
||||
## Deployment
|
||||
|
||||
```
|
||||
mcopy -o -i <floppy.img> bin/dvx.exe ::DVX.EXE
|
||||
mcopy -s -o -i <floppy.img> bin/apps ::APPS
|
||||
```
|
||||
|
||||
The `apps/` directory structure must be preserved on the target — Program Manager
|
||||
recursively scans `apps/` for `.app` files at startup.
|
||||
|
||||
## Documentation
|
||||
|
||||
Each component directory has its own README with detailed API reference:
|
||||
Full reference documentation ships with the system as browsable help:
|
||||
|
||||
- [`dvx/README.md`](dvx/README.md) — GUI library architecture and API
|
||||
- [`tasks/README.md`](tasks/README.md) — Task switcher API
|
||||
- [`dvxshell/README.md`](dvxshell/README.md) — Shell internals and DXE app contract
|
||||
- [`apps/README.md`](apps/README.md) — Writing DXE applications
|
||||
- [`rs232/README.md`](rs232/README.md) — Serial port driver
|
||||
- [`packet/README.md`](packet/README.md) — Packet transport protocol
|
||||
- [`security/README.md`](security/README.md) — Cryptographic primitives
|
||||
- [`seclink/README.md`](seclink/README.md) — Secure serial link
|
||||
- [`proxy/README.md`](proxy/README.md) — Linux SecLink proxy
|
||||
- [`termdemo/README.md`](termdemo/README.md) — Encrypted terminal demo
|
||||
* `docs/dvx_system_reference.html` -- system API, widget catalog
|
||||
* `docs/dvx_basic_reference.html` -- DVX BASIC language reference
|
||||
* `docs/dvx_help_viewer.html` -- help viewer guide
|
||||
|
||||
The same content is available from inside DVX via the Help Viewer and
|
||||
Program Manager.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
MIT. See `LICENSE.txt`.
|
||||
|
|
|
|||
54
analyze.sh
Executable file
54
analyze.sh
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# analyze.sh -- run the DJGPP build under gcc's -fanalyzer.
|
||||
#
|
||||
# Usage:
|
||||
# ./analyze.sh # analyse the whole build (slow: 2-5x)
|
||||
# ./analyze.sh 2>&1 | tee analyze.log
|
||||
#
|
||||
# How it works:
|
||||
#
|
||||
# The project Makefiles hardcode -Werror in CFLAGS. gcc evaluates
|
||||
# diagnostic flags in order, so a trailing -Wno-error (appended
|
||||
# AFTER CFLAGS at compile time) demotes analyzer findings back to
|
||||
# warnings -- otherwise the first analyzer hit aborts the build.
|
||||
#
|
||||
# We do that by setting CC to a wrapper that execs the real compiler
|
||||
# with -fanalyzer prepended and -Wno-error appended. make -k keeps
|
||||
# building after individual failures so we see every hit in one run.
|
||||
#
|
||||
# Runs the existing top-level Makefile with an override CC; no
|
||||
# Makefile edits required. Output goes to stderr; redirect with
|
||||
# '2>&1 | tee analyze.log' to capture.
|
||||
|
||||
set -u
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
REAL_CC="$HOME/djgpp/djgpp/bin/i586-pc-msdosdjgpp-gcc"
|
||||
|
||||
if [ ! -x "$REAL_CC" ]; then
|
||||
echo "analyze.sh: $REAL_CC not executable" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build the wrapper in a temp dir that the spawned makes can see.
|
||||
WRAP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WRAP_DIR"' EXIT
|
||||
WRAP="$WRAP_DIR/analyze-cc"
|
||||
|
||||
cat > "$WRAP" <<WRAPEOF
|
||||
#!/bin/bash
|
||||
exec "$REAL_CC" -fanalyzer "\$@" -Wno-error
|
||||
WRAPEOF
|
||||
|
||||
chmod +x "$WRAP"
|
||||
|
||||
echo "analyze.sh: running full build under gcc -fanalyzer..." >&2
|
||||
echo "analyze.sh: this will be slow (2-5x normal build time)." >&2
|
||||
|
||||
# -k = keep going after errors so we collect every analyzer hit
|
||||
# CC=... overrides the assignment in each submakefile
|
||||
make -k CC="$WRAP" 2>&1
|
||||
|
||||
echo "analyze.sh: done. Grep for 'Wanalyzer' to see findings." >&2
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
# DVX Shell Applications Makefile — builds DXE3 modules
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
|
||||
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../dvx -I../tasks -I../dvxshell
|
||||
|
||||
OBJDIR = ../obj/apps
|
||||
BINDIR = ../bin/apps
|
||||
|
||||
# App definitions: each is a subdir with a single .c file
|
||||
APPS = progman notepad clock dvxdemo
|
||||
|
||||
.PHONY: all clean $(APPS)
|
||||
|
||||
all: $(APPS)
|
||||
|
||||
progman: $(BINDIR)/progman/progman.app
|
||||
notepad: $(BINDIR)/notepad/notepad.app
|
||||
clock: $(BINDIR)/clock/clock.app
|
||||
dvxdemo: $(BINDIR)/dvxdemo/dvxdemo.app
|
||||
|
||||
$(BINDIR)/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/progman
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
|
||||
$(BINDIR)/notepad/notepad.app: $(OBJDIR)/notepad.o | $(BINDIR)/notepad
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
|
||||
$(BINDIR)/clock/clock.app: $(OBJDIR)/clock.o | $(BINDIR)/clock
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -E _appShutdown -U $<
|
||||
|
||||
DVXDEMO_BMPS = logo.bmp new.bmp open.bmp sample.bmp save.bmp
|
||||
|
||||
$(BINDIR)/dvxdemo/dvxdemo.app: $(OBJDIR)/dvxdemo.o $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) | $(BINDIR)/dvxdemo
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/dvxdemo/
|
||||
|
||||
$(OBJDIR)/progman.o: progman/progman.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/notepad.o: notepad/notepad.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/clock.o: clock/clock.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(BINDIR)/progman:
|
||||
mkdir -p $(BINDIR)/progman
|
||||
|
||||
$(BINDIR)/notepad:
|
||||
mkdir -p $(BINDIR)/notepad
|
||||
|
||||
$(BINDIR)/clock:
|
||||
mkdir -p $(BINDIR)/clock
|
||||
|
||||
$(BINDIR)/dvxdemo:
|
||||
mkdir -p $(BINDIR)/dvxdemo
|
||||
|
||||
# Dependencies
|
||||
$(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 $(BINDIR)/progman/progman.app
|
||||
rm -f $(BINDIR)/notepad/notepad.app
|
||||
rm -f $(BINDIR)/clock/clock.app
|
||||
rm -f $(BINDIR)/dvxdemo/dvxdemo.app $(addprefix $(BINDIR)/dvxdemo/,$(DVXDEMO_BMPS))
|
||||
141
apps/README.md
141
apps/README.md
|
|
@ -1,141 +0,0 @@
|
|||
# DVX Shell Applications
|
||||
|
||||
DXE3 shared library applications loaded at runtime by the DVX Shell. Each app
|
||||
is a `.app` file (DXE3 format) placed under the `apps/` directory tree. The
|
||||
Program Manager scans this directory recursively and displays all discovered
|
||||
apps.
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
make # builds all .app files into ../bin/apps/<name>/
|
||||
make clean # removes objects and binaries
|
||||
```
|
||||
|
||||
Requires `lib/libdvx.a`, `lib/libtasks.a`, and the DXE3 tools (`dxe3gen`)
|
||||
from the DJGPP toolchain.
|
||||
|
||||
## Applications
|
||||
|
||||
| App | File | Type | Description |
|
||||
|-----|------|------|-------------|
|
||||
| Program Manager | `progman/progman.c` | Callback | Desktop app: app launcher grid, Task Manager (Ctrl+Esc), window management |
|
||||
| Notepad | `notepad/notepad.c` | Callback | Text editor with file I/O, dirty tracking via hash |
|
||||
| Clock | `clock/clock.c` | Main-loop | Digital clock, multi-instance capable |
|
||||
| DVX Demo | `dvxdemo/dvxdemo.c` | Callback | Widget system showcase with all widget types |
|
||||
|
||||
## Writing a New App
|
||||
|
||||
### Minimal callback-only app
|
||||
|
||||
```c
|
||||
#include "dvxApp.h"
|
||||
#include "dvxWidget.h"
|
||||
#include "shellApp.h"
|
||||
|
||||
AppDescriptorT appDescriptor = {
|
||||
.name = "My App",
|
||||
.hasMainLoop = false,
|
||||
.stackSize = SHELL_STACK_DEFAULT,
|
||||
.priority = TS_PRIORITY_NORMAL
|
||||
};
|
||||
|
||||
static DxeAppContextT *sCtx = NULL;
|
||||
static WindowT *sWin = NULL;
|
||||
|
||||
static void onClose(WindowT *win) {
|
||||
dvxDestroyWindow(sCtx->shellCtx, win);
|
||||
sWin = NULL;
|
||||
}
|
||||
|
||||
int32_t appMain(DxeAppContextT *ctx) {
|
||||
sCtx = ctx;
|
||||
AppContextT *ac = ctx->shellCtx;
|
||||
|
||||
sWin = dvxCreateWindow(ac, "My App", 100, 100, 300, 200, true);
|
||||
if (!sWin) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
sWin->onClose = onClose;
|
||||
|
||||
WidgetT *root = wgtInitWindow(ac, sWin);
|
||||
wgtLabel(root, "Hello, DVX!");
|
||||
|
||||
wgtInvalidate(root);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Minimal main-loop app
|
||||
|
||||
```c
|
||||
#include "dvxApp.h"
|
||||
#include "dvxWidget.h"
|
||||
#include "dvxWm.h"
|
||||
#include "shellApp.h"
|
||||
#include "taskswitch.h"
|
||||
|
||||
AppDescriptorT appDescriptor = {
|
||||
.name = "My Task App",
|
||||
.hasMainLoop = true,
|
||||
.stackSize = SHELL_STACK_DEFAULT,
|
||||
.priority = TS_PRIORITY_NORMAL
|
||||
};
|
||||
|
||||
static bool sQuit = false;
|
||||
|
||||
static void onClose(WindowT *win) {
|
||||
sQuit = true;
|
||||
}
|
||||
|
||||
int32_t appMain(DxeAppContextT *ctx) {
|
||||
AppContextT *ac = ctx->shellCtx;
|
||||
|
||||
WindowT *win = dvxCreateWindow(ac, "My Task App", 100, 100, 200, 100, false);
|
||||
if (!win) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
win->onClose = onClose;
|
||||
|
||||
while (!sQuit) {
|
||||
// Do work, update window content
|
||||
tsYield();
|
||||
}
|
||||
|
||||
dvxDestroyWindow(ac, win);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Adding to the build
|
||||
|
||||
Add your app directory and source to `apps/Makefile`. Each app is compiled to
|
||||
an object file, then linked into a `.app` via `dxe3gen`:
|
||||
|
||||
```makefile
|
||||
$(BIN_DIR)/myapp.app: $(OBJ_DIR)/myapp/myapp.o
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
```
|
||||
|
||||
The `-E` flags export the required symbols. `-U` marks unresolved symbols as
|
||||
imports to be resolved from the shell's export table at load time.
|
||||
|
||||
## App Guidelines
|
||||
|
||||
- Include `shellApp.h` for `AppDescriptorT`, `DxeAppContextT`, and
|
||||
`SHELL_STACK_DEFAULT`.
|
||||
- Use `ctx->shellCtx` (the `AppContextT *`) for all DVX API calls.
|
||||
- Callback-only apps must destroy their own windows in `onClose` via
|
||||
`dvxDestroyWindow()`. The shell detects the last window closing and
|
||||
reaps the app.
|
||||
- Main-loop apps must call `tsYield()` regularly. A task that never yields
|
||||
blocks the entire system.
|
||||
- Use file-scoped `static` variables for app state. Each DXE has its own data
|
||||
segment, so there is no collision between apps.
|
||||
- Set `multiInstance = true` in the descriptor if the app can safely run
|
||||
multiple copies simultaneously.
|
||||
- Avoid `static inline` functions in shared headers. Code inlined into the DXE
|
||||
binary cannot be updated without recompiling the app. Use macros for trivial
|
||||
expressions or regular functions exported through the shell's DXE table.
|
||||
|
|
@ -1,725 +0,0 @@
|
|||
// progman.c — Program Manager application for DVX Shell
|
||||
//
|
||||
// Displays a grid of available apps from the apps/ directory.
|
||||
// Double-click or Enter launches an app. Includes Task Manager (Ctrl+Esc).
|
||||
//
|
||||
// DXE App Contract:
|
||||
// This is a callback-only DXE app (hasMainLoop = false). It exports two
|
||||
// symbols: appDescriptor (metadata) and appMain (entry point). The shell
|
||||
// calls appMain once; we create windows, register callbacks, and return 0.
|
||||
// From that point on, the shell's event loop drives everything through
|
||||
// our window callbacks (onClose, onMenu, widget onClick, etc.).
|
||||
//
|
||||
// Because we have no main loop, we don't need a dedicated task stack.
|
||||
// The shell runs our callbacks in task 0 during dvxUpdate().
|
||||
//
|
||||
// Progman is special: it's the desktop app. It calls
|
||||
// shellRegisterDesktopUpdate() so the shell notifies us whenever an app
|
||||
// is loaded, reaped, or crashes, keeping our status bar and Task Manager
|
||||
// list current without polling.
|
||||
|
||||
#include "dvxApp.h"
|
||||
#include "dvxDialog.h"
|
||||
#include "dvxWidget.h"
|
||||
#include "dvxWm.h"
|
||||
#include "shellApp.h"
|
||||
#include "shellInfo.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
// ============================================================
|
||||
// Constants
|
||||
// ============================================================
|
||||
|
||||
// 64 entries is generous; limited by screen real estate before this cap
|
||||
#define MAX_APP_FILES 64
|
||||
// DOS 8.3 paths are short, but long names under DJGPP can reach ~260
|
||||
#define MAX_PATH_LEN 260
|
||||
// Grid layout for app buttons: 4 columns, rows created dynamically
|
||||
#define PM_GRID_COLS 4
|
||||
#define PM_BTN_W 100
|
||||
#define PM_BTN_H 24
|
||||
|
||||
// Menu command IDs
|
||||
#define CMD_RUN 100
|
||||
#define CMD_EXIT 101
|
||||
#define CMD_CASCADE 200
|
||||
#define CMD_TILE 201
|
||||
#define CMD_TILE_H 202
|
||||
#define CMD_TILE_V 203
|
||||
#define CMD_MIN_ON_RUN 104
|
||||
#define CMD_ABOUT 300
|
||||
#define CMD_TASK_MGR 301
|
||||
#define CMD_SYSINFO 302
|
||||
|
||||
// Task Manager column count
|
||||
#define TM_COL_COUNT 4
|
||||
|
||||
// ============================================================
|
||||
// Module state
|
||||
// ============================================================
|
||||
|
||||
// Each discovered .app file in the apps/ directory tree
|
||||
typedef struct {
|
||||
char name[SHELL_APP_NAME_MAX]; // display name (filename without .app)
|
||||
char path[MAX_PATH_LEN]; // full path
|
||||
} AppEntryT;
|
||||
|
||||
// Module-level statics (s prefix). DXE apps use file-scoped statics because
|
||||
// each DXE is a separate shared object with its own data segment. No risk
|
||||
// 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;
|
||||
static AppEntryT sAppFiles[MAX_APP_FILES];
|
||||
static int32_t sAppCount = 0;
|
||||
|
||||
// ============================================================
|
||||
// Prototypes
|
||||
// ============================================================
|
||||
|
||||
int32_t appMain(DxeAppContextT *ctx);
|
||||
static void buildPmWindow(void);
|
||||
static void desktopUpdate(void);
|
||||
static void onAppButtonClick(WidgetT *w);
|
||||
static void onPmClose(WindowT *win);
|
||||
static void onPmMenu(WindowT *win, int32_t menuId);
|
||||
static void scanAppsDir(void);
|
||||
static void scanAppsDirRecurse(const char *dirPath);
|
||||
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
|
||||
// ============================================================
|
||||
|
||||
// The shell reads this exported symbol to determine how to manage the app.
|
||||
// hasMainLoop = false means the shell won't create a dedicated task; our
|
||||
// appMain runs to completion and all subsequent work happens via callbacks.
|
||||
// stackSize = 0 means "use the shell default" (irrelevant for callback apps).
|
||||
AppDescriptorT appDescriptor = {
|
||||
.name = "Program Manager",
|
||||
.hasMainLoop = false,
|
||||
.stackSize = SHELL_STACK_DEFAULT,
|
||||
.priority = TS_PRIORITY_NORMAL
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Static functions (alphabetical)
|
||||
// ============================================================
|
||||
|
||||
// Build the main Program Manager window with app buttons, menus, and status bar.
|
||||
// Window is centered horizontally and placed in the upper quarter vertically
|
||||
// so spawned app windows don't hide behind it.
|
||||
static void buildPmWindow(void) {
|
||||
int32_t screenW = sAc->display.width;
|
||||
int32_t screenH = sAc->display.height;
|
||||
int32_t winW = 440;
|
||||
int32_t winH = 340;
|
||||
int32_t winX = (screenW - winW) / 2;
|
||||
int32_t winY = (screenH - winH) / 4;
|
||||
|
||||
sPmWindow = dvxCreateWindow(sAc, "Program Manager", winX, winY, winW, winH, true);
|
||||
|
||||
if (!sPmWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
sPmWindow->onClose = onPmClose;
|
||||
sPmWindow->onMenu = onPmMenu;
|
||||
|
||||
// Menu bar
|
||||
MenuBarT *menuBar = wmAddMenuBar(sPmWindow);
|
||||
MenuT *fileMenu = wmAddMenu(menuBar, "&File");
|
||||
wmAddMenuItem(fileMenu, "&Run...", CMD_RUN);
|
||||
wmAddMenuSeparator(fileMenu);
|
||||
wmAddMenuItem(fileMenu, "E&xit Shell", CMD_EXIT);
|
||||
|
||||
MenuT *optMenu = wmAddMenu(menuBar, "&Options");
|
||||
wmAddMenuCheckItem(optMenu, "&Minimize on Run", CMD_MIN_ON_RUN, false);
|
||||
|
||||
MenuT *windowMenu = wmAddMenu(menuBar, "&Window");
|
||||
wmAddMenuItem(windowMenu, "&Cascade", CMD_CASCADE);
|
||||
wmAddMenuItem(windowMenu, "&Tile", CMD_TILE);
|
||||
wmAddMenuItem(windowMenu, "Tile &Horizontally", CMD_TILE_H);
|
||||
wmAddMenuItem(windowMenu, "Tile &Vertically", CMD_TILE_V);
|
||||
|
||||
MenuT *helpMenu = wmAddMenu(menuBar, "&Help");
|
||||
wmAddMenuItem(helpMenu, "&About DVX Shell...", CMD_ABOUT);
|
||||
wmAddMenuItem(helpMenu, "&System Information...", CMD_SYSINFO);
|
||||
wmAddMenuSeparator(helpMenu);
|
||||
wmAddMenuItem(helpMenu, "&Task Manager\tCtrl+Esc", CMD_TASK_MGR);
|
||||
|
||||
// wgtInitWindow creates the root VBox widget that the window's content
|
||||
// area maps to. All child widgets are added to this root.
|
||||
WidgetT *root = wgtInitWindow(sAc, sPmWindow);
|
||||
|
||||
// App button grid in a labeled frame. weight=100 tells the layout engine
|
||||
// this frame should consume all available vertical space (flex weight).
|
||||
WidgetT *appFrame = wgtFrame(root, "Applications");
|
||||
appFrame->weight = 100;
|
||||
|
||||
if (sAppCount == 0) {
|
||||
WidgetT *lbl = wgtLabel(appFrame, "(No applications found in apps/ directory)");
|
||||
(void)lbl;
|
||||
} else {
|
||||
// Build rows of buttons. Each row is an HBox holding PM_GRID_COLS
|
||||
// buttons. userData points back to the AppEntryT so the click
|
||||
// callback knows which app to launch.
|
||||
int32_t row = 0;
|
||||
WidgetT *hbox = NULL;
|
||||
|
||||
for (int32_t i = 0; i < sAppCount; i++) {
|
||||
if (i % PM_GRID_COLS == 0) {
|
||||
hbox = wgtHBox(appFrame);
|
||||
hbox->spacing = wgtPixels(8);
|
||||
row++;
|
||||
}
|
||||
|
||||
WidgetT *btn = wgtButton(hbox, sAppFiles[i].name);
|
||||
btn->prefW = wgtPixels(PM_BTN_W);
|
||||
btn->prefH = wgtPixels(PM_BTN_H);
|
||||
btn->userData = &sAppFiles[i];
|
||||
btn->onDblClick = onAppButtonClick;
|
||||
btn->onClick = onAppButtonClick;
|
||||
}
|
||||
|
||||
(void)row;
|
||||
}
|
||||
|
||||
// Status bar at bottom; weight=100 on the label makes it fill the bar
|
||||
// width so text can be left-aligned naturally.
|
||||
WidgetT *statusBar = wgtStatusBar(root);
|
||||
sStatusLabel = wgtLabel(statusBar, "");
|
||||
sStatusLabel->weight = 100;
|
||||
updateStatusText();
|
||||
|
||||
// dvxFitWindow sizes the window to tightly fit the widget tree,
|
||||
// honoring preferred sizes. Without this, the window would use the
|
||||
// initial dimensions from dvxCreateWindow even if widgets don't fit.
|
||||
dvxFitWindow(sAc, sPmWindow);
|
||||
}
|
||||
|
||||
|
||||
// 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.
|
||||
static void desktopUpdate(void) {
|
||||
updateStatusText();
|
||||
|
||||
if (sTmWindow) {
|
||||
refreshTaskList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Widget click handler for app grid buttons. userData was set to the
|
||||
// AppEntryT pointer during window construction, giving us the .app path.
|
||||
static void onAppButtonClick(WidgetT *w) {
|
||||
AppEntryT *entry = (AppEntryT *)w->userData;
|
||||
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
shellLoadApp(sAc, entry->path);
|
||||
updateStatusText();
|
||||
|
||||
if (sMinOnRun && sPmWindow) {
|
||||
dvxMinimizeWindow(sAc, sPmWindow);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Closing the Program Manager is equivalent to shutting down the entire shell.
|
||||
// dvxQuit() signals the main event loop to exit, which triggers
|
||||
// shellTerminateAllApps() to gracefully tear down all loaded DXEs.
|
||||
static void onPmClose(WindowT *win) {
|
||||
(void)win;
|
||||
int32_t result = dvxMessageBox(sAc, "Exit Shell", "Are you sure you want to exit DVX Shell?", MB_YESNO | MB_ICONQUESTION);
|
||||
|
||||
if (result == ID_YES) {
|
||||
dvxQuit(sAc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void onPmMenu(WindowT *win, int32_t menuId) {
|
||||
(void)win;
|
||||
|
||||
switch (menuId) {
|
||||
case CMD_RUN:
|
||||
{
|
||||
FileFilterT filters[] = {
|
||||
{ "Applications (*.app)", "*.app" },
|
||||
{ "All Files (*.*)", "*.*" }
|
||||
};
|
||||
char path[MAX_PATH_LEN];
|
||||
|
||||
if (dvxFileDialog(sAc, "Run Application", FD_OPEN, "apps", filters, 2, path, sizeof(path))) {
|
||||
shellLoadApp(sAc, path);
|
||||
updateStatusText();
|
||||
|
||||
if (sMinOnRun && sPmWindow) {
|
||||
dvxMinimizeWindow(sAc, sPmWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case CMD_EXIT:
|
||||
onPmClose(sPmWindow);
|
||||
break;
|
||||
|
||||
case CMD_CASCADE:
|
||||
dvxCascadeWindows(sAc);
|
||||
break;
|
||||
|
||||
case CMD_TILE:
|
||||
dvxTileWindows(sAc);
|
||||
break;
|
||||
|
||||
case CMD_TILE_H:
|
||||
dvxTileWindowsH(sAc);
|
||||
break;
|
||||
|
||||
case CMD_TILE_V:
|
||||
dvxTileWindowsV(sAc);
|
||||
break;
|
||||
|
||||
case CMD_MIN_ON_RUN:
|
||||
sMinOnRun = !sMinOnRun;
|
||||
break;
|
||||
|
||||
case CMD_ABOUT:
|
||||
showAboutDialog();
|
||||
break;
|
||||
|
||||
case CMD_SYSINFO:
|
||||
showSystemInfo();
|
||||
break;
|
||||
|
||||
case CMD_TASK_MGR:
|
||||
buildTaskManager();
|
||||
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.
|
||||
static void scanAppsDir(void) {
|
||||
sAppCount = 0;
|
||||
scanAppsDirRecurse("apps");
|
||||
shellLog("Progman: found %ld app(s)", (long)sAppCount);
|
||||
}
|
||||
|
||||
|
||||
// Recursive directory walker. Subdirectories under apps/ allow organizing
|
||||
// apps (e.g., apps/games/, apps/tools/). Each .app file is a DXE3 shared
|
||||
// object that the shell can dlopen(). We skip progman.app to avoid listing
|
||||
// ourselves in the launcher grid.
|
||||
static void scanAppsDirRecurse(const char *dirPath) {
|
||||
DIR *dir = opendir(dirPath);
|
||||
|
||||
if (!dir) {
|
||||
if (sAppCount == 0) {
|
||||
shellLog("Progman: %s directory not found", dirPath);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
struct dirent *ent;
|
||||
|
||||
while ((ent = readdir(dir)) != NULL && sAppCount < MAX_APP_FILES) {
|
||||
// Skip . and ..
|
||||
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char fullPath[MAX_PATH_LEN];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s/%s", dirPath, ent->d_name);
|
||||
|
||||
// Check if this is a directory — recurse into it
|
||||
struct stat st;
|
||||
|
||||
if (stat(fullPath, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||
scanAppsDirRecurse(fullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t len = strlen(ent->d_name);
|
||||
|
||||
if (len < 5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for .app extension (case-insensitive)
|
||||
const char *ext = ent->d_name + len - 4;
|
||||
|
||||
if (strcmp(ext, ".app") != 0 && strcmp(ext, ".APP") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip ourselves
|
||||
if (strcmp(ent->d_name, "progman.app") == 0 || strcmp(ent->d_name, "PROGMAN.APP") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AppEntryT *entry = &sAppFiles[sAppCount];
|
||||
|
||||
// Name = filename without extension
|
||||
int32_t nameLen = len - 4;
|
||||
|
||||
if (nameLen >= SHELL_APP_NAME_MAX) {
|
||||
nameLen = SHELL_APP_NAME_MAX - 1;
|
||||
}
|
||||
|
||||
memcpy(entry->name, ent->d_name, nameLen);
|
||||
entry->name[nameLen] = '\0';
|
||||
|
||||
// Capitalize first letter for display
|
||||
if (entry->name[0] >= 'a' && entry->name[0] <= 'z') {
|
||||
entry->name[0] -= 32;
|
||||
}
|
||||
|
||||
snprintf(entry->path, sizeof(entry->path), "%s", fullPath);
|
||||
sAppCount++;
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
|
||||
static void showAboutDialog(void) {
|
||||
dvxMessageBox(sAc, "About DVX Shell",
|
||||
"DVX Shell 1.0\nA DOS Visual eXecutive desktop shell for DJGPP/DPMI. Using DXE3 dynamic loading for application modules.",
|
||||
MB_OK | MB_ICONINFO);
|
||||
}
|
||||
|
||||
|
||||
static void showSystemInfo(void) {
|
||||
const char *info = shellGetSystemInfo();
|
||||
|
||||
if (!info || !info[0]) {
|
||||
dvxMessageBox(sAc, "System Information", "No system information available.", MB_OK | MB_ICONINFO);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a window with a read-only text area
|
||||
int32_t screenW = sAc->display.width;
|
||||
int32_t screenH = sAc->display.height;
|
||||
int32_t winW = 400;
|
||||
int32_t winH = 360;
|
||||
int32_t winX = (screenW - winW) / 2;
|
||||
int32_t winY = (screenH - winH) / 4;
|
||||
|
||||
WindowT *win = dvxCreateWindow(sAc, "System Information", winX, winY, winW, winH, true);
|
||||
|
||||
if (!win) {
|
||||
return;
|
||||
}
|
||||
|
||||
WidgetT *root = wgtInitWindow(sAc, win);
|
||||
WidgetT *ta = wgtTextArea(root, 4096);
|
||||
ta->weight = 100;
|
||||
wgtSetText(ta, info);
|
||||
|
||||
// Don't disable — wgtSetEnabled(false) blocks all input including scrollbar
|
||||
wgtSetReadOnly(ta, true);
|
||||
}
|
||||
|
||||
|
||||
static void updateStatusText(void) {
|
||||
if (!sStatusLabel) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
snprintf(buf, sizeof(buf), "No applications running");
|
||||
} else if (count == 1) {
|
||||
snprintf(buf, sizeof(buf), "1 application running");
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%ld applications running", (long)count);
|
||||
}
|
||||
|
||||
wgtSetText(sStatusLabel, buf);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Entry point
|
||||
// ============================================================
|
||||
|
||||
// The shell calls appMain exactly once after dlopen() resolves our symbols.
|
||||
// We scan for apps, build the UI, register our desktop update callback, then
|
||||
// return 0 (success). From here on the shell drives us through callbacks.
|
||||
// Returning non-zero would signal a load failure and the shell would unload us.
|
||||
int32_t appMain(DxeAppContextT *ctx) {
|
||||
sCtx = ctx;
|
||||
sAc = ctx->shellCtx;
|
||||
sMyAppId = ctx->appId;
|
||||
|
||||
scanAppsDir();
|
||||
buildPmWindow();
|
||||
|
||||
// Register for state change notifications from the shell so our status
|
||||
// bar and Task Manager stay current without polling
|
||||
shellRegisterDesktopUpdate(desktopUpdate);
|
||||
|
||||
return 0;
|
||||
}
|
||||
BIN
assets/DVX Help Logo.xcf
(Stored with Git LFS)
Normal file
BIN
assets/DVX Help Logo.xcf
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/DVX Logo.xcf
(Stored with Git LFS)
Normal file
BIN
assets/DVX Logo.xcf
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/DVX Text.xcf
(Stored with Git LFS)
Normal file
BIN
assets/DVX Text.xcf
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/help.png
(Stored with Git LFS)
Normal file
BIN
assets/help.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/splash.bmp
(Stored with Git LFS)
Normal file
BIN
assets/splash.bmp
(Stored with Git LFS)
Normal file
Binary file not shown.
157
config/README.md
Normal file
157
config/README.md
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# DVX Configuration Files
|
||||
|
||||
Runtime configuration, theme files, wallpaper images, and module
|
||||
dependency files. These are copied into `bin/config/` (INI, themes,
|
||||
wallpapers) or `bin/libs/` and `bin/widgets/` (dep files) during the
|
||||
build. Text files are converted to DOS line endings (CR+LF) via sed.
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
| Source File | Build Output | Description |
|
||||
|-------------|-------------|-------------|
|
||||
| `dvx.ini` | `bin/config/dvx.ini` | System configuration |
|
||||
| `themes/cde.thm` | `bin/config/themes/cde.thm` | CDE color theme |
|
||||
| `themes/geos.thm` | `bin/config/themes/geos.thm` | GEOS Ensemble theme |
|
||||
| `themes/win31.thm` | `bin/config/themes/win31.thm` | Windows 3.1 theme |
|
||||
| `wpaper/blueglow.jpg` | `bin/config/wpaper/blueglow.jpg` | Wallpaper image |
|
||||
| `wpaper/swoop.jpg` | `bin/config/wpaper/swoop.jpg` | Wallpaper image |
|
||||
| `wpaper/triangle.jpg` | `bin/config/wpaper/triangle.jpg` | Wallpaper image |
|
||||
| `libdvx.dep` | `bin/libs/libdvx.dep` | libdvx dependency file |
|
||||
| `texthelp.dep` | `bin/libs/texthelp.dep` | texthelp dependency file |
|
||||
| `listhelp.dep` | `bin/libs/listhelp.dep` | listhelp dependency file |
|
||||
| `dvxshell.dep` | `bin/libs/dvxshell.dep` | dvxshell dependency file |
|
||||
| `textinpt.dep` | `bin/widgets/textinpt.dep` | TextInput widget dep file |
|
||||
| `combobox.dep` | `bin/widgets/combobox.dep` | ComboBox widget dep file |
|
||||
| `spinner.dep` | `bin/widgets/spinner.dep` | Spinner widget dep file |
|
||||
| `terminal.dep` | `bin/widgets/terminal.dep` | AnsiTerm widget dep file |
|
||||
| `dropdown.dep` | `bin/widgets/dropdown.dep` | Dropdown widget dep file |
|
||||
| `listbox.dep` | `bin/widgets/listbox.dep` | ListBox widget dep file |
|
||||
| `listview.dep` | `bin/widgets/listview.dep` | ListView widget dep file |
|
||||
| `treeview.dep` | `bin/widgets/treeview.dep` | TreeView widget dep file |
|
||||
| `basrt.dep` | `bin/libs/basrt.dep` | BASIC runtime dep file |
|
||||
| `serial.dep` | `bin/libs/serial.dep` | Serial communications dep file |
|
||||
|
||||
|
||||
## dvx.ini Format
|
||||
|
||||
Standard INI format with `[section]` headers and `key = value` pairs.
|
||||
Comments start with `;`. The shell loads this at startup via
|
||||
`prefsLoad("CONFIG/DVX.INI")`.
|
||||
|
||||
### [video] Section
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `width` | 640 | Requested horizontal resolution |
|
||||
| `height` | 480 | Requested vertical resolution |
|
||||
| `bpp` | 16 | Preferred color depth (8, 15, 16, 24, 32) |
|
||||
|
||||
The system picks the closest available VESA mode.
|
||||
|
||||
### [mouse] Section
|
||||
|
||||
| Key | Default | Values | Description |
|
||||
|-----|---------|--------|-------------|
|
||||
| `wheel` | normal | normal, reversed | Scroll wheel direction |
|
||||
| `doubleclick` | 500 | 200-900 | Double-click speed (ms) |
|
||||
| `acceleration` | medium | off, low, medium, high | Mouse acceleration |
|
||||
|
||||
### [shell] Section
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `desktop` | apps/progman/progman.app | Path to the desktop app loaded at startup |
|
||||
|
||||
### [colors] Section
|
||||
|
||||
All 20 system colors as `R,G,B` triplets (0-255). Missing keys use
|
||||
compiled-in defaults.
|
||||
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| `desktop` | Desktop background |
|
||||
| `windowFace` | Window frame and widget face |
|
||||
| `windowHighlight` | Bevel highlight (top/left) |
|
||||
| `windowShadow` | Bevel shadow (bottom/right) |
|
||||
| `activeTitleBg` | Focused window title bar background |
|
||||
| `activeTitleFg` | Focused window title bar text |
|
||||
| `inactiveTitleBg` | Unfocused window title bar background |
|
||||
| `inactiveTitleFg` | Unfocused window title bar text |
|
||||
| `contentBg` | Content area background |
|
||||
| `contentFg` | Content area text |
|
||||
| `menuBg` | Menu background |
|
||||
| `menuFg` | Menu text |
|
||||
| `menuHighlightBg` | Menu selection background |
|
||||
| `menuHighlightFg` | Menu selection text |
|
||||
| `buttonFace` | Button face color |
|
||||
| `scrollbarBg` | Scrollbar background |
|
||||
| `scrollbarFg` | Scrollbar foreground |
|
||||
| `scrollbarTrough` | Scrollbar track color |
|
||||
| `cursorColor` | Mouse cursor foreground |
|
||||
| `cursorOutline` | Mouse cursor outline |
|
||||
|
||||
### [desktop] Section
|
||||
|
||||
| Key | Default | Values | Description |
|
||||
|-----|---------|--------|-------------|
|
||||
| `wallpaper` | (none) | file path | Path to wallpaper image |
|
||||
| `mode` | stretch | stretch, tile, center | Wallpaper display mode |
|
||||
|
||||
|
||||
## Theme Files (.thm)
|
||||
|
||||
Theme files use the same INI format as dvx.ini but contain only a
|
||||
`[colors]` section with the 20 color keys. The Control Panel can
|
||||
load and save themes via `dvxLoadTheme()` / `dvxSaveTheme()`.
|
||||
|
||||
Bundled themes:
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `cde.thm` | CDE (Common Desktop Environment) -- warm tan/blue palette |
|
||||
| `geos.thm` | GEOS Ensemble -- cyan/grey palette |
|
||||
| `win31.thm` | Windows 3.1 -- grey/navy palette |
|
||||
|
||||
|
||||
## Dependency Files (.dep)
|
||||
|
||||
Plain text files listing module base names that must be loaded before
|
||||
this module. One name per line. Empty lines and `#` comments are
|
||||
ignored. Names are case-insensitive.
|
||||
|
||||
### Library Dependencies
|
||||
|
||||
| Dep File | Module | Dependencies |
|
||||
|----------|--------|--------------|
|
||||
| `libdvx.dep` | libdvx.lib | libtasks |
|
||||
| `texthelp.dep` | texthelp.lib | libtasks, libdvx |
|
||||
| `listhelp.dep` | listhelp.lib | libtasks, libdvx |
|
||||
| `dvxshell.dep` | dvxshell.lib | libtasks, libdvx, texthelp, listhelp |
|
||||
| `basrt.dep` | basrt.lib | libtasks, libdvx |
|
||||
| `serial.dep` | serial.lib | libtasks, libdvx |
|
||||
|
||||
### Widget Dependencies
|
||||
|
||||
| Dep File | Widget | Dependencies |
|
||||
|----------|--------|--------------|
|
||||
| `textinpt.dep` | textinpt.wgt | texthelp |
|
||||
| `combobox.dep` | combobox.wgt | texthelp, listhelp |
|
||||
| `spinner.dep` | spinner.wgt | texthelp |
|
||||
| `terminal.dep` | terminal.wgt | texthelp |
|
||||
| `dropdown.dep` | dropdown.wgt | listhelp |
|
||||
| `listbox.dep` | listbox.wgt | listhelp |
|
||||
| `listview.dep` | listview.wgt | listhelp |
|
||||
| `treeview.dep` | treeview.wgt | listhelp |
|
||||
|
||||
|
||||
## Wallpaper Images
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `wpaper/blueglow.jpg` | Blue gradient glow |
|
||||
| `wpaper/swoop.jpg` | Curved swoosh pattern |
|
||||
| `wpaper/triangle.jpg` | Geometric triangle pattern |
|
||||
|
||||
Wallpapers support BMP, PNG, JPEG, and GIF formats. They are
|
||||
pre-rendered to screen dimensions in native pixel format at load time.
|
||||
|
|
@ -5,16 +5,26 @@
|
|||
; Supported color depths: 8, 15, 16, 24, 32
|
||||
|
||||
[video]
|
||||
width = 640
|
||||
height = 480
|
||||
width = 1024
|
||||
height = 768
|
||||
bpp = 16
|
||||
|
||||
; Mouse settings.
|
||||
; wheel: normal or reversed
|
||||
; wheelspeed: lines per wheel notch (1-10, default 3)
|
||||
; doubleclick: double-click speed in milliseconds (200-900, default 500)
|
||||
; acceleration: off, low, medium, high (default medium)
|
||||
; speed: cursor speed (2-32, default 8; higher = faster)
|
||||
|
||||
[mouse]
|
||||
wheel = normal
|
||||
wheelspeed = 3
|
||||
doubleclick = 500
|
||||
acceleration = medium
|
||||
speed = 8
|
||||
|
||||
; Shell settings.
|
||||
; desktop: path to the desktop app loaded at startup
|
||||
|
||||
[shell]
|
||||
desktop = apps/kpunch/progman/progman.app
|
||||
24
config/themes/cde.thm
Normal file
24
config/themes/cde.thm
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
; 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
|
||||
cursorColor = 255,255,255
|
||||
cursorOutline = 0,0,0
|
||||
24
config/themes/geos.thm
Normal file
24
config/themes/geos.thm
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
; 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
|
||||
cursorColor = 255,255,255
|
||||
cursorOutline = 0,0,0
|
||||
25
config/themes/hotdog.thm
Normal file
25
config/themes/hotdog.thm
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
; DVX Color Theme - Hot Dog Stand
|
||||
; The infamous Windows 3.1 color scheme. Yellow, red, black.
|
||||
; Included here for historical authenticity and eye strain.
|
||||
|
||||
[colors]
|
||||
desktop = 255,255,0
|
||||
windowFace = 255,255,0
|
||||
windowHighlight = 255,255,255
|
||||
windowShadow = 0,0,0
|
||||
activeTitleBg = 255,0,0
|
||||
activeTitleFg = 255,255,255
|
||||
inactiveTitleBg = 255,255,255
|
||||
inactiveTitleFg = 0,0,0
|
||||
contentBg = 255,255,255
|
||||
contentFg = 0,0,0
|
||||
menuBg = 255,255,255
|
||||
menuFg = 0,0,0
|
||||
menuHighlightBg = 255,0,0
|
||||
menuHighlightFg = 255,255,255
|
||||
buttonFace = 255,255,0
|
||||
scrollbarBg = 255,255,0
|
||||
scrollbarFg = 0,0,0
|
||||
scrollbarTrough = 255,255,255
|
||||
cursorColor = 255,255,0
|
||||
cursorOutline = 0,0,0
|
||||
24
config/themes/win31.thm
Normal file
24
config/themes/win31.thm
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
; 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
|
||||
cursorColor = 255,255,255
|
||||
cursorOutline = 0,0,0
|
||||
BIN
config/wpaper/blueglow.jpg
(Stored with Git LFS)
Normal file
BIN
config/wpaper/blueglow.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
config/wpaper/swoop.jpg
(Stored with Git LFS)
Normal file
BIN
config/wpaper/swoop.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
config/wpaper/triangle.jpg
(Stored with Git LFS)
Normal file
BIN
config/wpaper/triangle.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
76
cppcheck.sh
Executable file
76
cppcheck.sh
Executable file
|
|
@ -0,0 +1,76 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# cppcheck.sh -- static analysis for the DVX source tree.
|
||||
#
|
||||
# Usage:
|
||||
# ./cppcheck.sh # whole tree
|
||||
# ./cppcheck.sh src/widgets # one subtree
|
||||
# ./cppcheck.sh src/libs/kpunch/texthelp/textHelp.c # one file
|
||||
#
|
||||
# Exits 0 even when warnings are found -- cppcheck's exit status is
|
||||
# not useful as a gate for this codebase (too many DJGPP-isms it can't
|
||||
# resolve). Read the output.
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
TARGETS="${@:-src}"
|
||||
|
||||
INCLUDES=(
|
||||
-Isrc/libs/kpunch/libdvx
|
||||
-Isrc/libs/kpunch/libdvx/platform
|
||||
-Isrc/libs/kpunch/libdvx/thirdparty
|
||||
-Isrc/libs/kpunch/libtasks
|
||||
-Isrc/libs/kpunch/texthelp
|
||||
-Isrc/libs/kpunch/listhelp
|
||||
-Isrc/libs/kpunch/dvxshell
|
||||
-Isrc/libs/kpunch/taskmgr
|
||||
-Isrc/libs/kpunch/serial
|
||||
-Isrc/widgets/kpunch
|
||||
-Isrc/apps/kpunch/dvxbasic
|
||||
-Isrc/apps/kpunch/dvxbasic/compiler
|
||||
-Isrc/apps/kpunch/dvxbasic/runtime
|
||||
-Isrc/apps/kpunch/dvxbasic/formrt
|
||||
)
|
||||
|
||||
# DJGPP / platform macros cppcheck can't find. Expand them to nothing
|
||||
# so the preprocessor leaves the code visible to the analyser instead
|
||||
# of eliding whole functions behind unknown macro calls.
|
||||
DEFINES=(
|
||||
-DDXE_EXPORT=
|
||||
-DDVX_WIDGET_IMPL
|
||||
-D__DJGPP__=2
|
||||
)
|
||||
|
||||
# Suppressions: things cppcheck flags that aren't actually wrong for
|
||||
# this codebase. Keep the list short so real issues stay visible.
|
||||
SUPPRESS=(
|
||||
--suppress=missingIncludeSystem # system headers aren't on host
|
||||
--suppress=unusedFunction # many public exports look "unused" to a .c-only scan
|
||||
--suppress=constParameterPointer # noisy on this style
|
||||
--suppress=constVariablePointer # ditto
|
||||
--suppress=normalCheckLevelMaxBranches
|
||||
# stb_ds arrput is a macro -- cppcheck can't see that the value
|
||||
# (including heap pointers inside it) is stored, so it reports
|
||||
# spurious null-derefs on the stb_ds dynamic-array handle.
|
||||
--suppress=nullPointerRedundantCheck
|
||||
--suppress=nullPointerArithmeticRedundantCheck
|
||||
# Thirdparty headers -- their own style isn't our problem.
|
||||
--suppress='*:src/libs/kpunch/libdvx/thirdparty/*'
|
||||
--suppress='*:src/libs/kpunch/sql/thirdparty/*'
|
||||
)
|
||||
|
||||
exec cppcheck \
|
||||
--enable=warning,style,performance,portability \
|
||||
--inline-suppr \
|
||||
--std=c99 \
|
||||
--platform=unix32 \
|
||||
--quiet \
|
||||
-j"$(nproc)" \
|
||||
-i src/libs/kpunch/sql/thirdparty \
|
||||
-i src/libs/kpunch/libdvx/thirdparty \
|
||||
"${INCLUDES[@]}" \
|
||||
"${DEFINES[@]}" \
|
||||
"${SUPPRESS[@]}" \
|
||||
$TARGETS
|
||||
3319
docs/dvx_basic_reference.html
Normal file
3319
docs/dvx_basic_reference.html
Normal file
File diff suppressed because it is too large
Load diff
171
docs/dvx_help_viewer.html
Normal file
171
docs/dvx_help_viewer.html
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>DVX Help Viewer</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 0; padding: 0; display: flex; }
|
||||
nav { width: 250px; min-width: 250px; background: #f0f0f0; padding: 16px;
|
||||
border-right: 1px solid #ccc; height: 100vh; overflow-y: auto;
|
||||
position: sticky; top: 0; box-sizing: border-box; }
|
||||
nav ul { list-style: none; padding-left: 16px; margin: 4px 0; }
|
||||
nav > ul { padding-left: 0; }
|
||||
nav a { text-decoration: none; color: #0066cc; }
|
||||
nav a:hover { text-decoration: underline; }
|
||||
main { flex: 1; padding: 24px 32px; max-width: 800px; }
|
||||
h1 { border-bottom: 2px solid #333; padding-bottom: 4px; }
|
||||
h2 { border-bottom: 1px solid #999; padding-bottom: 2px; margin-top: 32px; }
|
||||
h3 { margin-top: 24px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; padding: 8px;
|
||||
overflow-x: auto; font-size: 14px; }
|
||||
blockquote { background: #fffde7; border-left: 4px solid #ffc107;
|
||||
padding: 8px 12px; margin: 12px 0; }
|
||||
hr { border: none; border-top: 1px solid #ccc; margin: 24px 0; }
|
||||
img { max-width: 100%; }
|
||||
.topic { margin-bottom: 48px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<h3>Contents</h3>
|
||||
<ul>
|
||||
<li><a href="#help.overview">DVX Help Viewer</a></li>
|
||||
<li><a href="#help.format">Help Source Format</a></li>
|
||||
<li><a href="#help.compiler">Help Compiler</a></li>
|
||||
<li><a href="#help.integration">Application Integration</a></li>
|
||||
</ul>
|
||||
<h3>Index</h3>
|
||||
<ul>
|
||||
<li><a href="#help.format">.dhs</a></li>
|
||||
<li><a href="#help.compiler">Compiler</a></li>
|
||||
<li><a href="#help.integration">Context Help</a></li>
|
||||
<li><a href="#help.format">Directives</a></li>
|
||||
<li><a href="#help.overview">DVX Help</a></li>
|
||||
<li><a href="#help.compiler">dvxhlpc</a></li>
|
||||
<li><a href="#help.integration">F1</a></li>
|
||||
<li><a href="#help.overview">Help Viewer</a></li>
|
||||
<li><a href="#help.integration">helpFile</a></li>
|
||||
<li><a href="#help.integration">helpTopic</a></li>
|
||||
<li><a href="#help.integration">shellLoadAppWithArgs</a></li>
|
||||
<li><a href="#help.format">Source Format</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main>
|
||||
<div class="topic" id="help.overview">
|
||||
<h1>DVX Help Viewer</h1>
|
||||
<p>The DVX Help Viewer displays .hlp help files compiled from .dhs source documents. It provides a tree-based table of contents, scrollable content with word-wrapped text, clickable hyperlinks, full-text search, and a keyword index.</p>
|
||||
<h2>Opening Help</h2>
|
||||
<p>Press F1 from any DVX application to open context-sensitive help. Applications can register their own help file and topic so F1 opens the relevant page.</p>
|
||||
<p>You can also launch the help viewer from an application's Help menu, or by clicking the DVX Help icon in the Program Manager.</p>
|
||||
<h2>Navigation</h2>
|
||||
<ul>
|
||||
<li>Click a topic in the tree on the left to display it</li>
|
||||
<li>Click underlined links in the content to jump to other topics</li>
|
||||
<li>Use the Back and Forward buttons (or Navigate menu) to retrace your steps</li>
|
||||
<li>Use Navigate > Index to browse an alphabetical keyword list</li>
|
||||
</ul>
|
||||
<p>Use Navigate > Search to find topics by keyword</p>
|
||||
<h2>Keyboard Shortcuts</h2>
|
||||
<pre> Alt+Left Back
|
||||
Alt+Right Forward
|
||||
Ctrl+F Search
|
||||
Escape Close viewer</pre>
|
||||
</div>
|
||||
<div class="topic" id="help.format">
|
||||
<h1>Help Source Format (.dhs)</h1>
|
||||
<p>Help files are authored as plain text .dhs source files using a simple line-oriented directive format. Lines beginning with a period at column 0 are directives. All other lines are body text, which is automatically word-wrapped by the viewer at display time.</p>
|
||||
<h2>Topic Directives</h2>
|
||||
<pre> .topic <id> Start a new topic with a unique string ID
|
||||
.title <text> Set the topic's display title
|
||||
.toc <depth> <text> Add a table of contents entry (0=root, 1=child, etc.)
|
||||
.default Mark this topic as the one shown when the file opens</pre>
|
||||
<h2>Content Directives</h2>
|
||||
<pre> .h1 <text> Level 1 heading (colored bar)
|
||||
.h2 <text> Level 2 heading (underlined)
|
||||
.h3 <text> Level 3 heading (plain)
|
||||
.hr Horizontal rule
|
||||
.link <id> <text> Hyperlink to another topic
|
||||
.image <file.bmp> Inline image (BMP format)</pre>
|
||||
<h2>Block Directives</h2>
|
||||
<pre> .list Start a bulleted list
|
||||
.item <text> List item (must be inside .list)
|
||||
.endlist End the bulleted list
|
||||
.table Start a preformatted table block
|
||||
.endtable End table block
|
||||
.code Start a preformatted code block
|
||||
.endcode End code block
|
||||
.note [info|tip|warning] Start a callout box
|
||||
.endnote End callout box</pre>
|
||||
<h2>Index Directives</h2>
|
||||
<pre> .index <keyword> Add a keyword to the index pointing to this topic</pre>
|
||||
<h2>Example</h2>
|
||||
<pre><code>.topic intro
|
||||
.title Welcome
|
||||
.toc 0 Welcome
|
||||
.default
|
||||
.index Welcome
|
||||
|
||||
.h1 Welcome
|
||||
|
||||
This is a paragraph of body text. It will be
|
||||
automatically word-wrapped by the viewer.
|
||||
|
||||
.list
|
||||
.item First item
|
||||
.item Second item
|
||||
.endlist
|
||||
|
||||
.link other.topic See also: Other Topic
|
||||
|
||||
.note info
|
||||
This is an informational note.
|
||||
.endnote
|
||||
|
||||
.note tip
|
||||
This is a helpful tip.
|
||||
.endnote
|
||||
|
||||
.note warning
|
||||
This is a warning message.
|
||||
.endnote</code></pre>
|
||||
<h2>Callout Boxes</h2>
|
||||
<p>Three types of callout boxes are available, each with a distinct colored accent bar:</p>
|
||||
<blockquote><strong>Note:</strong> Use info notes for general supplementary information.</blockquote>
|
||||
<blockquote><strong>Tip:</strong> Use tip notes for helpful suggestions and best practices.</blockquote>
|
||||
<blockquote><strong>Warning:</strong> Use warning notes for important cautions the reader should be aware of.</blockquote>
|
||||
</div>
|
||||
<div class="topic" id="help.compiler">
|
||||
<h1>Help Compiler (dvxhlpc)</h1>
|
||||
<p>The dvxhlpc tool runs on the host (Linux) and compiles .dhs source files into binary .hlp files for the viewer, and optionally into self-contained HTML.</p>
|
||||
<h2>Usage</h2>
|
||||
<pre><code>dvxhlpc -o output.hlp [-i imagedir] [--html out.html] input.dhs [...]</code></pre>
|
||||
<h2>Options</h2>
|
||||
<pre> -o output.hlp Output binary help file (required)
|
||||
-i imagedir Directory to find .image files (default: current dir)
|
||||
--html out.html Also emit a self-contained HTML file</pre>
|
||||
<p>Multiple input files are merged into a single help file. This allows per-widget or per-feature documentation fragments to be combined automatically.</p>
|
||||
<h2>Build Integration</h2>
|
||||
<p>The standard build pattern globs all fragments:</p>
|
||||
<pre><code>dvxhlpc -o dvxhelp.hlp docs/src/overview.dhs widgets/*/*.dhs</code></pre>
|
||||
<p>New widgets or features just drop a .dhs file in their source directory and it appears in the help on the next build.</p>
|
||||
<h2>HTML Output</h2>
|
||||
<p>The --html flag produces a single self-contained HTML file with a sidebar table of contents, styled headings, lists, code blocks, notes, and embedded images (base64 data URIs). This is useful for viewing documentation on the host machine without running the DOS help viewer.</p>
|
||||
</div>
|
||||
<div class="topic" id="help.integration">
|
||||
<h1>Application Integration</h1>
|
||||
<p>Any DVX application can provide context-sensitive help via the F1 key.</p>
|
||||
<h2>Setting Up Help</h2>
|
||||
<p>In your appMain, set the help file path on the app context:</p>
|
||||
<pre><code>snprintf(ctx->helpFile, sizeof(ctx->helpFile),
|
||||
"%s%cMYAPP.HLP", ctx->appDir, DVX_PATH_SEP);</code></pre>
|
||||
<h2>Context-Sensitive Topics</h2>
|
||||
<p>Update helpTopic as the user navigates your application:</p>
|
||||
<pre><code>snprintf(ctx->helpTopic, sizeof(ctx->helpTopic), "settings.video");</code></pre>
|
||||
<p>When the user presses F1, the shell launches the help viewer with your help file opened to the specified topic.</p>
|
||||
<h2>Launching Help from Menus</h2>
|
||||
<p>To add a Help menu item that opens your help file:</p>
|
||||
<pre><code>shellLoadAppWithArgs(ctx, viewerPath, helpFilePath);</code></pre>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
7189
docs/dvx_system_reference.html
Normal file
7189
docs/dvx_system_reference.html
Normal file
File diff suppressed because it is too large
Load diff
37
dosbox-staging-overrides.conf
Normal file
37
dosbox-staging-overrides.conf
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[sdl]
|
||||
windowresolution = 1024x768
|
||||
|
||||
[dosbox]
|
||||
machine = svga_s3
|
||||
memsize = 16
|
||||
|
||||
[cpu]
|
||||
core = dynamic
|
||||
#cputype = 486
|
||||
#cpu_cycles = 25000
|
||||
#cpu_cycles_protected = 25000
|
||||
|
||||
[render]
|
||||
aspect = true
|
||||
|
||||
[dos]
|
||||
umb = true
|
||||
xms = true
|
||||
ems = true
|
||||
ver = 6.22
|
||||
|
||||
[mouse]
|
||||
mouse_capture = seamless
|
||||
dos_mouse_immediate = true
|
||||
|
||||
[serial]
|
||||
serial1 = nullmodem server:127.0.0.1 port:2323 transparent:1
|
||||
|
||||
[autoexec]
|
||||
mount d ~/dos
|
||||
rem d:\ctmouse\bin\ctmouse.exe /O
|
||||
mount c .
|
||||
c:
|
||||
cd bin
|
||||
dvx
|
||||
rem exit
|
||||
|
|
@ -26,12 +26,15 @@ vesa oldvbe10 = false
|
|||
umb = true
|
||||
xms = true
|
||||
ems = true
|
||||
ver = 6.22
|
||||
lfn = false
|
||||
|
||||
[serial]
|
||||
serial1 = nullmodem server:127.0.0.1 port:2323 transparent:1
|
||||
|
||||
[autoexec]
|
||||
mount d ~/dos
|
||||
mount c .
|
||||
c:
|
||||
cd bin
|
||||
dir
|
||||
dvx
|
||||
148
dvx/Makefile
148
dvx/Makefile
|
|
@ -1,148 +0,0 @@
|
|||
# DVX GUI Library Makefile for DJGPP cross-compilation
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
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
|
||||
|
||||
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 \
|
||||
widgets/widgetCore.c \
|
||||
widgets/widgetScrollbar.c \
|
||||
widgets/widgetLayout.c \
|
||||
widgets/widgetEvent.c \
|
||||
widgets/widgetOps.c \
|
||||
widgets/widgetBox.c \
|
||||
widgets/widgetButton.c \
|
||||
widgets/widgetCheckbox.c \
|
||||
widgets/widgetComboBox.c \
|
||||
widgets/widgetDropdown.c \
|
||||
widgets/widgetCanvas.c \
|
||||
widgets/widgetImage.c \
|
||||
widgets/widgetImageButton.c \
|
||||
widgets/widgetLabel.c \
|
||||
widgets/widgetListBox.c \
|
||||
widgets/widgetListView.c \
|
||||
widgets/widgetProgressBar.c \
|
||||
widgets/widgetRadio.c \
|
||||
widgets/widgetScrollPane.c \
|
||||
widgets/widgetSeparator.c \
|
||||
widgets/widgetSplitter.c \
|
||||
widgets/widgetSlider.c \
|
||||
widgets/widgetSpacer.c \
|
||||
widgets/widgetSpinner.c \
|
||||
widgets/widgetStatusBar.c \
|
||||
widgets/widgetTabControl.c \
|
||||
widgets/widgetTextInput.c \
|
||||
widgets/widgetToolbar.c \
|
||||
widgets/widgetTreeView.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)
|
||||
$(RANLIB) $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(POBJDIR)/%.o: platform/%.c | $(POBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(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)
|
||||
|
||||
$(POBJDIR):
|
||||
mkdir -p $(POBJDIR)
|
||||
|
||||
$(WOBJDIR):
|
||||
mkdir -p $(WOBJDIR)
|
||||
|
||||
$(TOBJDIR):
|
||||
mkdir -p $(TOBJDIR)
|
||||
|
||||
$(LIBDIR):
|
||||
mkdir -p $(LIBDIR)
|
||||
|
||||
# Dependencies
|
||||
$(OBJDIR)/dvxVideo.o: dvxVideo.c dvxVideo.h platform/dvxPlatform.h dvxTypes.h dvxPalette.h
|
||||
$(OBJDIR)/dvxDraw.o: dvxDraw.c dvxDraw.h platform/dvxPlatform.h dvxTypes.h
|
||||
$(OBJDIR)/dvxComp.o: dvxComp.c dvxComp.h platform/dvxPlatform.h dvxTypes.h
|
||||
$(OBJDIR)/dvxWm.o: dvxWm.c dvxWm.h dvxTypes.h dvxDraw.h dvxComp.h dvxVideo.h dvxWidget.h thirdparty/stb_image.h
|
||||
$(OBJDIR)/dvxIcon.o: dvxIcon.c thirdparty/stb_image.h
|
||||
$(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
|
||||
|
||||
# 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
|
||||
$(WOBJDIR)/widgetClass.o: widgets/widgetClass.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetAnsiTerm.o: widgets/widgetAnsiTerm.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetCore.o: widgets/widgetCore.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetLayout.o: widgets/widgetLayout.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetEvent.o: widgets/widgetEvent.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetOps.o: widgets/widgetOps.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetBox.o: widgets/widgetBox.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetCanvas.o: widgets/widgetCanvas.c $(WIDGET_DEPS) thirdparty/stb_image.h thirdparty/stb_image_write.h
|
||||
$(WOBJDIR)/widgetButton.o: widgets/widgetButton.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetCheckbox.o: widgets/widgetCheckbox.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetComboBox.o: widgets/widgetComboBox.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetDropdown.o: widgets/widgetDropdown.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetImage.o: widgets/widgetImage.c $(WIDGET_DEPS) thirdparty/stb_image.h
|
||||
$(WOBJDIR)/widgetImageButton.o: widgets/widgetImageButton.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetLabel.o: widgets/widgetLabel.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetListBox.o: widgets/widgetListBox.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetListView.o: widgets/widgetListView.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetProgressBar.o: widgets/widgetProgressBar.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetRadio.o: widgets/widgetRadio.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetScrollPane.o: widgets/widgetScrollPane.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetSeparator.o: widgets/widgetSeparator.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetSplitter.o: widgets/widgetSplitter.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetSlider.o: widgets/widgetSlider.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetSpacer.o: widgets/widgetSpacer.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetSpinner.o: widgets/widgetSpinner.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetStatusBar.o: widgets/widgetStatusBar.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetTabControl.o: widgets/widgetTabControl.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetTextInput.o: widgets/widgetTextInput.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetToolbar.o: widgets/widgetToolbar.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetScrollbar.o: widgets/widgetScrollbar.c $(WIDGET_DEPS)
|
||||
$(WOBJDIR)/widgetTreeView.o: widgets/widgetTreeView.c $(WIDGET_DEPS)
|
||||
|
||||
clean:
|
||||
rm -f $(OBJS) $(POBJS) $(WOBJS) $(TOBJS) $(TARGET)
|
||||
1578
dvx/README.md
1578
dvx/README.md
File diff suppressed because it is too large
Load diff
|
|
@ -1,80 +0,0 @@
|
|||
// dvxDialog.h — Modal dialogs for DVX GUI
|
||||
//
|
||||
// Provides pre-built modal dialog boxes (message box, file dialog) that
|
||||
// block the caller and run their own event loop via dvxUpdate() until the
|
||||
// user dismisses them. Modal dialogs set ctx->modalWindow to prevent input
|
||||
// from reaching other windows during the dialog's lifetime.
|
||||
//
|
||||
// The flag encoding uses separate bit fields for button configuration
|
||||
// (low nibble) and icon type (high nibble) so they can be OR'd together
|
||||
// in a single int32_t argument, matching the Win16 MessageBox() convention.
|
||||
#ifndef DVX_DIALOG_H
|
||||
#define DVX_DIALOG_H
|
||||
|
||||
#include "dvxApp.h"
|
||||
|
||||
// ============================================================
|
||||
// Message box button flags (low nibble)
|
||||
// ============================================================
|
||||
|
||||
#define MB_OK 0x0000
|
||||
#define MB_OKCANCEL 0x0001
|
||||
#define MB_YESNO 0x0002
|
||||
#define MB_YESNOCANCEL 0x0003
|
||||
#define MB_RETRYCANCEL 0x0004
|
||||
|
||||
// ============================================================
|
||||
// Message box icon flags (high nibble, OR with button flags)
|
||||
// ============================================================
|
||||
|
||||
#define MB_ICONINFO 0x0010
|
||||
#define MB_ICONWARNING 0x0020
|
||||
#define MB_ICONERROR 0x0030
|
||||
#define MB_ICONQUESTION 0x0040
|
||||
|
||||
// ============================================================
|
||||
// Message box return values
|
||||
// ============================================================
|
||||
|
||||
#define ID_OK 1
|
||||
#define ID_CANCEL 2
|
||||
#define ID_YES 3
|
||||
#define ID_NO 4
|
||||
#define ID_RETRY 5
|
||||
|
||||
// Display a modal message box with the specified button and icon combination.
|
||||
// Blocks the caller by running dvxUpdate() in a loop until a button is
|
||||
// pressed or the dialog is closed. Returns the ID_xxx value of the button
|
||||
// that was pressed. The dialog window is automatically destroyed on return.
|
||||
int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags);
|
||||
|
||||
// ============================================================
|
||||
// File dialog flags
|
||||
// ============================================================
|
||||
|
||||
#define FD_OPEN 0x0000 // Open file (default)
|
||||
#define FD_SAVE 0x0001 // Save file
|
||||
|
||||
// ============================================================
|
||||
// File dialog filter
|
||||
// ============================================================
|
||||
//
|
||||
// Filters are displayed in a dropdown at the bottom of the file dialog.
|
||||
// Pattern matching is case-insensitive and supports only single glob
|
||||
// patterns (no semicolon-separated lists). This keeps the matching code
|
||||
// trivial for a DOS filesystem where filenames are short and simple.
|
||||
|
||||
typedef struct {
|
||||
const char *label; // e.g. "Text Files (*.txt)"
|
||||
const char *pattern; // e.g. "*.txt" (case-insensitive, single pattern)
|
||||
} FileFilterT;
|
||||
|
||||
// Display a modal file open/save dialog. The dialog shows a directory
|
||||
// listing with navigation (parent directory, drive letters on DOS), a
|
||||
// filename text input, and an optional filter dropdown. Blocks the caller
|
||||
// via dvxUpdate() loop. Returns true if the user selected a file (path
|
||||
// written to outPath), false if cancelled or closed. initialDir may be
|
||||
// NULL to start in the current working directory.
|
||||
bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const char *initialDir, const FileFilterT *filters, int32_t filterCount, char *outPath, int32_t outPathSize);
|
||||
|
||||
#endif // DVX_DIALOG_H
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
// dvxIcon.c — stb_image implementation for DVX GUI
|
||||
//
|
||||
// This file exists solely to instantiate the stb_image implementation.
|
||||
// stb_image is a single-header library: you #define STB_IMAGE_IMPLEMENTATION
|
||||
// in exactly one .c file to generate the actual function bodies. Putting
|
||||
// this in its own translation unit keeps the stb code isolated so it:
|
||||
// 1. Compiles once, not in every file that #includes stb_image.h
|
||||
// 2. Can have its own warning suppressions without affecting project code
|
||||
// 3. Doesn't slow down incremental rebuilds (only recompiles if stb changes)
|
||||
//
|
||||
// stb_image was chosen over libpng/libjpeg because:
|
||||
// - Single header, no external dependencies — critical for DJGPP cross-compile
|
||||
// - Supports BMP, PNG, JPEG, GIF with one include
|
||||
// - Public domain license, no linking restrictions
|
||||
// - Small code footprint suitable for DOS targets
|
||||
//
|
||||
// STBI_ONLY_* defines strip out support for unused formats (TGA, PSD, HDR,
|
||||
// PIC, PNM) to reduce binary size. STBI_NO_SIMD is required because DJGPP
|
||||
// targets 486/Pentium which lack SSE; on the Linux/SDL build it's harmless.
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||
|
||||
#define STBI_ONLY_BMP
|
||||
#define STBI_ONLY_PNG
|
||||
#define STBI_ONLY_JPEG
|
||||
#define STBI_ONLY_GIF
|
||||
#define STBI_NO_SIMD
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "thirdparty/stb_image.h"
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// dvxImageWrite.c — stb_image_write implementation for DVX GUI
|
||||
//
|
||||
// Companion to dvxIcon.c: instantiates stb_image_write for PNG output
|
||||
// (used by dvxScreenshot and dvxWindowScreenshot). Same rationale as
|
||||
// dvxIcon.c for using stb — zero external dependencies, single header,
|
||||
// public domain. Kept in a separate translation unit from the read side
|
||||
// so projects that don't need screenshot support can omit this file and
|
||||
// save the code size.
|
||||
//
|
||||
// STBI_WRITE_NO_SIMD disables SSE codepaths for the same reason as
|
||||
// STBI_NO_SIMD in dvxIcon.c: the DOS target lacks SSE support.
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||
|
||||
#define STBI_WRITE_NO_SIMD
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include "thirdparty/stb_image_write.h"
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
101
dvx/dvxPrefs.c
101
dvx/dvxPrefs.c
|
|
@ -1,101 +0,0 @@
|
|||
// dvxPrefs.c — INI-based preferences system
|
||||
//
|
||||
// Thin wrapper around rxi/ini that adds typed accessors with defaults.
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include "dvxPrefs.h"
|
||||
#include "thirdparty/ini/src/ini.h"
|
||||
|
||||
|
||||
static ini_t *sIni = NULL;
|
||||
|
||||
|
||||
// ============================================================
|
||||
// prefsFree
|
||||
// ============================================================
|
||||
|
||||
void prefsFree(void) {
|
||||
if (sIni) {
|
||||
ini_free(sIni);
|
||||
sIni = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// prefsGetBool
|
||||
// ============================================================
|
||||
|
||||
bool prefsGetBool(const char *section, const char *key, bool defaultVal) {
|
||||
const char *val = prefsGetString(section, key, NULL);
|
||||
|
||||
if (!val) {
|
||||
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') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (c == 'f' || c == 'n' || c == '0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// prefsGetInt
|
||||
// ============================================================
|
||||
|
||||
int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal) {
|
||||
const char *val = prefsGetString(section, key, NULL);
|
||||
|
||||
if (!val) {
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
char *end = NULL;
|
||||
long n = strtol(val, &end, 10);
|
||||
|
||||
if (end == val) {
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
return (int32_t)n;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// prefsGetString
|
||||
// ============================================================
|
||||
|
||||
const char *prefsGetString(const char *section, const char *key, const char *defaultVal) {
|
||||
if (!sIni) {
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
const char *val = ini_get(sIni, section, key);
|
||||
|
||||
return val ? val : defaultVal;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// prefsLoad
|
||||
// ============================================================
|
||||
|
||||
bool prefsLoad(const char *filename) {
|
||||
prefsFree();
|
||||
|
||||
sIni = ini_load(filename);
|
||||
|
||||
return sIni != NULL;
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
// dvxPrefs.h — INI-based preferences system
|
||||
//
|
||||
// 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).
|
||||
|
||||
#ifndef DVX_PREFS_H
|
||||
#define DVX_PREFS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Load an INI file into memory. Returns true on success, false if the
|
||||
// file could not be opened (all getters will return their defaults).
|
||||
// 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.
|
||||
void prefsFree(void);
|
||||
|
||||
// Retrieve a string value. Returns defaultVal if the section/key pair
|
||||
// is not present. The returned pointer is valid until 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.
|
||||
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.
|
||||
bool prefsGetBool(const char *section, const char *key, bool defaultVal);
|
||||
|
||||
#endif
|
||||
971
dvx/dvxWidget.h
971
dvx/dvxWidget.h
|
|
@ -1,971 +0,0 @@
|
|||
// dvxWidget.h — Widget system for DVX GUI
|
||||
//
|
||||
// A retained-mode widget toolkit layered on top of the DVX window manager.
|
||||
// Widgets form a tree (parent-child via firstChild/lastChild/nextSibling
|
||||
// pointers) rooted at a per-window VBox container. Layout is automatic:
|
||||
// the engine measures minimum sizes bottom-up, then allocates space top-down
|
||||
// using a flexbox-like algorithm with weights for extra-space distribution.
|
||||
//
|
||||
// Design decisions and rationale:
|
||||
//
|
||||
// - Single WidgetT struct with a tagged union: avoids the overhead of a
|
||||
// class hierarchy and virtual dispatch (no C++ vtable indirection).
|
||||
// The wclass pointer provides vtable-style dispatch only for the few
|
||||
// operations that genuinely differ per widget type (paint, layout, events).
|
||||
// Type-specific data lives in the `as` union to keep the struct compact.
|
||||
//
|
||||
// - Tagged size values (wgtPixels/wgtChars/wgtPercent): encoding the unit
|
||||
// in the high bits of a single int32_t avoids extra struct fields for
|
||||
// unit type and lets size hints be passed as plain integers. The 30-bit
|
||||
// value range (up to ~1 billion) is more than sufficient for pixel counts.
|
||||
//
|
||||
// - Tree linkage uses firstChild/lastChild/nextSibling (no prevSibling):
|
||||
// this halves the pointer overhead per widget and insertion/removal is
|
||||
// still O(n) in the worst case, which is acceptable given typical tree
|
||||
// depths of 5-10 nodes.
|
||||
//
|
||||
// - Large widget data (AnsiTermDataT, ListViewDataT) is separately
|
||||
// allocated and stored as a pointer in the union rather than inlined,
|
||||
// because these structures are hundreds of bytes and would bloat every
|
||||
// WidgetT even for simple labels and buttons.
|
||||
#ifndef DVX_WIDGET_H
|
||||
#define DVX_WIDGET_H
|
||||
|
||||
#include "dvxTypes.h"
|
||||
|
||||
#include <time.h>
|
||||
|
||||
// Forward declarations
|
||||
struct AppContextT;
|
||||
struct WidgetClassT;
|
||||
|
||||
// ============================================================
|
||||
// Size specifications
|
||||
// ============================================================
|
||||
//
|
||||
// Tagged size values encode both a unit type and a numeric value in a
|
||||
// single int32_t. The top 2 bits select the unit (pixels, character widths,
|
||||
// or percentage of parent), and the low 30 bits hold the numeric value.
|
||||
// A raw 0 means "auto" (use the widget's natural/minimum size).
|
||||
//
|
||||
// This encoding avoids a separate enum field for the unit type, keeping
|
||||
// size hints as simple scalar assignments: w->minW = wgtChars(40);
|
||||
// The wgtResolveSize() function in the layout engine decodes these tagged
|
||||
// values back into pixel counts using the font metrics and parent dimensions.
|
||||
|
||||
#define WGT_SIZE_TYPE_MASK 0xC0000000
|
||||
#define WGT_SIZE_VAL_MASK 0x3FFFFFFF
|
||||
#define WGT_SIZE_PIXELS 0x00000000
|
||||
#define WGT_SIZE_CHARS 0x40000000
|
||||
#define WGT_SIZE_PERCENT 0x80000000
|
||||
|
||||
#define wgtPixels(v) ((int32_t)(WGT_SIZE_PIXELS | ((uint32_t)(v) & WGT_SIZE_VAL_MASK)))
|
||||
#define wgtChars(v) ((int32_t)(WGT_SIZE_CHARS | ((uint32_t)(v) & WGT_SIZE_VAL_MASK)))
|
||||
#define wgtPercent(v) ((int32_t)(WGT_SIZE_PERCENT | ((uint32_t)(v) & WGT_SIZE_VAL_MASK)))
|
||||
|
||||
// ============================================================
|
||||
// Widget type enum
|
||||
// ============================================================
|
||||
//
|
||||
// Used as the index into widgetClassTable[] (in widgetInternal.h) to
|
||||
// look up the vtable for each widget type. Adding a new widget type
|
||||
// requires adding an enum value here, a corresponding union member in
|
||||
// WidgetT, and a WidgetClassT entry in widgetClassTable[].
|
||||
|
||||
typedef enum {
|
||||
WidgetVBoxE,
|
||||
WidgetHBoxE,
|
||||
WidgetLabelE,
|
||||
WidgetButtonE,
|
||||
WidgetCheckboxE,
|
||||
WidgetRadioGroupE,
|
||||
WidgetRadioE,
|
||||
WidgetTextInputE,
|
||||
WidgetTextAreaE,
|
||||
WidgetListBoxE,
|
||||
WidgetSpacerE,
|
||||
WidgetSeparatorE,
|
||||
WidgetFrameE,
|
||||
WidgetDropdownE,
|
||||
WidgetComboBoxE,
|
||||
WidgetProgressBarE,
|
||||
WidgetSliderE,
|
||||
WidgetTabControlE,
|
||||
WidgetTabPageE,
|
||||
WidgetStatusBarE,
|
||||
WidgetToolbarE,
|
||||
WidgetTreeViewE,
|
||||
WidgetTreeItemE,
|
||||
WidgetImageE,
|
||||
WidgetImageButtonE,
|
||||
WidgetCanvasE,
|
||||
WidgetAnsiTermE,
|
||||
WidgetListViewE,
|
||||
WidgetSpinnerE,
|
||||
WidgetScrollPaneE,
|
||||
WidgetSplitterE
|
||||
} WidgetTypeE;
|
||||
|
||||
// ============================================================
|
||||
// ListView types
|
||||
// ============================================================
|
||||
|
||||
#define LISTVIEW_MAX_COLS 16
|
||||
|
||||
typedef enum {
|
||||
ListViewAlignLeftE,
|
||||
ListViewAlignCenterE,
|
||||
ListViewAlignRightE
|
||||
} ListViewAlignE;
|
||||
|
||||
typedef enum {
|
||||
ListViewSortNoneE,
|
||||
ListViewSortAscE,
|
||||
ListViewSortDescE
|
||||
} ListViewSortE;
|
||||
|
||||
typedef struct {
|
||||
const char *title;
|
||||
int32_t width; // tagged size (wgtPixels/wgtChars/wgtPercent, 0 = auto)
|
||||
ListViewAlignE align;
|
||||
} ListViewColT;
|
||||
|
||||
// ============================================================
|
||||
// Alignment enum
|
||||
// ============================================================
|
||||
//
|
||||
// Controls main-axis alignment of children within a container.
|
||||
// HBox: AlignStartE=left, AlignCenterE=center, AlignEndE=right
|
||||
// VBox: AlignStartE=top, AlignCenterE=center, AlignEndE=bottom
|
||||
|
||||
typedef enum {
|
||||
AlignStartE,
|
||||
AlignCenterE,
|
||||
AlignEndE
|
||||
} WidgetAlignE;
|
||||
|
||||
// ============================================================
|
||||
// Frame style enum
|
||||
// ============================================================
|
||||
|
||||
typedef enum {
|
||||
FrameInE, // beveled inward (sunken) — default
|
||||
FrameOutE, // beveled outward (raised)
|
||||
FrameFlatE // solid color line
|
||||
} FrameStyleE;
|
||||
|
||||
// ============================================================
|
||||
// Text input mode enum
|
||||
// ============================================================
|
||||
|
||||
typedef enum {
|
||||
InputNormalE, // default free-form text
|
||||
InputPasswordE, // displays bullets, no copy
|
||||
InputMaskedE // format mask (e.g. "(###) ###-####")
|
||||
} InputModeE;
|
||||
|
||||
// ============================================================
|
||||
// Large widget data (separately allocated to reduce WidgetT size)
|
||||
// ============================================================
|
||||
//
|
||||
// AnsiTermDataT and ListViewDataT are heap-allocated and stored as
|
||||
// pointers in the WidgetT union. Inlining them would add hundreds of
|
||||
// bytes to every WidgetT instance (even labels and buttons), wasting
|
||||
// memory on the 30+ simple widgets that don't need terminal or listview
|
||||
// state. The pointer indirection adds one dereference but saves
|
||||
// significant memory across a typical widget tree.
|
||||
|
||||
// AnsiTermDataT — full VT100/ANSI terminal emulator state.
|
||||
// Implements a subset of DEC VT100 escape sequences sufficient for BBS
|
||||
// and DOS ANSI art rendering: cursor movement, color attributes (16-color
|
||||
// with bold-as-bright), scrolling regions, and blink. The parser is a
|
||||
// simple state machine (normal -> ESC -> CSI) with parameter accumulation.
|
||||
typedef struct {
|
||||
uint8_t *cells; // character cells: (ch, attr) pairs, cols*rows*2 bytes
|
||||
int32_t cols; // columns (default 80)
|
||||
int32_t rows; // rows (default 25)
|
||||
int32_t cursorRow; // 0-based cursor row
|
||||
int32_t cursorCol; // 0-based cursor column
|
||||
bool cursorVisible;
|
||||
bool wrapMode; // auto-wrap at right margin
|
||||
bool bold; // SGR bold flag (brightens foreground)
|
||||
bool originMode; // cursor positioning relative to scroll region
|
||||
bool csiPrivate; // '?' prefix in CSI sequence
|
||||
uint8_t curAttr; // current text attribute (fg | bg<<4)
|
||||
uint8_t parseState; // 0=normal, 1=ESC, 2=CSI
|
||||
int32_t params[8]; // CSI parameter accumulator
|
||||
int32_t paramCount; // number of CSI params collected
|
||||
int32_t savedRow; // saved cursor position (SCP)
|
||||
int32_t savedCol;
|
||||
// Scrolling region (0-based, inclusive)
|
||||
int32_t scrollTop; // top row of scroll region
|
||||
int32_t scrollBot; // bottom row of scroll region
|
||||
// Scrollback — circular buffer so old lines age out naturally without
|
||||
// memmove. Each line is cols*2 bytes (same ch/attr format as cells).
|
||||
// scrollPos tracks the view offset: when equal to scrollbackCount,
|
||||
// the user sees the live screen; when less, they're viewing history.
|
||||
uint8_t *scrollback; // circular buffer of scrollback lines
|
||||
int32_t scrollbackMax; // max lines in scrollback buffer
|
||||
int32_t scrollbackCount; // current number of lines stored
|
||||
int32_t scrollbackHead; // write position (circular index)
|
||||
int32_t scrollPos; // view position (scrollbackCount = live)
|
||||
// Blink support
|
||||
bool blinkVisible; // current blink phase (true = text visible)
|
||||
clock_t blinkTime; // timestamp of last blink toggle
|
||||
// Cursor blink
|
||||
bool cursorOn; // current cursor blink phase
|
||||
clock_t cursorTime; // timestamp of last cursor toggle
|
||||
// Dirty tracking — a 32-bit bitmask where each bit corresponds to one
|
||||
// terminal row. Only dirty rows are repainted, which is critical because
|
||||
// the ANSI terminal can receive data every frame (at 9600+ baud) and
|
||||
// re-rendering all 25 rows of 80 columns each frame would dominate the
|
||||
// CPU budget. Limits terminal height to 32 rows, which is fine for the
|
||||
// 25-row default target.
|
||||
uint32_t dirtyRows; // bitmask of rows needing repaint
|
||||
int32_t lastCursorRow; // cursor row at last repaint
|
||||
int32_t lastCursorCol; // cursor col at last repaint
|
||||
// Pre-packed 16-color palette avoids calling packColor() (which involves
|
||||
// shift/mask arithmetic) 80*25 = 2000 times per full repaint.
|
||||
uint32_t packedPalette[16];
|
||||
bool paletteValid;
|
||||
// Selection (line indices in scrollback+screen space)
|
||||
int32_t selStartLine;
|
||||
int32_t selStartCol;
|
||||
int32_t selEndLine;
|
||||
int32_t selEndCol;
|
||||
bool selecting;
|
||||
// Communications interface — abstracted so the terminal can connect to
|
||||
// different backends (serial port, secLink channel, local pipe) without
|
||||
// knowing the transport details. When all are NULL, the terminal is in
|
||||
// offline/disconnected mode (useful for viewing .ANS files).
|
||||
void *commCtx;
|
||||
int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen);
|
||||
int32_t (*commWrite)(void *ctx, const uint8_t *buf, int32_t len);
|
||||
} AnsiTermDataT;
|
||||
|
||||
// ListViewDataT — multi-column list with sortable headers and optional
|
||||
// multi-select. cellData is a flat array of strings indexed as
|
||||
// cellData[row * colCount + col]. The sortIndex is an indirection array
|
||||
// that maps displayed row numbers to data row numbers, allowing sort
|
||||
// without rearranging the actual data. resolvedColW[] caches pixel widths
|
||||
// after resolving tagged column sizes, avoiding re-resolution on every paint.
|
||||
typedef struct {
|
||||
const ListViewColT *cols;
|
||||
int32_t colCount;
|
||||
const char **cellData;
|
||||
int32_t rowCount;
|
||||
int32_t selectedIdx;
|
||||
int32_t scrollPos;
|
||||
int32_t scrollPosH;
|
||||
int32_t sortCol;
|
||||
ListViewSortE sortDir;
|
||||
int32_t resolvedColW[LISTVIEW_MAX_COLS];
|
||||
int32_t totalColW;
|
||||
int32_t *sortIndex;
|
||||
bool multiSelect;
|
||||
int32_t anchorIdx;
|
||||
uint8_t *selBits;
|
||||
bool reorderable;
|
||||
int32_t dragIdx;
|
||||
int32_t dropIdx;
|
||||
void (*onHeaderClick)(struct WidgetT *w, int32_t col, ListViewSortE dir);
|
||||
} ListViewDataT;
|
||||
|
||||
// ============================================================
|
||||
// Widget structure
|
||||
// ============================================================
|
||||
|
||||
#define MAX_WIDGET_NAME 32
|
||||
|
||||
typedef struct WidgetT {
|
||||
WidgetTypeE type;
|
||||
// wclass points to the vtable for this widget type. Looked up once at
|
||||
// creation from widgetClassTable[type]. This avoids a switch on type
|
||||
// in every paint/layout/event dispatch — the cost is one pointer per
|
||||
// widget, which is negligible.
|
||||
const struct WidgetClassT *wclass;
|
||||
char name[MAX_WIDGET_NAME];
|
||||
// djb2 hash of the name string, computed at wgtSetName() time.
|
||||
// wgtFind() compares hashes before strcmp, making name lookups fast
|
||||
// for the common no-match case in a tree with many unnamed widgets.
|
||||
uint32_t nameHash; // djb2 hash of name, 0 if unnamed
|
||||
|
||||
// Tree linkage
|
||||
struct WidgetT *parent;
|
||||
struct WidgetT *firstChild;
|
||||
struct WidgetT *lastChild;
|
||||
struct WidgetT *nextSibling;
|
||||
WindowT *window;
|
||||
|
||||
// Computed geometry (relative to window content area)
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
int32_t w;
|
||||
int32_t h;
|
||||
|
||||
// Computed minimum size — set bottom-up by calcMinSize during layout.
|
||||
// These represent the smallest possible size for this widget (including
|
||||
// its children if it's a container). The layout engine uses these as
|
||||
// the starting point for space allocation.
|
||||
int32_t calcMinW;
|
||||
int32_t calcMinH;
|
||||
|
||||
// Size hints (tagged: wgtPixels/wgtChars/wgtPercent, 0 = auto).
|
||||
// These are set by the application and influence the layout engine:
|
||||
// minW/minH override calcMinW/H if larger, maxW/maxH clamp the final
|
||||
// size, and prefW/prefH request a specific size (layout may override).
|
||||
int32_t minW;
|
||||
int32_t minH;
|
||||
int32_t maxW; // 0 = no limit
|
||||
int32_t maxH;
|
||||
int32_t prefW; // preferred size, 0 = auto
|
||||
int32_t prefH;
|
||||
// weight controls how extra space beyond minimum is distributed among
|
||||
// siblings in a VBox/HBox. weight=0 means fixed size (no stretching),
|
||||
// weight=100 is the default for flexible widgets. A widget with
|
||||
// weight=200 gets twice as much extra space as one with weight=100.
|
||||
int32_t weight; // extra-space distribution (0 = fixed, 100 = normal)
|
||||
|
||||
// Container properties
|
||||
WidgetAlignE align; // main-axis alignment for children
|
||||
int32_t spacing; // tagged size for spacing between children (0 = default)
|
||||
int32_t padding; // tagged size for internal padding (0 = default)
|
||||
|
||||
// Colors (0 = use color scheme defaults)
|
||||
uint32_t fgColor;
|
||||
uint32_t bgColor;
|
||||
|
||||
// State
|
||||
bool visible;
|
||||
bool enabled;
|
||||
bool readOnly;
|
||||
bool focused;
|
||||
char accelKey; // lowercase accelerator character, 0 if none
|
||||
|
||||
// User data and callbacks
|
||||
void *userData;
|
||||
const char *tooltip; // tooltip text (NULL = none, caller owns string)
|
||||
MenuT *contextMenu; // right-click context menu (NULL = none, caller owns)
|
||||
void (*onClick)(struct WidgetT *w);
|
||||
void (*onChange)(struct WidgetT *w);
|
||||
void (*onDblClick)(struct WidgetT *w);
|
||||
|
||||
// Type-specific data — tagged union keyed by the `type` field.
|
||||
// Only the member corresponding to `type` is valid. This is the C
|
||||
// equivalent of a discriminated union / variant type. Using a union
|
||||
// instead of separate structs per widget type keeps all widget data
|
||||
// in a single allocation, which simplifies memory management and
|
||||
// avoids pointer chasing during layout/paint traversal.
|
||||
union {
|
||||
struct {
|
||||
const char *text;
|
||||
} label;
|
||||
|
||||
struct {
|
||||
const char *text;
|
||||
bool pressed;
|
||||
} button;
|
||||
|
||||
struct {
|
||||
const char *text;
|
||||
bool checked;
|
||||
} checkbox;
|
||||
|
||||
struct {
|
||||
int32_t selectedIdx;
|
||||
} radioGroup;
|
||||
|
||||
struct {
|
||||
const char *text;
|
||||
int32_t index;
|
||||
} radio;
|
||||
|
||||
// Text input has its own edit buffer (not a pointer to external
|
||||
// storage) so the widget fully owns its text lifecycle. The undo
|
||||
// buffer holds a single-level snapshot taken before each edit
|
||||
// operation — Ctrl+Z restores to the snapshot. This is simpler
|
||||
// than a full undo stack but sufficient for single-line fields.
|
||||
struct {
|
||||
char *buf;
|
||||
int32_t bufSize;
|
||||
int32_t len;
|
||||
int32_t cursorPos;
|
||||
int32_t scrollOff;
|
||||
int32_t selStart; // selection anchor (-1 = none)
|
||||
int32_t selEnd; // selection end (-1 = none)
|
||||
char *undoBuf;
|
||||
int32_t undoLen;
|
||||
int32_t undoCursor;
|
||||
InputModeE inputMode;
|
||||
const char *mask; // format mask for InputMaskedE
|
||||
} textInput;
|
||||
|
||||
// Multi-line text editor. desiredCol implements the "sticky column"
|
||||
// behavior where pressing Up/Down tries to return to the column
|
||||
// the cursor was at before traversing shorter lines (standard
|
||||
// text editor UX). cachedLines/cachedMaxLL cache values that
|
||||
// require full-buffer scans, invalidated to -1 on any text change.
|
||||
struct {
|
||||
char *buf;
|
||||
int32_t bufSize;
|
||||
int32_t len;
|
||||
int32_t cursorRow;
|
||||
int32_t cursorCol;
|
||||
int32_t scrollRow;
|
||||
int32_t scrollCol;
|
||||
int32_t desiredCol; // sticky column for up/down movement
|
||||
int32_t selAnchor; // selection anchor byte offset (-1 = none)
|
||||
int32_t selCursor; // selection cursor byte offset (-1 = none)
|
||||
char *undoBuf;
|
||||
int32_t undoLen;
|
||||
int32_t undoCursor; // byte offset at time of snapshot
|
||||
int32_t cachedLines; // cached line count (-1 = dirty)
|
||||
int32_t cachedMaxLL; // cached max line length (-1 = dirty)
|
||||
} textArea;
|
||||
|
||||
struct {
|
||||
const char **items;
|
||||
int32_t itemCount;
|
||||
int32_t selectedIdx; // cursor position (always valid); also sole selection in single-select
|
||||
int32_t scrollPos;
|
||||
int32_t maxItemLen; // cached max strlen of items
|
||||
bool multiSelect;
|
||||
int32_t anchorIdx; // anchor for shift+click range selection
|
||||
uint8_t *selBits; // per-item selection flags (multi-select only)
|
||||
bool reorderable; // allow drag-reorder of items
|
||||
int32_t dragIdx; // item being dragged (-1 = none)
|
||||
int32_t dropIdx; // insertion point (-1 = none)
|
||||
} listBox;
|
||||
|
||||
struct {
|
||||
bool vertical;
|
||||
} separator;
|
||||
|
||||
struct {
|
||||
const char *title;
|
||||
FrameStyleE style; // FrameInE (default), FrameOutE, FrameFlatE
|
||||
uint32_t color; // border color for FrameFlatE (0 = use windowShadow)
|
||||
} frame;
|
||||
|
||||
struct {
|
||||
const char **items;
|
||||
int32_t itemCount;
|
||||
int32_t selectedIdx;
|
||||
bool open;
|
||||
int32_t hoverIdx;
|
||||
int32_t scrollPos;
|
||||
int32_t maxItemLen; // cached max strlen of items
|
||||
} dropdown;
|
||||
|
||||
struct {
|
||||
char *buf;
|
||||
int32_t bufSize;
|
||||
int32_t len;
|
||||
int32_t cursorPos;
|
||||
int32_t scrollOff;
|
||||
int32_t selStart; // selection anchor (-1 = none)
|
||||
int32_t selEnd; // selection end (-1 = none)
|
||||
char *undoBuf;
|
||||
int32_t undoLen;
|
||||
int32_t undoCursor;
|
||||
const char **items;
|
||||
int32_t itemCount;
|
||||
int32_t selectedIdx;
|
||||
bool open;
|
||||
int32_t hoverIdx;
|
||||
int32_t listScrollPos;
|
||||
int32_t maxItemLen; // cached max strlen of items
|
||||
} comboBox;
|
||||
|
||||
struct {
|
||||
int32_t value;
|
||||
int32_t maxValue;
|
||||
bool vertical;
|
||||
} progressBar;
|
||||
|
||||
struct {
|
||||
int32_t value;
|
||||
int32_t minValue;
|
||||
int32_t maxValue;
|
||||
bool vertical;
|
||||
} slider;
|
||||
|
||||
struct {
|
||||
int32_t activeTab;
|
||||
int32_t scrollOffset; // horizontal scroll of tab headers
|
||||
} tabControl;
|
||||
|
||||
struct {
|
||||
const char *title;
|
||||
} tabPage;
|
||||
|
||||
// TreeView uses widget children (WidgetTreeItemE) as its items,
|
||||
// unlike ListBox which uses a string array. This allows nested
|
||||
// hierarchies with expand/collapse state per item. The tree is
|
||||
// rendered by flattening visible items during paint, with TREE_INDENT
|
||||
// pixels of indentation per nesting level.
|
||||
struct {
|
||||
int32_t scrollPos;
|
||||
int32_t scrollPosH;
|
||||
struct WidgetT *selectedItem;
|
||||
struct WidgetT *anchorItem; // anchor for shift+click range selection
|
||||
bool multiSelect;
|
||||
bool reorderable; // allow drag-reorder of items
|
||||
struct WidgetT *dragItem; // item being dragged (NULL = none)
|
||||
struct WidgetT *dropTarget; // insertion target (NULL = none)
|
||||
bool dropAfter; // true = insert after target, false = before
|
||||
} treeView;
|
||||
|
||||
struct {
|
||||
const char *text;
|
||||
bool expanded;
|
||||
bool selected; // per-item flag for multi-select
|
||||
} treeItem;
|
||||
|
||||
struct {
|
||||
uint8_t *data; // pixel buffer in display format
|
||||
int32_t imgW;
|
||||
int32_t imgH;
|
||||
int32_t imgPitch;
|
||||
bool pressed;
|
||||
} image;
|
||||
|
||||
struct {
|
||||
uint8_t *data; // pixel buffer in display format
|
||||
int32_t imgW;
|
||||
int32_t imgH;
|
||||
int32_t imgPitch;
|
||||
bool pressed;
|
||||
} imageButton;
|
||||
|
||||
struct {
|
||||
uint8_t *data; // pixel buffer in display format
|
||||
int32_t canvasW;
|
||||
int32_t canvasH;
|
||||
int32_t canvasPitch;
|
||||
int32_t canvasBpp; // cached bytes per pixel (avoids pitch/w division)
|
||||
uint32_t penColor;
|
||||
int32_t penSize;
|
||||
int32_t lastX;
|
||||
int32_t lastY;
|
||||
void (*onMouse)(struct WidgetT *w, int32_t cx, int32_t cy, bool drag);
|
||||
} canvas;
|
||||
|
||||
AnsiTermDataT *ansiTerm;
|
||||
|
||||
ListViewDataT *listView;
|
||||
|
||||
struct {
|
||||
int32_t value;
|
||||
int32_t minValue;
|
||||
int32_t maxValue;
|
||||
int32_t step;
|
||||
char buf[16]; // formatted value text
|
||||
int32_t len;
|
||||
int32_t cursorPos;
|
||||
int32_t scrollOff;
|
||||
int32_t selStart; // selection anchor (-1 = none)
|
||||
int32_t selEnd; // selection end (-1 = none)
|
||||
char undoBuf[16]; // undo snapshot
|
||||
int32_t undoLen;
|
||||
int32_t undoCursor;
|
||||
bool editing; // true when user is typing
|
||||
} spinner;
|
||||
|
||||
struct {
|
||||
int32_t scrollPosV;
|
||||
int32_t scrollPosH;
|
||||
} scrollPane;
|
||||
|
||||
struct {
|
||||
int32_t dividerPos; // pixels from left/top edge
|
||||
bool vertical; // true = vertical divider (left|right panes)
|
||||
} splitter;
|
||||
} as;
|
||||
} WidgetT;
|
||||
|
||||
// ============================================================
|
||||
// Window integration
|
||||
// ============================================================
|
||||
|
||||
// Initialize the widget system for a window. Creates a root VBox container
|
||||
// that fills the window's content area, and installs callback handlers
|
||||
// (onPaint, onMouse, onKey, onResize) that dispatch events to the widget
|
||||
// tree. After this call, the window is fully managed by the widget system
|
||||
// — the application builds its UI by adding child widgets to the returned
|
||||
// root container. The window's userData is set to the AppContextT pointer.
|
||||
WidgetT *wgtInitWindow(struct AppContextT *ctx, WindowT *win);
|
||||
|
||||
// ============================================================
|
||||
// Container creation
|
||||
// ============================================================
|
||||
//
|
||||
// VBox and HBox are the primary layout containers, analogous to CSS
|
||||
// flexbox with column/row direction. Children are laid out sequentially
|
||||
// along the main axis (vertical for VBox, horizontal for HBox) with
|
||||
// spacing between them. Extra space is distributed according to each
|
||||
// child's weight. Frame is a titled groupbox container with a bevel border.
|
||||
|
||||
WidgetT *wgtVBox(WidgetT *parent);
|
||||
WidgetT *wgtHBox(WidgetT *parent);
|
||||
WidgetT *wgtFrame(WidgetT *parent, const char *title);
|
||||
|
||||
// ============================================================
|
||||
// Basic widgets
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtLabel(WidgetT *parent, const char *text);
|
||||
WidgetT *wgtButton(WidgetT *parent, const char *text);
|
||||
WidgetT *wgtCheckbox(WidgetT *parent, const char *text);
|
||||
WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen);
|
||||
WidgetT *wgtPasswordInput(WidgetT *parent, int32_t maxLen);
|
||||
WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask);
|
||||
|
||||
// ============================================================
|
||||
// Radio buttons
|
||||
// ============================================================
|
||||
//
|
||||
// Radio buttons must be children of a RadioGroup container. The group
|
||||
// tracks which child is selected (by index). Clicking a radio button
|
||||
// automatically deselects the previously selected sibling. This parent-
|
||||
// tracking approach avoids explicit "radio group ID" parameters.
|
||||
|
||||
WidgetT *wgtRadioGroup(WidgetT *parent);
|
||||
WidgetT *wgtRadio(WidgetT *parent, const char *text);
|
||||
|
||||
// ============================================================
|
||||
// Spacing and visual dividers
|
||||
// ============================================================
|
||||
//
|
||||
// Spacer is an invisible flexible widget (weight=100) that absorbs
|
||||
// extra space — useful for pushing subsequent siblings to the end of
|
||||
// a container (like CSS flex: 1 auto). Separators are thin beveled
|
||||
// lines for visual grouping.
|
||||
|
||||
WidgetT *wgtSpacer(WidgetT *parent);
|
||||
WidgetT *wgtHSeparator(WidgetT *parent);
|
||||
WidgetT *wgtVSeparator(WidgetT *parent);
|
||||
|
||||
// ============================================================
|
||||
// Complex widgets
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtListBox(WidgetT *parent);
|
||||
WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen);
|
||||
|
||||
// ============================================================
|
||||
// Dropdown and ComboBox
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtDropdown(WidgetT *parent);
|
||||
void wgtDropdownSetItems(WidgetT *w, const char **items, int32_t count);
|
||||
int32_t wgtDropdownGetSelected(const WidgetT *w);
|
||||
void wgtDropdownSetSelected(WidgetT *w, int32_t idx);
|
||||
|
||||
WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen);
|
||||
void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count);
|
||||
int32_t wgtComboBoxGetSelected(const WidgetT *w);
|
||||
void wgtComboBoxSetSelected(WidgetT *w, int32_t idx);
|
||||
|
||||
// ============================================================
|
||||
// ProgressBar
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtProgressBar(WidgetT *parent);
|
||||
WidgetT *wgtProgressBarV(WidgetT *parent);
|
||||
void wgtProgressBarSetValue(WidgetT *w, int32_t value);
|
||||
int32_t wgtProgressBarGetValue(const WidgetT *w);
|
||||
|
||||
// ============================================================
|
||||
// Slider (TrackBar)
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtSlider(WidgetT *parent, int32_t minVal, int32_t maxVal);
|
||||
void wgtSliderSetValue(WidgetT *w, int32_t value);
|
||||
int32_t wgtSliderGetValue(const WidgetT *w);
|
||||
|
||||
// ============================================================
|
||||
// Spinner (numeric input)
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtSpinner(WidgetT *parent, int32_t minVal, int32_t maxVal, int32_t step);
|
||||
void wgtSpinnerSetValue(WidgetT *w, int32_t value);
|
||||
int32_t wgtSpinnerGetValue(const WidgetT *w);
|
||||
void wgtSpinnerSetRange(WidgetT *w, int32_t minVal, int32_t maxVal);
|
||||
void wgtSpinnerSetStep(WidgetT *w, int32_t step);
|
||||
|
||||
// ============================================================
|
||||
// TabControl
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtTabControl(WidgetT *parent);
|
||||
WidgetT *wgtTabPage(WidgetT *parent, const char *title);
|
||||
void wgtTabControlSetActive(WidgetT *w, int32_t idx);
|
||||
int32_t wgtTabControlGetActive(const WidgetT *w);
|
||||
|
||||
// ============================================================
|
||||
// StatusBar and Toolbar
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtStatusBar(WidgetT *parent);
|
||||
WidgetT *wgtToolbar(WidgetT *parent);
|
||||
|
||||
// ============================================================
|
||||
// TreeView
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtTreeView(WidgetT *parent);
|
||||
WidgetT *wgtTreeViewGetSelected(const WidgetT *w);
|
||||
void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item);
|
||||
void wgtTreeViewSetMultiSelect(WidgetT *w, bool multi);
|
||||
void wgtTreeViewSetReorderable(WidgetT *w, bool reorderable);
|
||||
WidgetT *wgtTreeItem(WidgetT *parent, const char *text);
|
||||
void wgtTreeItemSetExpanded(WidgetT *w, bool expanded);
|
||||
bool wgtTreeItemIsExpanded(const WidgetT *w);
|
||||
bool wgtTreeItemIsSelected(const WidgetT *w);
|
||||
void wgtTreeItemSetSelected(WidgetT *w, bool selected);
|
||||
|
||||
// ============================================================
|
||||
// ListView (multi-column list)
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtListView(WidgetT *parent);
|
||||
void wgtListViewSetColumns(WidgetT *w, const ListViewColT *cols, int32_t count);
|
||||
void wgtListViewSetData(WidgetT *w, const char **cellData, int32_t rowCount);
|
||||
int32_t wgtListViewGetSelected(const WidgetT *w);
|
||||
void wgtListViewSetSelected(WidgetT *w, int32_t idx);
|
||||
void wgtListViewSetSort(WidgetT *w, int32_t col, ListViewSortE dir);
|
||||
void wgtListViewSetHeaderClickCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir));
|
||||
void wgtListViewSetMultiSelect(WidgetT *w, bool multi);
|
||||
bool wgtListViewIsItemSelected(const WidgetT *w, int32_t idx);
|
||||
void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected);
|
||||
void wgtListViewSelectAll(WidgetT *w);
|
||||
void wgtListViewClearSelection(WidgetT *w);
|
||||
void wgtListViewSetReorderable(WidgetT *w, bool reorderable);
|
||||
|
||||
// ============================================================
|
||||
// ScrollPane
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtScrollPane(WidgetT *parent);
|
||||
|
||||
// ============================================================
|
||||
// Splitter (draggable divider between two child regions)
|
||||
// ============================================================
|
||||
|
||||
// Create a splitter. If vertical is true, children are arranged left|right;
|
||||
// if false, top|bottom. Add exactly two children.
|
||||
WidgetT *wgtSplitter(WidgetT *parent, bool vertical);
|
||||
void wgtSplitterSetPos(WidgetT *w, int32_t pos);
|
||||
int32_t wgtSplitterGetPos(const WidgetT *w);
|
||||
|
||||
// ============================================================
|
||||
// ImageButton
|
||||
// ============================================================
|
||||
|
||||
// Create an image button from raw pixel data (display format).
|
||||
// Takes ownership of the data buffer (freed on destroy).
|
||||
WidgetT *wgtImageButton(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t pitch);
|
||||
|
||||
// Load an image button from a file (BMP, PNG, JPEG, GIF).
|
||||
// Returns NULL on load failure; falls through gracefully.
|
||||
WidgetT *wgtImageButtonFromFile(WidgetT *parent, const char *path);
|
||||
|
||||
// Replace the image data. Takes ownership of the new buffer.
|
||||
void wgtImageButtonSetData(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch);
|
||||
|
||||
// ============================================================
|
||||
// Image
|
||||
// ============================================================
|
||||
|
||||
// Create an image widget from raw pixel data (display format).
|
||||
// Takes ownership of the data buffer (freed on destroy).
|
||||
WidgetT *wgtImage(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t pitch);
|
||||
|
||||
// Load an image widget from a file (BMP, PNG, JPEG, GIF).
|
||||
// Returns NULL on load failure.
|
||||
WidgetT *wgtImageFromFile(WidgetT *parent, const char *path);
|
||||
|
||||
// Replace the image data. Takes ownership of the new buffer.
|
||||
void wgtImageSetData(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch);
|
||||
|
||||
// ============================================================
|
||||
// Canvas
|
||||
// ============================================================
|
||||
|
||||
// Create a drawable canvas widget with the given dimensions.
|
||||
WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h);
|
||||
|
||||
// Clear the canvas to the specified color.
|
||||
void wgtCanvasClear(WidgetT *w, uint32_t color);
|
||||
|
||||
// Set the pen color (in display pixel format).
|
||||
void wgtCanvasSetPenColor(WidgetT *w, uint32_t color);
|
||||
|
||||
// Set the pen size in pixels (diameter).
|
||||
void wgtCanvasSetPenSize(WidgetT *w, int32_t size);
|
||||
|
||||
// Set the mouse callback. Called on click (drag=false) and drag (drag=true)
|
||||
// with canvas-relative coordinates. If NULL (default), mouse events are ignored.
|
||||
void wgtCanvasSetMouseCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag));
|
||||
|
||||
// Save the canvas to a PNG file. Returns 0 on success, -1 on failure.
|
||||
int32_t wgtCanvasSave(WidgetT *w, const char *path);
|
||||
|
||||
// Load a PNG file onto the canvas. Returns 0 on success, -1 on failure.
|
||||
int32_t wgtCanvasLoad(WidgetT *w, const char *path);
|
||||
|
||||
// Programmatic drawing (coordinates are in canvas space)
|
||||
void wgtCanvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1);
|
||||
void wgtCanvasDrawRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height);
|
||||
void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height);
|
||||
void wgtCanvasFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius);
|
||||
void wgtCanvasSetPixel(WidgetT *w, int32_t x, int32_t y, uint32_t color);
|
||||
uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y);
|
||||
|
||||
// ============================================================
|
||||
// ANSI Terminal
|
||||
// ============================================================
|
||||
//
|
||||
// A VT100/ANSI terminal emulator widget. Supports the subset of escape
|
||||
// sequences needed for DOS BBS programs and ANSI art: cursor positioning,
|
||||
// SGR color/attribute codes, scrolling regions, and text blink. Pairs
|
||||
// with the secLink communications layer for remote serial connections
|
||||
// or can be used standalone for viewing .ANS files.
|
||||
|
||||
// Create an ANSI terminal widget (0 for cols/rows = 80x25 default)
|
||||
WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows);
|
||||
|
||||
// Write raw data through the ANSI escape sequence parser. Used for
|
||||
// loading .ANS files or feeding data from a source that isn't using
|
||||
// the comm interface.
|
||||
void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len);
|
||||
|
||||
// Clear the terminal screen and reset cursor to home
|
||||
void wgtAnsiTermClear(WidgetT *w);
|
||||
|
||||
// Set the communications interface (NULL function pointers = disconnected)
|
||||
void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t *, int32_t), int32_t (*writeFn)(void *, const uint8_t *, int32_t));
|
||||
|
||||
// Set the scrollback buffer size in lines (default 500). Clears existing scrollback.
|
||||
void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines);
|
||||
|
||||
// Poll the comm interface for incoming data and process it. Returns bytes processed.
|
||||
int32_t wgtAnsiTermPoll(WidgetT *w);
|
||||
|
||||
// Fast-path repaint for the terminal widget. Instead of going through the
|
||||
// full widget paint pipeline (which would repaint the entire widget), this
|
||||
// renders only the dirty rows (tracked via the dirtyRows bitmask) directly
|
||||
// into the window's content buffer. This is essential for responsive terminal
|
||||
// output — incoming serial data can dirty a few rows per frame, and
|
||||
// repainting only those rows keeps the cost proportional to the actual
|
||||
// change rather than the full 80x25 grid. Returns the number of rows
|
||||
// repainted; outY/outH report the affected region for dirty-rect tracking.
|
||||
int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH);
|
||||
|
||||
// ============================================================
|
||||
// Operations
|
||||
// ============================================================
|
||||
|
||||
// Walk from any widget up the tree to the root, then retrieve the
|
||||
// AppContextT stored in the window's userData. This lets any widget
|
||||
// access the full application context without passing it through every
|
||||
// function call.
|
||||
struct AppContextT *wgtGetContext(const WidgetT *w);
|
||||
|
||||
// Update text cursor blink state. Call once per frame from dvxUpdate.
|
||||
// Toggles the cursor visibility at 250ms intervals, matching the ANSI
|
||||
// terminal cursor rate.
|
||||
void wgtUpdateCursorBlink(void);
|
||||
|
||||
// Mark a widget as needing both re-layout (measure + position) and
|
||||
// repaint. Propagates upward to ancestors since a child's size change
|
||||
// can affect parent layout. Use this after structural changes (adding/
|
||||
// removing children, changing text that affects size).
|
||||
void wgtInvalidate(WidgetT *w);
|
||||
|
||||
// Mark a widget as needing repaint only, without re-layout. Use this
|
||||
// for visual-only changes that don't affect geometry (e.g. checkbox
|
||||
// toggle, selection highlight change, cursor blink).
|
||||
void wgtInvalidatePaint(WidgetT *w);
|
||||
|
||||
// Set/get widget text (label, button, textInput, etc.)
|
||||
void wgtSetText(WidgetT *w, const char *text);
|
||||
const char *wgtGetText(const WidgetT *w);
|
||||
|
||||
// Enable/disable a widget
|
||||
void wgtSetEnabled(WidgetT *w, bool enabled);
|
||||
|
||||
// Set read-only mode (allows scrolling/selection but blocks editing)
|
||||
void wgtSetReadOnly(WidgetT *w, bool readOnly);
|
||||
|
||||
// Set/get keyboard focus
|
||||
void wgtSetFocused(WidgetT *w);
|
||||
WidgetT *wgtGetFocused(void);
|
||||
|
||||
// Show/hide a widget
|
||||
void wgtSetVisible(WidgetT *w, bool visible);
|
||||
|
||||
// Set a widget's name (for lookup via wgtFind)
|
||||
void wgtSetName(WidgetT *w, const char *name);
|
||||
|
||||
// Find a widget by name (searches the subtree rooted at root)
|
||||
WidgetT *wgtFind(WidgetT *root, const char *name);
|
||||
|
||||
// Destroy a widget and all its children (removes from parent)
|
||||
void wgtDestroy(WidgetT *w);
|
||||
|
||||
// ============================================================
|
||||
// List box operations
|
||||
// ============================================================
|
||||
|
||||
void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count);
|
||||
int32_t wgtListBoxGetSelected(const WidgetT *w);
|
||||
void wgtListBoxSetSelected(WidgetT *w, int32_t idx);
|
||||
void wgtListBoxSetMultiSelect(WidgetT *w, bool multi);
|
||||
bool wgtListBoxIsItemSelected(const WidgetT *w, int32_t idx);
|
||||
void wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selected);
|
||||
void wgtListBoxSelectAll(WidgetT *w);
|
||||
void wgtListBoxClearSelection(WidgetT *w);
|
||||
void wgtListBoxSetReorderable(WidgetT *w, bool reorderable);
|
||||
|
||||
// ============================================================
|
||||
// Tooltip
|
||||
// ============================================================
|
||||
|
||||
// Set tooltip text for a widget (NULL to remove).
|
||||
// Caller owns the string — it must outlive the widget.
|
||||
void wgtSetTooltip(WidgetT *w, const char *text);
|
||||
|
||||
// ============================================================
|
||||
// Debug
|
||||
// ============================================================
|
||||
|
||||
// Draw borders around layout containers in ugly colors
|
||||
void wgtSetDebugLayout(struct AppContextT *ctx, bool enabled);
|
||||
|
||||
// ============================================================
|
||||
// Layout (called internally; available for manual trigger)
|
||||
// ============================================================
|
||||
|
||||
// Decode a tagged size value (WGT_SIZE_PIXELS/CHARS/PERCENT) into a
|
||||
// concrete pixel count. For CHARS, multiplies by charWidth; for PERCENT,
|
||||
// computes the fraction of parentSize. Returns 0 for a raw 0 input
|
||||
// (meaning "auto").
|
||||
int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth);
|
||||
|
||||
// Execute the full two-pass layout algorithm on the widget tree:
|
||||
// Pass 1 (bottom-up): calcMinSize on every widget to compute minimum sizes.
|
||||
// Pass 2 (top-down): allocate space within availW/availH, distributing
|
||||
// extra space according to weights and respecting min/max constraints.
|
||||
// Normally called automatically by the paint handler; exposed here for
|
||||
// cases where layout must be forced before the next paint (e.g. dvxFitWindow).
|
||||
void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT *font);
|
||||
|
||||
// Paint the entire widget tree by depth-first traversal. Each widget's
|
||||
// clip rect is set to its bounds before calling its paint function.
|
||||
// Overlays (dropdown popups, tooltips) are painted in a second pass
|
||||
// after the main tree so they render on top of everything.
|
||||
void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
|
||||
#endif // DVX_WIDGET_H
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
// dvxPlatform.h — Platform abstraction layer for DVX GUI
|
||||
//
|
||||
// All OS-specific and CPU-specific code is isolated behind this
|
||||
// interface. To port DVX to a new platform, implement a new
|
||||
// dvxPlatformXxx.c against this header.
|
||||
//
|
||||
// Currently two implementations exist:
|
||||
// dvxPlatformDos.c — DJGPP/DPMI: real VESA VBE, INT 33h mouse,
|
||||
// INT 16h keyboard, rep movsd/stosl asm spans
|
||||
// dvxPlatformLinux.c — SDL2: software rendering to an SDL window,
|
||||
// used for development and testing on Linux
|
||||
//
|
||||
// The abstraction covers five areas: video mode setup, framebuffer
|
||||
// flushing, optimized memory spans, mouse input, and keyboard input.
|
||||
// File system operations are minimal (just filename validation) because
|
||||
// the C standard library handles most file I/O portably.
|
||||
//
|
||||
// Design rule: functions in this header must be stateless or manage their
|
||||
// own internal state. They must not reference AppContextT or any layer
|
||||
// above dvxTypes.h. This ensures the platform layer can be compiled and
|
||||
// tested independently.
|
||||
#ifndef DVX_PLATFORM_H
|
||||
#define DVX_PLATFORM_H
|
||||
|
||||
#include "../dvxTypes.h"
|
||||
|
||||
// ============================================================
|
||||
// Keyboard event
|
||||
// ============================================================
|
||||
//
|
||||
// Separating ASCII value from scancode handles the DOS keyboard model
|
||||
// where extended keys (arrows, F-keys) produce a zero ASCII byte followed
|
||||
// by a scancode. The platform layer normalizes this into a single struct.
|
||||
|
||||
typedef struct {
|
||||
int32_t ascii; // ASCII value, 0 for extended/function keys
|
||||
int32_t scancode; // PC scan code (0x48=Up, 0x50=Down, etc.)
|
||||
} PlatformKeyEventT;
|
||||
|
||||
// ============================================================
|
||||
// System lifecycle
|
||||
// ============================================================
|
||||
|
||||
// One-time platform initialisation. On DOS this installs signal handlers
|
||||
// for clean shutdown on Ctrl+C/Ctrl+Break. On Linux this initializes SDL.
|
||||
void platformInit(void);
|
||||
|
||||
// Cooperative yield — give up the CPU timeslice when the event loop has
|
||||
// nothing to do. On DOS this calls __dpmi_yield() to be friendly to
|
||||
// multitaskers (Windows 3.x, OS/2, DESQview). On Linux this calls
|
||||
// SDL_Delay(1) to avoid busy-spinning at 100% CPU.
|
||||
void platformYield(void);
|
||||
|
||||
// ============================================================
|
||||
// Video
|
||||
// ============================================================
|
||||
|
||||
// Probe for a suitable video mode, enable it, map the framebuffer, and
|
||||
// allocate the system RAM backbuffer. On DOS this involves VBE BIOS calls
|
||||
// and DPMI physical memory mapping. On Linux this creates an SDL window
|
||||
// and software surface. Fills in all DisplayT fields on success.
|
||||
int32_t platformVideoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp);
|
||||
|
||||
// Restore the previous video mode and free all video resources. On DOS
|
||||
// this restores VGA text mode (mode 3) and frees the DPMI memory mapping.
|
||||
void platformVideoShutdown(DisplayT *d);
|
||||
|
||||
// Enumerate LFB-capable graphics modes. The callback is invoked for each
|
||||
// available mode. Used by videoInit() to find the best match for the
|
||||
// requested resolution and depth. On Linux, this reports a fixed set of
|
||||
// common resolutions since SDL doesn't enumerate modes the same way.
|
||||
void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *userData), void *userData);
|
||||
|
||||
// Program the VGA/VESA DAC palette registers (8-bit mode only). pal
|
||||
// points to RGB triplets (3 bytes per entry). On Linux this is a no-op
|
||||
// since the SDL surface is always truecolor.
|
||||
void platformVideoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t count);
|
||||
|
||||
// ============================================================
|
||||
// Framebuffer flush
|
||||
// ============================================================
|
||||
|
||||
// Copy a rectangle from the system RAM backbuffer (d->backBuf) to the
|
||||
// display surface (d->lfb). On DOS this copies to real video memory via
|
||||
// the LFB mapping — the critical path where PCI bus write speed matters.
|
||||
// On Linux this copies to the SDL surface, then SDL_UpdateRect is called.
|
||||
// Each scanline is copied as a contiguous block; rep movsd on DOS gives
|
||||
// near-optimal bus utilization for aligned 32-bit writes.
|
||||
void platformFlushRect(const DisplayT *d, const RectT *r);
|
||||
|
||||
// ============================================================
|
||||
// Optimised memory operations (span fill / copy)
|
||||
// ============================================================
|
||||
//
|
||||
// These are the innermost loops of the renderer — called once per
|
||||
// scanline of every rectangle fill, blit, and text draw. On DOS they
|
||||
// use inline assembly: rep stosl for fills (one instruction fills an
|
||||
// entire scanline) and rep movsd for copies. On Linux they use memset/
|
||||
// memcpy which the compiler can auto-vectorize.
|
||||
//
|
||||
// Three variants per operation (8/16/32 bpp) because the fill semantics
|
||||
// differ by depth: 8-bit fills a byte per pixel, 16-bit fills a word
|
||||
// (must handle odd counts), and 32-bit fills a dword. The copy variants
|
||||
// differ only in the byte count computation (count * bytesPerPixel).
|
||||
// drawInit() selects the right function pointers into BlitOpsT at startup.
|
||||
|
||||
void platformSpanFill8(uint8_t *dst, uint32_t color, int32_t count);
|
||||
void platformSpanFill16(uint8_t *dst, uint32_t color, int32_t count);
|
||||
void platformSpanFill32(uint8_t *dst, uint32_t color, int32_t count);
|
||||
void platformSpanCopy8(uint8_t *dst, const uint8_t *src, int32_t count);
|
||||
void platformSpanCopy16(uint8_t *dst, const uint8_t *src, int32_t count);
|
||||
void platformSpanCopy32(uint8_t *dst, const uint8_t *src, int32_t count);
|
||||
|
||||
// ============================================================
|
||||
// Input — Mouse
|
||||
// ============================================================
|
||||
|
||||
// Initialize the mouse driver and constrain movement to the screen bounds.
|
||||
// On DOS this calls INT 33h functions to detect the mouse, set the X/Y
|
||||
// range, and center the cursor. On Linux this initializes SDL mouse state.
|
||||
void platformMouseInit(int32_t screenW, int32_t screenH);
|
||||
|
||||
// Poll the current mouse state. Buttons is a bitmask: bit 0 = left,
|
||||
// bit 1 = right, bit 2 = middle. Polling (rather than event-driven
|
||||
// callbacks) is the natural model for a cooperative event loop — the
|
||||
// main loop polls once per frame and compares with the previous state
|
||||
// to detect press/release edges.
|
||||
void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons);
|
||||
|
||||
// Detect and activate mouse wheel support. Returns true if the mouse
|
||||
// driver supports the CuteMouse Wheel API (INT 33h AX=0011h). This
|
||||
// call also activates wheel reporting — after it returns true, function
|
||||
// 03h will return wheel delta in BH. Must be called after platformMouseInit.
|
||||
bool platformMouseWheelInit(void);
|
||||
|
||||
// Read the accumulated wheel delta since the last call. Positive = scroll
|
||||
// down, negative = scroll up. Returns 0 if no wheel movement or if wheel
|
||||
// is not supported. The delta is cleared on each read (accumulated by the
|
||||
// driver between polls).
|
||||
int32_t platformMouseWheelPoll(void);
|
||||
|
||||
// Set the double-speed threshold in mickeys/second. When the mouse
|
||||
// moves faster than this, cursor movement is doubled by the driver.
|
||||
// A very high value (e.g. 10000) effectively disables acceleration.
|
||||
void platformMouseSetAccel(int32_t threshold);
|
||||
|
||||
// Move the mouse cursor to an absolute screen position. Uses INT 33h
|
||||
// function 04h on DOS, SDL_WarpMouseInWindow on Linux. Used to clamp
|
||||
// the cursor to window edges during resize operations.
|
||||
void platformMouseWarp(int32_t x, int32_t y);
|
||||
|
||||
// ============================================================
|
||||
// Input — Keyboard
|
||||
// ============================================================
|
||||
|
||||
// Return the current modifier key state in BIOS shift-state format:
|
||||
// bits 0-1 = either shift, bit 2 = ctrl, bit 3 = alt. On DOS this
|
||||
// reads the BIOS data area at 0040:0017. On Linux this queries SDL
|
||||
// modifier state and translates to the same bit format.
|
||||
int32_t platformKeyboardGetModifiers(void);
|
||||
|
||||
// Non-blocking read of the next key from the keyboard buffer. Returns
|
||||
// true if a key was available. On DOS this uses INT 16h AH=11h (check)
|
||||
// + AH=10h (read). Extended keys (0xE0 prefix from enhanced keyboard)
|
||||
// are normalized by zeroing the ASCII byte so the scancode identifies
|
||||
// them unambiguously.
|
||||
bool platformKeyboardRead(PlatformKeyEventT *evt);
|
||||
|
||||
// Translate an Alt+key scancode to its corresponding ASCII character.
|
||||
// When Alt is held, DOS doesn't provide the ASCII value — only the
|
||||
// scancode. This function contains a lookup table mapping scancodes
|
||||
// to their unshifted letter/digit. Returns 0 for scancodes that don't
|
||||
// map to a printable character (e.g. Alt+F1).
|
||||
char platformAltScanToChar(int32_t scancode);
|
||||
|
||||
// ============================================================
|
||||
// System information
|
||||
// ============================================================
|
||||
|
||||
// Maximum size of the formatted system information text
|
||||
#define PLATFORM_SYSINFO_MAX 4096
|
||||
|
||||
// Gather hardware information (CPU, clock, memory, DOS/DPMI version,
|
||||
// video, mouse, disk drives) and return as a pre-formatted text string.
|
||||
// The display pointer provides the current video mode info. Returns a
|
||||
// pointer to a static buffer valid for the lifetime of the process.
|
||||
// On DOS this uses CPUID, RDTSC, DPMI, INT 21h, INT 33h, and VBE.
|
||||
// On other platforms it returns whatever the OS can report.
|
||||
const char *platformGetSystemInfo(const DisplayT *display);
|
||||
|
||||
// ============================================================
|
||||
// File system
|
||||
// ============================================================
|
||||
|
||||
// Validate a filename against platform-specific rules. On DOS this
|
||||
// enforces 8.3 naming (no long filenames), checks for reserved device
|
||||
// names (CON, PRN, etc.), and rejects characters illegal in FAT filenames.
|
||||
// On Linux the rules are much more permissive (just no slashes or NUL).
|
||||
// Returns NULL if the filename is valid, or a human-readable error string
|
||||
// describing why it's invalid. Used by the file dialog's save-as validation.
|
||||
const char *platformValidateFilename(const char *name);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
|
||||
#endif // DVX_PLATFORM_H
|
||||
20
dvx/thirdparty/ini/LICENSE
vendored
20
dvx/thirdparty/ini/LICENSE
vendored
|
|
@ -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.
|
||||
63
dvx/thirdparty/ini/README.md
vendored
63
dvx/thirdparty/ini/README.md
vendored
|
|
@ -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.
|
||||
274
dvx/thirdparty/ini/src/ini.c
vendored
274
dvx/thirdparty/ini/src/ini.c
vendored
|
|
@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
20
dvx/thirdparty/ini/src/ini.h
vendored
20
dvx/thirdparty/ini/src/ini.h
vendored
|
|
@ -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
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,133 +0,0 @@
|
|||
// widgetBox.c — VBox, HBox, and Frame container widgets
|
||||
//
|
||||
// VBox and HBox are the primary layout containers. They have no visual
|
||||
// representation of their own — they exist purely to arrange children
|
||||
// vertically or horizontally. The actual layout algorithm lives in
|
||||
// widgetLayout.c (widgetCalcMinSizeBox / widgetLayoutBox) which handles
|
||||
// weight-based space distribution, spacing, padding, and alignment.
|
||||
//
|
||||
// VBox and HBox are distinguished by a flag (WCLASS_HORIZ_CONTAINER) in
|
||||
// the class table rather than having separate code. This keeps the layout
|
||||
// engine unified — the same algorithm works in both orientations by
|
||||
// swapping which axis is "major" vs "minor".
|
||||
//
|
||||
// Frame is a labeled grouping box with a Motif-style beveled border.
|
||||
// It acts as a VBox for layout purposes (children stack vertically inside
|
||||
// the frame's padded interior). The title text sits centered vertically
|
||||
// on the top border line, with a small background-filled gap to visually
|
||||
// "break" the border behind the title — this is the classic Win3.1/Motif
|
||||
// group box appearance.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetFramePaint
|
||||
// ============================================================
|
||||
|
||||
// Paint the frame border and optional title. The border is offset down by
|
||||
// half the font height so the title text can sit centered on the top edge.
|
||||
// This creates the illusion of the title "interrupting" the border — a
|
||||
// background-colored rectangle is drawn behind the title to erase the
|
||||
// border pixels, then the title is drawn on top.
|
||||
//
|
||||
// Three border styles are supported:
|
||||
// FrameFlatE — single-pixel solid color outline
|
||||
// FrameInE — Motif "groove" (inset bevel: shadow-then-highlight)
|
||||
// FrameOutE — Motif "ridge" (outset bevel: highlight-then-shadow)
|
||||
// The groove/ridge are each two nested 1px bevels with swapped colors.
|
||||
void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
int32_t fb = widgetFrameBorderWidth(w);
|
||||
int32_t boxY = w->y + font->charHeight / 2;
|
||||
int32_t boxH = w->h - font->charHeight / 2;
|
||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||
|
||||
if (w->as.frame.style == FrameFlatE) {
|
||||
// Flat: solid color rectangle outline
|
||||
uint32_t fc = w->as.frame.color ? w->as.frame.color : colors->windowShadow;
|
||||
|
||||
drawHLine(d, ops, w->x, boxY, w->w, fc);
|
||||
drawHLine(d, ops, w->x, boxY + boxH - 1, w->w, fc);
|
||||
drawVLine(d, ops, w->x, boxY, boxH, fc);
|
||||
drawVLine(d, ops, w->x + w->w - 1, boxY, boxH, fc);
|
||||
} else {
|
||||
// Beveled groove/ridge: two nested 1px bevels
|
||||
BevelStyleT outer;
|
||||
BevelStyleT inner;
|
||||
|
||||
if (w->as.frame.style == FrameInE) {
|
||||
outer.highlight = colors->windowShadow;
|
||||
outer.shadow = colors->windowHighlight;
|
||||
inner.highlight = colors->windowHighlight;
|
||||
inner.shadow = colors->windowShadow;
|
||||
} else {
|
||||
outer.highlight = colors->windowHighlight;
|
||||
outer.shadow = colors->windowShadow;
|
||||
inner.highlight = colors->windowShadow;
|
||||
inner.shadow = colors->windowHighlight;
|
||||
}
|
||||
|
||||
outer.face = 0;
|
||||
outer.width = 1;
|
||||
inner.face = 0;
|
||||
inner.width = 1;
|
||||
drawBevel(d, ops, w->x, boxY, w->w, boxH, &outer);
|
||||
drawBevel(d, ops, w->x + 1, boxY + 1, w->w - 2, boxH - 2, &inner);
|
||||
}
|
||||
|
||||
// Draw title centered vertically on the top border line
|
||||
if (w->as.frame.title && w->as.frame.title[0]) {
|
||||
int32_t titleW = textWidthAccel(font, w->as.frame.title);
|
||||
int32_t titleX = w->x + DEFAULT_PADDING + fb;
|
||||
int32_t titleY = boxY + (fb - font->charHeight) / 2;
|
||||
|
||||
rectFill(d, ops, titleX - 2, titleY,
|
||||
titleW + 4, font->charHeight, bg);
|
||||
|
||||
if (!w->enabled) {
|
||||
drawTextAccelEmbossed(d, ops, font, titleX, titleY, w->as.frame.title, colors);
|
||||
} else {
|
||||
drawTextAccel(d, ops, font, titleX, titleY, w->as.frame.title, fg, bg, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtFrame
|
||||
// ============================================================
|
||||
|
||||
// Create a Frame container. The title string supports accelerator keys
|
||||
// (prefixed with '&') which are parsed by accelParse. The title pointer
|
||||
// is stored directly (not copied) — the caller must ensure it remains valid.
|
||||
WidgetT *wgtFrame(WidgetT *parent, const char *title) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetFrameE);
|
||||
|
||||
if (w) {
|
||||
w->as.frame.title = title;
|
||||
w->as.frame.style = FrameInE;
|
||||
w->as.frame.color = 0;
|
||||
w->accelKey = accelParse(title);
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtHBox
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtHBox(WidgetT *parent) {
|
||||
return widgetAlloc(parent, WidgetHBoxE);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtVBox
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtVBox(WidgetT *parent) {
|
||||
return widgetAlloc(parent, WidgetVBoxE);
|
||||
}
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
// widgetButton.c — Button widget
|
||||
//
|
||||
// Standard push button with text label, Motif-style 2px beveled border,
|
||||
// and press animation. The button uses a two-phase press model:
|
||||
// - Mouse press: sets pressed=true and stores the widget in sPressedButton.
|
||||
// The event dispatcher tracks mouse movement — if the mouse leaves the
|
||||
// button bounds, pressed is cleared (visual feedback), and if it re-enters,
|
||||
// pressed is re-set. The onClick callback fires only on mouse-up while
|
||||
// still inside the button. This gives the user a chance to cancel by
|
||||
// dragging away, matching standard GUI behavior.
|
||||
// - Keyboard press (Space/Enter): sets pressed=true and stores in
|
||||
// sKeyPressedBtn. The onClick fires on key-up. This uses a different
|
||||
// global than mouse press because key and mouse can't happen simultaneously.
|
||||
//
|
||||
// The press animation shifts text and focus rect by BUTTON_PRESS_OFFSET (1px)
|
||||
// down and right, and swaps the bevel highlight/shadow colors to create the
|
||||
// illusion of the button being pushed "into" the screen.
|
||||
//
|
||||
// Text supports accelerator keys via '&' prefix (e.g., "&OK" underlines 'O'
|
||||
// and Alt+O activates the button).
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtButton
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtButton(WidgetT *parent, const char *text) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetButtonE);
|
||||
|
||||
if (w) {
|
||||
w->as.button.text = text;
|
||||
w->as.button.pressed = false;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetButtonGetText
|
||||
// ============================================================
|
||||
|
||||
const char *widgetButtonGetText(const WidgetT *w) {
|
||||
return w->as.button.text;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetButtonSetText
|
||||
// ============================================================
|
||||
|
||||
void widgetButtonSetText(WidgetT *w, const char *text) {
|
||||
w->as.button.text = text;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetButtonCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
// Min size includes horizontal and vertical padding around the text. The text
|
||||
// width is computed by textWidthAccel which excludes the '&' accelerator prefix
|
||||
// character from the measurement.
|
||||
void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
w->calcMinW = textWidthAccel(font, w->as.button.text) + BUTTON_PAD_H * 2;
|
||||
w->calcMinH = font->charHeight + BUTTON_PAD_V * 2;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetButtonOnKey
|
||||
// ============================================================
|
||||
|
||||
void widgetButtonOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||
(void)mod;
|
||||
|
||||
if (key == ' ' || key == 0x0D) {
|
||||
w->as.button.pressed = true;
|
||||
sKeyPressedBtn = w;
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetButtonOnMouse
|
||||
// ============================================================
|
||||
|
||||
void widgetButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||
(void)root;
|
||||
(void)vx;
|
||||
(void)vy;
|
||||
w->focused = true;
|
||||
w->as.button.pressed = true;
|
||||
sPressedButton = w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetButtonPaint
|
||||
// ============================================================
|
||||
|
||||
// Paint: draws the beveled border, centered text, and optional focus rect.
|
||||
// When pressed, the bevel colors swap (highlight↔shadow) creating the sunken
|
||||
// appearance, and the text shifts by BUTTON_PRESS_OFFSET pixels. Disabled
|
||||
// buttons use the "embossed" text technique (highlight color at +1,+1, then
|
||||
// shadow color at 0,0) to create a chiseled/etched look — this is the
|
||||
// standard Windows 3.1 disabled control appearance.
|
||||
void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||
uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace;
|
||||
|
||||
BevelStyleT bevel;
|
||||
bevel.highlight = w->as.button.pressed ? colors->windowShadow : colors->windowHighlight;
|
||||
bevel.shadow = w->as.button.pressed ? colors->windowHighlight : colors->windowShadow;
|
||||
bevel.face = bgFace;
|
||||
bevel.width = 2;
|
||||
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
|
||||
|
||||
int32_t textW = textWidthAccel(font, w->as.button.text);
|
||||
int32_t textX = w->x + (w->w - textW) / 2;
|
||||
int32_t textY = w->y + (w->h - font->charHeight) / 2;
|
||||
|
||||
if (w->as.button.pressed) {
|
||||
textX += BUTTON_PRESS_OFFSET;
|
||||
textY += BUTTON_PRESS_OFFSET;
|
||||
}
|
||||
|
||||
if (!w->enabled) {
|
||||
drawTextAccelEmbossed(d, ops, font, textX, textY, w->as.button.text, colors);
|
||||
} else {
|
||||
drawTextAccel(d, ops, font, textX, textY, w->as.button.text, fg, bgFace, true);
|
||||
}
|
||||
|
||||
if (w->focused) {
|
||||
int32_t off = w->as.button.pressed ? BUTTON_PRESS_OFFSET : 0;
|
||||
drawFocusRect(d, ops, w->x + BUTTON_FOCUS_INSET + off, w->y + BUTTON_FOCUS_INSET + off, w->w - BUTTON_FOCUS_INSET * 2, w->h - BUTTON_FOCUS_INSET * 2, fg);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,726 +0,0 @@
|
|||
// widgetCanvas.c — Drawable canvas widget (freehand draw, PNG save/load)
|
||||
//
|
||||
// The canvas widget provides a pixel buffer in the display's native pixel
|
||||
// format that applications can draw into directly. It stores pixels in
|
||||
// display format (not always RGB) to avoid per-pixel conversion on every
|
||||
// repaint — the paint function just does a straight rectCopy blit from
|
||||
// the canvas buffer to the display. This is critical on a 486 where
|
||||
// per-pixel format conversion during repaint would be prohibitively slow.
|
||||
//
|
||||
// The tradeoff is that load/save operations must convert between RGB and
|
||||
// the display format, but those are one-time costs at I/O time rather
|
||||
// than per-frame costs.
|
||||
//
|
||||
// Drawing operations (dot, line, rect, circle) operate directly on the
|
||||
// canvas buffer using canvasPutPixel for pixel-level writes. The dot
|
||||
// primitive draws a filled circle using the pen size, and line uses
|
||||
// Bresenham's algorithm placing dots along the path. This gives smooth
|
||||
// freehand drawing with variable pen widths.
|
||||
//
|
||||
// Canvas coordinates are independent of widget position — (0,0) is the
|
||||
// top-left of the canvas content, not the widget. Mouse events translate
|
||||
// widget-space coordinates to canvas-space by subtracting the border offset.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
#define CANVAS_BORDER 2
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Prototypes
|
||||
// ============================================================
|
||||
|
||||
static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy);
|
||||
static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1);
|
||||
|
||||
|
||||
// ============================================================
|
||||
// canvasGetPixel / canvasPutPixel
|
||||
// ============================================================
|
||||
//
|
||||
// Read/write a single pixel at the given address, respecting the display's
|
||||
// bytes-per-pixel depth (1=8-bit palette, 2=16-bit hicolor, 4=32-bit truecolor).
|
||||
// These are inline because they're called per-pixel in tight loops (circle fill,
|
||||
// line draw) — the function call overhead would dominate on a 486. The bpp
|
||||
// branch is predictable since it doesn't change within a single draw operation.
|
||||
|
||||
static inline uint32_t canvasGetPixel(const uint8_t *src, int32_t bpp) {
|
||||
if (bpp == 1) {
|
||||
return *src;
|
||||
} else if (bpp == 2) {
|
||||
return *(const uint16_t *)src;
|
||||
} else {
|
||||
return *(const uint32_t *)src;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static inline void canvasPutPixel(uint8_t *dst, uint32_t color, int32_t bpp) {
|
||||
if (bpp == 1) {
|
||||
*dst = (uint8_t)color;
|
||||
} else if (bpp == 2) {
|
||||
*(uint16_t *)dst = (uint16_t)color;
|
||||
} else {
|
||||
*(uint32_t *)dst = color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// canvasDrawDot
|
||||
// ============================================================
|
||||
//
|
||||
// Draw a filled circle of diameter penSize at (cx, cy) in canvas coords.
|
||||
|
||||
static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy) {
|
||||
int32_t bpp = w->as.canvas.canvasBpp;
|
||||
int32_t pitch = w->as.canvas.canvasPitch;
|
||||
uint8_t *data = w->as.canvas.data;
|
||||
int32_t cw = w->as.canvas.canvasW;
|
||||
int32_t ch = w->as.canvas.canvasH;
|
||||
uint32_t color = w->as.canvas.penColor;
|
||||
int32_t rad = w->as.canvas.penSize / 2;
|
||||
|
||||
if (rad < 1) {
|
||||
// Single pixel
|
||||
if (cx >= 0 && cx < cw && cy >= 0 && cy < ch) {
|
||||
uint8_t *dst = data + cy * pitch + cx * bpp;
|
||||
|
||||
canvasPutPixel(dst, color, bpp);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Filled circle via per-row horizontal span. For each row (dy offset from
|
||||
// center), compute the horizontal extent using the circle equation
|
||||
// dx^2 + dy^2 <= r^2. The horizontal half-span is floor(sqrt(r^2 - dy^2)).
|
||||
// This approach is faster than checking each pixel individually because
|
||||
// the inner loop just fills a horizontal run — no per-pixel distance check.
|
||||
int32_t r2 = rad * rad;
|
||||
|
||||
for (int32_t dy = -rad; dy <= rad; dy++) {
|
||||
int32_t py = cy + dy;
|
||||
|
||||
if (py < 0 || py >= ch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compute horizontal half-span: dx² <= r² - dy²
|
||||
int32_t dy2 = dy * dy;
|
||||
int32_t rem = r2 - dy2;
|
||||
int32_t hspan = 0;
|
||||
|
||||
// Integer sqrt via Newton's method (Babylonian method). 8 iterations
|
||||
// is more than enough to converge for any radius that fits in int32_t.
|
||||
// Using integer sqrt avoids pulling in the FPU which may not be
|
||||
// present on 486SX systems, and avoids the float-to-int conversion
|
||||
// overhead even on systems with an FPU.
|
||||
if (rem > 0) {
|
||||
hspan = rad;
|
||||
|
||||
for (int32_t i = 0; i < 8; i++) {
|
||||
hspan = (hspan + rem / hspan) / 2;
|
||||
}
|
||||
|
||||
if (hspan * hspan > rem) {
|
||||
hspan--;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t x0 = cx - hspan;
|
||||
int32_t x1 = cx + hspan;
|
||||
|
||||
if (x0 < 0) {
|
||||
x0 = 0;
|
||||
}
|
||||
|
||||
if (x1 >= cw) {
|
||||
x1 = cw - 1;
|
||||
}
|
||||
|
||||
if (x0 <= x1) {
|
||||
uint8_t *dst = data + py * pitch + x0 * bpp;
|
||||
|
||||
for (int32_t px = x0; px <= x1; px++) {
|
||||
canvasPutPixel(dst, color, bpp);
|
||||
dst += bpp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// canvasDrawLine
|
||||
// ============================================================
|
||||
//
|
||||
// Bresenham line from (x0,y0) to (x1,y1), placing dots along the path.
|
||||
// Each point on the line gets a full pen dot (canvasDrawDot), which means
|
||||
// lines with large pen sizes are smooth rather than aliased. Bresenham was
|
||||
// chosen over DDA because it's pure integer arithmetic — no floating point
|
||||
// needed, which matters on 486SX (no FPU).
|
||||
|
||||
static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1) {
|
||||
int32_t dx = x1 - x0;
|
||||
int32_t dy = y1 - y0;
|
||||
int32_t sx = (dx >= 0) ? 1 : -1;
|
||||
int32_t sy = (dy >= 0) ? 1 : -1;
|
||||
|
||||
if (dx < 0) { dx = -dx; }
|
||||
if (dy < 0) { dy = -dy; }
|
||||
|
||||
int32_t err = dx - dy;
|
||||
|
||||
for (;;) {
|
||||
canvasDrawDot(w, x0, y0);
|
||||
|
||||
if (x0 == x1 && y0 == y1) {
|
||||
break;
|
||||
}
|
||||
|
||||
int32_t e2 = 2 * err;
|
||||
|
||||
if (e2 > -dy) {
|
||||
err -= dy;
|
||||
x0 += sx;
|
||||
}
|
||||
|
||||
if (e2 < dx) {
|
||||
err += dx;
|
||||
y0 += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtCanvas
|
||||
// ============================================================
|
||||
|
||||
// Create a canvas widget with the specified dimensions. The canvas buffer
|
||||
// is allocated in the display's native pixel format by walking up the widget
|
||||
// tree to find the AppContextT (which holds the display format info). This
|
||||
// tree-walk pattern is necessary because the widget doesn't have direct access
|
||||
// to the display — only the root widget's userData points to the AppContextT.
|
||||
// The buffer is initialized to white using spanFill for performance.
|
||||
WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) {
|
||||
if (!parent || w <= 0 || h <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Find the AppContextT to get display format
|
||||
WidgetT *root = parent;
|
||||
|
||||
while (root->parent) {
|
||||
root = root->parent;
|
||||
}
|
||||
|
||||
AppContextT *ctx = (AppContextT *)root->userData;
|
||||
|
||||
if (!ctx) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const DisplayT *d = &ctx->display;
|
||||
int32_t bpp = d->format.bytesPerPixel;
|
||||
int32_t pitch = w * bpp;
|
||||
|
||||
uint8_t *data = (uint8_t *)malloc(pitch * h);
|
||||
|
||||
if (!data) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Fill with white using span fill for performance
|
||||
uint32_t white = packColor(d, 255, 255, 255);
|
||||
BlitOpsT canvasOps;
|
||||
drawInit(&canvasOps, d);
|
||||
|
||||
for (int32_t y = 0; y < h; y++) {
|
||||
canvasOps.spanFill(data + y * pitch, white, w);
|
||||
}
|
||||
|
||||
WidgetT *wgt = widgetAlloc(parent, WidgetCanvasE);
|
||||
|
||||
if (wgt) {
|
||||
wgt->as.canvas.data = data;
|
||||
wgt->as.canvas.canvasW = w;
|
||||
wgt->as.canvas.canvasH = h;
|
||||
wgt->as.canvas.canvasPitch = pitch;
|
||||
wgt->as.canvas.canvasBpp = bpp;
|
||||
wgt->as.canvas.penColor = packColor(d, 0, 0, 0);
|
||||
wgt->as.canvas.penSize = 2;
|
||||
wgt->as.canvas.lastX = -1;
|
||||
wgt->as.canvas.lastY = -1;
|
||||
} else {
|
||||
free(data);
|
||||
}
|
||||
|
||||
return wgt;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtCanvasClear
|
||||
// ============================================================
|
||||
|
||||
void wgtCanvasClear(WidgetT *w, uint32_t color) {
|
||||
if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find BlitOps for span fill
|
||||
WidgetT *root = w;
|
||||
while (root->parent) {
|
||||
root = root->parent;
|
||||
}
|
||||
AppContextT *ctx = (AppContextT *)root->userData;
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t pitch = w->as.canvas.canvasPitch;
|
||||
int32_t cw = w->as.canvas.canvasW;
|
||||
int32_t ch = w->as.canvas.canvasH;
|
||||
|
||||
for (int32_t y = 0; y < ch; y++) {
|
||||
ctx->blitOps.spanFill(w->as.canvas.data + y * pitch, color, cw);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtCanvasDrawLine
|
||||
// ============================================================
|
||||
//
|
||||
// Draw a line using the current pen color and pen size.
|
||||
|
||||
void wgtCanvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1) {
|
||||
if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvasDrawLine(w, x0, y0, x1, y1);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtCanvasDrawRect
|
||||
// ============================================================
|
||||
//
|
||||
// Draw a 1px outlined rectangle using the current pen color.
|
||||
|
||||
void wgtCanvasDrawRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height) {
|
||||
if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t bpp = w->as.canvas.canvasBpp;
|
||||
int32_t pitch = w->as.canvas.canvasPitch;
|
||||
uint8_t *data = w->as.canvas.data;
|
||||
int32_t cw = w->as.canvas.canvasW;
|
||||
int32_t ch = w->as.canvas.canvasH;
|
||||
uint32_t color = w->as.canvas.penColor;
|
||||
|
||||
// Top and bottom edges
|
||||
for (int32_t px = x; px < x + width; px++) {
|
||||
if (px < 0 || px >= cw) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (y >= 0 && y < ch) {
|
||||
uint8_t *dst = data + y * pitch + px * bpp;
|
||||
|
||||
canvasPutPixel(dst, color, bpp);
|
||||
}
|
||||
|
||||
int32_t by = y + height - 1;
|
||||
|
||||
if (by >= 0 && by < ch && by != y) {
|
||||
uint8_t *dst = data + by * pitch + px * bpp;
|
||||
|
||||
canvasPutPixel(dst, color, bpp);
|
||||
}
|
||||
}
|
||||
|
||||
// Left and right edges (excluding corners already drawn)
|
||||
for (int32_t py = y + 1; py < y + height - 1; py++) {
|
||||
if (py < 0 || py >= ch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (x >= 0 && x < cw) {
|
||||
uint8_t *dst = data + py * pitch + x * bpp;
|
||||
|
||||
canvasPutPixel(dst, color, bpp);
|
||||
}
|
||||
|
||||
int32_t rx = x + width - 1;
|
||||
|
||||
if (rx >= 0 && rx < cw && rx != x) {
|
||||
uint8_t *dst = data + py * pitch + rx * bpp;
|
||||
|
||||
canvasPutPixel(dst, color, bpp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtCanvasFillCircle
|
||||
// ============================================================
|
||||
//
|
||||
// Draw a filled circle using the current pen color.
|
||||
|
||||
void wgtCanvasFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius) {
|
||||
if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (radius <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t bpp = w->as.canvas.canvasBpp;
|
||||
int32_t pitch = w->as.canvas.canvasPitch;
|
||||
uint8_t *data = w->as.canvas.data;
|
||||
int32_t cw = w->as.canvas.canvasW;
|
||||
int32_t ch = w->as.canvas.canvasH;
|
||||
uint32_t color = w->as.canvas.penColor;
|
||||
int32_t r2 = radius * radius;
|
||||
|
||||
for (int32_t dy = -radius; dy <= radius; dy++) {
|
||||
int32_t py = cy + dy;
|
||||
|
||||
if (py < 0 || py >= ch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compute horizontal half-span: dx² <= r² - dy²
|
||||
int32_t dy2 = dy * dy;
|
||||
int32_t rem = r2 - dy2;
|
||||
int32_t hspan = 0;
|
||||
|
||||
// Integer sqrt via Newton's method
|
||||
if (rem > 0) {
|
||||
hspan = radius;
|
||||
|
||||
for (int32_t i = 0; i < 8; i++) {
|
||||
hspan = (hspan + rem / hspan) / 2;
|
||||
}
|
||||
|
||||
if (hspan * hspan > rem) {
|
||||
hspan--;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t x0 = cx - hspan;
|
||||
int32_t x1 = cx + hspan;
|
||||
|
||||
if (x0 < 0) {
|
||||
x0 = 0;
|
||||
}
|
||||
|
||||
if (x1 >= cw) {
|
||||
x1 = cw - 1;
|
||||
}
|
||||
|
||||
if (x0 <= x1) {
|
||||
uint8_t *dst = data + py * pitch + x0 * bpp;
|
||||
|
||||
for (int32_t px = x0; px <= x1; px++) {
|
||||
canvasPutPixel(dst, color, bpp);
|
||||
dst += bpp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtCanvasFillRect
|
||||
// ============================================================
|
||||
//
|
||||
// Draw a filled rectangle using the current pen color.
|
||||
|
||||
void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height) {
|
||||
if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t bpp = w->as.canvas.canvasBpp;
|
||||
int32_t pitch = w->as.canvas.canvasPitch;
|
||||
uint8_t *data = w->as.canvas.data;
|
||||
int32_t cw = w->as.canvas.canvasW;
|
||||
int32_t ch = w->as.canvas.canvasH;
|
||||
uint32_t color = w->as.canvas.penColor;
|
||||
|
||||
// Clip to canvas bounds
|
||||
int32_t x0 = x < 0 ? 0 : x;
|
||||
int32_t y0 = y < 0 ? 0 : y;
|
||||
int32_t x1 = x + width > cw ? cw : x + width;
|
||||
int32_t y1 = y + height > ch ? ch : y + height;
|
||||
int32_t fillW = x1 - x0;
|
||||
|
||||
if (fillW <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find BlitOps for span fill
|
||||
WidgetT *root = w;
|
||||
while (root->parent) {
|
||||
root = root->parent;
|
||||
}
|
||||
AppContextT *ctx = (AppContextT *)root->userData;
|
||||
|
||||
// Use the optimized spanFill (which uses rep stosl on x86) when available,
|
||||
// falling back to per-pixel writes if the AppContextT can't be reached.
|
||||
// The spanFill path is ~4x faster for 32-bit modes because it writes
|
||||
// 4 bytes per iteration instead of going through the bpp switch.
|
||||
if (ctx) {
|
||||
for (int32_t py = y0; py < y1; py++) {
|
||||
ctx->blitOps.spanFill(data + py * pitch + x0 * bpp, color, fillW);
|
||||
}
|
||||
} else {
|
||||
for (int32_t py = y0; py < y1; py++) {
|
||||
for (int32_t px = x0; px < x1; px++) {
|
||||
canvasPutPixel(data + py * pitch + px * bpp, color, bpp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtCanvasGetPixel
|
||||
// ============================================================
|
||||
|
||||
uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y) {
|
||||
if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (x < 0 || x >= w->as.canvas.canvasW || y < 0 || y >= w->as.canvas.canvasH) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t bpp = w->as.canvas.canvasBpp;
|
||||
const uint8_t *src = w->as.canvas.data + y * w->as.canvas.canvasPitch + x * bpp;
|
||||
|
||||
return canvasGetPixel(src, bpp);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtCanvasLoad
|
||||
// ============================================================
|
||||
|
||||
// Load an image file into the canvas, replacing the current content.
|
||||
// Delegates to dvxLoadImage for format decoding and pixel conversion.
|
||||
// The old buffer is freed and replaced with the new one — canvas
|
||||
// dimensions change to match the loaded image.
|
||||
int32_t wgtCanvasLoad(WidgetT *w, const char *path) {
|
||||
if (!w || w->type != WidgetCanvasE || !path) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
AppContextT *ctx = wgtGetContext(w);
|
||||
|
||||
if (!ctx) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int32_t imgW;
|
||||
int32_t imgH;
|
||||
int32_t pitch;
|
||||
uint8_t *data = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch);
|
||||
|
||||
if (!data) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
free(w->as.canvas.data);
|
||||
w->as.canvas.data = data;
|
||||
w->as.canvas.canvasW = imgW;
|
||||
w->as.canvas.canvasH = imgH;
|
||||
w->as.canvas.canvasPitch = pitch;
|
||||
w->as.canvas.canvasBpp = ctx->display.format.bytesPerPixel;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtCanvasSave
|
||||
// ============================================================
|
||||
|
||||
// Save the canvas content to a PNG file. Delegates to dvxSaveImage
|
||||
// which handles native-to-RGB conversion and PNG encoding.
|
||||
int32_t wgtCanvasSave(WidgetT *w, const char *path) {
|
||||
if (!w || w->type != WidgetCanvasE || !path || !w->as.canvas.data) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
AppContextT *ctx = wgtGetContext(w);
|
||||
|
||||
if (!ctx) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return dvxSaveImage(ctx, w->as.canvas.data, w->as.canvas.canvasW, w->as.canvas.canvasH, w->as.canvas.canvasPitch, path);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtCanvasSetMouseCallback
|
||||
// ============================================================
|
||||
|
||||
void wgtCanvasSetMouseCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag)) {
|
||||
if (w && w->type == WidgetCanvasE) {
|
||||
w->as.canvas.onMouse = cb;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtCanvasSetPenColor
|
||||
// ============================================================
|
||||
|
||||
void wgtCanvasSetPenColor(WidgetT *w, uint32_t color) {
|
||||
if (w && w->type == WidgetCanvasE) {
|
||||
w->as.canvas.penColor = color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtCanvasSetPenSize
|
||||
// ============================================================
|
||||
|
||||
void wgtCanvasSetPenSize(WidgetT *w, int32_t size) {
|
||||
if (w && w->type == WidgetCanvasE && size > 0) {
|
||||
w->as.canvas.penSize = size;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtCanvasSetPixel
|
||||
// ============================================================
|
||||
|
||||
void wgtCanvasSetPixel(WidgetT *w, int32_t x, int32_t y, uint32_t color) {
|
||||
if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (x < 0 || x >= w->as.canvas.canvasW || y < 0 || y >= w->as.canvas.canvasH) {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t bpp = w->as.canvas.canvasBpp;
|
||||
uint8_t *dst = w->as.canvas.data + y * w->as.canvas.canvasPitch + x * bpp;
|
||||
|
||||
canvasPutPixel(dst, color, bpp);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetCanvasDestroy
|
||||
// ============================================================
|
||||
|
||||
void widgetCanvasDestroy(WidgetT *w) {
|
||||
free(w->as.canvas.data);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetCanvasCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
// The canvas requests exactly its pixel dimensions plus the sunken bevel
|
||||
// border. The font parameter is unused since the canvas has no text content.
|
||||
// The canvas is not designed to scale — it reports its exact size as the
|
||||
// minimum, and the layout engine should respect that.
|
||||
void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
(void)font;
|
||||
w->calcMinW = w->as.canvas.canvasW + CANVAS_BORDER * 2;
|
||||
w->calcMinH = w->as.canvas.canvasH + CANVAS_BORDER * 2;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetCanvasOnMouse
|
||||
// ============================================================
|
||||
|
||||
// Mouse handler: translates widget-space coordinates to canvas-space (subtracting
|
||||
// border offset) and invokes the application's mouse callback. The sDrawingCanvas
|
||||
// global tracks whether this is a drag (mouse was already down on this canvas)
|
||||
// vs a new click, so the callback can distinguish between starting a new stroke
|
||||
// and continuing one.
|
||||
void widgetCanvasOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||
(void)root;
|
||||
|
||||
if (!hit->as.canvas.onMouse) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert widget coords to canvas coords
|
||||
int32_t cx = vx - hit->x - CANVAS_BORDER;
|
||||
int32_t cy = vy - hit->y - CANVAS_BORDER;
|
||||
|
||||
bool drag = (sDrawingCanvas == hit);
|
||||
|
||||
if (!drag) {
|
||||
sDrawingCanvas = hit;
|
||||
}
|
||||
|
||||
hit->as.canvas.lastX = cx;
|
||||
hit->as.canvas.lastY = cy;
|
||||
|
||||
hit->as.canvas.onMouse(hit, cx, cy, drag);
|
||||
wgtInvalidatePaint(hit);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetCanvasPaint
|
||||
// ============================================================
|
||||
|
||||
// Paint: draws a sunken bevel border then blits the canvas buffer. Because
|
||||
// the canvas stores pixels in the display's native format, rectCopy is a
|
||||
// straight memcpy per scanline — no per-pixel conversion needed. This makes
|
||||
// repaint essentially free relative to the display bandwidth.
|
||||
void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
(void)font;
|
||||
|
||||
if (!w->as.canvas.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw a sunken bevel border around the canvas
|
||||
BevelStyleT sunken;
|
||||
sunken.highlight = colors->windowShadow;
|
||||
sunken.shadow = colors->windowHighlight;
|
||||
sunken.face = 0;
|
||||
sunken.width = CANVAS_BORDER;
|
||||
drawBevel(d, ops, w->x, w->y, w->w, w->h, &sunken);
|
||||
|
||||
// Blit the canvas data inside the border
|
||||
int32_t imgW = w->as.canvas.canvasW;
|
||||
int32_t imgH = w->as.canvas.canvasH;
|
||||
int32_t dx = w->x + CANVAS_BORDER;
|
||||
int32_t dy = w->y + CANVAS_BORDER;
|
||||
|
||||
rectCopy(d, ops, dx, dy,
|
||||
w->as.canvas.data, w->as.canvas.canvasPitch,
|
||||
0, 0, imgW, imgH);
|
||||
}
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
// widgetCheckbox.c — Checkbox widget
|
||||
//
|
||||
// Classic checkbox: a small box with a sunken bevel (1px) on the left, a text
|
||||
// label to the right. The check mark is drawn as two diagonal lines forming an
|
||||
// "X" pattern rather than a traditional checkmark glyph — this is simpler to
|
||||
// render with drawHLine primitives and matches the DV/X aesthetic.
|
||||
//
|
||||
// State management is simple: a boolean 'checked' flag toggles on each click
|
||||
// or Space/Enter keypress. The onChange callback fires after each toggle so
|
||||
// the application can respond immediately.
|
||||
//
|
||||
// Focus is shown via a dotted rectangle around the label text (not the box),
|
||||
// matching the Win3.1 convention where the focus indicator wraps the label.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtCheckbox
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtCheckbox(WidgetT *parent, const char *text) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetCheckboxE);
|
||||
|
||||
if (w) {
|
||||
w->as.checkbox.text = text;
|
||||
w->as.checkbox.checked = false;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetCheckboxGetText
|
||||
// ============================================================
|
||||
|
||||
const char *widgetCheckboxGetText(const WidgetT *w) {
|
||||
return w->as.checkbox.text;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetCheckboxSetText
|
||||
// ============================================================
|
||||
|
||||
void widgetCheckboxSetText(WidgetT *w, const char *text) {
|
||||
w->as.checkbox.text = text;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetCheckboxCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
// Min width = box + gap + text. Min height = whichever is taller (the box or
|
||||
// the font). This ensures the box and text are always vertically centered
|
||||
// relative to each other regardless of font size.
|
||||
void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
|
||||
textWidthAccel(font, w->as.checkbox.text);
|
||||
w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetCheckboxOnKey
|
||||
// ============================================================
|
||||
|
||||
void widgetCheckboxOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||
(void)mod;
|
||||
|
||||
if (key == ' ' || key == 0x0D) {
|
||||
w->as.checkbox.checked = !w->as.checkbox.checked;
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetCheckboxOnMouse
|
||||
// ============================================================
|
||||
|
||||
void widgetCheckboxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||
(void)root;
|
||||
(void)vx;
|
||||
(void)vy;
|
||||
w->focused = true;
|
||||
w->as.checkbox.checked = !w->as.checkbox.checked;
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetCheckboxPaint
|
||||
// ============================================================
|
||||
|
||||
void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||
int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2;
|
||||
|
||||
// Draw checkbox box
|
||||
BevelStyleT bevel;
|
||||
bevel.highlight = colors->windowShadow;
|
||||
bevel.shadow = colors->windowHighlight;
|
||||
bevel.face = bg;
|
||||
bevel.width = 1;
|
||||
drawBevel(d, ops, w->x, boxY, CHECKBOX_BOX_SIZE, CHECKBOX_BOX_SIZE, &bevel);
|
||||
|
||||
// Draw check mark if checked. The check is an X pattern drawn as
|
||||
// two diagonal lines of single pixels. Using 1-pixel drawHLine calls
|
||||
// instead of a real line drawing algorithm avoids Bresenham overhead
|
||||
// for what's always a small fixed-size glyph (6x6 pixels). The 3px
|
||||
// inset from the box edge keeps the mark visually centered.
|
||||
if (w->as.checkbox.checked) {
|
||||
int32_t cx = w->x + 3;
|
||||
int32_t cy = boxY + 3;
|
||||
int32_t cs = CHECKBOX_BOX_SIZE - 6;
|
||||
uint32_t checkFg = w->enabled ? fg : colors->windowShadow;
|
||||
|
||||
for (int32_t i = 0; i < cs; i++) {
|
||||
drawHLine(d, ops, cx + i, cy + i, 1, checkFg);
|
||||
drawHLine(d, ops, cx + cs - 1 - i, cy + i, 1, checkFg);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw label
|
||||
int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP;
|
||||
int32_t labelY = w->y + (w->h - font->charHeight) / 2;
|
||||
int32_t labelW = textWidthAccel(font, w->as.checkbox.text);
|
||||
|
||||
if (!w->enabled) {
|
||||
drawTextAccelEmbossed(d, ops, font, labelX, labelY, w->as.checkbox.text, colors);
|
||||
} else {
|
||||
drawTextAccel(d, ops, font, labelX, labelY, w->as.checkbox.text, fg, bg, false);
|
||||
}
|
||||
|
||||
if (w->focused) {
|
||||
drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,522 +0,0 @@
|
|||
// widgetClass.c — Widget class vtable definitions
|
||||
//
|
||||
// This file implements a C vtable pattern for the widget type system.
|
||||
// Each widget type has a static const WidgetClassT struct that defines
|
||||
// its behavior — paint, layout, mouse handling, keyboard handling, etc.
|
||||
// A master table (widgetClassTable[]) maps WidgetTypeE enum values to
|
||||
// their class definitions, enabling O(1) dispatch.
|
||||
//
|
||||
// Why a vtable approach instead of switch statements:
|
||||
// 1. Adding a new widget type is purely additive — define a new class
|
||||
// struct and add one entry to the table. No existing switch
|
||||
// statements need modification, reducing the risk of forgetting
|
||||
// a case.
|
||||
// 2. NULL function pointers indicate "no behavior" naturally. A box
|
||||
// container has no paint function because the framework paints
|
||||
// its children directly. A label has no onKey handler because
|
||||
// it's not interactive. This is cleaner than empty case blocks.
|
||||
// 3. The flags field combines multiple boolean properties into a
|
||||
// single bitmask, avoiding per-type if-chains in hot paths
|
||||
// like hit testing and layout.
|
||||
//
|
||||
// Class flags:
|
||||
// WCLASS_FOCUSABLE — widget can receive keyboard focus (Tab order)
|
||||
// WCLASS_BOX_CONTAINER — uses the generic box layout engine (VBox/HBox)
|
||||
// WCLASS_HORIZ_CONTAINER — lays out children horizontally (HBox variant)
|
||||
// WCLASS_PAINTS_CHILDREN — widget handles its own child painting (TabControl,
|
||||
// TreeView, ScrollPane, Splitter). The default paint
|
||||
// walker skips children for these widgets.
|
||||
// WCLASS_NO_HIT_RECURSE — hit testing stops at this widget instead of
|
||||
// recursing into children. Used by widgets that
|
||||
// manage their own internal click regions (TreeView,
|
||||
// ScrollPane, ListView, Splitter).
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
// ============================================================
|
||||
// Per-type class definitions
|
||||
// ============================================================
|
||||
//
|
||||
// Containers (VBox, HBox, RadioGroup, TabPage, StatusBar, Toolbar)
|
||||
// typically have NULL for most function pointers because their
|
||||
// behavior is handled generically by the box layout engine and
|
||||
// the default child-walking paint logic.
|
||||
//
|
||||
// Leaf widgets (Button, Label, TextInput, etc.) override paint,
|
||||
// calcMinSize, and usually onMouse/onKey for interactivity.
|
||||
|
||||
static const WidgetClassT sClassVBox = {
|
||||
.flags = WCLASS_BOX_CONTAINER,
|
||||
.paint = NULL,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = NULL,
|
||||
.layout = NULL,
|
||||
.onMouse = NULL,
|
||||
.onKey = NULL,
|
||||
.destroy = NULL,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassHBox = {
|
||||
.flags = WCLASS_BOX_CONTAINER | WCLASS_HORIZ_CONTAINER,
|
||||
.paint = NULL,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = NULL,
|
||||
.layout = NULL,
|
||||
.onMouse = NULL,
|
||||
.onKey = NULL,
|
||||
.destroy = NULL,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassLabel = {
|
||||
.flags = 0,
|
||||
.paint = widgetLabelPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetLabelCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = NULL,
|
||||
.onKey = NULL,
|
||||
.destroy = NULL,
|
||||
.getText = widgetLabelGetText,
|
||||
.setText = widgetLabelSetText
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassButton = {
|
||||
.flags = WCLASS_FOCUSABLE,
|
||||
.paint = widgetButtonPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetButtonCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = widgetButtonOnMouse,
|
||||
.onKey = widgetButtonOnKey,
|
||||
.destroy = NULL,
|
||||
.getText = widgetButtonGetText,
|
||||
.setText = widgetButtonSetText
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassCheckbox = {
|
||||
.flags = WCLASS_FOCUSABLE,
|
||||
.paint = widgetCheckboxPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetCheckboxCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = widgetCheckboxOnMouse,
|
||||
.onKey = widgetCheckboxOnKey,
|
||||
.destroy = NULL,
|
||||
.getText = widgetCheckboxGetText,
|
||||
.setText = widgetCheckboxSetText
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassRadioGroup = {
|
||||
.flags = WCLASS_BOX_CONTAINER,
|
||||
.paint = NULL,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = NULL,
|
||||
.layout = NULL,
|
||||
.onMouse = NULL,
|
||||
.onKey = NULL,
|
||||
.destroy = NULL,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassRadio = {
|
||||
.flags = WCLASS_FOCUSABLE,
|
||||
.paint = widgetRadioPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetRadioCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = widgetRadioOnMouse,
|
||||
.onKey = widgetRadioOnKey,
|
||||
.destroy = NULL,
|
||||
.getText = widgetRadioGetText,
|
||||
.setText = widgetRadioSetText
|
||||
};
|
||||
|
||||
// TextInput and TextArea have destroy functions because they dynamically
|
||||
// allocate their text buffer and undo buffer. Most other widgets store
|
||||
// all data inline in the WidgetT union and need no cleanup.
|
||||
static const WidgetClassT sClassTextInput = {
|
||||
.flags = WCLASS_FOCUSABLE,
|
||||
.paint = widgetTextInputPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetTextInputCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = widgetTextInputOnMouse,
|
||||
.onKey = widgetTextInputOnKey,
|
||||
.destroy = widgetTextInputDestroy,
|
||||
.getText = widgetTextInputGetText,
|
||||
.setText = widgetTextInputSetText
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassTextArea = {
|
||||
.flags = WCLASS_FOCUSABLE,
|
||||
.paint = widgetTextAreaPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetTextAreaCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = widgetTextAreaOnMouse,
|
||||
.onKey = widgetTextAreaOnKey,
|
||||
.destroy = widgetTextAreaDestroy,
|
||||
.getText = widgetTextAreaGetText,
|
||||
.setText = widgetTextAreaSetText
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassListBox = {
|
||||
.flags = WCLASS_FOCUSABLE,
|
||||
.paint = widgetListBoxPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetListBoxCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = widgetListBoxOnMouse,
|
||||
.onKey = widgetListBoxOnKey,
|
||||
.destroy = widgetListBoxDestroy,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassSpacer = {
|
||||
.flags = 0,
|
||||
.paint = NULL,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetSpacerCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = NULL,
|
||||
.onKey = NULL,
|
||||
.destroy = NULL,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassSeparator = {
|
||||
.flags = 0,
|
||||
.paint = widgetSeparatorPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetSeparatorCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = NULL,
|
||||
.onKey = NULL,
|
||||
.destroy = NULL,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassFrame = {
|
||||
.flags = WCLASS_BOX_CONTAINER,
|
||||
.paint = widgetFramePaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = NULL,
|
||||
.layout = NULL,
|
||||
.onMouse = NULL,
|
||||
.onKey = NULL,
|
||||
.destroy = NULL,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
// Dropdown and ComboBox use paintOverlay to draw their popup lists
|
||||
// on top of the entire widget tree. This is rendered in a separate
|
||||
// pass after the main paint, so the popup can overlap sibling widgets.
|
||||
static const WidgetClassT sClassDropdown = {
|
||||
.flags = WCLASS_FOCUSABLE,
|
||||
.paint = widgetDropdownPaint,
|
||||
.paintOverlay = widgetDropdownPaintPopup,
|
||||
.calcMinSize = widgetDropdownCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = widgetDropdownOnMouse,
|
||||
.onKey = widgetDropdownOnKey,
|
||||
.destroy = NULL,
|
||||
.getText = widgetDropdownGetText,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassComboBox = {
|
||||
.flags = WCLASS_FOCUSABLE,
|
||||
.paint = widgetComboBoxPaint,
|
||||
.paintOverlay = widgetComboBoxPaintPopup,
|
||||
.calcMinSize = widgetComboBoxCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = widgetComboBoxOnMouse,
|
||||
.onKey = widgetComboBoxOnKey,
|
||||
.destroy = widgetComboBoxDestroy,
|
||||
.getText = widgetComboBoxGetText,
|
||||
.setText = widgetComboBoxSetText
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassProgressBar = {
|
||||
.flags = 0,
|
||||
.paint = widgetProgressBarPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetProgressBarCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = NULL,
|
||||
.onKey = NULL,
|
||||
.destroy = NULL,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassSlider = {
|
||||
.flags = WCLASS_FOCUSABLE,
|
||||
.paint = widgetSliderPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetSliderCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = widgetSliderOnMouse,
|
||||
.onKey = widgetSliderOnKey,
|
||||
.destroy = NULL,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
// TabControl has both PAINTS_CHILDREN and a custom layout function
|
||||
// because it needs to show only the active tab page's children and
|
||||
// position them inside the tab content area (below the tab strip).
|
||||
static const WidgetClassT sClassTabControl = {
|
||||
.flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN,
|
||||
.paint = widgetTabControlPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetTabControlCalcMinSize,
|
||||
.layout = widgetTabControlLayout,
|
||||
.onMouse = widgetTabControlOnMouse,
|
||||
.onKey = widgetTabControlOnKey,
|
||||
.destroy = NULL,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassTabPage = {
|
||||
.flags = WCLASS_BOX_CONTAINER,
|
||||
.paint = NULL,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = NULL,
|
||||
.layout = NULL,
|
||||
.onMouse = NULL,
|
||||
.onKey = NULL,
|
||||
.destroy = NULL,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassStatusBar = {
|
||||
.flags = WCLASS_BOX_CONTAINER | WCLASS_HORIZ_CONTAINER,
|
||||
.paint = widgetStatusBarPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = NULL,
|
||||
.layout = NULL,
|
||||
.onMouse = NULL,
|
||||
.onKey = NULL,
|
||||
.destroy = NULL,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassToolbar = {
|
||||
.flags = WCLASS_BOX_CONTAINER | WCLASS_HORIZ_CONTAINER,
|
||||
.paint = widgetToolbarPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = NULL,
|
||||
.layout = NULL,
|
||||
.onMouse = NULL,
|
||||
.onKey = NULL,
|
||||
.destroy = NULL,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
// TreeView uses all three special flags:
|
||||
// PAINTS_CHILDREN — it renders tree items itself (with indentation,
|
||||
// expand/collapse buttons, and selection highlighting)
|
||||
// NO_HIT_RECURSE — mouse clicks go to the TreeView widget, which
|
||||
// figures out which tree item was clicked based on scroll position
|
||||
// and item Y coordinates, rather than letting the hit tester
|
||||
// recurse into child TreeItem widgets
|
||||
static const WidgetClassT sClassTreeView = {
|
||||
.flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE,
|
||||
.paint = widgetTreeViewPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetTreeViewCalcMinSize,
|
||||
.layout = widgetTreeViewLayout,
|
||||
.onMouse = widgetTreeViewOnMouse,
|
||||
.onKey = widgetTreeViewOnKey,
|
||||
.destroy = NULL,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
// TreeItem has no paint/mouse/key handlers — it's a data-only node.
|
||||
// The TreeView parent widget handles all rendering and interaction.
|
||||
// TreeItem exists as a WidgetT so it can participate in the tree
|
||||
// structure (parent/child/sibling links) for hierarchical data.
|
||||
static const WidgetClassT sClassTreeItem = {
|
||||
.flags = 0,
|
||||
.paint = NULL,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = NULL,
|
||||
.layout = NULL,
|
||||
.onMouse = NULL,
|
||||
.onKey = NULL,
|
||||
.destroy = NULL,
|
||||
.getText = widgetTreeItemGetText,
|
||||
.setText = widgetTreeItemSetText
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassImage = {
|
||||
.flags = 0,
|
||||
.paint = widgetImagePaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetImageCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = widgetImageOnMouse,
|
||||
.onKey = NULL,
|
||||
.destroy = widgetImageDestroy,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassImageButton = {
|
||||
.flags = WCLASS_FOCUSABLE,
|
||||
.paint = widgetImageButtonPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetImageButtonCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = widgetImageButtonOnMouse,
|
||||
.onKey = widgetImageButtonOnKey,
|
||||
.destroy = widgetImageButtonDestroy,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassCanvas = {
|
||||
.flags = 0,
|
||||
.paint = widgetCanvasPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetCanvasCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = widgetCanvasOnMouse,
|
||||
.onKey = NULL,
|
||||
.destroy = widgetCanvasDestroy,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassListView = {
|
||||
.flags = WCLASS_FOCUSABLE | WCLASS_NO_HIT_RECURSE,
|
||||
.paint = widgetListViewPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetListViewCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = widgetListViewOnMouse,
|
||||
.onKey = widgetListViewOnKey,
|
||||
.destroy = widgetListViewDestroy,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassAnsiTerm = {
|
||||
.flags = WCLASS_FOCUSABLE,
|
||||
.paint = widgetAnsiTermPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetAnsiTermCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = widgetAnsiTermOnMouse,
|
||||
.onKey = widgetAnsiTermOnKey,
|
||||
.destroy = widgetAnsiTermDestroy,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
// ScrollPane, Splitter: PAINTS_CHILDREN because they clip/position
|
||||
// children in a custom way; NO_HIT_RECURSE because they manage their
|
||||
// own scrollbar/divider hit regions.
|
||||
static const WidgetClassT sClassScrollPane = {
|
||||
.flags = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE,
|
||||
.paint = widgetScrollPanePaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetScrollPaneCalcMinSize,
|
||||
.layout = widgetScrollPaneLayout,
|
||||
.onMouse = widgetScrollPaneOnMouse,
|
||||
.onKey = widgetScrollPaneOnKey,
|
||||
.destroy = NULL,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassSplitter = {
|
||||
.flags = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE,
|
||||
.paint = widgetSplitterPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetSplitterCalcMinSize,
|
||||
.layout = widgetSplitterLayout,
|
||||
.onMouse = widgetSplitterOnMouse,
|
||||
.onKey = NULL,
|
||||
.destroy = NULL,
|
||||
.getText = NULL,
|
||||
.setText = NULL
|
||||
};
|
||||
|
||||
static const WidgetClassT sClassSpinner = {
|
||||
.flags = WCLASS_FOCUSABLE,
|
||||
.paint = widgetSpinnerPaint,
|
||||
.paintOverlay = NULL,
|
||||
.calcMinSize = widgetSpinnerCalcMinSize,
|
||||
.layout = NULL,
|
||||
.onMouse = widgetSpinnerOnMouse,
|
||||
.onKey = widgetSpinnerOnKey,
|
||||
.destroy = NULL,
|
||||
.getText = widgetSpinnerGetText,
|
||||
.setText = widgetSpinnerSetText
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Class table — indexed by WidgetTypeE
|
||||
// ============================================================
|
||||
//
|
||||
// This array is the central dispatch table for the widget system.
|
||||
// Indexed by the WidgetTypeE enum, it provides O(1) lookup of any
|
||||
// widget type's class definition. Every WidgetT stores a pointer
|
||||
// to its class (w->wclass) set at allocation time, so per-widget
|
||||
// dispatch doesn't even need to index this table — it's a direct
|
||||
// pointer dereference through the vtable.
|
||||
//
|
||||
// Using C99 designated initializers ensures the array slots match
|
||||
// the enum values even if the enum is reordered. If a new enum
|
||||
// value is added without a table entry, it will be NULL, which
|
||||
// callers check for before dispatching.
|
||||
|
||||
const WidgetClassT *widgetClassTable[] = {
|
||||
[WidgetVBoxE] = &sClassVBox,
|
||||
[WidgetHBoxE] = &sClassHBox,
|
||||
[WidgetLabelE] = &sClassLabel,
|
||||
[WidgetButtonE] = &sClassButton,
|
||||
[WidgetCheckboxE] = &sClassCheckbox,
|
||||
[WidgetRadioGroupE] = &sClassRadioGroup,
|
||||
[WidgetRadioE] = &sClassRadio,
|
||||
[WidgetTextInputE] = &sClassTextInput,
|
||||
[WidgetTextAreaE] = &sClassTextArea,
|
||||
[WidgetListBoxE] = &sClassListBox,
|
||||
[WidgetSpacerE] = &sClassSpacer,
|
||||
[WidgetSeparatorE] = &sClassSeparator,
|
||||
[WidgetFrameE] = &sClassFrame,
|
||||
[WidgetDropdownE] = &sClassDropdown,
|
||||
[WidgetComboBoxE] = &sClassComboBox,
|
||||
[WidgetProgressBarE] = &sClassProgressBar,
|
||||
[WidgetSliderE] = &sClassSlider,
|
||||
[WidgetTabControlE] = &sClassTabControl,
|
||||
[WidgetTabPageE] = &sClassTabPage,
|
||||
[WidgetStatusBarE] = &sClassStatusBar,
|
||||
[WidgetToolbarE] = &sClassToolbar,
|
||||
[WidgetTreeViewE] = &sClassTreeView,
|
||||
[WidgetTreeItemE] = &sClassTreeItem,
|
||||
[WidgetImageE] = &sClassImage,
|
||||
[WidgetImageButtonE] = &sClassImageButton,
|
||||
[WidgetCanvasE] = &sClassCanvas,
|
||||
[WidgetAnsiTermE] = &sClassAnsiTerm,
|
||||
[WidgetListViewE] = &sClassListView,
|
||||
[WidgetSpinnerE] = &sClassSpinner,
|
||||
[WidgetScrollPaneE] = &sClassScrollPane,
|
||||
[WidgetSplitterE] = &sClassSplitter
|
||||
};
|
||||
|
|
@ -1,385 +0,0 @@
|
|||
// widgetComboBox.c — ComboBox widget (editable text + dropdown list)
|
||||
//
|
||||
// Combines a single-line text input with a dropdown list. The text area
|
||||
// supports full editing (cursor movement, selection, undo, clipboard) via
|
||||
// the shared widgetTextEditOnKey helper, while the dropdown button opens
|
||||
// a popup list overlay.
|
||||
//
|
||||
// This is a "combo" box in the Windows sense: the user can either type a
|
||||
// value or select from the list. When an item is selected from the list,
|
||||
// its text is copied into the edit buffer. The edit buffer is independently
|
||||
// allocated (malloc'd) so the user can modify the text after selecting.
|
||||
//
|
||||
// The popup list is painted as an overlay (widgetComboBoxPaintPopup) that
|
||||
// renders on top of all other widgets. Popup visibility is coordinated
|
||||
// through the sOpenPopup global — only one popup can be open at a time.
|
||||
// The sClosedPopup mechanism prevents click-to-close from immediately
|
||||
// reopening the popup when the close click lands on the dropdown button.
|
||||
//
|
||||
// Text selection supports single-click (cursor placement + drag start),
|
||||
// double-click (word select), and triple-click (select all). Drag-select
|
||||
// is tracked via the sDragTextSelect global.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtComboBox
|
||||
// ============================================================
|
||||
|
||||
// Create a combo box. maxLen controls the edit buffer size (0 = default 256).
|
||||
// Two buffers are allocated: the edit buffer and an undo buffer (for Ctrl+Z
|
||||
// single-level undo). Selection indices start at -1 (nothing selected).
|
||||
// Default weight=100 makes combo boxes expand to fill available space in
|
||||
// a layout container, which is the typical desired behavior.
|
||||
WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetComboBoxE);
|
||||
|
||||
if (w) {
|
||||
int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256;
|
||||
w->as.comboBox.buf = (char *)malloc(bufSize);
|
||||
w->as.comboBox.undoBuf = (char *)malloc(bufSize);
|
||||
w->as.comboBox.bufSize = bufSize;
|
||||
|
||||
if (!w->as.comboBox.buf || !w->as.comboBox.undoBuf) {
|
||||
free(w->as.comboBox.buf);
|
||||
free(w->as.comboBox.undoBuf);
|
||||
w->as.comboBox.buf = NULL;
|
||||
w->as.comboBox.undoBuf = NULL;
|
||||
} else {
|
||||
w->as.comboBox.buf[0] = '\0';
|
||||
}
|
||||
|
||||
w->as.comboBox.selStart = -1;
|
||||
w->as.comboBox.selEnd = -1;
|
||||
w->as.comboBox.selectedIdx = -1;
|
||||
w->weight = 100;
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtComboBoxGetSelected
|
||||
// ============================================================
|
||||
|
||||
int32_t wgtComboBoxGetSelected(const WidgetT *w) {
|
||||
VALIDATE_WIDGET(w, WidgetComboBoxE, -1);
|
||||
|
||||
return w->as.comboBox.selectedIdx;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtComboBoxSetItems
|
||||
// ============================================================
|
||||
|
||||
void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetComboBoxE);
|
||||
|
||||
w->as.comboBox.items = items;
|
||||
w->as.comboBox.itemCount = count;
|
||||
|
||||
// Cache max item string length so calcMinSize doesn't need to re-scan
|
||||
// the entire item array on every layout pass. Items are stored as
|
||||
// external pointers (not copied) — the caller owns the string data.
|
||||
int32_t maxLen = 0;
|
||||
|
||||
for (int32_t i = 0; i < count; i++) {
|
||||
int32_t slen = (int32_t)strlen(items[i]);
|
||||
|
||||
if (slen > maxLen) {
|
||||
maxLen = slen;
|
||||
}
|
||||
}
|
||||
|
||||
w->as.comboBox.maxItemLen = maxLen;
|
||||
|
||||
if (w->as.comboBox.selectedIdx >= count) {
|
||||
w->as.comboBox.selectedIdx = -1;
|
||||
}
|
||||
|
||||
// wgtInvalidate (not wgtInvalidatePaint) triggers a full relayout because
|
||||
// changing items may change the widget's minimum width
|
||||
wgtInvalidate(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtComboBoxSetSelected
|
||||
// ============================================================
|
||||
|
||||
void wgtComboBoxSetSelected(WidgetT *w, int32_t idx) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetComboBoxE);
|
||||
|
||||
w->as.comboBox.selectedIdx = idx;
|
||||
|
||||
// Copy selected item text to buffer
|
||||
if (idx >= 0 && idx < w->as.comboBox.itemCount && w->as.comboBox.buf) {
|
||||
strncpy(w->as.comboBox.buf, w->as.comboBox.items[idx], w->as.comboBox.bufSize - 1);
|
||||
w->as.comboBox.buf[w->as.comboBox.bufSize - 1] = '\0';
|
||||
w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf);
|
||||
w->as.comboBox.cursorPos = w->as.comboBox.len;
|
||||
w->as.comboBox.scrollOff = 0;
|
||||
w->as.comboBox.selStart = -1;
|
||||
w->as.comboBox.selEnd = -1;
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetComboBoxCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
void widgetComboBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
int32_t maxItemW = w->as.comboBox.maxItemLen * font->charWidth;
|
||||
int32_t minW = font->charWidth * 8;
|
||||
|
||||
if (maxItemW < minW) {
|
||||
maxItemW = minW;
|
||||
}
|
||||
|
||||
w->calcMinW = maxItemW + DROPDOWN_BTN_WIDTH + TEXT_INPUT_PAD * 2 + 4;
|
||||
w->calcMinH = font->charHeight + TEXT_INPUT_PAD * 2;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetComboBoxDestroy
|
||||
// ============================================================
|
||||
|
||||
void widgetComboBoxDestroy(WidgetT *w) {
|
||||
free(w->as.comboBox.buf);
|
||||
free(w->as.comboBox.undoBuf);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetComboBoxGetText
|
||||
// ============================================================
|
||||
|
||||
const char *widgetComboBoxGetText(const WidgetT *w) {
|
||||
return w->as.comboBox.buf ? w->as.comboBox.buf : "";
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetComboBoxOnKey
|
||||
// ============================================================
|
||||
|
||||
// Key handling has two modes: when the popup is open, Up/Down navigate the list
|
||||
// and Enter confirms the selection. When closed, keys go to the text editor
|
||||
// (via widgetTextEditOnKey) except Down-arrow which opens the popup. This split
|
||||
// behavior is necessary because the same widget must serve as both a text input
|
||||
// and a list selector depending on popup state.
|
||||
// Key codes: 0x48|0x100 = Up, 0x50|0x100 = Down (BIOS scan codes with extended bit).
|
||||
void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||
if (w->as.comboBox.open) {
|
||||
if (key == (0x48 | 0x100)) {
|
||||
if (w->as.comboBox.hoverIdx > 0) {
|
||||
w->as.comboBox.hoverIdx--;
|
||||
|
||||
if (w->as.comboBox.hoverIdx < w->as.comboBox.listScrollPos) {
|
||||
w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx;
|
||||
}
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key == (0x50 | 0x100)) {
|
||||
if (w->as.comboBox.hoverIdx < w->as.comboBox.itemCount - 1) {
|
||||
w->as.comboBox.hoverIdx++;
|
||||
|
||||
if (w->as.comboBox.hoverIdx >= w->as.comboBox.listScrollPos + DROPDOWN_MAX_VISIBLE) {
|
||||
w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
|
||||
}
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key == 0x0D) {
|
||||
int32_t idx = w->as.comboBox.hoverIdx;
|
||||
|
||||
if (idx >= 0 && idx < w->as.comboBox.itemCount) {
|
||||
w->as.comboBox.selectedIdx = idx;
|
||||
|
||||
const char *itemText = w->as.comboBox.items[idx];
|
||||
strncpy(w->as.comboBox.buf, itemText, w->as.comboBox.bufSize - 1);
|
||||
w->as.comboBox.buf[w->as.comboBox.bufSize - 1] = '\0';
|
||||
w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf);
|
||||
w->as.comboBox.cursorPos = w->as.comboBox.len;
|
||||
w->as.comboBox.scrollOff = 0;
|
||||
w->as.comboBox.selStart = -1;
|
||||
w->as.comboBox.selEnd = -1;
|
||||
}
|
||||
|
||||
w->as.comboBox.open = false;
|
||||
sOpenPopup = NULL;
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Down arrow on closed combobox opens the popup
|
||||
if (!w->as.comboBox.open && key == (0x50 | 0x100)) {
|
||||
w->as.comboBox.open = true;
|
||||
w->as.comboBox.hoverIdx = w->as.comboBox.selectedIdx;
|
||||
sOpenPopup = w;
|
||||
|
||||
if (w->as.comboBox.hoverIdx >= w->as.comboBox.listScrollPos + DROPDOWN_MAX_VISIBLE) {
|
||||
w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
|
||||
}
|
||||
|
||||
if (w->as.comboBox.hoverIdx < w->as.comboBox.listScrollPos) {
|
||||
w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx;
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
return;
|
||||
}
|
||||
|
||||
// Text editing (when popup is closed, or non-navigation keys with popup open)
|
||||
if (!w->as.comboBox.buf) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearOtherSelections(w);
|
||||
|
||||
widgetTextEditOnKey(w, key, mod, w->as.comboBox.buf, w->as.comboBox.bufSize,
|
||||
&w->as.comboBox.len, &w->as.comboBox.cursorPos,
|
||||
&w->as.comboBox.scrollOff,
|
||||
&w->as.comboBox.selStart, &w->as.comboBox.selEnd,
|
||||
w->as.comboBox.undoBuf, &w->as.comboBox.undoLen,
|
||||
&w->as.comboBox.undoCursor);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetComboBoxOnMouse
|
||||
// ============================================================
|
||||
|
||||
void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||
w->focused = true;
|
||||
|
||||
// Check if click is on the button area
|
||||
int32_t textAreaW = w->w - DROPDOWN_BTN_WIDTH;
|
||||
|
||||
if (vx >= w->x + textAreaW) {
|
||||
// If this combobox's popup was just closed by click-outside, don't re-open
|
||||
if (w == sClosedPopup) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Button click — toggle popup
|
||||
w->as.comboBox.open = !w->as.comboBox.open;
|
||||
w->as.comboBox.hoverIdx = w->as.comboBox.selectedIdx;
|
||||
sOpenPopup = w->as.comboBox.open ? w : NULL;
|
||||
} else {
|
||||
// Text area click — focus for editing
|
||||
clearOtherSelections(w);
|
||||
|
||||
AppContextT *ctx = (AppContextT *)root->userData;
|
||||
widgetTextEditMouseClick(w, vx, vy, w->x + TEXT_INPUT_PAD, &ctx->font, w->as.comboBox.buf, w->as.comboBox.len, w->as.comboBox.scrollOff, &w->as.comboBox.cursorPos, &w->as.comboBox.selStart, &w->as.comboBox.selEnd, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetComboBoxPaint
|
||||
// ============================================================
|
||||
|
||||
// Paint: two regions side-by-side — a sunken text area (left) and a raised
|
||||
// dropdown button (right). The text area renders the edit buffer with optional
|
||||
// selection highlighting (up to 3 text runs: pre-selection, selection,
|
||||
// post-selection). The dropdown button has a small triangular arrow glyph
|
||||
// drawn as horizontal lines of decreasing width. When the popup is open,
|
||||
// the button bevel is inverted (sunken) to show it's active.
|
||||
void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow;
|
||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||
|
||||
// Sunken text area
|
||||
int32_t textAreaW = w->w - DROPDOWN_BTN_WIDTH;
|
||||
BevelStyleT bevel;
|
||||
bevel.highlight = colors->windowShadow;
|
||||
bevel.shadow = colors->windowHighlight;
|
||||
bevel.face = bg;
|
||||
bevel.width = 2;
|
||||
drawBevel(d, ops, w->x, w->y, textAreaW, w->h, &bevel);
|
||||
|
||||
// Draw text content
|
||||
if (w->as.comboBox.buf) {
|
||||
int32_t textX = w->x + TEXT_INPUT_PAD;
|
||||
int32_t textY = w->y + (w->h - font->charHeight) / 2;
|
||||
int32_t maxChars = (textAreaW - TEXT_INPUT_PAD * 2 - 4) / font->charWidth;
|
||||
int32_t off = w->as.comboBox.scrollOff;
|
||||
int32_t len = w->as.comboBox.len - off;
|
||||
|
||||
if (len > maxChars) {
|
||||
len = maxChars;
|
||||
}
|
||||
|
||||
widgetTextEditPaintLine(d, ops, font, colors, textX, textY, w->as.comboBox.buf + off, len, off, w->as.comboBox.cursorPos, w->as.comboBox.selStart, w->as.comboBox.selEnd, fg, bg, w->focused && w->enabled && !w->as.comboBox.open, w->x + TEXT_INPUT_PAD, w->x + textAreaW - TEXT_INPUT_PAD);
|
||||
}
|
||||
|
||||
// Drop button
|
||||
BevelStyleT btnBevel;
|
||||
btnBevel.highlight = w->as.comboBox.open ? colors->windowShadow : colors->windowHighlight;
|
||||
btnBevel.shadow = w->as.comboBox.open ? colors->windowHighlight : colors->windowShadow;
|
||||
btnBevel.face = colors->buttonFace;
|
||||
btnBevel.width = 2;
|
||||
drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel);
|
||||
|
||||
// Down arrow
|
||||
uint32_t arrowFg = w->enabled ? colors->contentFg : colors->windowShadow;
|
||||
int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2;
|
||||
int32_t arrowY = w->y + w->h / 2 - 1;
|
||||
|
||||
for (int32_t i = 0; i < 4; i++) {
|
||||
drawHLine(d, ops, arrowX - 3 + i, arrowY + i, 7 - i * 2, arrowFg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetComboBoxPaintPopup
|
||||
// ============================================================
|
||||
|
||||
void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
int32_t popX;
|
||||
int32_t popY;
|
||||
int32_t popW;
|
||||
int32_t popH;
|
||||
|
||||
widgetDropdownPopupRect(w, font, d->clipH, &popX, &popY, &popW, &popH);
|
||||
widgetPaintPopupList(d, ops, font, colors, popX, popY, popW, popH, w->as.comboBox.items, w->as.comboBox.itemCount, w->as.comboBox.hoverIdx, w->as.comboBox.listScrollPos);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetComboBoxSetText
|
||||
// ============================================================
|
||||
|
||||
void widgetComboBoxSetText(WidgetT *w, const char *text) {
|
||||
if (w->as.comboBox.buf) {
|
||||
strncpy(w->as.comboBox.buf, text, w->as.comboBox.bufSize - 1);
|
||||
w->as.comboBox.buf[w->as.comboBox.bufSize - 1] = '\0';
|
||||
w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf);
|
||||
w->as.comboBox.cursorPos = w->as.comboBox.len;
|
||||
w->as.comboBox.scrollOff = 0;
|
||||
w->as.comboBox.selStart = -1;
|
||||
w->as.comboBox.selEnd = -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,708 +0,0 @@
|
|||
// widgetCore.c — Core widget infrastructure (alloc, tree ops, helpers)
|
||||
//
|
||||
// This file provides the foundation for the widget tree: allocation,
|
||||
// parent-child linking, focus management, hit testing, and shared
|
||||
// utility functions used across multiple widget types.
|
||||
//
|
||||
// Widgets form a tree using intrusive linked lists (firstChild/lastChild/
|
||||
// nextSibling pointers inside each WidgetT). This is a singly-linked
|
||||
// child list with a tail pointer for O(1) append. The tree is owned
|
||||
// by its root, which is attached to a WindowT. Destroying the root
|
||||
// recursively destroys all descendants.
|
||||
//
|
||||
// Memory allocation is plain malloc/free rather than an arena or pool.
|
||||
// The widget count per window is typically small (tens to low hundreds),
|
||||
// so the allocation overhead is negligible on target hardware. An arena
|
||||
// approach was considered but rejected because widgets can be individually
|
||||
// created and destroyed at runtime (dialog dynamics, tree item insertion),
|
||||
// which doesn't map cleanly to an arena pattern.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
// ============================================================
|
||||
// Global state for drag and popup tracking
|
||||
// ============================================================
|
||||
//
|
||||
// These module-level pointers track ongoing UI interactions that span
|
||||
// multiple mouse events (drags, popups, button presses). They are global
|
||||
// rather than per-window because the DOS GUI is single-threaded and only
|
||||
// one interaction can be active at a time.
|
||||
//
|
||||
// Each pointer is set when an interaction begins (e.g. mouse-down on a
|
||||
// slider) and cleared when it ends (mouse-up). The event dispatcher in
|
||||
// widgetEvent.c checks these before normal hit testing — active drags
|
||||
// take priority over everything else.
|
||||
//
|
||||
// All of these must be NULLed when the pointed-to widget is destroyed,
|
||||
// otherwise dangling pointers would cause crashes. widgetDestroyChildren()
|
||||
// and wgtDestroy() handle this cleanup.
|
||||
|
||||
clock_t sDblClickTicks = 0; // set from ctx->dblClickTicks during first paint
|
||||
bool sDebugLayout = false;
|
||||
WidgetT *sFocusedWidget = NULL; // currently focused widget (O(1) access, avoids tree walk)
|
||||
WidgetT *sOpenPopup = NULL; // dropdown/combobox with open popup list
|
||||
WidgetT *sPressedButton = NULL; // button being held down (tracks mouse in/out)
|
||||
WidgetT *sDragSlider = NULL; // slider being dragged
|
||||
WidgetT *sDrawingCanvas = NULL; // canvas receiving paint strokes
|
||||
WidgetT *sDragTextSelect = NULL; // text widget in drag-select mode
|
||||
int32_t sDragOffset = 0; // pixel offset from drag start to thumb center
|
||||
WidgetT *sResizeListView = NULL; // ListView undergoing column resize
|
||||
int32_t sResizeCol = -1; // which column is being resized
|
||||
int32_t sResizeStartX = 0; // mouse X at resize start
|
||||
int32_t sResizeOrigW = 0; // column width at resize start
|
||||
bool sResizeDragging = false; // true once mouse moves during column resize
|
||||
WidgetT *sDragSplitter = NULL; // splitter being dragged
|
||||
int32_t sDragSplitStart = 0; // mouse offset from splitter edge at drag start
|
||||
WidgetT *sDragReorder = NULL; // list/tree widget in drag-reorder mode
|
||||
WidgetT *sDragScrollbar = NULL; // widget whose scrollbar thumb is being dragged
|
||||
int32_t sDragScrollbarOff = 0; // mouse offset within thumb at drag start
|
||||
int32_t sDragScrollbarOrient = 0; // 0=vertical, 1=horizontal
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetAddChild
|
||||
// ============================================================
|
||||
//
|
||||
// Appends a child to the end of the parent's child list. O(1)
|
||||
// thanks to the lastChild tail pointer. The child list is singly-
|
||||
// linked (nextSibling), which saves 4 bytes per widget vs doubly-
|
||||
// linked and is sufficient because child removal is infrequent.
|
||||
|
||||
void widgetAddChild(WidgetT *parent, WidgetT *child) {
|
||||
child->parent = parent;
|
||||
child->nextSibling = NULL;
|
||||
|
||||
if (parent->lastChild) {
|
||||
parent->lastChild->nextSibling = child;
|
||||
parent->lastChild = child;
|
||||
} else {
|
||||
parent->firstChild = child;
|
||||
parent->lastChild = child;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetAlloc
|
||||
// ============================================================
|
||||
//
|
||||
// Allocates and zero-initializes a new widget, links it to its
|
||||
// class vtable via widgetClassTable[], and optionally adds it as
|
||||
// a child of the given parent.
|
||||
//
|
||||
// The memset to 0 is intentional — it establishes sane defaults
|
||||
// for all fields: NULL pointers, zero coordinates, no focus,
|
||||
// no accel key, etc. Only visible and enabled default to true.
|
||||
//
|
||||
// The window pointer is inherited from the parent so that any
|
||||
// widget in the tree can find its owning window without walking
|
||||
// to the root.
|
||||
|
||||
WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type) {
|
||||
WidgetT *w = (WidgetT *)malloc(sizeof(WidgetT));
|
||||
|
||||
if (!w) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memset(w, 0, sizeof(*w));
|
||||
w->type = type;
|
||||
w->wclass = widgetClassTable[type];
|
||||
w->visible = true;
|
||||
w->enabled = true;
|
||||
|
||||
if (parent) {
|
||||
w->window = parent->window;
|
||||
widgetAddChild(parent, w);
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetClearFocus
|
||||
// ============================================================
|
||||
|
||||
void widgetClearFocus(WidgetT *root) {
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
root->focused = false;
|
||||
|
||||
for (WidgetT *c = root->firstChild; c; c = c->nextSibling) {
|
||||
widgetClearFocus(c);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetCountVisibleChildren
|
||||
// ============================================================
|
||||
|
||||
int32_t widgetCountVisibleChildren(const WidgetT *w) {
|
||||
int32_t count = 0;
|
||||
|
||||
for (const WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (c->visible) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetDestroyChildren
|
||||
// ============================================================
|
||||
//
|
||||
// Recursively destroys all descendants of a widget. Processes
|
||||
// children depth-first (destroy grandchildren before the child
|
||||
// itself) so that per-widget destroy callbacks see a consistent
|
||||
// tree state.
|
||||
//
|
||||
// Critically, this function clears all global state pointers that
|
||||
// reference destroyed widgets. Without this, any pending drag or
|
||||
// focus state would become a dangling pointer. Each global is
|
||||
// checked individually rather than cleared unconditionally to
|
||||
// avoid disrupting unrelated ongoing interactions.
|
||||
|
||||
void widgetDestroyChildren(WidgetT *w) {
|
||||
WidgetT *child = w->firstChild;
|
||||
|
||||
while (child) {
|
||||
WidgetT *next = child->nextSibling;
|
||||
widgetDestroyChildren(child);
|
||||
|
||||
if (child->wclass && child->wclass->destroy) {
|
||||
child->wclass->destroy(child);
|
||||
}
|
||||
|
||||
// Clear static references if they point to destroyed widgets
|
||||
if (sFocusedWidget == child) {
|
||||
sFocusedWidget = NULL;
|
||||
}
|
||||
|
||||
if (sOpenPopup == child) {
|
||||
sOpenPopup = NULL;
|
||||
}
|
||||
|
||||
if (sPressedButton == child) {
|
||||
sPressedButton = NULL;
|
||||
}
|
||||
|
||||
if (sDragSlider == child) {
|
||||
sDragSlider = NULL;
|
||||
}
|
||||
|
||||
if (sDrawingCanvas == child) {
|
||||
sDrawingCanvas = NULL;
|
||||
}
|
||||
|
||||
if (sResizeListView == child) {
|
||||
sResizeListView = NULL;
|
||||
sResizeCol = -1;
|
||||
sResizeDragging = false;
|
||||
}
|
||||
|
||||
if (sDragScrollbar == child) {
|
||||
sDragScrollbar = NULL;
|
||||
}
|
||||
|
||||
free(child);
|
||||
child = next;
|
||||
}
|
||||
|
||||
w->firstChild = NULL;
|
||||
w->lastChild = NULL;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetDropdownPopupRect
|
||||
// ============================================================
|
||||
//
|
||||
// Calculates the screen rectangle for a dropdown/combobox popup list.
|
||||
// Shared between Dropdown and ComboBox since they have identical
|
||||
// popup positioning logic.
|
||||
//
|
||||
// The popup tries to open below the widget first. If there isn't
|
||||
// enough room (popup would extend past the content area bottom),
|
||||
// it flips to open above instead. This ensures the popup is always
|
||||
// visible, even for dropdowns near the bottom of a window.
|
||||
//
|
||||
// Popup height is capped at DROPDOWN_MAX_VISIBLE items to prevent
|
||||
// huge popups from dominating the screen.
|
||||
|
||||
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) {
|
||||
int32_t itemCount = 0;
|
||||
|
||||
if (w->type == WidgetDropdownE) {
|
||||
itemCount = w->as.dropdown.itemCount;
|
||||
} else if (w->type == WidgetComboBoxE) {
|
||||
itemCount = w->as.comboBox.itemCount;
|
||||
}
|
||||
|
||||
int32_t visibleItems = itemCount;
|
||||
|
||||
if (visibleItems > DROPDOWN_MAX_VISIBLE) {
|
||||
visibleItems = DROPDOWN_MAX_VISIBLE;
|
||||
}
|
||||
|
||||
if (visibleItems < 1) {
|
||||
visibleItems = 1;
|
||||
}
|
||||
|
||||
*popX = w->x;
|
||||
*popW = w->w;
|
||||
*popH = visibleItems * font->charHeight + 4; // 2px border each side
|
||||
|
||||
// Try below first, then above if no room
|
||||
if (w->y + w->h + *popH <= contentH) {
|
||||
*popY = w->y + w->h;
|
||||
} else {
|
||||
*popY = w->y - *popH;
|
||||
|
||||
if (*popY < 0) {
|
||||
*popY = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetFindByAccel
|
||||
// ============================================================
|
||||
//
|
||||
// Finds a widget with the given Alt+key accelerator. Recurses the
|
||||
// tree depth-first, respecting visibility and enabled state.
|
||||
//
|
||||
// Special case for TabPage widgets: even if the tab page itself is
|
||||
// not visible (inactive tab), its accelKey is still checked. This
|
||||
// allows Alt+key to switch to a different tab. However, children
|
||||
// of invisible tab pages are NOT searched — their accelerators
|
||||
// should not be active when the tab is hidden.
|
||||
|
||||
WidgetT *widgetFindByAccel(WidgetT *root, char key) {
|
||||
if (!root || !root->enabled) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Invisible tab pages: match the page itself (for tab switching)
|
||||
// but don't recurse into children (their accels shouldn't be active)
|
||||
if (!root->visible) {
|
||||
if (root->type == WidgetTabPageE && root->accelKey == key) {
|
||||
return root;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (root->accelKey == key) {
|
||||
return root;
|
||||
}
|
||||
|
||||
for (WidgetT *c = root->firstChild; c; c = c->nextSibling) {
|
||||
WidgetT *found = widgetFindByAccel(c, key);
|
||||
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetFindNextFocusable
|
||||
// ============================================================
|
||||
//
|
||||
// Implements Tab-order navigation: finds the next focusable widget
|
||||
// after 'after' in depth-first tree order. The two-pass approach
|
||||
// (search from 'after' to end, then wrap to start) ensures circular
|
||||
// tabbing — Tab on the last focusable widget wraps to the first.
|
||||
//
|
||||
// The pastAfter flag tracks whether we've passed the 'after' widget
|
||||
// during traversal. Once past it, the next focusable widget is the
|
||||
// answer. This avoids collecting all focusable widgets into an array
|
||||
// just to find the next one — the common case returns quickly.
|
||||
|
||||
static WidgetT *findNextFocusableImpl(WidgetT *w, WidgetT *after, bool *pastAfter) {
|
||||
if (!w->visible || !w->enabled) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (after == NULL) {
|
||||
*pastAfter = true;
|
||||
}
|
||||
|
||||
if (w == after) {
|
||||
*pastAfter = true;
|
||||
} else if (*pastAfter && widgetIsFocusable(w->type)) {
|
||||
return w;
|
||||
}
|
||||
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
WidgetT *found = findNextFocusableImpl(c, after, pastAfter);
|
||||
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after) {
|
||||
bool pastAfter = false;
|
||||
WidgetT *found = findNextFocusableImpl(root, after, &pastAfter);
|
||||
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
|
||||
// Wrap around — search from the beginning
|
||||
pastAfter = true;
|
||||
return findNextFocusableImpl(root, NULL, &pastAfter);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetFindPrevFocusable
|
||||
// ============================================================
|
||||
//
|
||||
// Shift+Tab navigation: finds the previous focusable widget.
|
||||
// Unlike findNextFocusable which can short-circuit during traversal,
|
||||
// finding the PREVIOUS widget requires knowing the full order.
|
||||
// So this collects all focusable widgets into an array, finds the
|
||||
// target's index, and returns index-1 (with wraparound).
|
||||
//
|
||||
// The explicit stack-based DFS (rather than recursion) is used here
|
||||
// because we need to push children in reverse order to get the same
|
||||
// left-to-right depth-first ordering as the recursive version.
|
||||
// Fixed-size arrays (128 widgets, 64 stack depth) are adequate for
|
||||
// any reasonable dialog layout and avoid dynamic allocation.
|
||||
|
||||
WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) {
|
||||
WidgetT *list[128];
|
||||
int32_t count = 0;
|
||||
|
||||
// Collect all focusable widgets via depth-first traversal
|
||||
WidgetT *stack[64];
|
||||
int32_t top = 0;
|
||||
stack[top++] = root;
|
||||
|
||||
while (top > 0) {
|
||||
WidgetT *w = stack[--top];
|
||||
|
||||
if (!w->visible || !w->enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (widgetIsFocusable(w->type) && count < 128) {
|
||||
list[count++] = w;
|
||||
}
|
||||
|
||||
// Push children in reverse order so first child is processed first
|
||||
WidgetT *children[64];
|
||||
int32_t childCount = 0;
|
||||
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (childCount < 64) {
|
||||
children[childCount++] = c;
|
||||
}
|
||||
}
|
||||
|
||||
for (int32_t i = childCount - 1; i >= 0; i--) {
|
||||
if (top < 64) {
|
||||
stack[top++] = children[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Find 'before' in the list
|
||||
int32_t idx = -1;
|
||||
|
||||
for (int32_t i = 0; i < count; i++) {
|
||||
if (list[i] == before) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (idx <= 0) {
|
||||
return list[count - 1]; // Wrap to last
|
||||
}
|
||||
|
||||
return list[idx - 1];
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetFrameBorderWidth
|
||||
// ============================================================
|
||||
|
||||
int32_t widgetFrameBorderWidth(const WidgetT *w) {
|
||||
if (w->type != WidgetFrameE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (w->as.frame.style == FrameFlatE) {
|
||||
return FRAME_FLAT_BORDER;
|
||||
}
|
||||
|
||||
return FRAME_BEVEL_BORDER;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetHitTest
|
||||
// ============================================================
|
||||
//
|
||||
// Recursive hit testing: finds the deepest (most specific) widget
|
||||
// under the given coordinates. Returns the widget itself if no
|
||||
// child is hit, or NULL if the point is outside this widget.
|
||||
//
|
||||
// Children are iterated front-to-back (first to last in the linked
|
||||
// list), but the LAST match wins. This gives later siblings higher
|
||||
// Z-order, which matches the painting order (later children paint
|
||||
// on top of earlier ones). This is important for overlapping widgets,
|
||||
// though in practice the layout engine rarely produces overlap.
|
||||
//
|
||||
// Widgets with WCLASS_NO_HIT_RECURSE stop the recursion — the parent
|
||||
// widget handles all mouse events for its children. This is used by
|
||||
// TreeView, ScrollPane, ListView, and Splitter, which need to manage
|
||||
// their own internal regions (scrollbars, column headers, tree
|
||||
// expand buttons) that don't correspond to child widgets.
|
||||
|
||||
WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y) {
|
||||
if (!w->visible) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Widgets with WCLASS_NO_HIT_RECURSE manage their own children
|
||||
if (w->wclass && (w->wclass->flags & WCLASS_NO_HIT_RECURSE)) {
|
||||
return w;
|
||||
}
|
||||
|
||||
// Check children — take the last match (topmost in Z-order)
|
||||
WidgetT *hit = NULL;
|
||||
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
WidgetT *childHit = widgetHitTest(c, x, y);
|
||||
|
||||
if (childHit) {
|
||||
hit = childHit;
|
||||
}
|
||||
}
|
||||
|
||||
return hit ? hit : w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetIsFocusable
|
||||
// ============================================================
|
||||
|
||||
bool widgetIsFocusable(WidgetTypeE type) {
|
||||
return (widgetClassTable[type]->flags & WCLASS_FOCUSABLE) != 0;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetIsBoxContainer
|
||||
// ============================================================
|
||||
//
|
||||
// Returns true for widget types that use the generic box layout.
|
||||
|
||||
bool widgetIsBoxContainer(WidgetTypeE type) {
|
||||
return (widgetClassTable[type]->flags & WCLASS_BOX_CONTAINER) != 0;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetIsHorizContainer
|
||||
// ============================================================
|
||||
//
|
||||
// Returns true for container types that lay out children horizontally.
|
||||
|
||||
bool widgetIsHorizContainer(WidgetTypeE type) {
|
||||
return (widgetClassTable[type]->flags & WCLASS_HORIZ_CONTAINER) != 0;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetNavigateIndex
|
||||
// ============================================================
|
||||
//
|
||||
// Shared keyboard navigation for list-like widgets (ListBox, Dropdown,
|
||||
// ListView, etc.). Encapsulates the Up/Down/Home/End/PgUp/PgDn logic
|
||||
// so each widget doesn't have to reimplement index clamping.
|
||||
//
|
||||
// Key values use the 0x100 flag to mark extended scan codes (arrow
|
||||
// keys, Home, End, etc.) — this is the DVX convention for passing
|
||||
// scan codes through the same int32_t channel as ASCII values.
|
||||
//
|
||||
// Returns -1 for unrecognized keys so callers can check whether the
|
||||
// key was consumed.
|
||||
|
||||
int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize) {
|
||||
if (key == (0x50 | 0x100)) {
|
||||
// Down arrow
|
||||
if (current < count - 1) {
|
||||
return current + 1;
|
||||
}
|
||||
|
||||
return current < 0 ? 0 : current;
|
||||
}
|
||||
|
||||
if (key == (0x48 | 0x100)) {
|
||||
// Up arrow
|
||||
if (current > 0) {
|
||||
return current - 1;
|
||||
}
|
||||
|
||||
return current < 0 ? 0 : current;
|
||||
}
|
||||
|
||||
if (key == (0x47 | 0x100)) {
|
||||
// Home
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (key == (0x4F | 0x100)) {
|
||||
// End
|
||||
return count - 1;
|
||||
}
|
||||
|
||||
if (key == (0x51 | 0x100)) {
|
||||
// Page Down
|
||||
int32_t n = current + pageSize;
|
||||
return n >= count ? count - 1 : n;
|
||||
}
|
||||
|
||||
if (key == (0x49 | 0x100)) {
|
||||
// Page Up
|
||||
int32_t n = current - pageSize;
|
||||
return n < 0 ? 0 : n;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetPaintPopupList
|
||||
// ============================================================
|
||||
//
|
||||
// Shared popup list painting for Dropdown and ComboBox.
|
||||
|
||||
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos) {
|
||||
// Draw popup border
|
||||
BevelStyleT bevel;
|
||||
bevel.highlight = colors->windowHighlight;
|
||||
bevel.shadow = colors->windowShadow;
|
||||
bevel.face = colors->contentBg;
|
||||
bevel.width = 2;
|
||||
drawBevel(d, ops, popX, popY, popW, popH, &bevel);
|
||||
|
||||
// Draw items
|
||||
int32_t visibleItems = popH / font->charHeight;
|
||||
int32_t textX = popX + TEXT_INPUT_PAD;
|
||||
int32_t textY = popY + 2;
|
||||
int32_t textW = popW - TEXT_INPUT_PAD * 2 - 4;
|
||||
|
||||
for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) {
|
||||
int32_t idx = scrollPos + i;
|
||||
int32_t iy = textY + i * font->charHeight;
|
||||
uint32_t ifg = colors->contentFg;
|
||||
uint32_t ibg = colors->contentBg;
|
||||
|
||||
if (idx == hoverIdx) {
|
||||
ifg = colors->menuHighlightFg;
|
||||
ibg = colors->menuHighlightBg;
|
||||
rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg);
|
||||
}
|
||||
|
||||
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetScrollbarThumb
|
||||
// ============================================================
|
||||
//
|
||||
// Calculates thumb position and size for a scrollbar track.
|
||||
// Used by both the WM-level scrollbars and widget-internal scrollbars
|
||||
// (ListBox, TreeView, etc.) to maintain consistent scrollbar behavior.
|
||||
//
|
||||
// The thumb size is proportional to visibleSize/totalSize — a larger
|
||||
// visible area means a larger thumb, giving visual feedback about how
|
||||
// much content is scrollable. SB_MIN_THUMB prevents the thumb from
|
||||
// becoming too small to grab with a mouse.
|
||||
|
||||
void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize) {
|
||||
*thumbSize = (trackLen * visibleSize) / totalSize;
|
||||
|
||||
if (*thumbSize < SB_MIN_THUMB) {
|
||||
*thumbSize = SB_MIN_THUMB;
|
||||
}
|
||||
|
||||
if (*thumbSize > trackLen) {
|
||||
*thumbSize = trackLen;
|
||||
}
|
||||
|
||||
int32_t maxScroll = totalSize - visibleSize;
|
||||
|
||||
if (maxScroll > 0) {
|
||||
*thumbPos = ((trackLen - *thumbSize) * scrollPos) / maxScroll;
|
||||
} else {
|
||||
*thumbPos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetRemoveChild
|
||||
// ============================================================
|
||||
//
|
||||
// Unlinks a child from its parent's child list. O(n) in the number
|
||||
// of children because the singly-linked list requires walking to
|
||||
// find the predecessor. This is acceptable because child removal
|
||||
// is infrequent (widget destruction, tree item reordering).
|
||||
|
||||
void widgetRemoveChild(WidgetT *parent, WidgetT *child) {
|
||||
WidgetT *prev = NULL;
|
||||
|
||||
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
|
||||
if (c == child) {
|
||||
if (prev) {
|
||||
prev->nextSibling = c->nextSibling;
|
||||
} else {
|
||||
parent->firstChild = c->nextSibling;
|
||||
}
|
||||
|
||||
if (parent->lastChild == child) {
|
||||
parent->lastChild = prev;
|
||||
}
|
||||
|
||||
child->nextSibling = NULL;
|
||||
child->parent = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
prev = c;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,291 +0,0 @@
|
|||
// widgetDropdown.c — Dropdown (select) widget
|
||||
//
|
||||
// A non-editable dropdown list (HTML <select> equivalent). Unlike ComboBox,
|
||||
// the user cannot type a custom value — they can only choose from the
|
||||
// predefined items. This simplifies the widget considerably: no text buffer,
|
||||
// no undo, no cursor, no text editing key handling.
|
||||
//
|
||||
// The visual layout is identical to ComboBox (sunken display area + raised
|
||||
// dropdown button), but the display area shows the selected item's text
|
||||
// as read-only. A focus rect is drawn inside the display area when focused
|
||||
// (since there's no cursor to indicate focus).
|
||||
//
|
||||
// Keyboard behavior when closed: Up arrow decrements selection immediately
|
||||
// (inline cycling), Down/Space/Enter opens the popup. This matches the
|
||||
// Win3.1 dropdown behavior where arrow keys cycle through values without
|
||||
// opening the full list.
|
||||
//
|
||||
// Popup overlay painting and hit testing are shared with ComboBox via
|
||||
// widgetDropdownPopupRect and widgetPaintPopupList in widgetCore.c.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtDropdown
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtDropdown(WidgetT *parent) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetDropdownE);
|
||||
|
||||
if (w) {
|
||||
w->as.dropdown.selectedIdx = -1;
|
||||
w->as.dropdown.hoverIdx = -1;
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtDropdownGetSelected
|
||||
// ============================================================
|
||||
|
||||
int32_t wgtDropdownGetSelected(const WidgetT *w) {
|
||||
VALIDATE_WIDGET(w, WidgetDropdownE, -1);
|
||||
|
||||
return w->as.dropdown.selectedIdx;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtDropdownSetItems
|
||||
// ============================================================
|
||||
|
||||
void wgtDropdownSetItems(WidgetT *w, const char **items, int32_t count) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetDropdownE);
|
||||
|
||||
w->as.dropdown.items = items;
|
||||
w->as.dropdown.itemCount = count;
|
||||
|
||||
// Cache max item strlen to avoid recomputing in calcMinSize
|
||||
int32_t maxLen = 0;
|
||||
|
||||
for (int32_t i = 0; i < count; i++) {
|
||||
int32_t slen = (int32_t)strlen(items[i]);
|
||||
|
||||
if (slen > maxLen) {
|
||||
maxLen = slen;
|
||||
}
|
||||
}
|
||||
|
||||
w->as.dropdown.maxItemLen = maxLen;
|
||||
|
||||
if (w->as.dropdown.selectedIdx >= count) {
|
||||
w->as.dropdown.selectedIdx = -1;
|
||||
}
|
||||
|
||||
wgtInvalidate(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtDropdownSetSelected
|
||||
// ============================================================
|
||||
|
||||
void wgtDropdownSetSelected(WidgetT *w, int32_t idx) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetDropdownE);
|
||||
|
||||
w->as.dropdown.selectedIdx = idx;
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetDropdownGetText
|
||||
// ============================================================
|
||||
|
||||
const char *widgetDropdownGetText(const WidgetT *w) {
|
||||
if (w->as.dropdown.selectedIdx >= 0 && w->as.dropdown.selectedIdx < w->as.dropdown.itemCount) {
|
||||
return w->as.dropdown.items[w->as.dropdown.selectedIdx];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetDropdownCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
// Width: widest item + button width + border
|
||||
int32_t maxItemW = w->as.dropdown.maxItemLen * font->charWidth;
|
||||
int32_t minW = font->charWidth * 8;
|
||||
|
||||
if (maxItemW < minW) {
|
||||
maxItemW = minW;
|
||||
}
|
||||
|
||||
w->calcMinW = maxItemW + DROPDOWN_BTN_WIDTH + TEXT_INPUT_PAD * 2 + 4;
|
||||
w->calcMinH = font->charHeight + TEXT_INPUT_PAD * 2;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetDropdownOnKey
|
||||
// ============================================================
|
||||
|
||||
// Key handling: when popup is open, Up/Down navigate the hover index and
|
||||
// Enter/Space confirms selection. When closed, Down/Space/Enter opens the
|
||||
// popup, while Up decrements the selection inline (without opening). This
|
||||
// two-mode behavior matches the standard Windows combo box UX where quick
|
||||
// arrow-key cycling doesn't require the popup to be visible.
|
||||
void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||
(void)mod;
|
||||
|
||||
if (w->as.dropdown.open) {
|
||||
// Popup is open — navigate items
|
||||
if (key == (0x48 | 0x100)) {
|
||||
if (w->as.dropdown.hoverIdx > 0) {
|
||||
w->as.dropdown.hoverIdx--;
|
||||
|
||||
if (w->as.dropdown.hoverIdx < w->as.dropdown.scrollPos) {
|
||||
w->as.dropdown.scrollPos = w->as.dropdown.hoverIdx;
|
||||
}
|
||||
}
|
||||
} else if (key == (0x50 | 0x100)) {
|
||||
if (w->as.dropdown.hoverIdx < w->as.dropdown.itemCount - 1) {
|
||||
w->as.dropdown.hoverIdx++;
|
||||
|
||||
if (w->as.dropdown.hoverIdx >= w->as.dropdown.scrollPos + DROPDOWN_MAX_VISIBLE) {
|
||||
w->as.dropdown.scrollPos = w->as.dropdown.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
|
||||
}
|
||||
}
|
||||
} else if (key == 0x0D || key == ' ') {
|
||||
w->as.dropdown.selectedIdx = w->as.dropdown.hoverIdx;
|
||||
w->as.dropdown.open = false;
|
||||
sOpenPopup = NULL;
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Popup is closed
|
||||
if (key == (0x50 | 0x100) || key == ' ' || key == 0x0D) {
|
||||
w->as.dropdown.open = true;
|
||||
w->as.dropdown.hoverIdx = w->as.dropdown.selectedIdx;
|
||||
sOpenPopup = w;
|
||||
|
||||
if (w->as.dropdown.hoverIdx >= w->as.dropdown.scrollPos + DROPDOWN_MAX_VISIBLE) {
|
||||
w->as.dropdown.scrollPos = w->as.dropdown.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
|
||||
}
|
||||
|
||||
if (w->as.dropdown.hoverIdx < w->as.dropdown.scrollPos) {
|
||||
w->as.dropdown.scrollPos = w->as.dropdown.hoverIdx;
|
||||
}
|
||||
} else if (key == (0x48 | 0x100)) {
|
||||
if (w->as.dropdown.selectedIdx > 0) {
|
||||
w->as.dropdown.selectedIdx--;
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetDropdownOnMouse
|
||||
// ============================================================
|
||||
|
||||
// Mouse click toggles the popup open/closed. The sClosedPopup guard prevents
|
||||
// a re-open race condition: when a click outside the popup closes it, the
|
||||
// widget event dispatcher first closes the popup (setting sClosedPopup),
|
||||
// then dispatches the click to whatever widget is under the cursor. If that
|
||||
// happens to be this dropdown's body, we'd immediately re-open without this
|
||||
// check. The sClosedPopup reference is cleared on the next event cycle.
|
||||
void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||
(void)root;
|
||||
(void)vx;
|
||||
(void)vy;
|
||||
w->focused = true;
|
||||
|
||||
// If this dropdown's popup was just closed by click-outside, don't re-open
|
||||
if (w == sClosedPopup) {
|
||||
return;
|
||||
}
|
||||
|
||||
w->as.dropdown.open = !w->as.dropdown.open;
|
||||
w->as.dropdown.hoverIdx = w->as.dropdown.selectedIdx;
|
||||
sOpenPopup = w->as.dropdown.open ? w : NULL;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetDropdownPaint
|
||||
// ============================================================
|
||||
|
||||
void widgetDropdownPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||
|
||||
// Sunken text area
|
||||
int32_t textAreaW = w->w - DROPDOWN_BTN_WIDTH;
|
||||
BevelStyleT bevel;
|
||||
bevel.highlight = colors->windowShadow;
|
||||
bevel.shadow = colors->windowHighlight;
|
||||
bevel.face = bg;
|
||||
bevel.width = 2;
|
||||
drawBevel(d, ops, w->x, w->y, textAreaW, w->h, &bevel);
|
||||
|
||||
// Draw selected item text
|
||||
if (w->as.dropdown.selectedIdx >= 0 && w->as.dropdown.selectedIdx < w->as.dropdown.itemCount) {
|
||||
int32_t textX = w->x + TEXT_INPUT_PAD;
|
||||
int32_t textY = w->y + (w->h - font->charHeight) / 2;
|
||||
|
||||
if (!w->enabled) {
|
||||
drawTextEmbossed(d, ops, font, textX, textY, w->as.dropdown.items[w->as.dropdown.selectedIdx], colors);
|
||||
} else {
|
||||
drawText(d, ops, font, textX, textY, w->as.dropdown.items[w->as.dropdown.selectedIdx], fg, bg, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Drop button
|
||||
BevelStyleT btnBevel;
|
||||
btnBevel.highlight = w->as.dropdown.open ? colors->windowShadow : colors->windowHighlight;
|
||||
btnBevel.shadow = w->as.dropdown.open ? colors->windowHighlight : colors->windowShadow;
|
||||
btnBevel.face = colors->buttonFace;
|
||||
btnBevel.width = 2;
|
||||
drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel);
|
||||
|
||||
// Down arrow glyph in button — a small filled triangle drawn as horizontal
|
||||
// lines of decreasing width (7, 5, 3, 1 pixels). This creates a 4-pixel
|
||||
// tall downward-pointing triangle centered in the button.
|
||||
uint32_t arrowFg = w->enabled ? colors->contentFg : colors->windowShadow;
|
||||
int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2;
|
||||
int32_t arrowY = w->y + w->h / 2 - 1;
|
||||
|
||||
for (int32_t i = 0; i < 4; i++) {
|
||||
drawHLine(d, ops, arrowX - 3 + i, arrowY + i, 7 - i * 2, arrowFg);
|
||||
}
|
||||
|
||||
if (w->focused) {
|
||||
drawFocusRect(d, ops, w->x + 3, w->y + 3, textAreaW - 6, w->h - 6, fg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetDropdownPaintPopup
|
||||
// ============================================================
|
||||
|
||||
// Paint the popup overlay list. Delegates to widgetDropdownPopupRect (which
|
||||
// computes position, handling screen-edge flip to ensure the popup stays
|
||||
// on-screen) and widgetPaintPopupList (which renders the bordered, scrollable
|
||||
// item list with hover highlighting). These shared helpers are also used by
|
||||
// ComboBox, keeping the popup rendering consistent between both widgets.
|
||||
void widgetDropdownPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
int32_t popX;
|
||||
int32_t popY;
|
||||
int32_t popW;
|
||||
int32_t popH;
|
||||
|
||||
widgetDropdownPopupRect(w, font, d->clipH, &popX, &popY, &popW, &popH);
|
||||
widgetPaintPopupList(d, ops, font, colors, popX, popY, popW, popH, w->as.dropdown.items, w->as.dropdown.itemCount, w->as.dropdown.hoverIdx, w->as.dropdown.scrollPos);
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,162 +0,0 @@
|
|||
// widgetImage.c — Image widget (displays bitmap, responds to clicks)
|
||||
//
|
||||
// Displays a bitmap image, optionally responding to clicks. The image data
|
||||
// must be in the display's native pixel format (pre-converted). Two creation
|
||||
// paths are provided:
|
||||
// - wgtImage: from raw pixel data already in display format (takes ownership)
|
||||
// - wgtImageFromFile: loads from file via stb_image and converts to display
|
||||
// format during load
|
||||
//
|
||||
// The widget supports a simple press effect (1px offset on click) and fires
|
||||
// onClick immediately on mouse-down. Unlike Button which has press/release
|
||||
// tracking, Image fires instantly — this is intentional for image-based
|
||||
// click targets where visual press feedback is less important than
|
||||
// responsiveness.
|
||||
//
|
||||
// No border or bevel is drawn — the image fills its widget bounds with
|
||||
// centering if the widget is larger than the image.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtImage
|
||||
// ============================================================
|
||||
//
|
||||
// Create an image widget from raw pixel data already in display format.
|
||||
// Takes ownership of the data buffer (freed on destroy).
|
||||
|
||||
WidgetT *wgtImage(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t pitch) {
|
||||
WidgetT *wgt = widgetAlloc(parent, WidgetImageE);
|
||||
|
||||
if (wgt) {
|
||||
wgt->as.image.data = data;
|
||||
wgt->as.image.imgW = w;
|
||||
wgt->as.image.imgH = h;
|
||||
wgt->as.image.imgPitch = pitch;
|
||||
wgt->as.image.pressed = false;
|
||||
}
|
||||
|
||||
return wgt;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtImageFromFile
|
||||
// ============================================================
|
||||
//
|
||||
// Load an image from a file (BMP, PNG, JPEG, GIF), convert to
|
||||
// display pixel format, and create an image widget. Delegates to
|
||||
// dvxLoadImage for format decoding and pixel conversion.
|
||||
WidgetT *wgtImageFromFile(WidgetT *parent, const char *path) {
|
||||
if (!parent || !path) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
AppContextT *ctx = wgtGetContext(parent);
|
||||
|
||||
if (!ctx) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int32_t imgW;
|
||||
int32_t imgH;
|
||||
int32_t pitch;
|
||||
uint8_t *buf = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch);
|
||||
|
||||
if (!buf) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return wgtImage(parent, buf, imgW, imgH, pitch);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtImageSetData
|
||||
// ============================================================
|
||||
|
||||
void wgtImageSetData(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetImageE);
|
||||
|
||||
free(w->as.image.data);
|
||||
w->as.image.data = data;
|
||||
w->as.image.imgW = imgW;
|
||||
w->as.image.imgH = imgH;
|
||||
w->as.image.imgPitch = pitch;
|
||||
wgtInvalidate(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetImageDestroy
|
||||
// ============================================================
|
||||
|
||||
void widgetImageDestroy(WidgetT *w) {
|
||||
free(w->as.image.data);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetImageCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
(void)font;
|
||||
w->calcMinW = w->as.image.imgW;
|
||||
w->calcMinH = w->as.image.imgH;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetImageOnMouse
|
||||
// ============================================================
|
||||
|
||||
// Mouse click: briefly sets pressed=true (for the 1px offset effect),
|
||||
// invalidates to show the press, fires onClick, then immediately clears
|
||||
// pressed. The press is purely visual — there's no release tracking like
|
||||
// Button has, since Image clicks are meant for instant actions (e.g.,
|
||||
// clicking a logo or icon area).
|
||||
void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||
(void)root;
|
||||
(void)vx;
|
||||
(void)vy;
|
||||
w->as.image.pressed = true;
|
||||
wgtInvalidatePaint(w);
|
||||
|
||||
if (w->onClick) {
|
||||
w->onClick(w);
|
||||
}
|
||||
|
||||
w->as.image.pressed = false;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetImagePaint
|
||||
// ============================================================
|
||||
|
||||
void widgetImagePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
(void)font;
|
||||
(void)colors;
|
||||
|
||||
if (!w->as.image.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Center the image within the widget bounds
|
||||
int32_t imgW = w->as.image.imgW;
|
||||
int32_t imgH = w->as.image.imgH;
|
||||
int32_t dx = w->x + (w->w - imgW) / 2;
|
||||
int32_t dy = w->y + (w->h - imgH) / 2;
|
||||
|
||||
// Offset by 1px when pressed (button-press effect)
|
||||
if (w->as.image.pressed) {
|
||||
dx++;
|
||||
dy++;
|
||||
}
|
||||
|
||||
rectCopy(d, ops, dx, dy,
|
||||
w->as.image.data, w->as.image.imgPitch,
|
||||
0, 0, imgW, imgH);
|
||||
}
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
// widgetImageButton.c — Image button widget (button with image instead of text)
|
||||
//
|
||||
// Combines a Button's beveled border and press behavior with an Image's
|
||||
// bitmap rendering. The image is centered within the button bounds and
|
||||
// shifts by 1px on press, just like text in a regular button.
|
||||
//
|
||||
// Uses the same two-phase press model as Button: mouse press stores in
|
||||
// sPressedButton, keyboard press (Space/Enter) stores in sKeyPressedBtn,
|
||||
// and the event dispatcher handles release/cancel. The onClick callback
|
||||
// fires on release, not press.
|
||||
//
|
||||
// The widget takes ownership of the image data buffer — if widget creation
|
||||
// fails, the data is freed to prevent leaks.
|
||||
//
|
||||
// The 4px added to min size (widgetImageButtonCalcMinSize) accounts for
|
||||
// the 2px bevel on each side — no extra padding is added beyond that,
|
||||
// keeping image buttons compact for toolbar use.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtImageButton
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtImageButton(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t pitch) {
|
||||
if (!parent || !data || w <= 0 || h <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
WidgetT *wgt = widgetAlloc(parent, WidgetImageButtonE);
|
||||
|
||||
if (wgt) {
|
||||
wgt->as.imageButton.data = data;
|
||||
wgt->as.imageButton.imgW = w;
|
||||
wgt->as.imageButton.imgH = h;
|
||||
wgt->as.imageButton.imgPitch = pitch;
|
||||
wgt->as.imageButton.pressed = false;
|
||||
} else {
|
||||
free(data);
|
||||
}
|
||||
|
||||
return wgt;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtImageButtonFromFile
|
||||
// ============================================================
|
||||
//
|
||||
// Load an image from disk and create an ImageButton widget.
|
||||
// Delegates to dvxLoadImage for format decoding and pixel conversion.
|
||||
|
||||
WidgetT *wgtImageButtonFromFile(WidgetT *parent, const char *path) {
|
||||
if (!parent || !path) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
AppContextT *ctx = wgtGetContext(parent);
|
||||
|
||||
if (!ctx) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int32_t imgW;
|
||||
int32_t imgH;
|
||||
int32_t pitch;
|
||||
uint8_t *buf = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch);
|
||||
|
||||
if (!buf) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return wgtImageButton(parent, buf, imgW, imgH, pitch);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtImageButtonSetData
|
||||
// ============================================================
|
||||
|
||||
void wgtImageButtonSetData(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetImageButtonE);
|
||||
|
||||
free(w->as.imageButton.data);
|
||||
w->as.imageButton.data = data;
|
||||
w->as.imageButton.imgW = imgW;
|
||||
w->as.imageButton.imgH = imgH;
|
||||
w->as.imageButton.imgPitch = pitch;
|
||||
wgtInvalidate(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetImageButtonDestroy
|
||||
// ============================================================
|
||||
|
||||
void widgetImageButtonDestroy(WidgetT *w) {
|
||||
free(w->as.imageButton.data);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetImageButtonCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
void widgetImageButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
(void)font;
|
||||
|
||||
// Bevel border only, no extra padding
|
||||
w->calcMinW = w->as.imageButton.imgW + 4;
|
||||
w->calcMinH = w->as.imageButton.imgH + 4;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetImageButtonOnKey
|
||||
// ============================================================
|
||||
|
||||
void widgetImageButtonOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||
(void)mod;
|
||||
|
||||
if (key == ' ' || key == 0x0D) {
|
||||
w->as.imageButton.pressed = true;
|
||||
sKeyPressedBtn = w;
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetImageButtonOnMouse
|
||||
// ============================================================
|
||||
|
||||
void widgetImageButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||
(void)root;
|
||||
(void)vx;
|
||||
(void)vy;
|
||||
w->focused = true;
|
||||
w->as.imageButton.pressed = true;
|
||||
sPressedButton = w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetImageButtonPaint
|
||||
// ============================================================
|
||||
|
||||
void widgetImageButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
(void)font;
|
||||
|
||||
uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace;
|
||||
bool pressed = w->as.imageButton.pressed && w->enabled;
|
||||
|
||||
BevelStyleT bevel;
|
||||
bevel.highlight = pressed ? colors->windowShadow : colors->windowHighlight;
|
||||
bevel.shadow = pressed ? colors->windowHighlight : colors->windowShadow;
|
||||
bevel.face = bgFace;
|
||||
bevel.width = 2;
|
||||
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
|
||||
|
||||
if (w->as.imageButton.data) {
|
||||
int32_t imgX = w->x + (w->w - w->as.imageButton.imgW) / 2;
|
||||
int32_t imgY = w->y + (w->h - w->as.imageButton.imgH) / 2;
|
||||
|
||||
if (pressed) {
|
||||
imgX++;
|
||||
imgY++;
|
||||
}
|
||||
|
||||
rectCopy(d, ops, imgX, imgY,
|
||||
w->as.imageButton.data, w->as.imageButton.imgPitch,
|
||||
0, 0, w->as.imageButton.imgW, w->as.imageButton.imgH);
|
||||
}
|
||||
|
||||
if (w->focused) {
|
||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||
int32_t off = pressed ? 1 : 0;
|
||||
drawFocusRect(d, ops, w->x + 3 + off, w->y + 3 + off, w->w - 6, w->h - 6, fg);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,508 +0,0 @@
|
|||
// widgetInternal.h — Shared internal header for widget implementation files
|
||||
//
|
||||
// This header is included only by widget implementation .c files (in the
|
||||
// widgets/ directory), never by application code. It exposes the vtable
|
||||
// system, shared module-level state, and internal helper functions that
|
||||
// widget implementations need but the public API should not expose.
|
||||
//
|
||||
// The widget system is split across multiple .c files by concern:
|
||||
// widgetCore.c — allocation, tree ops, focus management, shared helpers
|
||||
// widgetLayout.c — measure + layout algorithms for box containers
|
||||
// widgetEvent.c — mouse/keyboard dispatch, scrollbar management
|
||||
// widgetOps.c — paint dispatch, overlay rendering
|
||||
// widget*.c — per-type paint, event, and layout implementations
|
||||
#ifndef WIDGET_INTERNAL_H
|
||||
#define WIDGET_INTERNAL_H
|
||||
|
||||
#include "../dvxWidget.h"
|
||||
#include "../dvxApp.h"
|
||||
#include "../dvxDraw.h"
|
||||
#include "../dvxWm.h"
|
||||
#include "../dvxVideo.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
// ============================================================
|
||||
// Widget class vtable
|
||||
// ============================================================
|
||||
//
|
||||
// Each widget type has a WidgetClassT entry in widgetClassTable[], indexed
|
||||
// by WidgetTypeE. This is a C implementation of virtual dispatch: instead
|
||||
// of a switch(type) in every operation, the code calls w->wclass->paint()
|
||||
// etc. The vtable pointer is set once at widget creation and never changes.
|
||||
//
|
||||
// Flags encode static properties that the framework needs without calling
|
||||
// into the vtable:
|
||||
// FOCUSABLE — can receive keyboard focus (Tab navigation)
|
||||
// BOX_CONTAINER — uses the generic VBox/HBox layout algorithm
|
||||
// HORIZ_CONTAINER — lays out children horizontally (vs. default vertical)
|
||||
// PAINTS_CHILDREN — the widget's paint function handles child rendering
|
||||
// (e.g. TabControl only paints the active tab page)
|
||||
// NO_HIT_RECURSE — hit testing stops at this widget and doesn't recurse
|
||||
// into children (e.g. ListBox handles its own items)
|
||||
//
|
||||
// NULL function pointers are valid and mean "no-op" for that operation.
|
||||
// paintOverlay is used by widgets that need to draw outside their bounds
|
||||
// (dropdown popup lists), rendered in a separate pass after all widgets.
|
||||
|
||||
#define WCLASS_FOCUSABLE 0x0001
|
||||
#define WCLASS_BOX_CONTAINER 0x0002
|
||||
#define WCLASS_HORIZ_CONTAINER 0x0004
|
||||
#define WCLASS_PAINTS_CHILDREN 0x0008
|
||||
#define WCLASS_NO_HIT_RECURSE 0x0010
|
||||
|
||||
typedef struct WidgetClassT {
|
||||
uint32_t flags;
|
||||
void (*paint)(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void (*paintOverlay)(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void (*calcMinSize)(WidgetT *w, const BitmapFontT *font);
|
||||
void (*layout)(WidgetT *w, const BitmapFontT *font);
|
||||
void (*onMouse)(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void (*onKey)(WidgetT *w, int32_t key, int32_t mod);
|
||||
void (*destroy)(WidgetT *w);
|
||||
const char *(*getText)(const WidgetT *w);
|
||||
void (*setText)(WidgetT *w, const char *text);
|
||||
} WidgetClassT;
|
||||
|
||||
// Global vtable array — one entry per WidgetTypeE value. Defined in
|
||||
// widgetCore.c. Must stay in sync with the WidgetTypeE enum order.
|
||||
extern const WidgetClassT *widgetClassTable[];
|
||||
|
||||
// ============================================================
|
||||
// Validation macros
|
||||
// ============================================================
|
||||
//
|
||||
// Defensive type-checking at the top of public widget API functions.
|
||||
// Since WidgetT is a tagged union, calling a ListBox function with a
|
||||
// Button widget would access the wrong union member and corrupt state.
|
||||
// These macros provide a consistent early-return guard pattern.
|
||||
|
||||
#define VALIDATE_WIDGET(w, wtype, retval) \
|
||||
do { if (!(w) || (w)->type != (wtype)) { return (retval); } } while (0)
|
||||
|
||||
#define VALIDATE_WIDGET_VOID(w, wtype) \
|
||||
do { if (!(w) || (w)->type != (wtype)) { return; } } while (0)
|
||||
|
||||
// ============================================================
|
||||
// Constants
|
||||
// ============================================================
|
||||
|
||||
// Modifier flags (BIOS INT 16h shift state bits). Duplicated from
|
||||
// dvxTypes.h ACCEL_xxx constants because widget code references them
|
||||
// frequently and the KEY_MOD_ prefix reads better in event handlers.
|
||||
#define KEY_MOD_SHIFT 0x03
|
||||
#define KEY_MOD_CTRL 0x04
|
||||
#define KEY_MOD_ALT 0x08
|
||||
|
||||
// Layout and geometry constants. These define the visual metrics of
|
||||
// each widget type and are tuned for readability at 640x480 with 8px
|
||||
// wide fonts. Changing these affects every instance of the widget type.
|
||||
#define DEFAULT_SPACING 4
|
||||
#define DEFAULT_PADDING 4
|
||||
#define SEPARATOR_THICKNESS 2
|
||||
#define BUTTON_PAD_H 8
|
||||
#define BUTTON_PAD_V 4
|
||||
#define BUTTON_FOCUS_INSET 3 // focus rect inset from button edge
|
||||
#define BUTTON_PRESS_OFFSET 1 // text/focus shift when button is pressed
|
||||
#define CHECKBOX_BOX_SIZE 12
|
||||
#define CHECKBOX_GAP 4
|
||||
#define FRAME_BEVEL_BORDER 2
|
||||
#define FRAME_FLAT_BORDER 1
|
||||
#define TEXT_INPUT_PAD 3
|
||||
#define DROPDOWN_BTN_WIDTH 16
|
||||
#define DROPDOWN_MAX_VISIBLE 8
|
||||
#define SLIDER_TRACK_H 4
|
||||
#define SLIDER_THUMB_W 11
|
||||
#define TAB_PAD_H 8
|
||||
#define TAB_PAD_V 4
|
||||
#define TAB_BORDER 2
|
||||
#define LISTBOX_BORDER 2
|
||||
#define LISTVIEW_BORDER 2
|
||||
#define LISTVIEW_MIN_COL_W 20 // minimum column width during resize
|
||||
#define TREE_INDENT 16
|
||||
#define TREE_EXPAND_SIZE 9
|
||||
#define TREE_ICON_GAP 4
|
||||
#define TREE_BORDER 2
|
||||
// WGT_SB_W is the widget-internal scrollbar width (slightly narrower than
|
||||
// the window-level SCROLLBAR_WIDTH) to fit within widget content areas.
|
||||
#define WGT_SB_W 14
|
||||
#define TREE_MIN_ROWS 4
|
||||
#define SB_MIN_THUMB 14
|
||||
#define SPLITTER_BAR_W 5
|
||||
#define SPLITTER_MIN_PANE 20
|
||||
#define TOOLBAR_PAD 2 // padding inside toolbar/statusbar
|
||||
#define TOOLBAR_GAP 2 // gap between toolbar/statusbar children
|
||||
|
||||
// ============================================================
|
||||
// Inline helpers
|
||||
// ============================================================
|
||||
|
||||
static inline int32_t clampInt(int32_t val, int32_t lo, int32_t hi) {
|
||||
if (val < lo) { return lo; }
|
||||
if (val > hi) { return hi; }
|
||||
return val;
|
||||
}
|
||||
|
||||
// Classic Windows 3.1 embossed (etched) text for disabled widgets.
|
||||
// The illusion works by drawing the text twice: first offset +1,+1 in
|
||||
// the highlight color (creating a light "shadow" behind the text), then
|
||||
// at the original position in the shadow color. The result is text that
|
||||
// appears chiseled into the surface — the universal visual indicator for
|
||||
// "greyed out" in Motif and Windows 3.x era GUIs.
|
||||
static inline void drawTextEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) {
|
||||
drawText(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false);
|
||||
drawText(d, ops, font, x, y, text, colors->windowShadow, 0, false);
|
||||
}
|
||||
|
||||
static inline void drawTextAccelEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) {
|
||||
drawTextAccel(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false);
|
||||
drawTextAccel(d, ops, font, x, y, text, colors->windowShadow, 0, false);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Shared state (defined in widgetCore.c)
|
||||
// ============================================================
|
||||
//
|
||||
// Module-level globals for tracking active interactive operations.
|
||||
// Only one of these can be active at a time (e.g. you can't drag a
|
||||
// slider and resize a listview column simultaneously). Using globals
|
||||
// rather than per-widget state avoids bloating every widget with fields
|
||||
// that are only relevant during a single drag operation. The tradeoff
|
||||
// is that these are truly global — but since the GUI is single-threaded
|
||||
// and only one mouse can exist, this is safe.
|
||||
|
||||
extern bool sCursorBlinkOn; // text cursor blink phase (toggled by wgtUpdateCursorBlink)
|
||||
extern clock_t sDblClickTicks; // double-click threshold (set from AppContextT.dblClickTicks)
|
||||
extern bool sDebugLayout;
|
||||
extern WidgetT *sClosedPopup; // popup that was just closed (prevents immediate reopen)
|
||||
extern WidgetT *sFocusedWidget; // currently focused widget across all windows
|
||||
extern WidgetT *sKeyPressedBtn; // button being held via keyboard (Space/Enter)
|
||||
extern WidgetT *sOpenPopup; // dropdown/combobox with open popup list
|
||||
extern WidgetT *sPressedButton; // button/imagebutton being held via mouse
|
||||
extern WidgetT *sDragSlider; // slider being dragged
|
||||
extern WidgetT *sDrawingCanvas; // canvas receiving drag events
|
||||
extern WidgetT *sDragTextSelect; // text widget with active mouse selection drag
|
||||
extern int32_t sDragOffset; // mouse offset within thumb/handle at drag start
|
||||
extern WidgetT *sResizeListView; // listview whose column is being resized
|
||||
extern int32_t sResizeCol; // which column is being resized
|
||||
extern int32_t sResizeStartX; // mouse X at start of column resize
|
||||
extern int32_t sResizeOrigW; // original column width at start of resize
|
||||
extern bool sResizeDragging; // true once mouse has moved from click point
|
||||
extern WidgetT *sDragSplitter; // splitter being dragged
|
||||
extern int32_t sDragSplitStart; // mouse position at start of splitter drag
|
||||
extern WidgetT *sDragReorder; // listbox/treeview item being drag-reordered
|
||||
extern WidgetT *sDragScrollbar; // widget whose scrollbar thumb is being dragged
|
||||
extern int32_t sDragScrollbarOff; // mouse offset within thumb at drag start
|
||||
extern int32_t sDragScrollbarOrient; // 0=vertical, 1=horizontal
|
||||
|
||||
// ============================================================
|
||||
// Core functions (widgetCore.c)
|
||||
// ============================================================
|
||||
|
||||
// Tree manipulation
|
||||
void widgetAddChild(WidgetT *parent, WidgetT *child);
|
||||
void widgetRemoveChild(WidgetT *parent, WidgetT *child);
|
||||
void widgetDestroyChildren(WidgetT *w);
|
||||
|
||||
// Allocate a new widget, set its type and vtable, and add it as a child.
|
||||
WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type);
|
||||
|
||||
// Focus management — Tab/Shift-Tab navigation walks the tree in creation
|
||||
// order, skipping non-focusable and hidden widgets.
|
||||
void widgetClearFocus(WidgetT *root);
|
||||
WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after);
|
||||
WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before);
|
||||
|
||||
// Alt+key accelerator lookup — searches the tree for a widget whose
|
||||
// accelKey matches, used for keyboard navigation of labeled controls.
|
||||
WidgetT *widgetFindByAccel(WidgetT *root, char key);
|
||||
|
||||
// Utility queries
|
||||
int32_t widgetCountVisibleChildren(const WidgetT *w);
|
||||
int32_t widgetFrameBorderWidth(const WidgetT *w);
|
||||
bool widgetIsFocusable(WidgetTypeE type);
|
||||
bool widgetIsBoxContainer(WidgetTypeE type);
|
||||
bool widgetIsHorizContainer(WidgetTypeE type);
|
||||
|
||||
// Hit testing — find the deepest widget containing the point (x,y).
|
||||
// Respects WCLASS_NO_HIT_RECURSE to stop at list/tree widgets.
|
||||
WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y);
|
||||
|
||||
// Translate arrow/PgUp/PgDn/Home/End keys into list index changes.
|
||||
// Shared by ListBox, ListView, Dropdown, and TreeView.
|
||||
int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize);
|
||||
|
||||
// Compute popup position for a dropdown/combobox, clamped to screen bounds.
|
||||
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH);
|
||||
|
||||
// Paint a generic popup item list (used by dropdown, combobox, and popup menus).
|
||||
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos);
|
||||
|
||||
// Compute scrollbar thumb position and size for a given track length,
|
||||
// content size, visible size, and scroll position. Used by both widget-
|
||||
// internal scrollbars and the window-level scrollbar drawing code.
|
||||
void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize);
|
||||
|
||||
// ============================================================
|
||||
// Shared scrollbar functions (widgetScrollbar.c)
|
||||
// ============================================================
|
||||
//
|
||||
// Widget-internal scrollbar rendering and hit testing, shared across
|
||||
// ListBox, ListView, TreeView, TextArea, ScrollPane, and AnsiTerm.
|
||||
// These operate on abstract track coordinates (position + length) so
|
||||
// the same code handles both horizontal and vertical scrollbars.
|
||||
|
||||
typedef enum {
|
||||
ScrollHitNoneE,
|
||||
ScrollHitArrowDecE,
|
||||
ScrollHitArrowIncE,
|
||||
ScrollHitPageDecE,
|
||||
ScrollHitPageIncE,
|
||||
ScrollHitThumbE
|
||||
} ScrollHitE;
|
||||
|
||||
void widgetDrawScrollbarH(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbW, int32_t totalSize, int32_t visibleSize, int32_t scrollPos);
|
||||
void widgetDrawScrollbarV(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbH, int32_t totalSize, int32_t visibleSize, int32_t scrollPos);
|
||||
void widgetScrollbarDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY);
|
||||
ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSize, int32_t visibleSize, int32_t scrollPos);
|
||||
|
||||
// ============================================================
|
||||
// Layout functions (widgetLayout.c)
|
||||
// ============================================================
|
||||
//
|
||||
// The layout algorithm is a two-pass flexbox-like system:
|
||||
//
|
||||
// calcMinSize (bottom-up): each leaf widget reports its minimum size
|
||||
// (e.g. button = text width + padding). Containers sum their children's
|
||||
// minimums plus spacing along the main axis, and take the maximum of
|
||||
// children's minimums on the cross axis.
|
||||
//
|
||||
// layout (top-down): starting from the available space (window content
|
||||
// area), each container distributes space to children. Children that fit
|
||||
// at their minimum get their minimum. Remaining space is distributed
|
||||
// proportionally by weight to flexible children. This happens recursively
|
||||
// down the tree.
|
||||
|
||||
void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetLayoutBox(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font);
|
||||
|
||||
// ============================================================
|
||||
// Event functions (widgetEvent.c)
|
||||
// ============================================================
|
||||
//
|
||||
// These are the WindowT callback implementations installed by wgtInitWindow().
|
||||
// They bridge the window manager's callback interface to the widget tree's
|
||||
// event dispatch. Each one locates the appropriate widget (via hit-test for
|
||||
// mouse events, or the focused widget for keyboard events) and calls the
|
||||
// widget's vtable handler.
|
||||
|
||||
// Sync window-level scrollbar ranges to match the widget tree's overflow.
|
||||
void widgetManageScrollbars(WindowT *win, AppContextT *ctx);
|
||||
|
||||
void widgetOnKey(WindowT *win, int32_t key, int32_t mod);
|
||||
void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);
|
||||
void widgetOnPaint(WindowT *win, RectT *dirtyArea);
|
||||
void widgetOnResize(WindowT *win, int32_t newW, int32_t newH);
|
||||
void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value);
|
||||
|
||||
// ============================================================
|
||||
// Paint/ops functions (widgetOps.c)
|
||||
// ============================================================
|
||||
|
||||
void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
|
||||
// ============================================================
|
||||
// Per-widget paint functions
|
||||
// ============================================================
|
||||
|
||||
void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetImageButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetDropdownPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetDropdownPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetImagePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy);
|
||||
void widgetProgressBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetSeparatorPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetSplitterPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetStatusBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetToolbarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
|
||||
// ============================================================
|
||||
// Per-widget calcMinSize functions
|
||||
// ============================================================
|
||||
|
||||
void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetImageButtonCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetComboBoxCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetLabelCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetListViewCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetSeparatorCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetScrollPaneCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetSplitterCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetSpinnerCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetSpacerCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetTextAreaCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetTextInputCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||
|
||||
// ============================================================
|
||||
// Per-widget layout functions (for special containers)
|
||||
// ============================================================
|
||||
|
||||
void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetSplitterLayout(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font);
|
||||
void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font);
|
||||
|
||||
// ============================================================
|
||||
// Per-widget getText/setText functions
|
||||
// ============================================================
|
||||
|
||||
const char *widgetButtonGetText(const WidgetT *w);
|
||||
void widgetButtonSetText(WidgetT *w, const char *text);
|
||||
const char *widgetCheckboxGetText(const WidgetT *w);
|
||||
void widgetCheckboxSetText(WidgetT *w, const char *text);
|
||||
const char *widgetComboBoxGetText(const WidgetT *w);
|
||||
void widgetComboBoxSetText(WidgetT *w, const char *text);
|
||||
const char *widgetDropdownGetText(const WidgetT *w);
|
||||
const char *widgetLabelGetText(const WidgetT *w);
|
||||
void widgetLabelSetText(WidgetT *w, const char *text);
|
||||
const char *widgetRadioGetText(const WidgetT *w);
|
||||
void widgetRadioSetText(WidgetT *w, const char *text);
|
||||
const char *widgetTextAreaGetText(const WidgetT *w);
|
||||
void widgetTextAreaSetText(WidgetT *w, const char *text);
|
||||
const char *widgetTextInputGetText(const WidgetT *w);
|
||||
void widgetTextInputSetText(WidgetT *w, const char *text);
|
||||
const char *widgetTreeItemGetText(const WidgetT *w);
|
||||
void widgetTreeItemSetText(WidgetT *w, const char *text);
|
||||
|
||||
// ============================================================
|
||||
// Per-widget destroy functions
|
||||
// ============================================================
|
||||
|
||||
void widgetAnsiTermDestroy(WidgetT *w);
|
||||
void widgetCanvasDestroy(WidgetT *w);
|
||||
void widgetComboBoxDestroy(WidgetT *w);
|
||||
void widgetImageButtonDestroy(WidgetT *w);
|
||||
void widgetImageDestroy(WidgetT *w);
|
||||
void widgetListBoxDestroy(WidgetT *w);
|
||||
void widgetListViewDestroy(WidgetT *w);
|
||||
void widgetTextAreaDestroy(WidgetT *w);
|
||||
void widgetTextInputDestroy(WidgetT *w);
|
||||
|
||||
// ============================================================
|
||||
// Per-widget mouse/key functions
|
||||
// ============================================================
|
||||
//
|
||||
// Shared text editing helpers used by TextInput, TextArea, ComboBox, and
|
||||
// Spinner. The widgetTextEditOnKey function implements the common subset
|
||||
// of single-line text editing behavior (cursor movement, selection,
|
||||
// copy/paste, undo) shared across all text-editing widgets, so changes
|
||||
// to editing behavior only need to happen in one place.
|
||||
|
||||
// Clear text selections in all widgets except the given one (ensures only
|
||||
// one text selection is active at a time across the entire GUI).
|
||||
void clearOtherSelections(WidgetT *except);
|
||||
|
||||
void clipboardCopy(const char *text, int32_t len);
|
||||
const char *clipboardGet(int32_t *outLen);
|
||||
bool isWordChar(char c);
|
||||
|
||||
// Detect double/triple clicks based on timing and position proximity.
|
||||
// Returns 1 for single, 2 for double, 3 for triple click.
|
||||
int32_t multiClickDetect(int32_t vx, int32_t vy);
|
||||
void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||
void widgetAnsiTermOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetButtonOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||
void widgetButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetCanvasOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetCheckboxOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||
void widgetCheckboxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||
void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||
void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetImageButtonOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||
void widgetImageButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||
void widgetListBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||
void widgetListViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||
void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||
void widgetScrollPaneOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetSplitterClampPos(WidgetT *w, int32_t *pos);
|
||||
void widgetSplitterOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||
void widgetSliderOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||
void widgetSpinnerOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
const char *widgetSpinnerGetText(const WidgetT *w);
|
||||
void widgetSpinnerSetText(WidgetT *w, const char *text);
|
||||
void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||
void widgetTabControlOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||
void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
// Shared single-line text editing logic. Takes pointers to all the edit
|
||||
// state fields so the same code works for TextInput, ComboBox, and Spinner
|
||||
// (which store these fields in different union members). This avoids
|
||||
// duplicating the full editing logic (cursor movement, word selection,
|
||||
// clipboard, undo) three times.
|
||||
void widgetTextEditDragUpdateLine(int32_t vx, int32_t leftEdge, int32_t maxChars, const BitmapFontT *font, int32_t len, int32_t *pCursorPos, int32_t *pScrollOff, int32_t *pSelEnd);
|
||||
void widgetTextEditMouseClick(WidgetT *w, int32_t vx, int32_t vy, int32_t textLeftX, const BitmapFontT *font, const char *buf, int32_t len, int32_t scrollOff, int32_t *pCursorPos, int32_t *pSelStart, int32_t *pSelEnd, bool wordSelect, bool dragSelect);
|
||||
void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor);
|
||||
void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t textX, int32_t textY, const char *buf, int32_t visLen, int32_t scrollOff, int32_t cursorPos, int32_t selStart, int32_t selEnd, uint32_t fg, uint32_t bg, bool showCursor, int32_t cursorMinX, int32_t cursorMaxX);
|
||||
void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||
void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
int32_t wordEnd(const char *buf, int32_t len, int32_t pos);
|
||||
int32_t wordStart(const char *buf, int32_t pos);
|
||||
void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||
void widgetTreeViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||
|
||||
// Drag-reorder helpers — generic drag-and-drop reordering shared by
|
||||
// ListBox, ListView, and TreeView. The update function tracks the mouse
|
||||
// position and computes the drop target, the drop function commits the
|
||||
// reorder by relinking the item in the data structure.
|
||||
void widgetReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
|
||||
void widgetReorderDrop(WidgetT *w);
|
||||
|
||||
// Iterate to the next visible item in a tree view (skipping collapsed
|
||||
// subtrees). Used for keyboard navigation and rendering.
|
||||
WidgetT *widgetTreeViewNextVisible(WidgetT *item, WidgetT *treeView);
|
||||
|
||||
#endif // WIDGET_INTERNAL_H
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
// widgetLabel.c — Label widget
|
||||
//
|
||||
// Static text display — the simplest widget. Not focusable, not interactive.
|
||||
// Supports accelerator keys via '&' prefix: when the user presses Alt+key,
|
||||
// focus moves to the next focusable widget after this label. This follows
|
||||
// the Win3.1 convention where labels act as keyboard shortcuts for adjacent
|
||||
// controls (e.g., a label "&Name:" before a text input).
|
||||
//
|
||||
// The text pointer is stored directly (not copied) — the caller must ensure
|
||||
// the string remains valid for the widget's lifetime, or use widgetLabelSetText
|
||||
// to update it. This avoids unnecessary allocations for the common case of
|
||||
// literal string labels.
|
||||
//
|
||||
// Background is transparent by default (bgColor == 0 means use the parent's
|
||||
// content background color from the color scheme). The +1 in calcMinH adds a
|
||||
// pixel of breathing room below the text baseline.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtLabel
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtLabel(WidgetT *parent, const char *text) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetLabelE);
|
||||
|
||||
if (w) {
|
||||
w->as.label.text = text;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetLabelGetText
|
||||
// ============================================================
|
||||
|
||||
const char *widgetLabelGetText(const WidgetT *w) {
|
||||
return w->as.label.text;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetLabelSetText
|
||||
// ============================================================
|
||||
|
||||
void widgetLabelSetText(WidgetT *w, const char *text) {
|
||||
w->as.label.text = text;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetLabelCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
void widgetLabelCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
w->calcMinW = textWidthAccel(font, w->as.label.text);
|
||||
w->calcMinH = font->charHeight + 1;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetLabelPaint
|
||||
// ============================================================
|
||||
|
||||
void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
int32_t textY = w->y + (w->h - font->charHeight) / 2;
|
||||
|
||||
if (!w->enabled) {
|
||||
drawTextAccelEmbossed(d, ops, font, w->x, textY, w->as.label.text, colors);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||
|
||||
drawTextAccel(d, ops, font, w->x, textY, w->as.label.text, fg, bg, false);
|
||||
}
|
||||
|
|
@ -1,641 +0,0 @@
|
|||
// widgetListBox.c — ListBox widget (single and multi-select)
|
||||
//
|
||||
// Scrollable list of text items with single-select or multi-select modes.
|
||||
// Items are stored as external string pointers (not copied), with a vertical
|
||||
// scrollbar appearing when the item count exceeds visible rows.
|
||||
//
|
||||
// Multi-select uses a parallel selBits array (one byte per item, 0 or 1)
|
||||
// rather than a bitfield. Using a full byte per item wastes some memory but
|
||||
// makes individual item toggle/test trivial without shift/mask operations,
|
||||
// which matters when the selection code runs on every click and keyboard event.
|
||||
//
|
||||
// The selection model follows Windows explorer conventions:
|
||||
// - Plain click: select one item, clear others, set anchor
|
||||
// - Ctrl+click: toggle one item, update anchor
|
||||
// - Shift+click: range select from anchor to clicked item
|
||||
// - Shift+arrow: range select from anchor to cursor
|
||||
// - Space: toggle current item (multi-select)
|
||||
// - Ctrl+A: select all (multi-select)
|
||||
//
|
||||
// The "anchor" concept is key: it's the starting point for shift-select
|
||||
// ranges. It's updated on non-shift clicks but stays fixed during shift
|
||||
// operations, allowing the user to extend/shrink the selection by
|
||||
// shift-clicking different endpoints.
|
||||
//
|
||||
// Drag-reorder support allows items to be rearranged by dragging. When
|
||||
// enabled, a mouse-down initiates a drag (tracked via sDragReorder global),
|
||||
// and a 2px horizontal line indicator shows the insertion point. The actual
|
||||
// reordering is handled by the application's onReorder callback.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define LISTBOX_PAD 2
|
||||
#define LISTBOX_MIN_ROWS 4
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Prototypes
|
||||
// ============================================================
|
||||
|
||||
static void allocSelBits(WidgetT *w);
|
||||
static void ensureScrollVisible(WidgetT *w, int32_t idx);
|
||||
static void selectRange(WidgetT *w, int32_t from, int32_t to);
|
||||
|
||||
|
||||
// ============================================================
|
||||
// allocSelBits
|
||||
// ============================================================
|
||||
|
||||
// Allocate (or re-allocate) the selection bits array. Only allocated in
|
||||
// multi-select mode — in single-select, selectedIdx alone tracks the
|
||||
// selection. calloc initializes all bits to 0 (nothing selected).
|
||||
static void allocSelBits(WidgetT *w) {
|
||||
if (w->as.listBox.selBits) {
|
||||
free(w->as.listBox.selBits);
|
||||
w->as.listBox.selBits = NULL;
|
||||
}
|
||||
|
||||
int32_t count = w->as.listBox.itemCount;
|
||||
|
||||
if (count > 0 && w->as.listBox.multiSelect) {
|
||||
w->as.listBox.selBits = (uint8_t *)calloc(count, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// ensureScrollVisible
|
||||
// ============================================================
|
||||
|
||||
// Adjust scroll position so the item at idx is within the visible viewport.
|
||||
// If the item is above the viewport, scroll up to it. If below, scroll down
|
||||
// to show it at the bottom. If already visible, do nothing.
|
||||
static void ensureScrollVisible(WidgetT *w, int32_t idx) {
|
||||
if (idx < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppContextT *ctx = wgtGetContext(w);
|
||||
const BitmapFontT *font = &ctx->font;
|
||||
int32_t innerH = w->h - LISTBOX_BORDER * 2;
|
||||
int32_t visibleRows = innerH / font->charHeight;
|
||||
|
||||
if (visibleRows < 1) {
|
||||
visibleRows = 1;
|
||||
}
|
||||
|
||||
if (idx < w->as.listBox.scrollPos) {
|
||||
w->as.listBox.scrollPos = idx;
|
||||
} else if (idx >= w->as.listBox.scrollPos + visibleRows) {
|
||||
w->as.listBox.scrollPos = idx - visibleRows + 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// selectRange
|
||||
// ============================================================
|
||||
|
||||
// Set selection bits for all items in the range [from, to] (inclusive,
|
||||
// order-independent). Used for shift-click and shift-arrow range selection.
|
||||
static void selectRange(WidgetT *w, int32_t from, int32_t to) {
|
||||
if (!w->as.listBox.selBits) {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t lo = from < to ? from : to;
|
||||
int32_t hi = from > to ? from : to;
|
||||
|
||||
if (lo < 0) {
|
||||
lo = 0;
|
||||
}
|
||||
|
||||
if (hi >= w->as.listBox.itemCount) {
|
||||
hi = w->as.listBox.itemCount - 1;
|
||||
}
|
||||
|
||||
for (int32_t i = lo; i <= hi; i++) {
|
||||
w->as.listBox.selBits[i] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetListBoxDestroy
|
||||
// ============================================================
|
||||
|
||||
void widgetListBoxDestroy(WidgetT *w) {
|
||||
if (w->as.listBox.selBits) {
|
||||
free(w->as.listBox.selBits);
|
||||
w->as.listBox.selBits = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtListBox
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtListBox(WidgetT *parent) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetListBoxE);
|
||||
|
||||
if (w) {
|
||||
w->as.listBox.selectedIdx = -1;
|
||||
w->as.listBox.anchorIdx = -1;
|
||||
w->as.listBox.dragIdx = -1;
|
||||
w->as.listBox.dropIdx = -1;
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtListBoxClearSelection
|
||||
// ============================================================
|
||||
|
||||
void wgtListBoxClearSelection(WidgetT *w) {
|
||||
if (!w || w->type != WidgetListBoxE || !w->as.listBox.selBits) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(w->as.listBox.selBits, 0, w->as.listBox.itemCount);
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtListBoxGetSelected
|
||||
// ============================================================
|
||||
|
||||
int32_t wgtListBoxGetSelected(const WidgetT *w) {
|
||||
VALIDATE_WIDGET(w, WidgetListBoxE, -1);
|
||||
|
||||
return w->as.listBox.selectedIdx;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtListBoxIsItemSelected
|
||||
// ============================================================
|
||||
|
||||
bool wgtListBoxIsItemSelected(const WidgetT *w, int32_t idx) {
|
||||
VALIDATE_WIDGET(w, WidgetListBoxE, false);
|
||||
|
||||
if (!w->as.listBox.multiSelect) {
|
||||
return idx == w->as.listBox.selectedIdx;
|
||||
}
|
||||
|
||||
if (!w->as.listBox.selBits || idx < 0 || idx >= w->as.listBox.itemCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return w->as.listBox.selBits[idx] != 0;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtListBoxSelectAll
|
||||
// ============================================================
|
||||
|
||||
void wgtListBoxSelectAll(WidgetT *w) {
|
||||
if (!w || w->type != WidgetListBoxE || !w->as.listBox.selBits) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(w->as.listBox.selBits, 1, w->as.listBox.itemCount);
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtListBoxSetItemSelected
|
||||
// ============================================================
|
||||
|
||||
void wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selected) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetListBoxE);
|
||||
|
||||
if (!w->as.listBox.selBits || idx < 0 || idx >= w->as.listBox.itemCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
w->as.listBox.selBits[idx] = selected ? 1 : 0;
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtListBoxSetItems
|
||||
// ============================================================
|
||||
|
||||
void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetListBoxE);
|
||||
|
||||
w->as.listBox.items = items;
|
||||
w->as.listBox.itemCount = count;
|
||||
|
||||
// Cache max item strlen to avoid recomputing in calcMinSize
|
||||
int32_t maxLen = 0;
|
||||
|
||||
for (int32_t i = 0; i < count; i++) {
|
||||
int32_t slen = (int32_t)strlen(items[i]);
|
||||
|
||||
if (slen > maxLen) {
|
||||
maxLen = slen;
|
||||
}
|
||||
}
|
||||
|
||||
w->as.listBox.maxItemLen = maxLen;
|
||||
|
||||
if (w->as.listBox.selectedIdx >= count) {
|
||||
w->as.listBox.selectedIdx = count > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
if (w->as.listBox.selectedIdx < 0 && count > 0) {
|
||||
w->as.listBox.selectedIdx = 0;
|
||||
}
|
||||
|
||||
w->as.listBox.anchorIdx = w->as.listBox.selectedIdx;
|
||||
|
||||
// Reallocate selection bits
|
||||
allocSelBits(w);
|
||||
|
||||
// Pre-select the cursor item
|
||||
if (w->as.listBox.selBits && w->as.listBox.selectedIdx >= 0) {
|
||||
w->as.listBox.selBits[w->as.listBox.selectedIdx] = 1;
|
||||
}
|
||||
|
||||
wgtInvalidate(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtListBoxSetReorderable
|
||||
// ============================================================
|
||||
|
||||
void wgtListBoxSetReorderable(WidgetT *w, bool reorderable) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetListBoxE);
|
||||
|
||||
w->as.listBox.reorderable = reorderable;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtListBoxSetMultiSelect
|
||||
// ============================================================
|
||||
|
||||
void wgtListBoxSetMultiSelect(WidgetT *w, bool multi) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetListBoxE);
|
||||
|
||||
w->as.listBox.multiSelect = multi;
|
||||
allocSelBits(w);
|
||||
|
||||
// Sync: mark current selection
|
||||
if (w->as.listBox.selBits && w->as.listBox.selectedIdx >= 0) {
|
||||
w->as.listBox.selBits[w->as.listBox.selectedIdx] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtListBoxSetSelected
|
||||
// ============================================================
|
||||
|
||||
void wgtListBoxSetSelected(WidgetT *w, int32_t idx) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetListBoxE);
|
||||
|
||||
w->as.listBox.selectedIdx = idx;
|
||||
w->as.listBox.anchorIdx = idx;
|
||||
|
||||
// In multi-select, clear all then select just this one
|
||||
if (w->as.listBox.selBits) {
|
||||
memset(w->as.listBox.selBits, 0, w->as.listBox.itemCount);
|
||||
|
||||
if (idx >= 0 && idx < w->as.listBox.itemCount) {
|
||||
w->as.listBox.selBits[idx] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetListBoxCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
// Min size accounts for the widest item, a scrollbar, padding, and border.
|
||||
// Height is based on LISTBOX_MIN_ROWS (4 rows) so the listbox has a usable
|
||||
// minimum height even when empty. The 8-character minimum width prevents
|
||||
// the listbox from collapsing too narrow when items are short or absent.
|
||||
void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
int32_t maxItemW = w->as.listBox.maxItemLen * font->charWidth;
|
||||
int32_t minW = font->charWidth * 8;
|
||||
|
||||
if (maxItemW < minW) {
|
||||
maxItemW = minW;
|
||||
}
|
||||
|
||||
w->calcMinW = maxItemW + LISTBOX_PAD * 2 + LISTBOX_BORDER * 2 + WGT_SB_W;
|
||||
w->calcMinH = LISTBOX_MIN_ROWS * font->charHeight + LISTBOX_BORDER * 2;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetListBoxOnKey
|
||||
// ============================================================
|
||||
|
||||
// Key handling: delegates to widgetNavigateIndex for cursor movement
|
||||
// (Up, Down, Home, End, PgUp, PgDn) which returns the new index after
|
||||
// navigation. This shared helper ensures consistent keyboard navigation
|
||||
// across ListBox, ListView, and other scrollable widgets.
|
||||
void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||
if (!w || w->type != WidgetListBoxE || w->as.listBox.itemCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool multi = w->as.listBox.multiSelect;
|
||||
bool shift = (mod & KEY_MOD_SHIFT) != 0;
|
||||
bool ctrl = (mod & KEY_MOD_CTRL) != 0;
|
||||
int32_t sel = w->as.listBox.selectedIdx;
|
||||
|
||||
// Ctrl+A — select all (multi-select only)
|
||||
if (multi && ctrl && (key == 'a' || key == 'A' || key == 1)) {
|
||||
wgtListBoxSelectAll(w);
|
||||
return;
|
||||
}
|
||||
|
||||
// Space — toggle current item (multi-select only)
|
||||
if (multi && key == ' ') {
|
||||
if (sel >= 0 && w->as.listBox.selBits) {
|
||||
w->as.listBox.selBits[sel] ^= 1;
|
||||
w->as.listBox.anchorIdx = sel;
|
||||
}
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
return;
|
||||
}
|
||||
|
||||
AppContextT *ctx = wgtGetContext(w);
|
||||
const BitmapFontT *font = &ctx->font;
|
||||
int32_t visibleRows = (w->h - LISTBOX_BORDER * 2) / font->charHeight;
|
||||
|
||||
if (visibleRows < 1) {
|
||||
visibleRows = 1;
|
||||
}
|
||||
|
||||
int32_t newSel = widgetNavigateIndex(key, sel, w->as.listBox.itemCount, visibleRows);
|
||||
|
||||
if (newSel < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newSel == sel) {
|
||||
return;
|
||||
}
|
||||
|
||||
w->as.listBox.selectedIdx = newSel;
|
||||
|
||||
// Update selection
|
||||
if (multi && w->as.listBox.selBits) {
|
||||
if (shift) {
|
||||
// Shift+arrow: range from anchor to new cursor
|
||||
memset(w->as.listBox.selBits, 0, w->as.listBox.itemCount);
|
||||
selectRange(w, w->as.listBox.anchorIdx, newSel);
|
||||
}
|
||||
// Plain arrow: just move cursor, leave selections untouched
|
||||
}
|
||||
|
||||
ensureScrollVisible(w, newSel);
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetListBoxOnMouse
|
||||
// ============================================================
|
||||
|
||||
// Mouse handling: first checks if the click is on the scrollbar (right edge),
|
||||
// then falls through to item click handling. The scrollbar hit-test uses
|
||||
// widgetScrollbarHitTest which divides the scrollbar into arrow buttons, page
|
||||
// regions, and thumb based on the click position. Item clicks are translated
|
||||
// from pixel coordinates to item index using integer division by charHeight.
|
||||
void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||
AppContextT *ctx = (AppContextT *)root->userData;
|
||||
const BitmapFontT *font = &ctx->font;
|
||||
|
||||
int32_t innerH = hit->h - LISTBOX_BORDER * 2;
|
||||
int32_t visibleRows = innerH / font->charHeight;
|
||||
bool needSb = (hit->as.listBox.itemCount > visibleRows);
|
||||
|
||||
// Clamp scroll
|
||||
int32_t maxScroll = hit->as.listBox.itemCount - visibleRows;
|
||||
|
||||
if (maxScroll < 0) {
|
||||
maxScroll = 0;
|
||||
}
|
||||
|
||||
hit->as.listBox.scrollPos = clampInt(hit->as.listBox.scrollPos, 0, maxScroll);
|
||||
|
||||
// Check if click is on the scrollbar
|
||||
if (needSb) {
|
||||
int32_t sbX = hit->x + hit->w - LISTBOX_BORDER - WGT_SB_W;
|
||||
|
||||
if (vx >= sbX) {
|
||||
int32_t relY = vy - (hit->y + LISTBOX_BORDER);
|
||||
ScrollHitE sh = widgetScrollbarHitTest(innerH, relY, hit->as.listBox.itemCount, visibleRows, hit->as.listBox.scrollPos);
|
||||
|
||||
if (sh == ScrollHitArrowDecE) {
|
||||
if (hit->as.listBox.scrollPos > 0) {
|
||||
hit->as.listBox.scrollPos--;
|
||||
}
|
||||
} else if (sh == ScrollHitArrowIncE) {
|
||||
if (hit->as.listBox.scrollPos < maxScroll) {
|
||||
hit->as.listBox.scrollPos++;
|
||||
}
|
||||
} else if (sh == ScrollHitPageDecE) {
|
||||
hit->as.listBox.scrollPos -= visibleRows;
|
||||
hit->as.listBox.scrollPos = clampInt(hit->as.listBox.scrollPos, 0, maxScroll);
|
||||
} else if (sh == ScrollHitPageIncE) {
|
||||
hit->as.listBox.scrollPos += visibleRows;
|
||||
hit->as.listBox.scrollPos = clampInt(hit->as.listBox.scrollPos, 0, maxScroll);
|
||||
} else if (sh == ScrollHitThumbE) {
|
||||
int32_t trackLen = innerH - WGT_SB_W * 2;
|
||||
int32_t thumbPos;
|
||||
int32_t thumbSize;
|
||||
widgetScrollbarThumb(trackLen, hit->as.listBox.itemCount, visibleRows, hit->as.listBox.scrollPos, &thumbPos, &thumbSize);
|
||||
|
||||
sDragScrollbar = hit;
|
||||
sDragScrollbarOrient = 0;
|
||||
sDragScrollbarOff = relY - WGT_SB_W - thumbPos;
|
||||
}
|
||||
|
||||
hit->focused = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Click on item area
|
||||
int32_t innerY = hit->y + LISTBOX_BORDER;
|
||||
int32_t relY = vy - innerY;
|
||||
|
||||
if (relY < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t clickedRow = relY / font->charHeight;
|
||||
int32_t idx = hit->as.listBox.scrollPos + clickedRow;
|
||||
|
||||
if (idx < 0 || idx >= hit->as.listBox.itemCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool multi = hit->as.listBox.multiSelect;
|
||||
bool shift = (ctx->keyModifiers & KEY_MOD_SHIFT) != 0;
|
||||
bool ctrl = (ctx->keyModifiers & KEY_MOD_CTRL) != 0;
|
||||
|
||||
hit->as.listBox.selectedIdx = idx;
|
||||
hit->focused = true;
|
||||
|
||||
if (multi && hit->as.listBox.selBits) {
|
||||
if (ctrl) {
|
||||
// Ctrl+click: toggle item, update anchor
|
||||
hit->as.listBox.selBits[idx] ^= 1;
|
||||
hit->as.listBox.anchorIdx = idx;
|
||||
} else if (shift) {
|
||||
// Shift+click: range from anchor to clicked
|
||||
memset(hit->as.listBox.selBits, 0, hit->as.listBox.itemCount);
|
||||
selectRange(hit, hit->as.listBox.anchorIdx, idx);
|
||||
} else {
|
||||
// Plain click: select only this item, update anchor
|
||||
memset(hit->as.listBox.selBits, 0, hit->as.listBox.itemCount);
|
||||
hit->as.listBox.selBits[idx] = 1;
|
||||
hit->as.listBox.anchorIdx = idx;
|
||||
}
|
||||
}
|
||||
|
||||
if (hit->onChange) {
|
||||
hit->onChange(hit);
|
||||
}
|
||||
|
||||
if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) {
|
||||
hit->onDblClick(hit);
|
||||
}
|
||||
|
||||
// Initiate drag-reorder if enabled (not from modifier clicks)
|
||||
if (hit->as.listBox.reorderable && !shift && !ctrl) {
|
||||
hit->as.listBox.dragIdx = idx;
|
||||
hit->as.listBox.dropIdx = idx;
|
||||
sDragReorder = hit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetListBoxPaint
|
||||
// ============================================================
|
||||
|
||||
// Paint: sunken bevel border, then iterate visible rows drawing text.
|
||||
// Selected items get highlight background (full row width) with contrasting
|
||||
// text color. In multi-select mode, the cursor item (selectedIdx) gets a
|
||||
// dotted focus rect overlay — this is separate from the selection highlight
|
||||
// so the user can see which item the keyboard cursor is on even when multiple
|
||||
// items are selected. The scrollbar is drawn only when needed (item count
|
||||
// exceeds visible rows), and the content width is reduced accordingly.
|
||||
void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||
|
||||
int32_t innerH = w->h - LISTBOX_BORDER * 2;
|
||||
int32_t visibleRows = innerH / font->charHeight;
|
||||
bool needSb = (w->as.listBox.itemCount > visibleRows);
|
||||
int32_t contentW = w->w - LISTBOX_BORDER * 2;
|
||||
|
||||
if (needSb) {
|
||||
contentW -= WGT_SB_W;
|
||||
}
|
||||
|
||||
// Sunken border
|
||||
BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, 2);
|
||||
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
|
||||
|
||||
// Clamp scroll position
|
||||
int32_t maxScroll = w->as.listBox.itemCount - visibleRows;
|
||||
|
||||
if (maxScroll < 0) {
|
||||
maxScroll = 0;
|
||||
}
|
||||
|
||||
w->as.listBox.scrollPos = clampInt(w->as.listBox.scrollPos, 0, maxScroll);
|
||||
|
||||
// Draw items
|
||||
int32_t innerX = w->x + LISTBOX_BORDER + LISTBOX_PAD;
|
||||
int32_t innerY = w->y + LISTBOX_BORDER;
|
||||
int32_t scrollPos = w->as.listBox.scrollPos;
|
||||
bool multi = w->as.listBox.multiSelect;
|
||||
uint8_t *selBits = w->as.listBox.selBits;
|
||||
|
||||
for (int32_t i = 0; i < visibleRows && (scrollPos + i) < w->as.listBox.itemCount; i++) {
|
||||
int32_t idx = scrollPos + i;
|
||||
int32_t iy = innerY + i * font->charHeight;
|
||||
uint32_t ifg = fg;
|
||||
uint32_t ibg = bg;
|
||||
|
||||
bool isSelected;
|
||||
|
||||
if (multi && selBits) {
|
||||
isSelected = selBits[idx] != 0;
|
||||
} else {
|
||||
isSelected = (idx == w->as.listBox.selectedIdx);
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
ifg = colors->menuHighlightFg;
|
||||
ibg = colors->menuHighlightBg;
|
||||
rectFill(d, ops, w->x + LISTBOX_BORDER, iy, contentW, font->charHeight, ibg);
|
||||
}
|
||||
|
||||
drawText(d, ops, font, innerX, iy, w->as.listBox.items[idx], ifg, ibg, false);
|
||||
|
||||
// Draw cursor indicator in multi-select (dotted focus rect on cursor item)
|
||||
if (multi && idx == w->as.listBox.selectedIdx && w->focused) {
|
||||
drawFocusRect(d, ops, w->x + LISTBOX_BORDER, iy, contentW, font->charHeight, fg);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw drag-reorder insertion indicator: a 2px horizontal line at the
|
||||
// drop position. The line is drawn between items (not on top of them)
|
||||
// so it's clear where the dragged item will be inserted.
|
||||
if (w->as.listBox.reorderable && w->as.listBox.dragIdx >= 0 && w->as.listBox.dropIdx >= 0) {
|
||||
int32_t drop = w->as.listBox.dropIdx;
|
||||
int32_t lineY = innerY + (drop - scrollPos) * font->charHeight;
|
||||
|
||||
if (lineY >= innerY && lineY <= innerY + innerH) {
|
||||
drawHLine(d, ops, w->x + LISTBOX_BORDER, lineY, contentW, colors->contentFg);
|
||||
drawHLine(d, ops, w->x + LISTBOX_BORDER, lineY + 1, contentW, colors->contentFg);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw scrollbar
|
||||
if (needSb) {
|
||||
int32_t sbX = w->x + w->w - LISTBOX_BORDER - WGT_SB_W;
|
||||
int32_t sbY = w->y + LISTBOX_BORDER;
|
||||
widgetDrawScrollbarV(d, ops, colors, sbX, sbY, innerH, w->as.listBox.itemCount, visibleRows, w->as.listBox.scrollPos);
|
||||
}
|
||||
|
||||
if (w->focused) {
|
||||
drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,161 +0,0 @@
|
|||
// widgetProgressBar.c — ProgressBar widget
|
||||
//
|
||||
// A non-interactive fill-bar widget for displaying bounded progress.
|
||||
// Supports both horizontal and vertical orientations via separate
|
||||
// constructors rather than a runtime flag, because orientation is
|
||||
// typically fixed at UI construction time and the two constructors
|
||||
// read more clearly at the call site.
|
||||
//
|
||||
// Rendering: sunken 2px bevel border (inverted highlight/shadow to
|
||||
// look recessed) with a solid fill rect proportional to value/maxValue.
|
||||
// Uses integer division for fill size -- no floating point, which would
|
||||
// be prohibitively expensive on a 486 without an FPU.
|
||||
//
|
||||
// The widget has no mouse or keyboard handlers -- it is purely display.
|
||||
// Value is controlled externally through the set/get API.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtProgressBar
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtProgressBar(WidgetT *parent) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetProgressBarE);
|
||||
|
||||
if (w) {
|
||||
w->as.progressBar.value = 0;
|
||||
w->as.progressBar.maxValue = 100;
|
||||
w->as.progressBar.vertical = false;
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtProgressBarV
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtProgressBarV(WidgetT *parent) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetProgressBarE);
|
||||
|
||||
if (w) {
|
||||
w->as.progressBar.value = 0;
|
||||
w->as.progressBar.maxValue = 100;
|
||||
w->as.progressBar.vertical = true;
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtProgressBarGetValue
|
||||
// ============================================================
|
||||
|
||||
int32_t wgtProgressBarGetValue(const WidgetT *w) {
|
||||
VALIDATE_WIDGET(w, WidgetProgressBarE, 0);
|
||||
|
||||
return w->as.progressBar.value;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtProgressBarSetValue
|
||||
// ============================================================
|
||||
|
||||
void wgtProgressBarSetValue(WidgetT *w, int32_t value) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetProgressBarE);
|
||||
|
||||
if (value < 0) {
|
||||
value = 0;
|
||||
}
|
||||
|
||||
if (value > w->as.progressBar.maxValue) {
|
||||
value = w->as.progressBar.maxValue;
|
||||
}
|
||||
|
||||
w->as.progressBar.value = value;
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetProgressBarCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
// Min size is based on font metrics to keep the bar proportional to
|
||||
// surrounding text. The cross-axis uses charHeight+4 (room for the
|
||||
// bar plus 2px bevel on each side). The main axis uses 12 char widths
|
||||
// so the bar is wide enough to show meaningful progress granularity.
|
||||
void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
if (w->as.progressBar.vertical) {
|
||||
w->calcMinW = font->charHeight + 4;
|
||||
w->calcMinH = font->charWidth * 12;
|
||||
} else {
|
||||
w->calcMinW = font->charWidth * 12;
|
||||
w->calcMinH = font->charHeight + 4;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetProgressBarPaint
|
||||
// ============================================================
|
||||
|
||||
// Paint uses a two-pass approach: first the sunken bevel border, then
|
||||
// the fill rect. The fill color defaults to activeTitleBg (the window
|
||||
// title bar color) to provide strong visual contrast inside the
|
||||
// recessed trough -- matching classic Win3.1/Motif progress bar style.
|
||||
// When disabled, the fill becomes windowShadow to look grayed out.
|
||||
//
|
||||
// Vertical bars fill from bottom-up (natural "filling" metaphor),
|
||||
// which requires computing the Y offset as (innerH - fillH).
|
||||
void widgetProgressBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
(void)font;
|
||||
uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->activeTitleBg) : colors->windowShadow;
|
||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||
|
||||
// Sunken border -- highlight/shadow are swapped vs raised bevel
|
||||
// to create the "pressed in" trough appearance
|
||||
BevelStyleT bevel;
|
||||
bevel.highlight = colors->windowShadow;
|
||||
bevel.shadow = colors->windowHighlight;
|
||||
bevel.face = bg;
|
||||
bevel.width = 2;
|
||||
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
|
||||
|
||||
// Fill bar
|
||||
int32_t maxVal = w->as.progressBar.maxValue;
|
||||
|
||||
if (maxVal <= 0) {
|
||||
maxVal = 100;
|
||||
}
|
||||
|
||||
int32_t innerW = w->w - 4;
|
||||
int32_t innerH = w->h - 4;
|
||||
|
||||
if (w->as.progressBar.vertical) {
|
||||
int32_t fillH = (innerH * w->as.progressBar.value) / maxVal;
|
||||
|
||||
if (fillH > innerH) {
|
||||
fillH = innerH;
|
||||
}
|
||||
|
||||
if (fillH > 0) {
|
||||
rectFill(d, ops, w->x + 2, w->y + 2 + innerH - fillH, innerW, fillH, fg);
|
||||
}
|
||||
} else {
|
||||
int32_t fillW = (innerW * w->as.progressBar.value) / maxVal;
|
||||
|
||||
if (fillW > innerW) {
|
||||
fillW = innerW;
|
||||
}
|
||||
|
||||
if (fillW > 0) {
|
||||
rectFill(d, ops, w->x + 2, w->y + 2, fillW, innerH, fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,282 +0,0 @@
|
|||
// widgetRadio.c — RadioGroup and Radio button widgets
|
||||
//
|
||||
// Two-level architecture: RadioGroupE is an invisible container that
|
||||
// holds the selection state (selectedIdx), while RadioE children are
|
||||
// the visible buttons. This separation means the group tracks which
|
||||
// index is selected while each radio button only knows its own index,
|
||||
// keeping the per-button state minimal.
|
||||
//
|
||||
// Selection state is stored as an integer index on the parent group
|
||||
// rather than a pointer, because indices survive widget reordering
|
||||
// and are trivially serializable. The index is auto-assigned at
|
||||
// construction time based on sibling order.
|
||||
//
|
||||
// Rendering: diamond-shaped indicator (Motif-style) using scanline
|
||||
// horizontal lines rather than a circle algorithm. This avoids any
|
||||
// need for anti-aliasing or trigonometry -- purely integer H/V lines.
|
||||
// The selection dot inside uses a hardcoded width table for a 6-row
|
||||
// filled diamond, sized to look crisp at the fixed 12px box size.
|
||||
//
|
||||
// Keyboard: Space/Enter select the current radio. Arrow keys move
|
||||
// focus AND selection together (standard radio group behavior --
|
||||
// focus and selection are coupled, unlike checkboxes).
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtRadio
|
||||
// ============================================================
|
||||
|
||||
// Auto-index is computed by counting RadioE siblings before this one.
|
||||
// This means radio buttons must be added in order and not reordered
|
||||
// after construction -- which is the normal pattern for radio groups.
|
||||
WidgetT *wgtRadio(WidgetT *parent, const char *text) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetRadioE);
|
||||
|
||||
if (w) {
|
||||
w->as.radio.text = text;
|
||||
w->accelKey = accelParse(text);
|
||||
|
||||
// Auto-assign index based on position in parent
|
||||
int32_t idx = 0;
|
||||
|
||||
for (WidgetT *c = parent->firstChild; c != w; c = c->nextSibling) {
|
||||
if (c->type == WidgetRadioE) {
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
w->as.radio.index = idx;
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtRadioGroup
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtRadioGroup(WidgetT *parent) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetRadioGroupE);
|
||||
|
||||
if (w) {
|
||||
w->as.radioGroup.selectedIdx = 0;
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetRadioGetText
|
||||
// ============================================================
|
||||
|
||||
const char *widgetRadioGetText(const WidgetT *w) {
|
||||
return w->as.radio.text;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetRadioSetText
|
||||
// ============================================================
|
||||
|
||||
void widgetRadioSetText(WidgetT *w, const char *text) {
|
||||
w->as.radio.text = text;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetRadioCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
// Shares CHECKBOX_BOX_SIZE/GAP constants with the checkbox widget
|
||||
// so radio buttons and checkboxes align when placed in the same
|
||||
// column, and the indicator + label layout is visually consistent.
|
||||
void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
|
||||
textWidthAccel(font, w->as.radio.text);
|
||||
w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetRadioOnKey
|
||||
// ============================================================
|
||||
|
||||
// Arrow keys move focus AND selection simultaneously -- this is the
|
||||
// standard radio group behavior where navigating with arrows also
|
||||
// commits the selection. This differs from checkboxes where arrows
|
||||
// just move focus and Space toggles. The coupling is deliberate:
|
||||
// radio groups represent a single mutually-exclusive choice, so
|
||||
// "looking at" an option means "choosing" it.
|
||||
//
|
||||
// Key codes use DOS BIOS scancode convention: high byte 0x01 flag
|
||||
// ORed with the scancode. 0x50=Down, 0x4D=Right, 0x48=Up, 0x4B=Left.
|
||||
void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||
(void)mod;
|
||||
|
||||
if (key == ' ' || key == 0x0D) {
|
||||
// Select this radio
|
||||
if (w->parent && w->parent->type == WidgetRadioGroupE) {
|
||||
w->parent->as.radioGroup.selectedIdx = w->as.radio.index;
|
||||
|
||||
if (w->parent->onChange) {
|
||||
w->parent->onChange(w->parent);
|
||||
}
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
} else if (key == (0x50 | 0x100) || key == (0x4D | 0x100)) {
|
||||
// Down or Right — next radio in group
|
||||
if (w->parent && w->parent->type == WidgetRadioGroupE) {
|
||||
WidgetT *next = NULL;
|
||||
|
||||
for (WidgetT *s = w->nextSibling; s; s = s->nextSibling) {
|
||||
if (s->type == WidgetRadioE && s->visible && s->enabled) {
|
||||
next = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (next) {
|
||||
w->focused = false;
|
||||
next->focused = true;
|
||||
sFocusedWidget = next;
|
||||
next->parent->as.radioGroup.selectedIdx = next->as.radio.index;
|
||||
|
||||
if (next->parent->onChange) {
|
||||
next->parent->onChange(next->parent);
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(next);
|
||||
}
|
||||
}
|
||||
} else if (key == (0x48 | 0x100) || key == (0x4B | 0x100)) {
|
||||
// Up or Left — previous radio in group
|
||||
if (w->parent && w->parent->type == WidgetRadioGroupE) {
|
||||
WidgetT *prev = NULL;
|
||||
|
||||
for (WidgetT *s = w->parent->firstChild; s && s != w; s = s->nextSibling) {
|
||||
if (s->type == WidgetRadioE && s->visible && s->enabled) {
|
||||
prev = s;
|
||||
}
|
||||
}
|
||||
|
||||
if (prev) {
|
||||
w->focused = false;
|
||||
prev->focused = true;
|
||||
sFocusedWidget = prev;
|
||||
prev->parent->as.radioGroup.selectedIdx = prev->as.radio.index;
|
||||
|
||||
if (prev->parent->onChange) {
|
||||
prev->parent->onChange(prev->parent);
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(prev);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetRadioOnMouse
|
||||
// ============================================================
|
||||
|
||||
void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||
(void)root;
|
||||
(void)vx;
|
||||
(void)vy;
|
||||
w->focused = true;
|
||||
|
||||
if (w->parent && w->parent->type == WidgetRadioGroupE) {
|
||||
w->parent->as.radioGroup.selectedIdx = w->as.radio.index;
|
||||
|
||||
if (w->parent->onChange) {
|
||||
w->parent->onChange(w->parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetRadioPaint
|
||||
// ============================================================
|
||||
|
||||
// The diamond radio indicator is rendered in three passes:
|
||||
// 1. Interior fill -- scanline-based diamond fill for the background
|
||||
// 2. Border -- upper-left edges in shadow, lower-right in highlight
|
||||
// (sunken appearance, same lighting model as bevels)
|
||||
// 3. Selection dot -- smaller filled diamond from a hardcoded 6-row
|
||||
// width table. The static const avoids recomputation per paint.
|
||||
//
|
||||
// This approach avoids floating point entirely. Each scanline's
|
||||
// left/right extent is computed with simple integer distance from
|
||||
// the midpoint, making the diamond perfectly symmetric at any size.
|
||||
void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||
int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2;
|
||||
|
||||
// Draw diamond-shaped radio box
|
||||
int32_t bx = w->x;
|
||||
int32_t mid = CHECKBOX_BOX_SIZE / 2;
|
||||
uint32_t hi = colors->windowShadow;
|
||||
uint32_t sh = colors->windowHighlight;
|
||||
|
||||
// Fill interior
|
||||
for (int32_t i = 0; i < CHECKBOX_BOX_SIZE; i++) {
|
||||
int32_t dist = i < mid ? i : CHECKBOX_BOX_SIZE - 1 - i;
|
||||
int32_t left = mid - dist;
|
||||
int32_t right = mid + dist;
|
||||
|
||||
if (right > left) {
|
||||
drawHLine(d, ops, bx + left + 1, boxY + i, right - left - 1, bg);
|
||||
}
|
||||
}
|
||||
|
||||
// Diamond border — upper-left edges get highlight, lower-right get shadow
|
||||
for (int32_t i = 0; i < mid; i++) {
|
||||
int32_t left = mid - i;
|
||||
int32_t right = mid + i;
|
||||
drawHLine(d, ops, bx + left, boxY + i, 1, hi);
|
||||
drawHLine(d, ops, bx + right, boxY + i, 1, sh);
|
||||
}
|
||||
|
||||
for (int32_t i = mid; i < CHECKBOX_BOX_SIZE; i++) {
|
||||
int32_t left = mid - (CHECKBOX_BOX_SIZE - 1 - i);
|
||||
int32_t right = mid + (CHECKBOX_BOX_SIZE - 1 - i);
|
||||
drawHLine(d, ops, bx + left, boxY + i, 1, hi);
|
||||
drawHLine(d, ops, bx + right, boxY + i, 1, sh);
|
||||
}
|
||||
|
||||
// Draw filled diamond if selected
|
||||
if (w->parent && w->parent->type == WidgetRadioGroupE &&
|
||||
w->parent->as.radioGroup.selectedIdx == w->as.radio.index) {
|
||||
uint32_t dotFg = w->enabled ? fg : colors->windowShadow;
|
||||
|
||||
static const int32_t dotW[] = {2, 4, 6, 6, 4, 2};
|
||||
|
||||
for (int32_t i = 0; i < 6; i++) {
|
||||
int32_t dw = dotW[i];
|
||||
drawHLine(d, ops, bx + mid - dw / 2, boxY + mid - 3 + i, dw, dotFg);
|
||||
}
|
||||
}
|
||||
|
||||
int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP;
|
||||
int32_t labelY = w->y + (w->h - font->charHeight) / 2;
|
||||
int32_t labelW = textWidthAccel(font, w->as.radio.text);
|
||||
|
||||
if (!w->enabled) {
|
||||
drawTextAccelEmbossed(d, ops, font, labelX, labelY, w->as.radio.text, colors);
|
||||
} else {
|
||||
drawTextAccel(d, ops, font, labelX, labelY, w->as.radio.text, fg, bg, false);
|
||||
}
|
||||
|
||||
if (w->focused) {
|
||||
drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,700 +0,0 @@
|
|||
// widgetScrollPane.c — ScrollPane container widget
|
||||
//
|
||||
// A clipping container that allows its children (laid out as a
|
||||
// vertical box) to overflow and be scrolled into view. This is the
|
||||
// general-purpose scrollable container -- unlike TreeView or ListBox
|
||||
// which have item-specific scrolling, ScrollPane can wrap any
|
||||
// collection of arbitrary child widgets.
|
||||
//
|
||||
// Architecture: ScrollPane is both a layout container and a paint
|
||||
// container (WCLASS_PAINTS_CHILDREN flag). It lays out children at
|
||||
// their virtual positions (offset by scroll position), then during
|
||||
// paint, sets a clip rect to the inner content area before painting
|
||||
// children. This means children are positioned at coordinates that
|
||||
// may be outside the visible area -- the clip rect handles hiding
|
||||
// the overflow. This is simpler and more efficient than per-widget
|
||||
// visibility culling for the small widget counts typical on 486 DOS.
|
||||
//
|
||||
// Scrollbar visibility uses a two-pass determination: first check if
|
||||
// V scrollbar is needed, then check H (accounting for the space the
|
||||
// V scrollbar consumed), then re-check V in case H scrollbar's
|
||||
// appearance reduced available height. This handles the mutual
|
||||
// dependency where adding one scrollbar may trigger the other.
|
||||
//
|
||||
// The scroll pane has its own copies of the scrollbar drawing routines
|
||||
// (drawSPHScrollbar, drawSPVScrollbar) rather than using the shared
|
||||
// widgetDrawScrollbarH/V because it uses its own SP_SB_W constant.
|
||||
// This is a minor duplication tradeoff for allowing different scrollbar
|
||||
// widths in different contexts.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
#define SP_BORDER 2
|
||||
#define SP_SB_W 14
|
||||
#define SP_PAD 0
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Prototypes
|
||||
// ============================================================
|
||||
|
||||
static void drawSPHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbW, int32_t totalW, int32_t visibleW);
|
||||
static void drawSPVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbH, int32_t totalH, int32_t visibleH);
|
||||
static void spCalcNeeds(WidgetT *w, const BitmapFontT *font, int32_t *contentMinW, int32_t *contentMinH, int32_t *innerW, int32_t *innerH, bool *needVSb, bool *needHSb);
|
||||
|
||||
|
||||
// ============================================================
|
||||
// drawSPHScrollbar
|
||||
// ============================================================
|
||||
|
||||
static void drawSPHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbW, int32_t totalW, int32_t visibleW) {
|
||||
if (sbW < SP_SB_W * 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t fg = colors->contentFg;
|
||||
|
||||
// Trough
|
||||
BevelStyleT troughBevel = BEVEL_TROUGH(colors);
|
||||
drawBevel(d, ops, sbX, sbY, sbW, SP_SB_W, &troughBevel);
|
||||
|
||||
// Left arrow button
|
||||
BevelStyleT btnBevel = BEVEL_RAISED(colors, 1);
|
||||
drawBevel(d, ops, sbX, sbY, SP_SB_W, SP_SB_W, &btnBevel);
|
||||
|
||||
{
|
||||
int32_t cx = sbX + SP_SB_W / 2;
|
||||
int32_t cy = sbY + SP_SB_W / 2;
|
||||
|
||||
for (int32_t i = 0; i < 4; i++) {
|
||||
drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, fg);
|
||||
}
|
||||
}
|
||||
|
||||
// Right arrow button
|
||||
int32_t rightX = sbX + sbW - SP_SB_W;
|
||||
drawBevel(d, ops, rightX, sbY, SP_SB_W, SP_SB_W, &btnBevel);
|
||||
|
||||
{
|
||||
int32_t cx = rightX + SP_SB_W / 2;
|
||||
int32_t cy = sbY + SP_SB_W / 2;
|
||||
|
||||
for (int32_t i = 0; i < 4; i++) {
|
||||
drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, fg);
|
||||
}
|
||||
}
|
||||
|
||||
// Thumb
|
||||
int32_t trackLen = sbW - SP_SB_W * 2;
|
||||
|
||||
if (trackLen > 0 && totalW > 0) {
|
||||
int32_t thumbPos;
|
||||
int32_t thumbSize;
|
||||
widgetScrollbarThumb(trackLen, totalW, visibleW, w->as.scrollPane.scrollPosH, &thumbPos, &thumbSize);
|
||||
drawBevel(d, ops, sbX + SP_SB_W + thumbPos, sbY, thumbSize, SP_SB_W, &btnBevel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// drawSPVScrollbar
|
||||
// ============================================================
|
||||
|
||||
static void drawSPVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbH, int32_t totalH, int32_t visibleH) {
|
||||
if (sbH < SP_SB_W * 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t fg = colors->contentFg;
|
||||
|
||||
// Trough
|
||||
BevelStyleT troughBevel = BEVEL_TROUGH(colors);
|
||||
drawBevel(d, ops, sbX, sbY, SP_SB_W, sbH, &troughBevel);
|
||||
|
||||
// Up arrow button
|
||||
BevelStyleT btnBevel = BEVEL_RAISED(colors, 1);
|
||||
drawBevel(d, ops, sbX, sbY, SP_SB_W, SP_SB_W, &btnBevel);
|
||||
|
||||
{
|
||||
int32_t cx = sbX + SP_SB_W / 2;
|
||||
int32_t cy = sbY + SP_SB_W / 2;
|
||||
|
||||
for (int32_t i = 0; i < 4; i++) {
|
||||
drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, fg);
|
||||
}
|
||||
}
|
||||
|
||||
// Down arrow button
|
||||
int32_t downY = sbY + sbH - SP_SB_W;
|
||||
drawBevel(d, ops, sbX, downY, SP_SB_W, SP_SB_W, &btnBevel);
|
||||
|
||||
{
|
||||
int32_t cx = sbX + SP_SB_W / 2;
|
||||
int32_t cy = downY + SP_SB_W / 2;
|
||||
|
||||
for (int32_t i = 0; i < 4; i++) {
|
||||
drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, fg);
|
||||
}
|
||||
}
|
||||
|
||||
// Thumb
|
||||
int32_t trackLen = sbH - SP_SB_W * 2;
|
||||
|
||||
if (trackLen > 0 && totalH > 0) {
|
||||
int32_t thumbPos;
|
||||
int32_t thumbSize;
|
||||
widgetScrollbarThumb(trackLen, totalH, visibleH, w->as.scrollPane.scrollPosV, &thumbPos, &thumbSize);
|
||||
drawBevel(d, ops, sbX, sbY + SP_SB_W + thumbPos, SP_SB_W, thumbSize, &btnBevel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// spCalcNeeds — determine scrollbar needs and inner dimensions
|
||||
// ============================================================
|
||||
|
||||
// Central sizing function called by layout, paint, and mouse handlers.
|
||||
// Computes the total content min size from children, then determines
|
||||
// which scrollbars are needed and adjusts inner dimensions accordingly.
|
||||
// The two-pass scrollbar dependency resolution handles the case where
|
||||
// adding a V scrollbar shrinks the width enough to need an H scrollbar,
|
||||
// which in turn shrinks the height enough to need a V scrollbar.
|
||||
static void spCalcNeeds(WidgetT *w, const BitmapFontT *font, int32_t *contentMinW, int32_t *contentMinH, int32_t *innerW, int32_t *innerH, bool *needVSb, bool *needHSb) {
|
||||
// Measure children
|
||||
int32_t totalMinW = 0;
|
||||
int32_t totalMinH = 0;
|
||||
int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth);
|
||||
int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth);
|
||||
int32_t count = 0;
|
||||
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (!c->visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
totalMinH += c->calcMinH;
|
||||
totalMinW = DVX_MAX(totalMinW, c->calcMinW);
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count > 1) {
|
||||
totalMinH += gap * (count - 1);
|
||||
}
|
||||
|
||||
totalMinW += pad * 2;
|
||||
totalMinH += pad * 2;
|
||||
|
||||
*contentMinW = totalMinW;
|
||||
*contentMinH = totalMinH;
|
||||
|
||||
// Available inner area
|
||||
*innerW = w->w - SP_BORDER * 2;
|
||||
*innerH = w->h - SP_BORDER * 2;
|
||||
|
||||
// Determine scrollbar needs (two-pass for mutual dependency)
|
||||
*needVSb = (totalMinH > *innerH);
|
||||
*needHSb = false;
|
||||
|
||||
if (*needVSb) {
|
||||
*innerW -= SP_SB_W;
|
||||
}
|
||||
|
||||
if (totalMinW > *innerW) {
|
||||
*needHSb = true;
|
||||
*innerH -= SP_SB_W;
|
||||
|
||||
if (!*needVSb && totalMinH > *innerH) {
|
||||
*needVSb = true;
|
||||
*innerW -= SP_SB_W;
|
||||
}
|
||||
}
|
||||
|
||||
if (*innerW < 0) {
|
||||
*innerW = 0;
|
||||
}
|
||||
|
||||
if (*innerH < 0) {
|
||||
*innerH = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetScrollPaneCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
// The scroll pane reports a deliberately small min size (just enough
|
||||
// for the scrollbar chrome) because its whole purpose is to contain
|
||||
// content that doesn't fit. However, children still need their min
|
||||
// sizes computed so spCalcNeeds can determine scrollbar visibility
|
||||
// and the layout pass can distribute space correctly.
|
||||
void widgetScrollPaneCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
// Recursively measure children so they have valid calcMinW/H
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
widgetCalcMinSizeTree(c, font);
|
||||
}
|
||||
|
||||
// The scroll pane's own min size is small — the whole point is scrolling
|
||||
w->calcMinW = SP_SB_W * 3 + SP_BORDER * 2;
|
||||
w->calcMinH = SP_SB_W * 3 + SP_BORDER * 2;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetScrollPaneLayout
|
||||
// ============================================================
|
||||
|
||||
// Layout is a vertical box layout offset by the scroll position.
|
||||
// Children are positioned at their "virtual" coordinates (baseX/baseY
|
||||
// incorporate the negative scroll offset), so they may have negative
|
||||
// or very large Y values. The paint pass clips to the visible area.
|
||||
// This means child coordinates are always absolute screen coords,
|
||||
// keeping the draw path simple -- no coordinate translation needed
|
||||
// at paint time.
|
||||
//
|
||||
// Extra space distribution uses the same weight-based algorithm as
|
||||
// the generic box layout: each child gets a share of surplus space
|
||||
// proportional to its weight/totalWeight ratio. This allows stretch
|
||||
// children inside the scrollable area.
|
||||
void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font) {
|
||||
int32_t contentMinW;
|
||||
int32_t contentMinH;
|
||||
int32_t innerW;
|
||||
int32_t innerH;
|
||||
bool needVSb;
|
||||
bool needHSb;
|
||||
|
||||
spCalcNeeds(w, font, &contentMinW, &contentMinH, &innerW, &innerH, &needVSb, &needHSb);
|
||||
|
||||
// Clamp scroll positions
|
||||
int32_t maxScrollV = contentMinH - innerH;
|
||||
int32_t maxScrollH = contentMinW - innerW;
|
||||
|
||||
if (maxScrollV < 0) {
|
||||
maxScrollV = 0;
|
||||
}
|
||||
|
||||
if (maxScrollH < 0) {
|
||||
maxScrollH = 0;
|
||||
}
|
||||
|
||||
w->as.scrollPane.scrollPosV = clampInt(w->as.scrollPane.scrollPosV, 0, maxScrollV);
|
||||
w->as.scrollPane.scrollPosH = clampInt(w->as.scrollPane.scrollPosH, 0, maxScrollH);
|
||||
|
||||
// Layout children as a vertical box at virtual size
|
||||
int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth);
|
||||
int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth);
|
||||
int32_t virtualW = DVX_MAX(innerW, contentMinW);
|
||||
int32_t virtualH = DVX_MAX(innerH, contentMinH);
|
||||
int32_t baseX = w->x + SP_BORDER - w->as.scrollPane.scrollPosH;
|
||||
int32_t baseY = w->y + SP_BORDER - w->as.scrollPane.scrollPosV;
|
||||
int32_t childW = virtualW - pad * 2;
|
||||
int32_t pos = baseY + pad;
|
||||
|
||||
if (childW < 0) {
|
||||
childW = 0;
|
||||
}
|
||||
|
||||
// Sum min sizes and weights for distribution
|
||||
int32_t totalMin = 0;
|
||||
int32_t totalWeight = 0;
|
||||
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (!c->visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
totalMin += c->calcMinH;
|
||||
totalWeight += c->weight;
|
||||
}
|
||||
|
||||
int32_t count = widgetCountVisibleChildren(w);
|
||||
int32_t totalGap = (count > 1) ? gap * (count - 1) : 0;
|
||||
int32_t availMain = virtualH - pad * 2 - totalGap;
|
||||
int32_t extraSpace = availMain - totalMin;
|
||||
|
||||
if (extraSpace < 0) {
|
||||
extraSpace = 0;
|
||||
}
|
||||
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (!c->visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t mainSize = c->calcMinH;
|
||||
|
||||
if (totalWeight > 0 && c->weight > 0 && extraSpace > 0) {
|
||||
mainSize += (extraSpace * c->weight) / totalWeight;
|
||||
}
|
||||
|
||||
c->x = baseX + pad;
|
||||
c->y = pos;
|
||||
c->w = childW;
|
||||
c->h = mainSize;
|
||||
|
||||
if (c->maxW) {
|
||||
int32_t maxPx = wgtResolveSize(c->maxW, childW, font->charWidth);
|
||||
|
||||
if (c->w > maxPx) {
|
||||
c->w = maxPx;
|
||||
}
|
||||
}
|
||||
|
||||
if (c->maxH) {
|
||||
int32_t maxPx = wgtResolveSize(c->maxH, mainSize, font->charWidth);
|
||||
|
||||
if (c->h > maxPx) {
|
||||
c->h = maxPx;
|
||||
}
|
||||
}
|
||||
|
||||
pos += mainSize + gap;
|
||||
|
||||
// Recurse into child containers
|
||||
widgetLayoutChildren(c, font);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetScrollPaneOnKey
|
||||
// ============================================================
|
||||
|
||||
// Keyboard scrolling uses font metrics for step sizes: charHeight for
|
||||
// vertical line scroll, charWidth for horizontal, and the full inner
|
||||
// height for page scroll. This makes scroll distance proportional to
|
||||
// content size, which feels natural. The early return for unhandled
|
||||
// keys avoids unnecessary invalidation.
|
||||
void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||
(void)mod;
|
||||
|
||||
AppContextT *ctx = wgtGetContext(w);
|
||||
const BitmapFontT *font = &ctx->font;
|
||||
|
||||
int32_t contentMinW;
|
||||
int32_t contentMinH;
|
||||
int32_t innerW;
|
||||
int32_t innerH;
|
||||
bool needVSb;
|
||||
bool needHSb;
|
||||
|
||||
spCalcNeeds(w, font, &contentMinW, &contentMinH, &innerW, &innerH, &needVSb, &needHSb);
|
||||
|
||||
int32_t maxScrollV = contentMinH - innerH;
|
||||
int32_t maxScrollH = contentMinW - innerW;
|
||||
|
||||
if (maxScrollV < 0) {
|
||||
maxScrollV = 0;
|
||||
}
|
||||
|
||||
if (maxScrollH < 0) {
|
||||
maxScrollH = 0;
|
||||
}
|
||||
|
||||
int32_t step = font->charHeight;
|
||||
|
||||
if (key == (0x48 | 0x100)) {
|
||||
// Up
|
||||
w->as.scrollPane.scrollPosV -= step;
|
||||
} else if (key == (0x50 | 0x100)) {
|
||||
// Down
|
||||
w->as.scrollPane.scrollPosV += step;
|
||||
} else if (key == (0x49 | 0x100)) {
|
||||
// Page Up
|
||||
w->as.scrollPane.scrollPosV -= innerH;
|
||||
} else if (key == (0x51 | 0x100)) {
|
||||
// Page Down
|
||||
w->as.scrollPane.scrollPosV += innerH;
|
||||
} else if (key == (0x47 | 0x100)) {
|
||||
// Home
|
||||
w->as.scrollPane.scrollPosV = 0;
|
||||
} else if (key == (0x4F | 0x100)) {
|
||||
// End
|
||||
w->as.scrollPane.scrollPosV = maxScrollV;
|
||||
} else if (key == (0x4B | 0x100)) {
|
||||
// Left
|
||||
w->as.scrollPane.scrollPosH -= font->charWidth;
|
||||
} else if (key == (0x4D | 0x100)) {
|
||||
// Right
|
||||
w->as.scrollPane.scrollPosH += font->charWidth;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
w->as.scrollPane.scrollPosV = clampInt(w->as.scrollPane.scrollPosV, 0, maxScrollV);
|
||||
w->as.scrollPane.scrollPosH = clampInt(w->as.scrollPane.scrollPosH, 0, maxScrollH);
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetScrollPaneOnMouse
|
||||
// ============================================================
|
||||
|
||||
// Mouse handling has priority order: V scrollbar > H scrollbar > dead
|
||||
// corner > child content. The dead corner (where H and V scrollbars
|
||||
// meet) is explicitly handled to prevent clicks from falling through
|
||||
// to content behind it. Content clicks do recursive hit-testing into
|
||||
// children and forward the mouse event, handling focus management
|
||||
// along the way. This is necessary because scroll pane has
|
||||
// WCLASS_NO_HIT_RECURSE -- the generic hit-test doesn't descend
|
||||
// into scroll pane children since their coordinates may be outside
|
||||
// the pane's visible bounds.
|
||||
void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||
AppContextT *ctx = (AppContextT *)root->userData;
|
||||
const BitmapFontT *font = &ctx->font;
|
||||
|
||||
int32_t contentMinW;
|
||||
int32_t contentMinH;
|
||||
int32_t innerW;
|
||||
int32_t innerH;
|
||||
bool needVSb;
|
||||
bool needHSb;
|
||||
|
||||
spCalcNeeds(hit, font, &contentMinW, &contentMinH, &innerW, &innerH, &needVSb, &needHSb);
|
||||
|
||||
// Clamp scroll positions
|
||||
int32_t maxScrollV = contentMinH - innerH;
|
||||
int32_t maxScrollH = contentMinW - innerW;
|
||||
|
||||
if (maxScrollV < 0) {
|
||||
maxScrollV = 0;
|
||||
}
|
||||
|
||||
if (maxScrollH < 0) {
|
||||
maxScrollH = 0;
|
||||
}
|
||||
|
||||
hit->as.scrollPane.scrollPosV = clampInt(hit->as.scrollPane.scrollPosV, 0, maxScrollV);
|
||||
hit->as.scrollPane.scrollPosH = clampInt(hit->as.scrollPane.scrollPosH, 0, maxScrollH);
|
||||
|
||||
// Check vertical scrollbar
|
||||
if (needVSb) {
|
||||
int32_t sbX = hit->x + hit->w - SP_BORDER - SP_SB_W;
|
||||
|
||||
if (vx >= sbX && vy >= hit->y + SP_BORDER && vy < hit->y + SP_BORDER + innerH) {
|
||||
int32_t sbY = hit->y + SP_BORDER;
|
||||
int32_t sbH = innerH;
|
||||
int32_t relY = vy - sbY;
|
||||
int32_t trackLen = sbH - SP_SB_W * 2;
|
||||
int32_t pageSize = innerH - font->charHeight;
|
||||
|
||||
if (pageSize < font->charHeight) {
|
||||
pageSize = font->charHeight;
|
||||
}
|
||||
|
||||
if (relY < SP_SB_W) {
|
||||
hit->as.scrollPane.scrollPosV -= font->charHeight;
|
||||
} else if (relY >= sbH - SP_SB_W) {
|
||||
hit->as.scrollPane.scrollPosV += font->charHeight;
|
||||
} else if (trackLen > 0) {
|
||||
int32_t thumbPos;
|
||||
int32_t thumbSize;
|
||||
widgetScrollbarThumb(trackLen, contentMinH, innerH, hit->as.scrollPane.scrollPosV, &thumbPos, &thumbSize);
|
||||
|
||||
int32_t trackRelY = relY - SP_SB_W;
|
||||
|
||||
if (trackRelY < thumbPos) {
|
||||
hit->as.scrollPane.scrollPosV -= pageSize;
|
||||
} else if (trackRelY >= thumbPos + thumbSize) {
|
||||
hit->as.scrollPane.scrollPosV += pageSize;
|
||||
} else {
|
||||
sDragScrollbar = hit;
|
||||
sDragScrollbarOrient = 0;
|
||||
sDragScrollbarOff = trackRelY - thumbPos;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
hit->as.scrollPane.scrollPosV = clampInt(hit->as.scrollPane.scrollPosV, 0, maxScrollV);
|
||||
wgtInvalidatePaint(hit);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check horizontal scrollbar
|
||||
if (needHSb) {
|
||||
int32_t sbY = hit->y + hit->h - SP_BORDER - SP_SB_W;
|
||||
|
||||
if (vy >= sbY && vx >= hit->x + SP_BORDER && vx < hit->x + SP_BORDER + innerW) {
|
||||
int32_t sbX = hit->x + SP_BORDER;
|
||||
int32_t sbW = innerW;
|
||||
int32_t relX = vx - sbX;
|
||||
int32_t trackLen = sbW - SP_SB_W * 2;
|
||||
int32_t pageSize = innerW - font->charWidth;
|
||||
|
||||
if (pageSize < font->charWidth) {
|
||||
pageSize = font->charWidth;
|
||||
}
|
||||
|
||||
if (relX < SP_SB_W) {
|
||||
hit->as.scrollPane.scrollPosH -= font->charWidth;
|
||||
} else if (relX >= sbW - SP_SB_W) {
|
||||
hit->as.scrollPane.scrollPosH += font->charWidth;
|
||||
} else if (trackLen > 0) {
|
||||
int32_t thumbPos;
|
||||
int32_t thumbSize;
|
||||
widgetScrollbarThumb(trackLen, contentMinW, innerW, hit->as.scrollPane.scrollPosH, &thumbPos, &thumbSize);
|
||||
|
||||
int32_t trackRelX = relX - SP_SB_W;
|
||||
|
||||
if (trackRelX < thumbPos) {
|
||||
hit->as.scrollPane.scrollPosH -= pageSize;
|
||||
} else if (trackRelX >= thumbPos + thumbSize) {
|
||||
hit->as.scrollPane.scrollPosH += pageSize;
|
||||
} else {
|
||||
sDragScrollbar = hit;
|
||||
sDragScrollbarOrient = 1;
|
||||
sDragScrollbarOff = trackRelX - thumbPos;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
hit->as.scrollPane.scrollPosH = clampInt(hit->as.scrollPane.scrollPosH, 0, maxScrollH);
|
||||
wgtInvalidatePaint(hit);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Dead corner
|
||||
if (needVSb && needHSb) {
|
||||
int32_t cornerX = hit->x + hit->w - SP_BORDER - SP_SB_W;
|
||||
int32_t cornerY = hit->y + hit->h - SP_BORDER - SP_SB_W;
|
||||
|
||||
if (vx >= cornerX && vy >= cornerY) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Click on content area — forward to child widgets
|
||||
// Children are already positioned at scroll-adjusted coordinates
|
||||
WidgetT *child = NULL;
|
||||
|
||||
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
|
||||
WidgetT *ch = widgetHitTest(c, vx, vy);
|
||||
|
||||
if (ch) {
|
||||
child = ch;
|
||||
}
|
||||
}
|
||||
|
||||
if (child && child->enabled && child->wclass && child->wclass->onMouse) {
|
||||
// Clear old focus
|
||||
if (sFocusedWidget && sFocusedWidget != child) {
|
||||
sFocusedWidget->focused = false;
|
||||
}
|
||||
|
||||
child->wclass->onMouse(child, root, vx, vy);
|
||||
|
||||
if (child->focused) {
|
||||
sFocusedWidget = child;
|
||||
}
|
||||
} else {
|
||||
hit->focused = true;
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(hit);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetScrollPanePaint
|
||||
// ============================================================
|
||||
|
||||
// Paint saves and restores the clip rect around child painting.
|
||||
// Children are painted with a clip rect that excludes the scrollbar
|
||||
// area, so children that extend past the visible content area are
|
||||
// automatically clipped. Scrollbars are painted after restoring the
|
||||
// clip rect so they're always fully visible. The dead corner (when
|
||||
// both scrollbars are present) is filled with windowFace color.
|
||||
void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||
|
||||
int32_t contentMinW;
|
||||
int32_t contentMinH;
|
||||
int32_t innerW;
|
||||
int32_t innerH;
|
||||
bool needVSb;
|
||||
bool needHSb;
|
||||
|
||||
spCalcNeeds(w, font, &contentMinW, &contentMinH, &innerW, &innerH, &needVSb, &needHSb);
|
||||
|
||||
// Clamp scroll
|
||||
int32_t maxScrollV = contentMinH - innerH;
|
||||
int32_t maxScrollH = contentMinW - innerW;
|
||||
|
||||
if (maxScrollV < 0) {
|
||||
maxScrollV = 0;
|
||||
}
|
||||
|
||||
if (maxScrollH < 0) {
|
||||
maxScrollH = 0;
|
||||
}
|
||||
|
||||
w->as.scrollPane.scrollPosV = clampInt(w->as.scrollPane.scrollPosV, 0, maxScrollV);
|
||||
w->as.scrollPane.scrollPosH = clampInt(w->as.scrollPane.scrollPosH, 0, maxScrollH);
|
||||
|
||||
// Sunken border
|
||||
BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, SP_BORDER);
|
||||
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
|
||||
|
||||
// Clip to content area and paint children
|
||||
int32_t oldClipX = d->clipX;
|
||||
int32_t oldClipY = d->clipY;
|
||||
int32_t oldClipW = d->clipW;
|
||||
int32_t oldClipH = d->clipH;
|
||||
setClipRect(d, w->x + SP_BORDER, w->y + SP_BORDER, innerW, innerH);
|
||||
|
||||
// Fill background
|
||||
rectFill(d, ops, w->x + SP_BORDER, w->y + SP_BORDER, innerW, innerH, bg);
|
||||
|
||||
// Paint children (already positioned by layout with scroll offset)
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
widgetPaintOne(c, d, ops, font, colors);
|
||||
}
|
||||
|
||||
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
||||
|
||||
// Draw scrollbars
|
||||
if (needVSb) {
|
||||
int32_t sbX = w->x + w->w - SP_BORDER - SP_SB_W;
|
||||
int32_t sbY = w->y + SP_BORDER;
|
||||
drawSPVScrollbar(w, d, ops, colors, sbX, sbY, innerH, contentMinH, innerH);
|
||||
}
|
||||
|
||||
if (needHSb) {
|
||||
int32_t sbX = w->x + SP_BORDER;
|
||||
int32_t sbY = w->y + w->h - SP_BORDER - SP_SB_W;
|
||||
drawSPHScrollbar(w, d, ops, colors, sbX, sbY, innerW, contentMinW, innerW);
|
||||
|
||||
if (needVSb) {
|
||||
rectFill(d, ops, sbX + innerW, sbY, SP_SB_W, SP_SB_W, colors->windowFace);
|
||||
}
|
||||
}
|
||||
|
||||
if (w->focused) {
|
||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||
drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtScrollPane
|
||||
// ============================================================
|
||||
|
||||
// Default weight=100 so the scroll pane stretches to fill available
|
||||
// space in its parent container. Without this, a scroll pane in a
|
||||
// vertical box would collapse to its minimal size.
|
||||
WidgetT *wgtScrollPane(WidgetT *parent) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetScrollPaneE);
|
||||
|
||||
if (w) {
|
||||
w->as.scrollPane.scrollPosV = 0;
|
||||
w->as.scrollPane.scrollPosH = 0;
|
||||
w->weight = 100;
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
|
@ -1,575 +0,0 @@
|
|||
// widgetScrollbar.c — Shared scrollbar painting and hit-testing
|
||||
//
|
||||
// These are not widgets themselves -- they are stateless rendering and
|
||||
// hit-testing utilities shared by ScrollPane, TreeView, TextArea,
|
||||
// ListBox, and ListView. Each owning widget stores its own scroll
|
||||
// position; these functions are purely geometric.
|
||||
//
|
||||
// The scrollbar model uses three parts: two arrow buttons (one at each
|
||||
// end) and a proportional thumb in the track between them. Thumb size
|
||||
// is proportional to (visibleSize / totalSize), clamped to SB_MIN_THUMB
|
||||
// to remain grabbable even when content is very large. Thumb position
|
||||
// maps linearly from scrollPos to track position.
|
||||
//
|
||||
// Arrow triangles are drawn with simple loop-based scanlines (4 rows),
|
||||
// producing 7-pixel-wide arrow glyphs. This avoids any font or bitmap
|
||||
// dependency for the scrollbar chrome.
|
||||
//
|
||||
// The minimum scrollbar length guard (sbW < WGT_SB_W * 3) ensures
|
||||
// there is at least room for both arrow buttons plus a minimal track.
|
||||
// If the container is too small, the scrollbar is simply not drawn
|
||||
// rather than rendering a corrupted mess.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
// Constants duplicated from widgetTextInput.c and widgetScrollPane.c
|
||||
// for use in widgetScrollbarDragUpdate. These are file-local in their
|
||||
// source files, so we repeat the values here to avoid cross-file
|
||||
// coupling. They must stay in sync with the originals.
|
||||
#define TEXTAREA_BORDER 2
|
||||
#define TEXTAREA_PAD 2
|
||||
#define TEXTAREA_SB_W 14
|
||||
#define SP_BORDER 2
|
||||
#define SP_SB_W 14
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetDrawScrollbarH
|
||||
// ============================================================
|
||||
|
||||
void widgetDrawScrollbarH(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbW, int32_t totalSize, int32_t visibleSize, int32_t scrollPos) {
|
||||
if (sbW < WGT_SB_W * 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Trough background
|
||||
BevelStyleT troughBevel = BEVEL_TROUGH(colors);
|
||||
drawBevel(d, ops, sbX, sbY, sbW, WGT_SB_W, &troughBevel);
|
||||
|
||||
// Left arrow button
|
||||
BevelStyleT btnBevel = BEVEL_RAISED(colors, 1);
|
||||
drawBevel(d, ops, sbX, sbY, WGT_SB_W, WGT_SB_W, &btnBevel);
|
||||
|
||||
// Left arrow triangle
|
||||
{
|
||||
int32_t cx = sbX + WGT_SB_W / 2;
|
||||
int32_t cy = sbY + WGT_SB_W / 2;
|
||||
uint32_t fg = colors->contentFg;
|
||||
|
||||
for (int32_t i = 0; i < 4; i++) {
|
||||
drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, fg);
|
||||
}
|
||||
}
|
||||
|
||||
// Right arrow button
|
||||
int32_t rightX = sbX + sbW - WGT_SB_W;
|
||||
drawBevel(d, ops, rightX, sbY, WGT_SB_W, WGT_SB_W, &btnBevel);
|
||||
|
||||
// Right arrow triangle
|
||||
{
|
||||
int32_t cx = rightX + WGT_SB_W / 2;
|
||||
int32_t cy = sbY + WGT_SB_W / 2;
|
||||
uint32_t fg = colors->contentFg;
|
||||
|
||||
for (int32_t i = 0; i < 4; i++) {
|
||||
drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, fg);
|
||||
}
|
||||
}
|
||||
|
||||
// Thumb
|
||||
int32_t trackLen = sbW - WGT_SB_W * 2;
|
||||
|
||||
if (trackLen > 0 && totalSize > 0) {
|
||||
int32_t thumbPos;
|
||||
int32_t thumbSize;
|
||||
widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
|
||||
|
||||
drawBevel(d, ops, sbX + WGT_SB_W + thumbPos, sbY, thumbSize, WGT_SB_W, &btnBevel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetDrawScrollbarV
|
||||
// ============================================================
|
||||
|
||||
void widgetDrawScrollbarV(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbH, int32_t totalSize, int32_t visibleSize, int32_t scrollPos) {
|
||||
if (sbH < WGT_SB_W * 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Trough background
|
||||
BevelStyleT troughBevel = BEVEL_TROUGH(colors);
|
||||
drawBevel(d, ops, sbX, sbY, WGT_SB_W, sbH, &troughBevel);
|
||||
|
||||
// Up arrow button
|
||||
BevelStyleT btnBevel = BEVEL_RAISED(colors, 1);
|
||||
drawBevel(d, ops, sbX, sbY, WGT_SB_W, WGT_SB_W, &btnBevel);
|
||||
|
||||
// Up arrow triangle
|
||||
{
|
||||
int32_t cx = sbX + WGT_SB_W / 2;
|
||||
int32_t cy = sbY + WGT_SB_W / 2;
|
||||
uint32_t fg = colors->contentFg;
|
||||
|
||||
for (int32_t i = 0; i < 4; i++) {
|
||||
drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, fg);
|
||||
}
|
||||
}
|
||||
|
||||
// Down arrow button
|
||||
int32_t downY = sbY + sbH - WGT_SB_W;
|
||||
drawBevel(d, ops, sbX, downY, WGT_SB_W, WGT_SB_W, &btnBevel);
|
||||
|
||||
// Down arrow triangle
|
||||
{
|
||||
int32_t cx = sbX + WGT_SB_W / 2;
|
||||
int32_t cy = downY + WGT_SB_W / 2;
|
||||
uint32_t fg = colors->contentFg;
|
||||
|
||||
for (int32_t i = 0; i < 4; i++) {
|
||||
drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, fg);
|
||||
}
|
||||
}
|
||||
|
||||
// Thumb
|
||||
int32_t trackLen = sbH - WGT_SB_W * 2;
|
||||
|
||||
if (trackLen > 0 && totalSize > 0) {
|
||||
int32_t thumbPos;
|
||||
int32_t thumbSize;
|
||||
widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
|
||||
|
||||
drawBevel(d, ops, sbX, sbY + WGT_SB_W + thumbPos, WGT_SB_W, thumbSize, &btnBevel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetScrollbarDragUpdate
|
||||
// ============================================================
|
||||
|
||||
// Handles ongoing scrollbar thumb drag for widget-internal scrollbars.
|
||||
// Converts the mouse pixel position into a scroll value using linear
|
||||
// interpolation, matching the WM-level wmScrollbarDrag logic.
|
||||
// orient: 0=vertical, 1=horizontal.
|
||||
// dragOff: mouse offset within thumb captured at drag start.
|
||||
//
|
||||
// Each widget type stores scroll state differently (row counts vs
|
||||
// pixel offsets, different struct fields), so this function switches
|
||||
// on widget type to extract the scrollbar geometry and update the
|
||||
// correct scroll field.
|
||||
void widgetScrollbarDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) {
|
||||
// Determine scrollbar geometry per widget type.
|
||||
// sbOrigin: screen coordinate of the scrollbar's top/left edge
|
||||
// sbLen: total length of the scrollbar (including arrow buttons)
|
||||
// totalSize: total content size (items, pixels, or columns)
|
||||
// visibleSize: visible portion of content
|
||||
// scrollPos: current scroll position (pointer to update)
|
||||
// maxScroll: maximum scroll value
|
||||
int32_t sbOrigin = 0;
|
||||
int32_t sbLen = 0;
|
||||
int32_t totalSize = 0;
|
||||
int32_t visibleSize = 0;
|
||||
int32_t maxScroll = 0;
|
||||
int32_t sbWidth = WGT_SB_W;
|
||||
|
||||
if (w->type == WidgetTextAreaE) {
|
||||
sbWidth = TEXTAREA_SB_W;
|
||||
|
||||
if (orient == 0) {
|
||||
// Vertical scrollbar
|
||||
AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData;
|
||||
const BitmapFontT *font = &ctx->font;
|
||||
|
||||
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W;
|
||||
int32_t visCols = innerW / font->charWidth;
|
||||
int32_t maxLL = 0;
|
||||
|
||||
// Compute max line length by scanning lines
|
||||
const char *buf = w->as.textArea.buf;
|
||||
int32_t len = w->as.textArea.len;
|
||||
int32_t lineStart = 0;
|
||||
|
||||
for (int32_t i = 0; i <= len; i++) {
|
||||
if (i == len || buf[i] == '\n') {
|
||||
int32_t ll = i - lineStart;
|
||||
|
||||
if (ll > maxLL) {
|
||||
maxLL = ll;
|
||||
}
|
||||
|
||||
lineStart = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
bool needHSb = (maxLL > visCols);
|
||||
int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0);
|
||||
int32_t visRows = innerH / font->charHeight;
|
||||
|
||||
if (visRows < 1) {
|
||||
visRows = 1;
|
||||
}
|
||||
|
||||
// Count total lines
|
||||
int32_t totalLines = 1;
|
||||
|
||||
for (int32_t i = 0; i < len; i++) {
|
||||
if (buf[i] == '\n') {
|
||||
totalLines++;
|
||||
}
|
||||
}
|
||||
|
||||
sbOrigin = w->y + TEXTAREA_BORDER;
|
||||
sbLen = innerH;
|
||||
totalSize = totalLines;
|
||||
visibleSize = visRows;
|
||||
maxScroll = totalLines - visRows;
|
||||
} else {
|
||||
// Horizontal scrollbar
|
||||
AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData;
|
||||
const BitmapFontT *font = &ctx->font;
|
||||
|
||||
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W;
|
||||
int32_t visCols = innerW / font->charWidth;
|
||||
int32_t maxLL = 0;
|
||||
|
||||
const char *buf = w->as.textArea.buf;
|
||||
int32_t len = w->as.textArea.len;
|
||||
int32_t lineStart = 0;
|
||||
|
||||
for (int32_t i = 0; i <= len; i++) {
|
||||
if (i == len || buf[i] == '\n') {
|
||||
int32_t ll = i - lineStart;
|
||||
|
||||
if (ll > maxLL) {
|
||||
maxLL = ll;
|
||||
}
|
||||
|
||||
lineStart = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t hsbW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_SB_W;
|
||||
|
||||
sbOrigin = w->x + TEXTAREA_BORDER;
|
||||
sbLen = hsbW;
|
||||
totalSize = maxLL;
|
||||
visibleSize = visCols;
|
||||
maxScroll = maxLL - visCols;
|
||||
}
|
||||
} else if (w->type == WidgetListBoxE) {
|
||||
// Vertical only
|
||||
AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData;
|
||||
const BitmapFontT *font = &ctx->font;
|
||||
|
||||
int32_t innerH = w->h - LISTBOX_BORDER * 2;
|
||||
int32_t visibleRows = innerH / font->charHeight;
|
||||
|
||||
sbOrigin = w->y + LISTBOX_BORDER;
|
||||
sbLen = innerH;
|
||||
totalSize = w->as.listBox.itemCount;
|
||||
visibleSize = visibleRows;
|
||||
maxScroll = totalSize - visibleSize;
|
||||
} else if (w->type == WidgetListViewE) {
|
||||
AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData;
|
||||
const BitmapFontT *font = &ctx->font;
|
||||
|
||||
int32_t headerH = font->charHeight + 4;
|
||||
int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH;
|
||||
int32_t innerW = w->w - LISTVIEW_BORDER * 2;
|
||||
int32_t visibleRows = innerH / font->charHeight;
|
||||
int32_t totalColW = w->as.listView->totalColW;
|
||||
bool needVSb = (w->as.listView->rowCount > visibleRows);
|
||||
|
||||
if (needVSb) {
|
||||
innerW -= WGT_SB_W;
|
||||
}
|
||||
|
||||
if (totalColW > innerW) {
|
||||
innerH -= WGT_SB_W;
|
||||
visibleRows = innerH / font->charHeight;
|
||||
|
||||
if (!needVSb && w->as.listView->rowCount > visibleRows) {
|
||||
innerW -= WGT_SB_W;
|
||||
}
|
||||
}
|
||||
|
||||
if (visibleRows < 1) {
|
||||
visibleRows = 1;
|
||||
}
|
||||
|
||||
if (orient == 0) {
|
||||
// Vertical
|
||||
sbOrigin = w->y + LISTVIEW_BORDER + headerH;
|
||||
sbLen = innerH;
|
||||
totalSize = w->as.listView->rowCount;
|
||||
visibleSize = visibleRows;
|
||||
maxScroll = totalSize - visibleSize;
|
||||
} else {
|
||||
// Horizontal
|
||||
sbOrigin = w->x + LISTVIEW_BORDER;
|
||||
sbLen = innerW;
|
||||
totalSize = totalColW;
|
||||
visibleSize = innerW;
|
||||
maxScroll = totalColW - innerW;
|
||||
}
|
||||
} else if (w->type == WidgetTreeViewE) {
|
||||
AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData;
|
||||
const BitmapFontT *font = &ctx->font;
|
||||
|
||||
int32_t totalH;
|
||||
int32_t totalW;
|
||||
int32_t innerH;
|
||||
int32_t innerW;
|
||||
bool needVSb;
|
||||
|
||||
// Walk the visible tree to compute total content dimensions.
|
||||
// This duplicates treeCalcScrollbarNeeds which is file-static,
|
||||
// so we compute it inline using the same logic.
|
||||
int32_t treeH = 0;
|
||||
int32_t treeW = 0;
|
||||
|
||||
// Count visible items and max width
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (c->type == WidgetTreeItemE && c->visible) {
|
||||
// Walk visible items
|
||||
WidgetT *item = c;
|
||||
|
||||
while (item) {
|
||||
treeH += font->charHeight;
|
||||
|
||||
// Compute depth
|
||||
int32_t depth = 0;
|
||||
WidgetT *p = item->parent;
|
||||
|
||||
while (p && p != w) {
|
||||
depth++;
|
||||
p = p->parent;
|
||||
}
|
||||
|
||||
int32_t itemW = depth * TREE_INDENT + TREE_EXPAND_SIZE + TREE_ICON_GAP;
|
||||
|
||||
if (item->as.treeItem.text) {
|
||||
itemW += (int32_t)strlen(item->as.treeItem.text) * font->charWidth;
|
||||
}
|
||||
|
||||
if (itemW > treeW) {
|
||||
treeW = itemW;
|
||||
}
|
||||
|
||||
item = widgetTreeViewNextVisible(item, w);
|
||||
}
|
||||
|
||||
break; // Only process first top-level visible chain
|
||||
}
|
||||
}
|
||||
|
||||
totalH = treeH;
|
||||
totalW = treeW;
|
||||
innerH = w->h - TREE_BORDER * 2;
|
||||
innerW = w->w - TREE_BORDER * 2;
|
||||
needVSb = (totalH > innerH);
|
||||
|
||||
if (needVSb) {
|
||||
innerW -= WGT_SB_W;
|
||||
}
|
||||
|
||||
if (totalW > innerW) {
|
||||
innerH -= WGT_SB_W;
|
||||
|
||||
if (!needVSb && totalH > innerH) {
|
||||
needVSb = true;
|
||||
innerW -= WGT_SB_W;
|
||||
}
|
||||
}
|
||||
|
||||
if (orient == 0) {
|
||||
// Vertical (pixel-based scroll)
|
||||
sbOrigin = w->y + TREE_BORDER;
|
||||
sbLen = innerH;
|
||||
totalSize = totalH;
|
||||
visibleSize = innerH;
|
||||
maxScroll = totalH - innerH;
|
||||
} else {
|
||||
// Horizontal (pixel-based scroll)
|
||||
sbOrigin = w->x + TREE_BORDER;
|
||||
sbLen = innerW;
|
||||
totalSize = totalW;
|
||||
visibleSize = innerW;
|
||||
maxScroll = totalW - innerW;
|
||||
}
|
||||
} else if (w->type == WidgetScrollPaneE) {
|
||||
sbWidth = SP_SB_W;
|
||||
|
||||
AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData;
|
||||
const BitmapFontT *font = &ctx->font;
|
||||
|
||||
// Compute content min size by measuring children
|
||||
int32_t contentMinW = 0;
|
||||
int32_t contentMinH = 0;
|
||||
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (c->visible) {
|
||||
if (c->wclass && c->wclass->calcMinSize) {
|
||||
c->wclass->calcMinSize(c, font);
|
||||
}
|
||||
|
||||
if (c->calcMinW > contentMinW) {
|
||||
contentMinW = c->calcMinW;
|
||||
}
|
||||
|
||||
contentMinH += c->calcMinH;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t innerH = w->h - SP_BORDER * 2;
|
||||
int32_t innerW = w->w - SP_BORDER * 2;
|
||||
bool needVSb = (contentMinH > innerH);
|
||||
|
||||
if (needVSb) {
|
||||
innerW -= SP_SB_W;
|
||||
}
|
||||
|
||||
if (contentMinW > innerW) {
|
||||
innerH -= SP_SB_W;
|
||||
|
||||
if (!needVSb && contentMinH > innerH) {
|
||||
needVSb = true;
|
||||
innerW -= SP_SB_W;
|
||||
}
|
||||
}
|
||||
|
||||
if (orient == 0) {
|
||||
// Vertical
|
||||
sbOrigin = w->y + SP_BORDER;
|
||||
sbLen = innerH;
|
||||
totalSize = contentMinH;
|
||||
visibleSize = innerH;
|
||||
maxScroll = contentMinH - innerH;
|
||||
} else {
|
||||
// Horizontal
|
||||
sbOrigin = w->x + SP_BORDER;
|
||||
sbLen = innerW;
|
||||
totalSize = contentMinW;
|
||||
visibleSize = innerW;
|
||||
maxScroll = contentMinW - innerW;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxScroll < 0) {
|
||||
maxScroll = 0;
|
||||
}
|
||||
|
||||
if (maxScroll == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute thumb geometry
|
||||
int32_t trackLen = sbLen - sbWidth * 2;
|
||||
|
||||
if (trackLen <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t thumbPos;
|
||||
int32_t thumbSize;
|
||||
widgetScrollbarThumb(trackLen, totalSize, visibleSize, 0, &thumbPos, &thumbSize);
|
||||
|
||||
if (trackLen <= thumbSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert mouse position to scroll value
|
||||
int32_t mousePos;
|
||||
|
||||
if (orient == 0) {
|
||||
mousePos = mouseY - sbOrigin - sbWidth - dragOff;
|
||||
} else {
|
||||
mousePos = mouseX - sbOrigin - sbWidth - dragOff;
|
||||
}
|
||||
|
||||
int32_t newScroll = (mousePos * maxScroll) / (trackLen - thumbSize);
|
||||
|
||||
if (newScroll < 0) {
|
||||
newScroll = 0;
|
||||
}
|
||||
|
||||
if (newScroll > maxScroll) {
|
||||
newScroll = maxScroll;
|
||||
}
|
||||
|
||||
// Update the widget's scroll position
|
||||
if (w->type == WidgetTextAreaE) {
|
||||
if (orient == 0) {
|
||||
w->as.textArea.scrollRow = newScroll;
|
||||
} else {
|
||||
w->as.textArea.scrollCol = newScroll;
|
||||
}
|
||||
} else if (w->type == WidgetListBoxE) {
|
||||
w->as.listBox.scrollPos = newScroll;
|
||||
} else if (w->type == WidgetListViewE) {
|
||||
if (orient == 0) {
|
||||
w->as.listView->scrollPos = newScroll;
|
||||
} else {
|
||||
w->as.listView->scrollPosH = newScroll;
|
||||
}
|
||||
} else if (w->type == WidgetTreeViewE) {
|
||||
if (orient == 0) {
|
||||
w->as.treeView.scrollPos = newScroll;
|
||||
} else {
|
||||
w->as.treeView.scrollPosH = newScroll;
|
||||
}
|
||||
} else if (w->type == WidgetScrollPaneE) {
|
||||
if (orient == 0) {
|
||||
w->as.scrollPane.scrollPosV = newScroll;
|
||||
} else {
|
||||
w->as.scrollPane.scrollPosH = newScroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetScrollbarHitTest
|
||||
// ============================================================
|
||||
|
||||
// Axis-agnostic hit test. The caller converts (vx,vy) into a 1D
|
||||
// position along the scrollbar axis (relPos) and the scrollbar
|
||||
// length (sbLen). Returns which zone was hit: arrow buttons,
|
||||
// page-up/page-down trough regions, or the thumb itself.
|
||||
// This factoring lets all scrollbar-owning widgets share the same
|
||||
// logic without duplicating per-axis code.
|
||||
ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSize, int32_t visibleSize, int32_t scrollPos) {
|
||||
if (relPos < WGT_SB_W) {
|
||||
return ScrollHitArrowDecE;
|
||||
}
|
||||
|
||||
if (relPos >= sbLen - WGT_SB_W) {
|
||||
return ScrollHitArrowIncE;
|
||||
}
|
||||
|
||||
int32_t trackLen = sbLen - WGT_SB_W * 2;
|
||||
|
||||
if (trackLen > 0) {
|
||||
int32_t thumbPos;
|
||||
int32_t thumbSize;
|
||||
widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
|
||||
|
||||
int32_t trackRel = relPos - WGT_SB_W;
|
||||
|
||||
if (trackRel < thumbPos) {
|
||||
return ScrollHitPageDecE;
|
||||
}
|
||||
|
||||
if (trackRel >= thumbPos + thumbSize) {
|
||||
return ScrollHitPageIncE;
|
||||
}
|
||||
|
||||
return ScrollHitThumbE;
|
||||
}
|
||||
|
||||
return ScrollHitNoneE;
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
// widgetSeparator.c — Separator widget (horizontal and vertical)
|
||||
//
|
||||
// A purely decorative widget that draws a 2px etched line (shadow +
|
||||
// highlight pair) to visually divide groups of widgets. The etched
|
||||
// line technique (dark line followed by light line offset by 1px)
|
||||
// creates a subtle 3D groove that matches the Motif/Win3.1 aesthetic.
|
||||
//
|
||||
// Separate constructors (wgtHSeparator/wgtVSeparator) rather than a
|
||||
// runtime orientation parameter because separators are always fixed
|
||||
// orientation at construction time.
|
||||
//
|
||||
// The min size in the cross-axis is SEPARATOR_THICKNESS (2px), while
|
||||
// the main axis min is 0 -- the separator stretches to fill the
|
||||
// available width/height in its parent layout. This makes separators
|
||||
// work correctly in both HBox and VBox containers without explicit
|
||||
// sizing.
|
||||
//
|
||||
// No mouse/key handlers -- purely decorative, non-focusable.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtHSeparator
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtHSeparator(WidgetT *parent) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetSeparatorE);
|
||||
|
||||
if (w) {
|
||||
w->as.separator.vertical = false;
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtVSeparator
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtVSeparator(WidgetT *parent) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetSeparatorE);
|
||||
|
||||
if (w) {
|
||||
w->as.separator.vertical = true;
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSeparatorCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
void widgetSeparatorCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
(void)font;
|
||||
|
||||
if (w->as.separator.vertical) {
|
||||
w->calcMinW = SEPARATOR_THICKNESS;
|
||||
w->calcMinH = 0;
|
||||
} else {
|
||||
w->calcMinW = 0;
|
||||
w->calcMinH = SEPARATOR_THICKNESS;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSeparatorPaint
|
||||
// ============================================================
|
||||
|
||||
void widgetSeparatorPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
(void)font;
|
||||
|
||||
if (w->as.separator.vertical) {
|
||||
int32_t cx = w->x + w->w / 2;
|
||||
drawVLine(d, ops, cx, w->y, w->h, colors->windowShadow);
|
||||
drawVLine(d, ops, cx + 1, w->y, w->h, colors->windowHighlight);
|
||||
} else {
|
||||
int32_t cy = w->y + w->h / 2;
|
||||
drawHLine(d, ops, w->x, cy, w->w, colors->windowShadow);
|
||||
drawHLine(d, ops, w->x, cy + 1, w->w, colors->windowHighlight);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,296 +0,0 @@
|
|||
// widgetSlider.c — Slider (trackbar) widget
|
||||
//
|
||||
// A continuous-value selector with a draggable thumb on a groove track.
|
||||
// Supports both horizontal and vertical orientation. The value maps
|
||||
// linearly to thumb position using integer arithmetic only.
|
||||
//
|
||||
// Rendering: thin sunken groove for the track, raised 2px-bevel thumb,
|
||||
// and a center tick line on the thumb for grip feedback. The groove
|
||||
// uses reversed highlight/shadow (same as progress bar trough) for
|
||||
// the recessed look. The thumb uses the standard raised bevel.
|
||||
//
|
||||
// Interaction model: clicking on the track jumps the thumb to that
|
||||
// position (instant seek), while clicking on the thumb starts a drag
|
||||
// via the global sDragSlider/sDragOffset state. The drag offset stores
|
||||
// where within the thumb the user grabbed, so the thumb doesn't snap
|
||||
// to the cursor center during drag -- this feels much more natural.
|
||||
//
|
||||
// Keyboard: arrow keys increment/decrement by a computed step that
|
||||
// scales with range (step = range/100 for ranges > 100, else 1).
|
||||
// Home/End jump to min/max. This gives ~100 keyboard steps regardless
|
||||
// of the actual value range.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtSlider
|
||||
// ============================================================
|
||||
|
||||
// Default weight=100 so the slider stretches in its parent layout.
|
||||
WidgetT *wgtSlider(WidgetT *parent, int32_t minVal, int32_t maxVal) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetSliderE);
|
||||
|
||||
if (w) {
|
||||
w->as.slider.value = minVal;
|
||||
w->as.slider.minValue = minVal;
|
||||
w->as.slider.maxValue = maxVal;
|
||||
w->as.slider.vertical = false;
|
||||
w->weight = 100;
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtSliderGetValue
|
||||
// ============================================================
|
||||
|
||||
int32_t wgtSliderGetValue(const WidgetT *w) {
|
||||
VALIDATE_WIDGET(w, WidgetSliderE, 0);
|
||||
|
||||
return w->as.slider.value;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtSliderSetValue
|
||||
// ============================================================
|
||||
|
||||
void wgtSliderSetValue(WidgetT *w, int32_t value) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetSliderE);
|
||||
|
||||
if (value < w->as.slider.minValue) {
|
||||
value = w->as.slider.minValue;
|
||||
}
|
||||
|
||||
if (value > w->as.slider.maxValue) {
|
||||
value = w->as.slider.maxValue;
|
||||
}
|
||||
|
||||
w->as.slider.value = value;
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSliderCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
// Min size: 5 thumb-widths along the main axis gives enough room for
|
||||
// the thumb to travel meaningfully. Cross-axis is thumb width + 4px
|
||||
// margin so the groove has visual breathing room around the thumb.
|
||||
void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
(void)font;
|
||||
|
||||
if (w->as.slider.vertical) {
|
||||
w->calcMinW = SLIDER_THUMB_W + 4;
|
||||
w->calcMinH = SLIDER_THUMB_W * 5;
|
||||
} else {
|
||||
w->calcMinW = SLIDER_THUMB_W * 5;
|
||||
w->calcMinH = SLIDER_THUMB_W + 4;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSliderOnKey
|
||||
// ============================================================
|
||||
|
||||
void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||
(void)mod;
|
||||
|
||||
int32_t step = 1;
|
||||
int32_t range = w->as.slider.maxValue - w->as.slider.minValue;
|
||||
|
||||
if (range > 100) {
|
||||
step = range / 100;
|
||||
}
|
||||
|
||||
if (w->as.slider.vertical) {
|
||||
if (key == (0x48 | 0x100)) {
|
||||
w->as.slider.value -= step;
|
||||
} else if (key == (0x50 | 0x100)) {
|
||||
w->as.slider.value += step;
|
||||
} else if (key == (0x47 | 0x100)) {
|
||||
w->as.slider.value = w->as.slider.minValue;
|
||||
} else if (key == (0x4F | 0x100)) {
|
||||
w->as.slider.value = w->as.slider.maxValue;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (key == (0x4B | 0x100)) {
|
||||
w->as.slider.value -= step;
|
||||
} else if (key == (0x4D | 0x100)) {
|
||||
w->as.slider.value += step;
|
||||
} else if (key == (0x47 | 0x100)) {
|
||||
w->as.slider.value = w->as.slider.minValue;
|
||||
} else if (key == (0x4F | 0x100)) {
|
||||
w->as.slider.value = w->as.slider.maxValue;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
w->as.slider.value = clampInt(w->as.slider.value, w->as.slider.minValue, w->as.slider.maxValue);
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSliderOnMouse
|
||||
// ============================================================
|
||||
|
||||
// Mouse click distinguishes between thumb hit (start drag) and track
|
||||
// hit (jump to position). The thumb hit area is the full SLIDER_THUMB_W
|
||||
// pixel range at the current thumb position. The jump-to-position
|
||||
// calculation centers the thumb at the click point by subtracting
|
||||
// half the thumb width before converting pixel position to value.
|
||||
//
|
||||
// Drag state is stored in globals (sDragSlider, sDragOffset) rather
|
||||
// than per-widget because only one slider can be dragged at a time.
|
||||
// The event loop checks sDragSlider on mouse-move to continue the drag.
|
||||
void widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||
(void)root;
|
||||
hit->focused = true;
|
||||
int32_t range = hit->as.slider.maxValue - hit->as.slider.minValue;
|
||||
|
||||
if (range <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t thumbRange;
|
||||
int32_t thumbPos;
|
||||
int32_t mousePos;
|
||||
|
||||
if (hit->as.slider.vertical) {
|
||||
thumbRange = hit->h - SLIDER_THUMB_W;
|
||||
thumbPos = ((hit->as.slider.value - hit->as.slider.minValue) * thumbRange) / range;
|
||||
mousePos = vy - hit->y;
|
||||
|
||||
if (mousePos >= thumbPos && mousePos < thumbPos + SLIDER_THUMB_W) {
|
||||
// Click on thumb — start drag
|
||||
sDragSlider = hit;
|
||||
sDragOffset = mousePos - thumbPos;
|
||||
} else {
|
||||
// Click on track — jump to position
|
||||
int32_t newVal = hit->as.slider.minValue + ((mousePos - SLIDER_THUMB_W / 2) * range) / thumbRange;
|
||||
|
||||
if (newVal < hit->as.slider.minValue) { newVal = hit->as.slider.minValue; }
|
||||
if (newVal > hit->as.slider.maxValue) { newVal = hit->as.slider.maxValue; }
|
||||
|
||||
hit->as.slider.value = newVal;
|
||||
|
||||
if (hit->onChange) {
|
||||
hit->onChange(hit);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
thumbRange = hit->w - SLIDER_THUMB_W;
|
||||
thumbPos = ((hit->as.slider.value - hit->as.slider.minValue) * thumbRange) / range;
|
||||
mousePos = vx - hit->x;
|
||||
|
||||
if (mousePos >= thumbPos && mousePos < thumbPos + SLIDER_THUMB_W) {
|
||||
// Click on thumb — start drag
|
||||
sDragSlider = hit;
|
||||
sDragOffset = mousePos - thumbPos;
|
||||
} else {
|
||||
// Click on track — jump to position
|
||||
int32_t newVal = hit->as.slider.minValue + ((mousePos - SLIDER_THUMB_W / 2) * range) / thumbRange;
|
||||
|
||||
if (newVal < hit->as.slider.minValue) { newVal = hit->as.slider.minValue; }
|
||||
if (newVal > hit->as.slider.maxValue) { newVal = hit->as.slider.maxValue; }
|
||||
|
||||
hit->as.slider.value = newVal;
|
||||
|
||||
if (hit->onChange) {
|
||||
hit->onChange(hit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSliderPaint
|
||||
// ============================================================
|
||||
|
||||
// Paint: groove track centered in the cross-axis, thumb at value
|
||||
// position. The thumb position formula is:
|
||||
// thumbPos = ((value - minValue) * thumbRange) / range
|
||||
// where thumbRange = widgetSize - SLIDER_THUMB_W.
|
||||
// This maps value linearly to pixel position using integer math only.
|
||||
// The groove is thin (SLIDER_TRACK_H = 4px) with a reversed bevel
|
||||
// for the recessed look, while the thumb is the full widget height
|
||||
// with a raised bevel for the 3D grab handle appearance.
|
||||
void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
(void)font;
|
||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||
uint32_t tickFg = w->enabled ? fg : colors->windowShadow;
|
||||
uint32_t thumbFg = w->enabled ? colors->buttonFace : colors->scrollbarTrough;
|
||||
|
||||
int32_t range = w->as.slider.maxValue - w->as.slider.minValue;
|
||||
|
||||
if (range <= 0) {
|
||||
range = 1;
|
||||
}
|
||||
|
||||
if (w->as.slider.vertical) {
|
||||
// Track groove
|
||||
int32_t trackX = w->x + (w->w - SLIDER_TRACK_H) / 2;
|
||||
BevelStyleT groove;
|
||||
groove.highlight = colors->windowShadow;
|
||||
groove.shadow = colors->windowHighlight;
|
||||
groove.face = colors->scrollbarTrough;
|
||||
groove.width = 1;
|
||||
drawBevel(d, ops, trackX, w->y, SLIDER_TRACK_H, w->h, &groove);
|
||||
|
||||
// Thumb
|
||||
int32_t thumbRange = w->h - SLIDER_THUMB_W;
|
||||
int32_t thumbY = w->y + ((w->as.slider.value - w->as.slider.minValue) * thumbRange) / range;
|
||||
|
||||
BevelStyleT thumb;
|
||||
thumb.highlight = colors->windowHighlight;
|
||||
thumb.shadow = colors->windowShadow;
|
||||
thumb.face = thumbFg;
|
||||
thumb.width = 2;
|
||||
drawBevel(d, ops, w->x, thumbY, w->w, SLIDER_THUMB_W, &thumb);
|
||||
|
||||
// Center tick on thumb
|
||||
drawHLine(d, ops, w->x + 3, thumbY + SLIDER_THUMB_W / 2, w->w - 6, tickFg);
|
||||
} else {
|
||||
// Track groove
|
||||
int32_t trackY = w->y + (w->h - SLIDER_TRACK_H) / 2;
|
||||
BevelStyleT groove;
|
||||
groove.highlight = colors->windowShadow;
|
||||
groove.shadow = colors->windowHighlight;
|
||||
groove.face = colors->scrollbarTrough;
|
||||
groove.width = 1;
|
||||
drawBevel(d, ops, w->x, trackY, w->w, SLIDER_TRACK_H, &groove);
|
||||
|
||||
// Thumb
|
||||
int32_t thumbRange = w->w - SLIDER_THUMB_W;
|
||||
int32_t thumbX = w->x + ((w->as.slider.value - w->as.slider.minValue) * thumbRange) / range;
|
||||
|
||||
BevelStyleT thumb;
|
||||
thumb.highlight = colors->windowHighlight;
|
||||
thumb.shadow = colors->windowShadow;
|
||||
thumb.face = thumbFg;
|
||||
thumb.width = 2;
|
||||
drawBevel(d, ops, thumbX, w->y, SLIDER_THUMB_W, w->h, &thumb);
|
||||
|
||||
// Center tick on thumb
|
||||
drawVLine(d, ops, thumbX + SLIDER_THUMB_W / 2, w->y + 3, w->h - 6, tickFg);
|
||||
}
|
||||
|
||||
if (w->focused) {
|
||||
drawFocusRect(d, ops, w->x, w->y, w->w, w->h, fg);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
// widgetSpacer.c — Spacer widget (invisible stretching element)
|
||||
//
|
||||
// A zero-sized invisible widget with weight=100, used purely for
|
||||
// layout control. It absorbs leftover space in box layouts, pushing
|
||||
// sibling widgets apart. Common use: place a spacer between buttons
|
||||
// in a toolbar to right-align some buttons, or between a label and
|
||||
// a control to create elastic spacing.
|
||||
//
|
||||
// Because calcMinSize returns 0x0, the spacer takes no space when
|
||||
// there is none to spare, but greedily absorbs extra space via its
|
||||
// weight. This is the simplest possible layout primitive -- no paint,
|
||||
// no mouse, no keyboard, no state. The entire widget is effectively
|
||||
// just a weight value attached to a position in the sibling list.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtSpacer
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtSpacer(WidgetT *parent) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetSpacerE);
|
||||
|
||||
if (w) {
|
||||
w->weight = 100; // spacers stretch by default
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSpacerCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
void widgetSpacerCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
(void)font;
|
||||
w->calcMinW = 0;
|
||||
w->calcMinH = 0;
|
||||
}
|
||||
|
|
@ -1,495 +0,0 @@
|
|||
// widgetSpinner.c — Spinner (numeric up/down) widget
|
||||
//
|
||||
// A hybrid widget combining a single-line text editor with up/down
|
||||
// arrow buttons for numeric value entry. The user can either click
|
||||
// the arrows, use Up/Down keys, or type a number directly.
|
||||
//
|
||||
// Design: the widget has two modes -- display mode (showing the
|
||||
// formatted value) and edit mode (allowing free-form text input).
|
||||
// Edit mode is entered on the first text-modifying keystroke and
|
||||
// committed on Enter or when arrows are clicked. Escape cancels
|
||||
// the edit and reverts to the pre-edit value. This two-mode design
|
||||
// keeps the display clean (always showing a properly formatted
|
||||
// number) while still allowing direct keyboard entry.
|
||||
//
|
||||
// The text editing delegates to widgetTextEditOnKey() -- the same
|
||||
// shared single-line editing logic used by TextInput. This gives
|
||||
// the spinner cursor movement, selection, cut/copy/paste, and undo
|
||||
// for free. Input validation filters non-digit characters before
|
||||
// they reach the editor, and only allows minus at position 0.
|
||||
//
|
||||
// Undo uses a single-level swap buffer (same as TextInput): the
|
||||
// current state is copied to undoBuf before each mutation, and
|
||||
// Ctrl+Z swaps current<->undo. This is simpler and cheaper than
|
||||
// a multi-level undo stack for the small buffers involved.
|
||||
//
|
||||
// Rendering: sunken border enclosing the text area + two stacked
|
||||
// raised-bevel arrow buttons on the right. The buttons extend to
|
||||
// the widget's right edge (including the border width) so they
|
||||
// look like they're part of the border chrome. The up/down buttons
|
||||
// split the widget height evenly.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define SPINNER_BTN_W 14
|
||||
#define SPINNER_BORDER 2
|
||||
#define SPINNER_PAD 3
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Prototypes
|
||||
// ============================================================
|
||||
|
||||
static void spinnerClampAndFormat(WidgetT *w);
|
||||
static void spinnerCommitEdit(WidgetT *w);
|
||||
static void spinnerFormat(WidgetT *w);
|
||||
static void spinnerStartEdit(WidgetT *w);
|
||||
|
||||
|
||||
// ============================================================
|
||||
// spinnerClampAndFormat
|
||||
// ============================================================
|
||||
|
||||
static void spinnerClampAndFormat(WidgetT *w) {
|
||||
if (w->as.spinner.value < w->as.spinner.minValue) {
|
||||
w->as.spinner.value = w->as.spinner.minValue;
|
||||
}
|
||||
|
||||
if (w->as.spinner.value > w->as.spinner.maxValue) {
|
||||
w->as.spinner.value = w->as.spinner.maxValue;
|
||||
}
|
||||
|
||||
spinnerFormat(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// spinnerCommitEdit
|
||||
// ============================================================
|
||||
|
||||
static void spinnerCommitEdit(WidgetT *w) {
|
||||
if (!w->as.spinner.editing) {
|
||||
return;
|
||||
}
|
||||
|
||||
w->as.spinner.editing = false;
|
||||
w->as.spinner.buf[w->as.spinner.len] = '\0';
|
||||
|
||||
int32_t val = (int32_t)strtol(w->as.spinner.buf, NULL, 10);
|
||||
w->as.spinner.value = val;
|
||||
spinnerClampAndFormat(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// spinnerFormat
|
||||
// ============================================================
|
||||
|
||||
// Format always places the cursor at the end and resets scroll/selection.
|
||||
// This is called after any value change to synchronize the text buffer
|
||||
// with the numeric value. The cursor-at-end position matches user
|
||||
// expectation after arrow-key increment/decrement.
|
||||
static void spinnerFormat(WidgetT *w) {
|
||||
w->as.spinner.len = snprintf(w->as.spinner.buf, sizeof(w->as.spinner.buf), "%d", (int)w->as.spinner.value);
|
||||
w->as.spinner.cursorPos = w->as.spinner.len;
|
||||
w->as.spinner.scrollOff = 0;
|
||||
w->as.spinner.selStart = -1;
|
||||
w->as.spinner.selEnd = -1;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// spinnerStartEdit
|
||||
// ============================================================
|
||||
|
||||
// Entering edit mode snapshots the buffer for undo so the user can
|
||||
// revert to the pre-edit formatted value. The snapshot is only taken
|
||||
// on the transition to editing, not on every keystroke, so repeated
|
||||
// typing within one edit session can be undone all at once.
|
||||
static void spinnerStartEdit(WidgetT *w) {
|
||||
if (!w->as.spinner.editing) {
|
||||
w->as.spinner.editing = true;
|
||||
|
||||
// Snapshot for undo
|
||||
memcpy(w->as.spinner.undoBuf, w->as.spinner.buf, sizeof(w->as.spinner.buf));
|
||||
w->as.spinner.undoLen = w->as.spinner.len;
|
||||
w->as.spinner.undoCursor = w->as.spinner.cursorPos;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSpinnerCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
void widgetSpinnerCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
w->calcMinW = font->charWidth * 6 + SPINNER_PAD * 2 + SPINNER_BORDER * 2 + SPINNER_BTN_W;
|
||||
w->calcMinH = font->charHeight + SPINNER_PAD * 2 + SPINNER_BORDER * 2;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSpinnerGetText
|
||||
// ============================================================
|
||||
|
||||
const char *widgetSpinnerGetText(const WidgetT *w) {
|
||||
return w->as.spinner.buf;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSpinnerOnKey
|
||||
// ============================================================
|
||||
|
||||
// Key handling has two distinct paths: navigation keys (Up/Down/PgUp/
|
||||
// PgDn) always commit any pending edit first, then adjust the numeric
|
||||
// value directly. Text keys enter edit mode and are forwarded to the
|
||||
// shared text editor. This split ensures arrow-key nudging always
|
||||
// operates on the committed value, not on partially typed text.
|
||||
//
|
||||
// Page Up/Down use step*10 for coarser adjustment, matching the
|
||||
// convention used by Windows spin controls.
|
||||
void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||
int32_t step = w->as.spinner.step;
|
||||
|
||||
// Up arrow — increment
|
||||
if (key == (0x48 | 0x100)) {
|
||||
spinnerCommitEdit(w);
|
||||
w->as.spinner.value += step;
|
||||
spinnerClampAndFormat(w);
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
return;
|
||||
}
|
||||
|
||||
// Down arrow — decrement
|
||||
if (key == (0x50 | 0x100)) {
|
||||
spinnerCommitEdit(w);
|
||||
w->as.spinner.value -= step;
|
||||
spinnerClampAndFormat(w);
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
return;
|
||||
}
|
||||
|
||||
// Page Up — increment by step * 10
|
||||
if (key == (0x49 | 0x100)) {
|
||||
spinnerCommitEdit(w);
|
||||
w->as.spinner.value += step * 10;
|
||||
spinnerClampAndFormat(w);
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
return;
|
||||
}
|
||||
|
||||
// Page Down — decrement by step * 10
|
||||
if (key == (0x51 | 0x100)) {
|
||||
spinnerCommitEdit(w);
|
||||
w->as.spinner.value -= step * 10;
|
||||
spinnerClampAndFormat(w);
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter — commit edit
|
||||
if (key == '\r' || key == '\n') {
|
||||
if (w->as.spinner.editing) {
|
||||
spinnerCommitEdit(w);
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Escape — cancel edit, revert to current value
|
||||
if (key == 27) {
|
||||
if (w->as.spinner.editing) {
|
||||
w->as.spinner.editing = false;
|
||||
spinnerFormat(w);
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter: only allow digits, minus, and control keys through to text editor
|
||||
bool isDigit = (key >= '0' && key <= '9');
|
||||
bool isMinus = (key == '-');
|
||||
bool isControl = (key < 0x20) || (key & 0x100);
|
||||
|
||||
if (!isDigit && !isMinus && !isControl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Minus only at position 0 (and only if min is negative)
|
||||
if (isMinus && (w->as.spinner.cursorPos != 0 || w->as.spinner.minValue >= 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter edit mode on first text-modifying key
|
||||
if (isDigit || isMinus || key == 8 || key == 127 || key == (0x53 | 0x100)) {
|
||||
spinnerStartEdit(w);
|
||||
}
|
||||
|
||||
// Delegate to shared text editing logic (handles cursor movement,
|
||||
// selection, cut/copy/paste, undo/redo, backspace, delete, etc.)
|
||||
widgetTextEditOnKey(w, key, mod,
|
||||
w->as.spinner.buf, (int32_t)sizeof(w->as.spinner.buf),
|
||||
&w->as.spinner.len, &w->as.spinner.cursorPos,
|
||||
&w->as.spinner.scrollOff,
|
||||
&w->as.spinner.selStart, &w->as.spinner.selEnd,
|
||||
w->as.spinner.undoBuf, &w->as.spinner.undoLen,
|
||||
&w->as.spinner.undoCursor);
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSpinnerOnMouse
|
||||
// ============================================================
|
||||
|
||||
// Mouse click regions: button area (right side) vs text area (left side).
|
||||
// Button area is split vertically at the midpoint -- top half increments,
|
||||
// bottom half decrements. Clicking a button commits any pending edit
|
||||
// before adjusting the value, same as arrow keys.
|
||||
//
|
||||
// Text area clicks compute cursor position from pixel offset using the
|
||||
// fixed-width font. Double-click selects all text (select-word doesn't
|
||||
// make sense for numbers), entering edit mode to allow replacement.
|
||||
void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||
hit->focused = true;
|
||||
|
||||
int32_t btnX = hit->x + hit->w - SPINNER_BORDER - SPINNER_BTN_W;
|
||||
int32_t midY = hit->y + hit->h / 2;
|
||||
|
||||
if (vx >= btnX) {
|
||||
// Click on button area
|
||||
spinnerCommitEdit(hit);
|
||||
|
||||
if (vy < midY) {
|
||||
hit->as.spinner.value += hit->as.spinner.step;
|
||||
} else {
|
||||
hit->as.spinner.value -= hit->as.spinner.step;
|
||||
}
|
||||
|
||||
spinnerClampAndFormat(hit);
|
||||
|
||||
if (hit->onChange) {
|
||||
hit->onChange(hit);
|
||||
}
|
||||
} else {
|
||||
// Click on text area
|
||||
AppContextT *ctx = (AppContextT *)root->userData;
|
||||
widgetTextEditMouseClick(hit, vx, vy, hit->x + SPINNER_BORDER + SPINNER_PAD, &ctx->font, hit->as.spinner.buf, hit->as.spinner.len, hit->as.spinner.scrollOff, &hit->as.spinner.cursorPos, &hit->as.spinner.selStart, &hit->as.spinner.selEnd, false, false);
|
||||
spinnerStartEdit(hit);
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(hit);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSpinnerPaint
|
||||
// ============================================================
|
||||
|
||||
// Paint uses the same 3-run text rendering approach as TextInput:
|
||||
// before-selection, selection (highlighted), after-selection. This
|
||||
// avoids overdraw and gives correct selection highlighting with only
|
||||
// one pass over the visible text. The scroll offset ensures the
|
||||
// cursor is always visible even when the number is wider than the
|
||||
// text area.
|
||||
//
|
||||
// The two buttons (up/down) extend SPINNER_BORDER pixels past the
|
||||
// button area into the widget's right border so they visually merge
|
||||
// with the outer bevel -- this is why btnW is btnW + SPINNER_BORDER
|
||||
// in the drawBevel calls.
|
||||
void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow;
|
||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||
|
||||
int32_t btnW = SPINNER_BTN_W;
|
||||
int32_t btnX = w->x + w->w - SPINNER_BORDER - btnW;
|
||||
|
||||
// Sunken border around entire widget
|
||||
BevelStyleT bevel;
|
||||
bevel.highlight = colors->windowShadow;
|
||||
bevel.shadow = colors->windowHighlight;
|
||||
bevel.face = bg;
|
||||
bevel.width = SPINNER_BORDER;
|
||||
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
|
||||
|
||||
// Text area
|
||||
int32_t textX = w->x + SPINNER_BORDER + SPINNER_PAD;
|
||||
int32_t textY = w->y + (w->h - font->charHeight) / 2;
|
||||
int32_t textW = btnX - (w->x + SPINNER_BORDER) - SPINNER_PAD * 2;
|
||||
int32_t maxChars = textW / font->charWidth;
|
||||
|
||||
if (maxChars < 0) {
|
||||
maxChars = 0;
|
||||
}
|
||||
|
||||
// Scroll to keep cursor visible
|
||||
if (w->as.spinner.cursorPos < w->as.spinner.scrollOff) {
|
||||
w->as.spinner.scrollOff = w->as.spinner.cursorPos;
|
||||
} else if (w->as.spinner.cursorPos > w->as.spinner.scrollOff + maxChars) {
|
||||
w->as.spinner.scrollOff = w->as.spinner.cursorPos - maxChars;
|
||||
}
|
||||
|
||||
int32_t off = w->as.spinner.scrollOff;
|
||||
int32_t len = w->as.spinner.len - off;
|
||||
|
||||
if (len > maxChars) {
|
||||
len = maxChars;
|
||||
}
|
||||
|
||||
if (len < 0) {
|
||||
len = 0;
|
||||
}
|
||||
|
||||
widgetTextEditPaintLine(d, ops, font, colors, textX, textY, &w->as.spinner.buf[off], len, off, w->as.spinner.cursorPos, w->as.spinner.selStart, w->as.spinner.selEnd, fg, bg, w->focused, w->x + SPINNER_BORDER, btnX - SPINNER_PAD);
|
||||
|
||||
// Up button (top half)
|
||||
int32_t btnTopH = w->h / 2;
|
||||
int32_t btnBotH = w->h - btnTopH;
|
||||
|
||||
BevelStyleT btnBevel = BEVEL_RAISED(colors, 1);
|
||||
drawBevel(d, ops, btnX, w->y, btnW + SPINNER_BORDER, btnTopH, &btnBevel);
|
||||
|
||||
// Up arrow triangle
|
||||
{
|
||||
int32_t cx = btnX + btnW / 2;
|
||||
int32_t cy = w->y + btnTopH / 2;
|
||||
|
||||
for (int32_t i = 0; i < 3; i++) {
|
||||
drawHLine(d, ops, cx - i, cy - 1 + i, 1 + i * 2, fg);
|
||||
}
|
||||
}
|
||||
|
||||
// Down button (bottom half)
|
||||
drawBevel(d, ops, btnX, w->y + btnTopH, btnW + SPINNER_BORDER, btnBotH, &btnBevel);
|
||||
|
||||
// Down arrow triangle
|
||||
{
|
||||
int32_t cx = btnX + btnW / 2;
|
||||
int32_t cy = w->y + btnTopH + btnBotH / 2;
|
||||
|
||||
for (int32_t i = 0; i < 3; i++) {
|
||||
drawHLine(d, ops, cx - i, cy + 1 - i, 1 + i * 2, fg);
|
||||
}
|
||||
}
|
||||
|
||||
// Focus rect around entire widget
|
||||
if (w->focused) {
|
||||
drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSpinnerSetText
|
||||
// ============================================================
|
||||
|
||||
void widgetSpinnerSetText(WidgetT *w, const char *text) {
|
||||
int32_t val = (int32_t)strtol(text, NULL, 10);
|
||||
w->as.spinner.value = val;
|
||||
spinnerClampAndFormat(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtSpinner
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtSpinner(WidgetT *parent, int32_t minVal, int32_t maxVal, int32_t step) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetSpinnerE);
|
||||
|
||||
if (w) {
|
||||
w->as.spinner.minValue = minVal;
|
||||
w->as.spinner.maxValue = maxVal;
|
||||
w->as.spinner.step = step > 0 ? step : 1;
|
||||
w->as.spinner.value = minVal;
|
||||
w->as.spinner.editing = false;
|
||||
w->as.spinner.selStart = -1;
|
||||
w->as.spinner.selEnd = -1;
|
||||
w->as.spinner.undoLen = 0;
|
||||
w->as.spinner.undoCursor = 0;
|
||||
spinnerFormat(w);
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtSpinnerGetValue
|
||||
// ============================================================
|
||||
|
||||
int32_t wgtSpinnerGetValue(const WidgetT *w) {
|
||||
VALIDATE_WIDGET(w, WidgetSpinnerE, 0);
|
||||
|
||||
return w->as.spinner.value;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtSpinnerSetRange
|
||||
// ============================================================
|
||||
|
||||
void wgtSpinnerSetRange(WidgetT *w, int32_t minVal, int32_t maxVal) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetSpinnerE);
|
||||
|
||||
w->as.spinner.minValue = minVal;
|
||||
w->as.spinner.maxValue = maxVal;
|
||||
spinnerClampAndFormat(w);
|
||||
wgtInvalidate(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtSpinnerSetStep
|
||||
// ============================================================
|
||||
|
||||
void wgtSpinnerSetStep(WidgetT *w, int32_t step) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetSpinnerE);
|
||||
|
||||
w->as.spinner.step = step > 0 ? step : 1;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtSpinnerSetValue
|
||||
// ============================================================
|
||||
|
||||
void wgtSpinnerSetValue(WidgetT *w, int32_t value) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetSpinnerE);
|
||||
|
||||
w->as.spinner.value = value;
|
||||
spinnerClampAndFormat(w);
|
||||
wgtInvalidate(w);
|
||||
}
|
||||
|
|
@ -1,389 +0,0 @@
|
|||
// widgetSplitter.c — Splitter (draggable divider between two panes)
|
||||
//
|
||||
// A container that divides its area between exactly two child widgets
|
||||
// with a draggable divider bar between them. The divider position
|
||||
// (dividerPos) is stored as a pixel offset from the leading edge
|
||||
// (left for vertical, top for horizontal).
|
||||
//
|
||||
// Architecture: the splitter is a special container (has its own
|
||||
// layout function) that manually positions its two children and the
|
||||
// divider bar. It does NOT use the generic box layout. Children are
|
||||
// clipped to their respective panes during painting to prevent
|
||||
// overflow into the other pane.
|
||||
//
|
||||
// The divider bar (SPLITTER_BAR_W = 5px) is drawn as a raised bevel
|
||||
// with a "gripper" pattern -- small embossed 2x2 bumps arranged
|
||||
// in a line centered on the bar. This provides visual feedback that
|
||||
// the bar is draggable, following the Win3.1/Motif convention.
|
||||
//
|
||||
// Drag state: divider dragging stores the clicked splitter widget
|
||||
// and the mouse offset within the bar in globals (sDragSplitter,
|
||||
// sDragSplitStart). The event loop handles mouse-move during drag
|
||||
// by computing the new divider position from mouse coordinates and
|
||||
// calling widgetSplitterClampPos to enforce minimum pane sizes.
|
||||
//
|
||||
// Minimum pane sizes come from children's calcMinW/H, with a floor
|
||||
// of SPLITTER_MIN_PANE (20px) to prevent panes from collapsing to
|
||||
// nothing. The clamp also ensures the divider can't be dragged past
|
||||
// where the second pane would violate its minimum.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Prototypes
|
||||
// ============================================================
|
||||
|
||||
static WidgetT *spFirstChild(WidgetT *w);
|
||||
static WidgetT *spSecondChild(WidgetT *w);
|
||||
|
||||
|
||||
// ============================================================
|
||||
// spFirstChild — get first visible child
|
||||
// ============================================================
|
||||
|
||||
// These helpers skip invisible children so the splitter works
|
||||
// correctly even if one pane is hidden. The splitter only cares
|
||||
// about the first two visible children; any additional children
|
||||
// are ignored (though this shouldn't happen in practice).
|
||||
static WidgetT *spFirstChild(WidgetT *w) {
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (c->visible) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// spSecondChild — get second visible child
|
||||
// ============================================================
|
||||
|
||||
static WidgetT *spSecondChild(WidgetT *w) {
|
||||
int32_t n = 0;
|
||||
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (c->visible) {
|
||||
n++;
|
||||
|
||||
if (n == 2) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSplitterClampPos — clamp divider to child minimums
|
||||
// ============================================================
|
||||
|
||||
void widgetSplitterClampPos(WidgetT *w, int32_t *pos) {
|
||||
WidgetT *c1 = spFirstChild(w);
|
||||
WidgetT *c2 = spSecondChild(w);
|
||||
bool vert = w->as.splitter.vertical;
|
||||
int32_t totalSize = vert ? w->w : w->h;
|
||||
|
||||
int32_t minFirst = c1 ? (vert ? c1->calcMinW : c1->calcMinH) : SPLITTER_MIN_PANE;
|
||||
int32_t minSecond = c2 ? (vert ? c2->calcMinW : c2->calcMinH) : SPLITTER_MIN_PANE;
|
||||
|
||||
if (minFirst < SPLITTER_MIN_PANE) {
|
||||
minFirst = SPLITTER_MIN_PANE;
|
||||
}
|
||||
|
||||
if (minSecond < SPLITTER_MIN_PANE) {
|
||||
minSecond = SPLITTER_MIN_PANE;
|
||||
}
|
||||
|
||||
int32_t maxPos = totalSize - SPLITTER_BAR_W - minSecond;
|
||||
|
||||
if (maxPos < minFirst) {
|
||||
maxPos = minFirst;
|
||||
}
|
||||
|
||||
*pos = clampInt(*pos, minFirst, maxPos);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSplitterCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
void widgetSplitterCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
// Recursively measure children
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
widgetCalcMinSizeTree(c, font);
|
||||
}
|
||||
|
||||
WidgetT *c1 = spFirstChild(w);
|
||||
WidgetT *c2 = spSecondChild(w);
|
||||
int32_t m1w = c1 ? c1->calcMinW : 0;
|
||||
int32_t m1h = c1 ? c1->calcMinH : 0;
|
||||
int32_t m2w = c2 ? c2->calcMinW : 0;
|
||||
int32_t m2h = c2 ? c2->calcMinH : 0;
|
||||
|
||||
if (w->as.splitter.vertical) {
|
||||
w->calcMinW = m1w + m2w + SPLITTER_BAR_W;
|
||||
w->calcMinH = DVX_MAX(m1h, m2h);
|
||||
} else {
|
||||
w->calcMinW = DVX_MAX(m1w, m2w);
|
||||
w->calcMinH = m1h + m2h + SPLITTER_BAR_W;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSplitterLayout
|
||||
// ============================================================
|
||||
|
||||
// Layout assigns each child its full pane area, then recurses into
|
||||
// child layouts. The first child gets [0, dividerPos) and the second
|
||||
// gets [dividerPos + SPLITTER_BAR_W, end). The divider position is
|
||||
// clamped before use to respect minimum pane sizes, even if the
|
||||
// user hasn't dragged yet (dividerPos=0 from construction gets
|
||||
// clamped to minFirst).
|
||||
void widgetSplitterLayout(WidgetT *w, const BitmapFontT *font) {
|
||||
WidgetT *c1 = spFirstChild(w);
|
||||
WidgetT *c2 = spSecondChild(w);
|
||||
int32_t pos = w->as.splitter.dividerPos;
|
||||
|
||||
widgetSplitterClampPos(w, &pos);
|
||||
w->as.splitter.dividerPos = pos;
|
||||
|
||||
if (w->as.splitter.vertical) {
|
||||
// Left pane
|
||||
if (c1) {
|
||||
c1->x = w->x;
|
||||
c1->y = w->y;
|
||||
c1->w = pos;
|
||||
c1->h = w->h;
|
||||
widgetLayoutChildren(c1, font);
|
||||
}
|
||||
|
||||
// Right pane
|
||||
if (c2) {
|
||||
c2->x = w->x + pos + SPLITTER_BAR_W;
|
||||
c2->y = w->y;
|
||||
c2->w = w->w - pos - SPLITTER_BAR_W;
|
||||
c2->h = w->h;
|
||||
|
||||
if (c2->w < 0) {
|
||||
c2->w = 0;
|
||||
}
|
||||
|
||||
widgetLayoutChildren(c2, font);
|
||||
}
|
||||
} else {
|
||||
// Top pane
|
||||
if (c1) {
|
||||
c1->x = w->x;
|
||||
c1->y = w->y;
|
||||
c1->w = w->w;
|
||||
c1->h = pos;
|
||||
widgetLayoutChildren(c1, font);
|
||||
}
|
||||
|
||||
// Bottom pane
|
||||
if (c2) {
|
||||
c2->x = w->x;
|
||||
c2->y = w->y + pos + SPLITTER_BAR_W;
|
||||
c2->w = w->w;
|
||||
c2->h = w->h - pos - SPLITTER_BAR_W;
|
||||
|
||||
if (c2->h < 0) {
|
||||
c2->h = 0;
|
||||
}
|
||||
|
||||
widgetLayoutChildren(c2, font);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSplitterOnMouse
|
||||
// ============================================================
|
||||
|
||||
// Mouse handling first checks if the click is on the divider bar.
|
||||
// If so, it starts a drag. If not, it recursively hit-tests into
|
||||
// children and forwards the event. This manual hit-test forwarding
|
||||
// is needed because the splitter has WCLASS_NO_HIT_RECURSE (the
|
||||
// generic hit-test would find the splitter but not recurse into its
|
||||
// clipped children).
|
||||
void widgetSplitterOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||
int32_t pos = hit->as.splitter.dividerPos;
|
||||
|
||||
// Check if click is on the divider bar
|
||||
bool onDivider;
|
||||
|
||||
if (hit->as.splitter.vertical) {
|
||||
int32_t barX = hit->x + pos;
|
||||
onDivider = (vx >= barX && vx < barX + SPLITTER_BAR_W);
|
||||
} else {
|
||||
int32_t barY = hit->y + pos;
|
||||
onDivider = (vy >= barY && vy < barY + SPLITTER_BAR_W);
|
||||
}
|
||||
|
||||
if (onDivider) {
|
||||
// Start dragging
|
||||
sDragSplitter = hit;
|
||||
|
||||
if (hit->as.splitter.vertical) {
|
||||
sDragSplitStart = vx - hit->x - pos;
|
||||
} else {
|
||||
sDragSplitStart = vy - hit->y - pos;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Forward click to child widgets
|
||||
WidgetT *child = NULL;
|
||||
|
||||
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
|
||||
WidgetT *ch = widgetHitTest(c, vx, vy);
|
||||
|
||||
if (ch) {
|
||||
child = ch;
|
||||
}
|
||||
}
|
||||
|
||||
if (child && child->enabled && child->wclass && child->wclass->onMouse) {
|
||||
if (sFocusedWidget && sFocusedWidget != child) {
|
||||
sFocusedWidget->focused = false;
|
||||
}
|
||||
|
||||
child->wclass->onMouse(child, root, vx, vy);
|
||||
|
||||
if (child->focused) {
|
||||
sFocusedWidget = child;
|
||||
}
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(hit);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetSplitterPaint
|
||||
// ============================================================
|
||||
|
||||
// Paint clips each child to its pane area, preventing overflow across
|
||||
// the divider. The clip rect is saved/restored around each child's
|
||||
// paint. The divider bar is painted last (on top) so it's always
|
||||
// visible even if a child overflows.
|
||||
//
|
||||
// The gripper bumps use the classic embossed-dot technique: a
|
||||
// highlight pixel at (x,y) and a shadow pixel at (x+1,y+1) create
|
||||
// a tiny raised bump. 11 bumps spaced 3px apart center vertically
|
||||
// (or horizontally) on the bar. This is purely decorative but
|
||||
// provides the expected visual affordance for "draggable".
|
||||
void widgetSplitterPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
int32_t pos = w->as.splitter.dividerPos;
|
||||
|
||||
// Paint first child with clip rect
|
||||
WidgetT *c1 = spFirstChild(w);
|
||||
WidgetT *c2 = spSecondChild(w);
|
||||
|
||||
int32_t oldClipX = d->clipX;
|
||||
int32_t oldClipY = d->clipY;
|
||||
int32_t oldClipW = d->clipW;
|
||||
int32_t oldClipH = d->clipH;
|
||||
|
||||
if (c1) {
|
||||
if (w->as.splitter.vertical) {
|
||||
setClipRect(d, w->x, w->y, pos, w->h);
|
||||
} else {
|
||||
setClipRect(d, w->x, w->y, w->w, pos);
|
||||
}
|
||||
|
||||
widgetPaintOne(c1, d, ops, font, colors);
|
||||
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
||||
}
|
||||
|
||||
if (c2) {
|
||||
if (w->as.splitter.vertical) {
|
||||
setClipRect(d, w->x + pos + SPLITTER_BAR_W, w->y, w->w - pos - SPLITTER_BAR_W, w->h);
|
||||
} else {
|
||||
setClipRect(d, w->x, w->y + pos + SPLITTER_BAR_W, w->w, w->h - pos - SPLITTER_BAR_W);
|
||||
}
|
||||
|
||||
widgetPaintOne(c2, d, ops, font, colors);
|
||||
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
||||
}
|
||||
|
||||
// Draw divider bar — raised bevel
|
||||
BevelStyleT bevel = BEVEL_RAISED(colors, 1);
|
||||
|
||||
if (w->as.splitter.vertical) {
|
||||
int32_t barX = w->x + pos;
|
||||
drawBevel(d, ops, barX, w->y, SPLITTER_BAR_W, w->h, &bevel);
|
||||
|
||||
// Gripper — row of embossed 2x2 bumps centered vertically
|
||||
int32_t gx = barX + 1;
|
||||
int32_t midY = w->y + w->h / 2;
|
||||
int32_t count = 5;
|
||||
|
||||
for (int32_t i = -count; i <= count; i++) {
|
||||
int32_t dy = midY + i * 3;
|
||||
drawHLine(d, ops, gx, dy, 2, colors->windowHighlight);
|
||||
drawHLine(d, ops, gx + 1, dy + 1, 2, colors->windowShadow);
|
||||
}
|
||||
} else {
|
||||
int32_t barY = w->y + pos;
|
||||
drawBevel(d, ops, w->x, barY, w->w, SPLITTER_BAR_W, &bevel);
|
||||
|
||||
// Gripper — row of embossed 2x2 bumps centered horizontally
|
||||
int32_t gy = barY + 1;
|
||||
int32_t midX = w->x + w->w / 2;
|
||||
int32_t count = 5;
|
||||
|
||||
for (int32_t i = -count; i <= count; i++) {
|
||||
int32_t dx = midX + i * 3;
|
||||
drawHLine(d, ops, dx, gy, 1, colors->windowHighlight);
|
||||
drawHLine(d, ops, dx + 1, gy, 1, colors->windowShadow);
|
||||
drawHLine(d, ops, dx, gy + 1, 1, colors->windowHighlight);
|
||||
drawHLine(d, ops, dx + 1, gy + 1, 1, colors->windowShadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtSplitter
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtSplitter(WidgetT *parent, bool vertical) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetSplitterE);
|
||||
|
||||
if (w) {
|
||||
w->as.splitter.vertical = vertical;
|
||||
w->as.splitter.dividerPos = 0;
|
||||
w->weight = 100;
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtSplitterGetPos
|
||||
// ============================================================
|
||||
|
||||
int32_t wgtSplitterGetPos(const WidgetT *w) {
|
||||
return w->as.splitter.dividerPos;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtSplitterSetPos
|
||||
// ============================================================
|
||||
|
||||
void wgtSplitterSetPos(WidgetT *w, int32_t pos) {
|
||||
w->as.splitter.dividerPos = pos;
|
||||
wgtInvalidate(w);
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
// widgetStatusBar.c — StatusBar widget
|
||||
//
|
||||
// A horizontal container that draws a sunken border around each visible
|
||||
// child to create the classic segmented status bar appearance. Children
|
||||
// are typically labels or other simple widgets, laid out by the generic
|
||||
// horizontal box layout. The status bar itself has no special layout
|
||||
// logic -- it's a standard HBox with tight padding/spacing and the
|
||||
// extra per-child sunken border decorations.
|
||||
//
|
||||
// The border drawing is done as an overlay on top of children rather
|
||||
// than as part of the child's own paint, because it wraps around
|
||||
// the child's allocated area (extending 1px past each edge). This
|
||||
// keeps the sunken-panel effect consistent regardless of the child
|
||||
// widget type.
|
||||
//
|
||||
// No mouse/key handlers -- the status bar is purely display. Children
|
||||
// that are interactive (e.g., a clickable label) handle their own events.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtStatusBar
|
||||
// ============================================================
|
||||
|
||||
// Tight 2px padding and spacing keeps the status bar compact, using
|
||||
// minimal vertical space at the bottom of a window.
|
||||
WidgetT *wgtStatusBar(WidgetT *parent) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetStatusBarE);
|
||||
|
||||
if (w) {
|
||||
w->padding = wgtPixels(2);
|
||||
w->spacing = wgtPixels(2);
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetStatusBarPaint
|
||||
// ============================================================
|
||||
|
||||
// Draws a 1px sunken bevel (reversed highlight/shadow) around each
|
||||
// child. The bevel.face=0 with width=1 means only the border lines
|
||||
// are drawn, not a filled interior -- the child paints its own content.
|
||||
void widgetStatusBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
(void)font;
|
||||
|
||||
// Draw sunken border around each child
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (!c->visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BevelStyleT bevel;
|
||||
bevel.highlight = colors->windowShadow;
|
||||
bevel.shadow = colors->windowHighlight;
|
||||
bevel.face = 0;
|
||||
bevel.width = 1;
|
||||
drawBevel(d, ops, c->x - 1, c->y - 1, c->w + 2, c->h + 2, &bevel);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,560 +0,0 @@
|
|||
// widgetTabControl.c — TabControl and TabPage widgets
|
||||
//
|
||||
// Two-level architecture: TabControlE is the container holding
|
||||
// selection state and rendering the tab header strip. TabPageE
|
||||
// children act as invisible sub-containers, each holding the content
|
||||
// widgets for that page. Only the active page's children are visible
|
||||
// and receive layout.
|
||||
//
|
||||
// Tab header rendering: each tab is a manually-drawn chrome piece
|
||||
// (not a button widget) with top/left/right edges in highlight/shadow.
|
||||
// The active tab is 2px taller than inactive tabs, extending down to
|
||||
// overlap the content panel's top border -- this creates the classic
|
||||
// "folder tab" illusion where the active tab appears connected to the
|
||||
// panel below it. The panel's top border is erased under the active
|
||||
// tab to complete the effect.
|
||||
//
|
||||
// Scrolling tab headers: when the total tab header width exceeds the
|
||||
// available space, left/right arrow buttons appear and the header area
|
||||
// becomes a clipped scrolling region. The scrollOffset tracks how many
|
||||
// pixels the tab strip has scrolled. tabEnsureVisible() auto-scrolls
|
||||
// to keep the active tab visible after keyboard navigation.
|
||||
//
|
||||
// Tab switching closes any open dropdown/combobox popup before
|
||||
// switching, because the popup's owning widget may be on the
|
||||
// now-hidden page and would become orphaned visually.
|
||||
//
|
||||
// Layout: all tab pages are positioned at the same content area
|
||||
// coordinates, but only the active page has visible=true. This means
|
||||
// widgetLayoutChildren is only called for the active page, saving
|
||||
// layout computation for hidden pages. When switching tabs, the old
|
||||
// page becomes invisible and the new page becomes visible + relaid out.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
#define TAB_ARROW_W 16
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Prototypes
|
||||
// ============================================================
|
||||
|
||||
static void tabClosePopup(void);
|
||||
static void tabEnsureVisible(WidgetT *w, const BitmapFontT *font);
|
||||
static int32_t tabHeaderTotalW(const WidgetT *w, const BitmapFontT *font);
|
||||
static bool tabNeedScroll(const WidgetT *w, const BitmapFontT *font);
|
||||
|
||||
|
||||
// ============================================================
|
||||
// tabClosePopup — close any open dropdown/combobox popup
|
||||
// ============================================================
|
||||
|
||||
static void tabClosePopup(void) {
|
||||
if (sOpenPopup) {
|
||||
if (sOpenPopup->type == WidgetDropdownE) {
|
||||
sOpenPopup->as.dropdown.open = false;
|
||||
} else if (sOpenPopup->type == WidgetComboBoxE) {
|
||||
sOpenPopup->as.comboBox.open = false;
|
||||
}
|
||||
|
||||
sOpenPopup = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// tabEnsureVisible — scroll so active tab is visible
|
||||
// ============================================================
|
||||
|
||||
static void tabEnsureVisible(WidgetT *w, const BitmapFontT *font) {
|
||||
if (!tabNeedScroll(w, font)) {
|
||||
w->as.tabControl.scrollOffset = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t headerW = w->w - TAB_ARROW_W * 2 - 4;
|
||||
|
||||
if (headerW < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find start and end X of the active tab
|
||||
int32_t tabX = 0;
|
||||
int32_t tabIdx = 0;
|
||||
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (c->type != WidgetTabPageE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t tw = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2;
|
||||
|
||||
if (tabIdx == w->as.tabControl.activeTab) {
|
||||
int32_t tabLeft = tabX - w->as.tabControl.scrollOffset;
|
||||
int32_t tabRight = tabLeft + tw;
|
||||
|
||||
if (tabLeft < 0) {
|
||||
w->as.tabControl.scrollOffset += tabLeft;
|
||||
} else if (tabRight > headerW) {
|
||||
w->as.tabControl.scrollOffset += tabRight - headerW;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
tabX += tw;
|
||||
tabIdx++;
|
||||
}
|
||||
|
||||
// Clamp
|
||||
int32_t totalW = tabHeaderTotalW(w, font);
|
||||
int32_t maxOff = totalW - headerW;
|
||||
|
||||
if (maxOff < 0) {
|
||||
maxOff = 0;
|
||||
}
|
||||
|
||||
w->as.tabControl.scrollOffset = clampInt(w->as.tabControl.scrollOffset, 0, maxOff);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// tabHeaderTotalW — total width of all tab headers
|
||||
// ============================================================
|
||||
|
||||
static int32_t tabHeaderTotalW(const WidgetT *w, const BitmapFontT *font) {
|
||||
int32_t total = 0;
|
||||
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (c->type == WidgetTabPageE) {
|
||||
total += textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// tabNeedScroll — do tab headers overflow?
|
||||
// ============================================================
|
||||
|
||||
static bool tabNeedScroll(const WidgetT *w, const BitmapFontT *font) {
|
||||
int32_t totalW = tabHeaderTotalW(w, font);
|
||||
return totalW > (w->w - 4);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtTabControl
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtTabControl(WidgetT *parent) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetTabControlE);
|
||||
|
||||
if (w) {
|
||||
w->as.tabControl.activeTab = 0;
|
||||
w->as.tabControl.scrollOffset = 0;
|
||||
w->weight = 100;
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtTabControlGetActive
|
||||
// ============================================================
|
||||
|
||||
int32_t wgtTabControlGetActive(const WidgetT *w) {
|
||||
VALIDATE_WIDGET(w, WidgetTabControlE, 0);
|
||||
|
||||
return w->as.tabControl.activeTab;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtTabControlSetActive
|
||||
// ============================================================
|
||||
|
||||
void wgtTabControlSetActive(WidgetT *w, int32_t idx) {
|
||||
VALIDATE_WIDGET_VOID(w, WidgetTabControlE);
|
||||
|
||||
w->as.tabControl.activeTab = idx;
|
||||
wgtInvalidate(w);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtTabPage
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtTabPage(WidgetT *parent, const char *title) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetTabPageE);
|
||||
|
||||
if (w) {
|
||||
w->as.tabPage.title = title;
|
||||
w->accelKey = accelParse(title);
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetTabControlCalcMinSize
|
||||
// ============================================================
|
||||
|
||||
// Min size: tab header height + the maximum min size across ALL pages
|
||||
// (not just the active one). This ensures the tab control reserves
|
||||
// enough space for the largest page, preventing resize flicker when
|
||||
// switching tabs. Children are recursively measured.
|
||||
void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
||||
int32_t maxPageW = 0;
|
||||
int32_t maxPageH = 0;
|
||||
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (c->type != WidgetTabPageE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
widgetCalcMinSizeTree(c, font);
|
||||
maxPageW = DVX_MAX(maxPageW, c->calcMinW);
|
||||
maxPageH = DVX_MAX(maxPageH, c->calcMinH);
|
||||
}
|
||||
|
||||
w->calcMinW = maxPageW + TAB_BORDER * 2;
|
||||
w->calcMinH = tabH + maxPageH + TAB_BORDER * 2;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetTabControlLayout
|
||||
// ============================================================
|
||||
|
||||
void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font) {
|
||||
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
||||
int32_t contentX = w->x + TAB_BORDER;
|
||||
int32_t contentY = w->y + tabH + TAB_BORDER;
|
||||
int32_t contentW = w->w - TAB_BORDER * 2;
|
||||
int32_t contentH = w->h - tabH - TAB_BORDER * 2;
|
||||
|
||||
if (contentW < 0) { contentW = 0; }
|
||||
if (contentH < 0) { contentH = 0; }
|
||||
|
||||
tabEnsureVisible(w, font);
|
||||
|
||||
int32_t idx = 0;
|
||||
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (c->type != WidgetTabPageE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
c->x = contentX;
|
||||
c->y = contentY;
|
||||
c->w = contentW;
|
||||
c->h = contentH;
|
||||
|
||||
if (idx == w->as.tabControl.activeTab) {
|
||||
c->visible = true;
|
||||
widgetLayoutChildren(c, font);
|
||||
} else {
|
||||
c->visible = false;
|
||||
}
|
||||
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetTabControlOnKey
|
||||
// ============================================================
|
||||
|
||||
// Keyboard navigation: Left/Right cycle through tabs with wrapping
|
||||
// (modular arithmetic). Home/End jump to first/last tab. The tab
|
||||
// control only handles these keys when it has focus -- if a child
|
||||
// widget inside the active page has focus, keys go there instead.
|
||||
void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||
(void)mod;
|
||||
|
||||
int32_t tabCount = 0;
|
||||
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (c->type == WidgetTabPageE) {
|
||||
tabCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (tabCount <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t active = w->as.tabControl.activeTab;
|
||||
|
||||
if (key == (0x4D | 0x100)) {
|
||||
active = (active + 1) % tabCount;
|
||||
} else if (key == (0x4B | 0x100)) {
|
||||
active = (active - 1 + tabCount) % tabCount;
|
||||
} else if (key == (0x47 | 0x100)) {
|
||||
active = 0;
|
||||
} else if (key == (0x4F | 0x100)) {
|
||||
active = tabCount - 1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (active != w->as.tabControl.activeTab) {
|
||||
tabClosePopup();
|
||||
w->as.tabControl.activeTab = active;
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
|
||||
wgtInvalidate(w);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetTabControlOnMouse
|
||||
// ============================================================
|
||||
|
||||
// Mouse clicks in the tab header area walk the tab list computing
|
||||
// accumulated X positions to find which tab was clicked. Only clicks
|
||||
// in the header strip (top tabH pixels) are handled here -- clicks
|
||||
// on the content area go through normal child hit-testing. Scroll
|
||||
// arrow clicks adjust scrollOffset by 4 character widths at a time.
|
||||
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||
hit->focused = true;
|
||||
AppContextT *ctx = (AppContextT *)root->userData;
|
||||
const BitmapFontT *font = &ctx->font;
|
||||
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
||||
|
||||
// Only handle clicks in the tab header area
|
||||
if (vy < hit->y || vy >= hit->y + tabH) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool scroll = tabNeedScroll(hit, font);
|
||||
|
||||
// Check scroll arrow clicks
|
||||
if (scroll) {
|
||||
int32_t totalW = tabHeaderTotalW(hit, font);
|
||||
int32_t headerW = hit->w - TAB_ARROW_W * 2 - 4;
|
||||
int32_t maxOff = totalW - headerW;
|
||||
|
||||
if (maxOff < 0) {
|
||||
maxOff = 0;
|
||||
}
|
||||
|
||||
// Left arrow
|
||||
if (vx >= hit->x && vx < hit->x + TAB_ARROW_W) {
|
||||
hit->as.tabControl.scrollOffset -= font->charWidth * 4;
|
||||
hit->as.tabControl.scrollOffset = clampInt(hit->as.tabControl.scrollOffset, 0, maxOff);
|
||||
wgtInvalidatePaint(hit);
|
||||
return;
|
||||
}
|
||||
|
||||
// Right arrow
|
||||
if (vx >= hit->x + hit->w - TAB_ARROW_W && vx < hit->x + hit->w) {
|
||||
hit->as.tabControl.scrollOffset += font->charWidth * 4;
|
||||
hit->as.tabControl.scrollOffset = clampInt(hit->as.tabControl.scrollOffset, 0, maxOff);
|
||||
wgtInvalidatePaint(hit);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Click on tab header
|
||||
int32_t headerLeft = hit->x + 2 + (scroll ? TAB_ARROW_W : 0);
|
||||
int32_t tabX = headerLeft - hit->as.tabControl.scrollOffset;
|
||||
int32_t tabIdx = 0;
|
||||
|
||||
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
|
||||
if (c->type != WidgetTabPageE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t tw = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2;
|
||||
|
||||
if (vx >= tabX && vx < tabX + tw && vx >= headerLeft) {
|
||||
if (tabIdx != hit->as.tabControl.activeTab) {
|
||||
tabClosePopup();
|
||||
hit->as.tabControl.activeTab = tabIdx;
|
||||
|
||||
if (hit->onChange) {
|
||||
hit->onChange(hit);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
tabX += tw;
|
||||
tabIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetTabControlPaint
|
||||
// ============================================================
|
||||
|
||||
// Paint order: content panel first (raised bevel below the tab strip),
|
||||
// then scroll arrows if needed, then tab headers in a clipped region,
|
||||
// then the active page's children. Tab headers are painted with a clip
|
||||
// rect so partially-scrolled tabs at the edges are cleanly truncated.
|
||||
//
|
||||
// The active tab is drawn 2px taller (extending from w->y instead of
|
||||
// w->y+2) and erases the panel's top border beneath it (2px of
|
||||
// contentBg), creating the visual connection between tab and panel.
|
||||
// Inactive tabs sit 2px lower and draw a bottom border to separate
|
||||
// them from the panel.
|
||||
//
|
||||
// Only the active page's children are painted (WCLASS_PAINTS_CHILDREN
|
||||
// flag means the generic paint won't descend into tab control children).
|
||||
// This is critical for performance on 486 -- we skip painting all
|
||||
// hidden pages entirely.
|
||||
void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
||||
bool scroll = tabNeedScroll(w, font);
|
||||
|
||||
// Content panel
|
||||
BevelStyleT panelBevel;
|
||||
panelBevel.highlight = colors->windowHighlight;
|
||||
panelBevel.shadow = colors->windowShadow;
|
||||
panelBevel.face = colors->contentBg;
|
||||
panelBevel.width = 2;
|
||||
drawBevel(d, ops, w->x, w->y + tabH, w->w, w->h - tabH, &panelBevel);
|
||||
|
||||
// Scroll arrows
|
||||
if (scroll) {
|
||||
int32_t totalW = tabHeaderTotalW(w, font);
|
||||
int32_t headerW = w->w - TAB_ARROW_W * 2 - 4;
|
||||
int32_t maxOff = totalW - headerW;
|
||||
|
||||
if (maxOff < 0) {
|
||||
maxOff = 0;
|
||||
}
|
||||
|
||||
w->as.tabControl.scrollOffset = clampInt(w->as.tabControl.scrollOffset, 0, maxOff);
|
||||
|
||||
uint32_t fg = colors->contentFg;
|
||||
BevelStyleT btnBevel = BEVEL_RAISED(colors, 1);
|
||||
|
||||
// Left arrow button
|
||||
drawBevel(d, ops, w->x, w->y, TAB_ARROW_W, tabH, &btnBevel);
|
||||
{
|
||||
int32_t cx = w->x + TAB_ARROW_W / 2;
|
||||
int32_t cy = w->y + tabH / 2;
|
||||
|
||||
for (int32_t i = 0; i < 4; i++) {
|
||||
drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, fg);
|
||||
}
|
||||
}
|
||||
|
||||
// Right arrow button
|
||||
int32_t rx = w->x + w->w - TAB_ARROW_W;
|
||||
drawBevel(d, ops, rx, w->y, TAB_ARROW_W, tabH, &btnBevel);
|
||||
{
|
||||
int32_t cx = rx + TAB_ARROW_W / 2;
|
||||
int32_t cy = w->y + tabH / 2;
|
||||
|
||||
for (int32_t i = 0; i < 4; i++) {
|
||||
drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tab headers — clip to header area
|
||||
int32_t headerLeft = w->x + 2 + (scroll ? TAB_ARROW_W : 0);
|
||||
int32_t headerRight = scroll ? (w->x + w->w - TAB_ARROW_W) : (w->x + w->w);
|
||||
|
||||
int32_t oldClipX = d->clipX;
|
||||
int32_t oldClipY = d->clipY;
|
||||
int32_t oldClipW = d->clipW;
|
||||
int32_t oldClipH = d->clipH;
|
||||
setClipRect(d, headerLeft, w->y, headerRight - headerLeft, tabH + 2);
|
||||
|
||||
int32_t tabX = headerLeft - w->as.tabControl.scrollOffset;
|
||||
int32_t tabIdx = 0;
|
||||
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (c->type != WidgetTabPageE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t tw = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2;
|
||||
bool isActive = (tabIdx == w->as.tabControl.activeTab);
|
||||
int32_t ty = isActive ? w->y : w->y + 2;
|
||||
int32_t th = isActive ? tabH + 2 : tabH;
|
||||
uint32_t tabFace = isActive ? colors->contentBg : colors->windowFace;
|
||||
|
||||
// Only draw tabs that are at least partially visible
|
||||
if (tabX + tw > headerLeft && tabX < headerRight) {
|
||||
// Fill tab background
|
||||
rectFill(d, ops, tabX + 2, ty + 2, tw - 4, th - 2, tabFace);
|
||||
|
||||
// Top edge
|
||||
drawHLine(d, ops, tabX + 2, ty, tw - 4, colors->windowHighlight);
|
||||
drawHLine(d, ops, tabX + 2, ty + 1, tw - 4, colors->windowHighlight);
|
||||
|
||||
// Left edge
|
||||
drawVLine(d, ops, tabX, ty + 2, th - 2, colors->windowHighlight);
|
||||
drawVLine(d, ops, tabX + 1, ty + 2, th - 2, colors->windowHighlight);
|
||||
|
||||
// Right edge
|
||||
drawVLine(d, ops, tabX + tw - 1, ty + 2, th - 2, colors->windowShadow);
|
||||
drawVLine(d, ops, tabX + tw - 2, ty + 2, th - 2, colors->windowShadow);
|
||||
|
||||
if (isActive) {
|
||||
// Erase panel top border under active tab
|
||||
rectFill(d, ops, tabX + 2, w->y + tabH, tw - 4, 2, colors->contentBg);
|
||||
} else {
|
||||
// Bottom edge for inactive tab
|
||||
drawHLine(d, ops, tabX, ty + th - 1, tw, colors->windowShadow);
|
||||
drawHLine(d, ops, tabX + 1, ty + th - 2, tw - 2, colors->windowShadow);
|
||||
}
|
||||
|
||||
// Tab label
|
||||
int32_t labelY = ty + TAB_PAD_V;
|
||||
|
||||
if (!isActive) {
|
||||
labelY++;
|
||||
}
|
||||
|
||||
drawTextAccel(d, ops, font, tabX + TAB_PAD_H, labelY, c->as.tabPage.title, colors->contentFg, tabFace, true);
|
||||
|
||||
if (isActive && w->focused) {
|
||||
drawFocusRect(d, ops, tabX + 3, ty + 3, tw - 6, th - 4, colors->contentFg);
|
||||
}
|
||||
}
|
||||
|
||||
tabX += tw;
|
||||
tabIdx++;
|
||||
}
|
||||
|
||||
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
||||
|
||||
// Paint only active tab page's children
|
||||
tabIdx = 0;
|
||||
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (c->type != WidgetTabPageE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tabIdx == w->as.tabControl.activeTab) {
|
||||
for (WidgetT *gc = c->firstChild; gc; gc = gc->nextSibling) {
|
||||
widgetPaintOne(gc, d, ops, font, colors);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
tabIdx++;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,54 +0,0 @@
|
|||
// widgetToolbar.c — Toolbar widget
|
||||
//
|
||||
// A horizontal container with a raised 1px bevel background, used to
|
||||
// hold buttons, separators, spacers, and other control widgets in a
|
||||
// strip at the top of a window. Like StatusBar, the toolbar is a
|
||||
// standard HBox layout with custom paint -- child widgets position
|
||||
// themselves via the generic horizontal box algorithm.
|
||||
//
|
||||
// The 1px raised bevel (vs the 2px used for window chrome) keeps the
|
||||
// toolbar visually distinct but lightweight. The bevel's bottom shadow
|
||||
// line serves as the separator between the toolbar and the content
|
||||
// area below it.
|
||||
//
|
||||
// Tight 2px padding and spacing match the StatusBar for visual
|
||||
// consistency between top and bottom chrome bars. Children (typically
|
||||
// ImageButtons or small Buttons) handle their own mouse/key events.
|
||||
|
||||
#include "widgetInternal.h"
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtToolbar
|
||||
// ============================================================
|
||||
|
||||
WidgetT *wgtToolbar(WidgetT *parent) {
|
||||
WidgetT *w = widgetAlloc(parent, WidgetToolbarE);
|
||||
|
||||
if (w) {
|
||||
w->padding = wgtPixels(2);
|
||||
w->spacing = wgtPixels(2);
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetToolbarPaint
|
||||
// ============================================================
|
||||
|
||||
// Raised bevel with windowFace fill. The 1px bevel width is
|
||||
// intentionally thinner than window chrome (2px) to keep the
|
||||
// toolbar from looking too heavy.
|
||||
void widgetToolbarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||
(void)font;
|
||||
|
||||
// Draw raised background and bottom separator
|
||||
BevelStyleT bevel;
|
||||
bevel.highlight = colors->windowHighlight;
|
||||
bevel.shadow = colors->windowShadow;
|
||||
bevel.face = colors->windowFace;
|
||||
bevel.width = 1;
|
||||
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,56 +0,0 @@
|
|||
# DVX Shell Makefile for DJGPP cross-compilation
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
EXE2COFF = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/exe2coff
|
||||
CWSDSTUB = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/CWSDSTUB.EXE
|
||||
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../dvx -I../tasks
|
||||
LDFLAGS = -L../lib -ldvx -ltasks -lm
|
||||
|
||||
OBJDIR = ../obj/dvxshell
|
||||
BINDIR = ../bin
|
||||
CONFIGDIR = ../bin/config
|
||||
LIBDIR = ../lib
|
||||
|
||||
SRCS = shellMain.c shellApp.c shellExport.c shellInfo.c
|
||||
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
||||
TARGET = $(BINDIR)/dvx.exe
|
||||
|
||||
.PHONY: all clean libs
|
||||
|
||||
all: libs $(TARGET) $(CONFIGDIR)/dvx.ini
|
||||
|
||||
libs:
|
||||
$(MAKE) -C ../dvx
|
||||
$(MAKE) -C ../tasks
|
||||
|
||||
$(TARGET): $(OBJS) $(LIBDIR)/libdvx.a $(LIBDIR)/libtasks.a | $(BINDIR)
|
||||
$(CC) $(CFLAGS) -o $@ $(OBJS) $(LDFLAGS) -Wl,-Map=$(BINDIR)/dvx.map
|
||||
$(EXE2COFF) $@
|
||||
cat $(CWSDSTUB) $(BINDIR)/dvx > $@
|
||||
rm -f $(BINDIR)/dvx
|
||||
|
||||
$(CONFIGDIR)/dvx.ini: ../dvx.ini | $(CONFIGDIR)
|
||||
cp $< $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(BINDIR):
|
||||
mkdir -p $(BINDIR)
|
||||
|
||||
$(CONFIGDIR):
|
||||
mkdir -p $(CONFIGDIR)
|
||||
|
||||
# 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
|
||||
clean:
|
||||
rm -f $(OBJS) $(TARGET) $(BINDIR)/dvx.map $(BINDIR)/dvx.log
|
||||
rm -rf $(CONFIGDIR)
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
# DVX Shell
|
||||
|
||||
Windows 3.x-style desktop shell for DOS. Loads applications as DXE3 shared
|
||||
libraries and includes crash
|
||||
recovery so one bad app doesn't take down the system.
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
make # builds ../bin/dvx.exe
|
||||
make clean # removes objects and binary
|
||||
```
|
||||
|
||||
Requires `lib/libdvx.a` and `lib/libtasks.a` to be built first.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `shellMain.c` | Entry point, main loop, crash recovery, logging |
|
||||
| `shellApp.c` | App loading (dlopen), lifecycle, reaping, resource tracking |
|
||||
| `shellApp.h` | ShellAppT, AppDescriptorT, AppStateE, DxeAppContextT, shell API |
|
||||
| `shellExport.c` | DXE export table and wrapper functions |
|
||||
| `Makefile` | Build rules, links `-ldvx -ltasks -ldxe -lm` |
|
||||
|
||||
## Shell Main Loop
|
||||
|
||||
Each iteration of the main loop:
|
||||
|
||||
1. `dvxUpdate()` — process input events, dispatch callbacks, composite dirty rects
|
||||
2. `tsYield()` — give CPU time to main-loop app tasks
|
||||
3. `shellReapApps()` — clean up apps that terminated this frame
|
||||
4. `desktopUpdate()` — notify the desktop app if anything changed
|
||||
|
||||
An idle callback (`idleYield`) yields to app tasks during quiet periods when
|
||||
there are no events or dirty rects to process.
|
||||
|
||||
## DXE App Contract
|
||||
|
||||
Every `.app` file must export these symbols:
|
||||
|
||||
```c
|
||||
// Required: app metadata
|
||||
AppDescriptorT appDescriptor = {
|
||||
.name = "My App",
|
||||
.hasMainLoop = false,
|
||||
.multiInstance = false,
|
||||
.stackSize = SHELL_STACK_DEFAULT,
|
||||
.priority = TS_PRIORITY_NORMAL
|
||||
};
|
||||
|
||||
// Required: entry point
|
||||
int32_t appMain(DxeAppContextT *ctx);
|
||||
|
||||
// Optional: graceful shutdown hook
|
||||
void appShutdown(void);
|
||||
```
|
||||
|
||||
### AppDescriptorT Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | `char[64]` | Display name shown in Task Manager |
|
||||
| `hasMainLoop` | `bool` | `true` = gets its own cooperative task; `false` = callback-only |
|
||||
| `multiInstance` | `bool` | `true` = allow multiple instances via temp file copy |
|
||||
| `stackSize` | `int32_t` | Task stack size (`SHELL_STACK_DEFAULT` for 8 KB default) |
|
||||
| `priority` | `int32_t` | Task priority (`TS_PRIORITY_LOW`/`NORMAL`/`HIGH`) |
|
||||
|
||||
### DxeAppContextT
|
||||
|
||||
Passed to `appMain()`:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `shellCtx` | `AppContextT *` | The shell's GUI context for creating windows, drawing, etc. |
|
||||
| `appId` | `int32_t` | This app's unique ID (1-based slot index) |
|
||||
| `appDir` | `char[260]` | Directory containing the `.app` file for relative resource paths |
|
||||
|
||||
## App Types
|
||||
|
||||
**Callback-only** (`hasMainLoop = false`):
|
||||
- `appMain` called in shell's task 0, creates windows, registers callbacks, returns 0
|
||||
- App lives through event callbacks dispatched by `dvxUpdate()`
|
||||
- Lifecycle ends when the last window is closed
|
||||
|
||||
**Main-loop** (`hasMainLoop = true`):
|
||||
- Shell creates a cooperative task via `tsCreate()`
|
||||
- `appMain` runs in that task with its own loop calling `tsYield()`
|
||||
- Lifecycle ends when `appMain` returns
|
||||
|
||||
## Multi-Instance Support
|
||||
|
||||
DXE3's `dlopen` is reference-counted per path: loading the same `.app` twice
|
||||
returns the same handle, sharing all global/static state. For apps that support
|
||||
multiple instances (`multiInstance = true`), the shell copies the `.app` to a
|
||||
temp file before loading, giving each instance independent code and data. The
|
||||
temp file is cleaned up when the app terminates.
|
||||
|
||||
Apps that don't support multiple instances (`multiInstance = false`, the default)
|
||||
are blocked from loading a second time with an error message.
|
||||
|
||||
Temp file paths use the `TEMP` or `TMP` environment variable if set, falling
|
||||
back to the current directory.
|
||||
|
||||
## Resource Tracking
|
||||
|
||||
The shell tracks which app owns which windows via `sCurrentAppId`, a global
|
||||
set before calling any app code. The shell's `dvxCreateWindow` wrapper stamps
|
||||
`win->appId` with the current app ID. On termination, the shell destroys all
|
||||
windows belonging to the app.
|
||||
|
||||
## Crash Recovery
|
||||
|
||||
Signal handlers for SIGSEGV, SIGFPE, and SIGILL `longjmp` back to the shell's
|
||||
main loop. The scheduler is fixed via `tsRecoverToMain()`, the crashed app is
|
||||
force-killed, and a diagnostic message is displayed. Register state and app
|
||||
identity are logged to `dvx.log`.
|
||||
|
||||
## DXE Export Table
|
||||
|
||||
The shell registers a symbol export table via `dlregsym()` before loading any
|
||||
apps. Most symbols (all `dvx*`, `wgt*`, `ts*`, drawing functions, and required
|
||||
libc functions) are exported directly. `dvxCreateWindow` and `dvxDestroyWindow`
|
||||
are exported as wrappers that add resource tracking.
|
||||
|
||||
## Shell API
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `shellAppInit()` | Initialize the app slot table |
|
||||
| `shellLoadApp(ctx, path)` | Load and start an app from a `.app` file |
|
||||
| `shellReapApps(ctx)` | Clean up terminated apps (call each frame) |
|
||||
| `shellReapApp(ctx, app)` | Gracefully shut down a single app |
|
||||
| `shellForceKillApp(ctx, app)` | Forcibly kill an app (skip shutdown hook) |
|
||||
| `shellTerminateAllApps(ctx)` | Kill all running apps (shell shutdown) |
|
||||
| `shellGetApp(appId)` | Get app slot by ID |
|
||||
| `shellRunningAppCount()` | Count running apps |
|
||||
| `shellLog(fmt, ...)` | Write to `dvx.log` |
|
||||
| `shellRegisterDesktopUpdate(fn)` | Register callback for app state changes |
|
||||
| `shellExportInit()` | Register DXE symbol export table |
|
||||
|
|
@ -1,500 +0,0 @@
|
|||
// shellExport.c — DXE export table and wrapper functions for DVX Shell
|
||||
//
|
||||
// Exports all dvx*/wgt*/ts* symbols that DXE apps need. A few functions
|
||||
// are wrapped for resource tracking (window ownership via appId).
|
||||
//
|
||||
// DXE3 is DJGPP's dynamic linking mechanism. Unlike ELF shared libraries,
|
||||
// DXE modules have no implicit access to the host's symbol table. Every
|
||||
// function or variable the DXE needs must be explicitly listed in an
|
||||
// export table registered via dlregsym() BEFORE any dlopen() call. If a
|
||||
// symbol is missing, dlopen() returns NULL with a "symbol not found" error.
|
||||
//
|
||||
// This file is essentially the ABI contract between the shell and apps.
|
||||
// Three categories of exports:
|
||||
//
|
||||
// 1. Wrapped functions: dvxCreateWindow, dvxCreateWindowCentered,
|
||||
// dvxDestroyWindow. These are intercepted to stamp win->appId for
|
||||
// resource ownership tracking. The DXE sees them under their original
|
||||
// names — the app code calls dvxCreateWindow() normally and gets our
|
||||
// wrapper transparently.
|
||||
//
|
||||
// 2. Direct exports: all other dvx/wgt/wm/ts functions. These are safe
|
||||
// to call without shell-side interception.
|
||||
//
|
||||
// 3. libc functions: DXE modules are statically linked against DJGPP's
|
||||
// libc, but DJGPP's DXE3 loader requires explicit re-export of any
|
||||
// libc symbols the module references. Without these entries, the DXE
|
||||
// would fail to load with unresolved symbol errors. This is a DXE3
|
||||
// design limitation — there's no automatic fallback to the host's libc.
|
||||
|
||||
#include "shellApp.h"
|
||||
#include "shellInfo.h"
|
||||
#include "dvxApp.h"
|
||||
#include "dvxDialog.h"
|
||||
#include "dvxWidget.h"
|
||||
#include "dvxDraw.h"
|
||||
#include "dvxPrefs.h"
|
||||
#include "dvxVideo.h"
|
||||
#include "dvxWm.h"
|
||||
#include "taskswitch.h"
|
||||
|
||||
#include <sys/dxe.h>
|
||||
#include <sys/stat.h>
|
||||
#include <dirent.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
#include <time.h>
|
||||
|
||||
// ============================================================
|
||||
// Prototypes
|
||||
// ============================================================
|
||||
|
||||
static void shellRegisterExports(void);
|
||||
static WindowT *shellWrapCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable);
|
||||
static WindowT *shellWrapCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w, int32_t h, bool resizable);
|
||||
static void shellWrapDestroyWindow(AppContextT *ctx, WindowT *win);
|
||||
|
||||
// ============================================================
|
||||
// Wrapper: dvxCreateWindow — stamps win->appId
|
||||
// ============================================================
|
||||
|
||||
// The wrapper calls the real dvxCreateWindow, then tags the result with
|
||||
// sCurrentAppId. This is how the shell knows which app owns which window,
|
||||
// enabling per-app window cleanup on crash/termination. The app never
|
||||
// sees the difference — the wrapper has the same signature and is
|
||||
// exported under the same name as the original function.
|
||||
static WindowT *shellWrapCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) {
|
||||
WindowT *win = dvxCreateWindow(ctx, title, x, y, w, h, resizable);
|
||||
|
||||
if (win) {
|
||||
win->appId = sCurrentAppId;
|
||||
}
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Wrapper: dvxCreateWindowCentered — stamps win->appId
|
||||
// ============================================================
|
||||
|
||||
static WindowT *shellWrapCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w, int32_t h, bool resizable) {
|
||||
WindowT *win = dvxCreateWindowCentered(ctx, title, w, h, resizable);
|
||||
|
||||
if (win) {
|
||||
win->appId = sCurrentAppId;
|
||||
}
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Wrapper: dvxDestroyWindow — checks for last-window reap
|
||||
// ============================================================
|
||||
|
||||
// Beyond just destroying the window, this wrapper implements the lifecycle
|
||||
// rule for callback-only apps: when their last window closes, they're done.
|
||||
// Main-loop apps manage their own lifetime (their task returns from
|
||||
// appMain), so this check only applies to callback-only apps.
|
||||
// The appId is captured before destruction because the window struct is
|
||||
// freed by dvxDestroyWindow.
|
||||
static void shellWrapDestroyWindow(AppContextT *ctx, WindowT *win) {
|
||||
int32_t appId = win->appId;
|
||||
|
||||
dvxDestroyWindow(ctx, win);
|
||||
|
||||
// If this was a callback-only app's last window, mark for reaping
|
||||
if (appId > 0) {
|
||||
ShellAppT *app = shellGetApp(appId);
|
||||
|
||||
if (app && !app->hasMainLoop && app->state == AppStateRunningE) {
|
||||
// Check if app still has any windows
|
||||
bool hasWindows = false;
|
||||
|
||||
for (int32_t i = 0; i < ctx->stack.count; i++) {
|
||||
if (ctx->stack.windows[i]->appId == appId) {
|
||||
hasWindows = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasWindows) {
|
||||
app->state = AppStateTerminatingE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Export table
|
||||
// ============================================================
|
||||
|
||||
// DXE_EXPORT_TABLE generates a DXE symbol table array. DXE_EXPORT(fn)
|
||||
// expands to { "_fn", (void *)fn } — the underscore prefix matches COFF
|
||||
// symbol naming. For wrapped functions we use raw entries with explicit
|
||||
// names so the DXE sees "_dvxCreateWindow" but gets our wrapper's address.
|
||||
|
||||
DXE_EXPORT_TABLE(shellExportTable)
|
||||
// Wrapped functions (exported under original names, but pointing to
|
||||
// our wrappers that add resource tracking)
|
||||
{ "_dvxCreateWindow", (void *)shellWrapCreateWindow },
|
||||
{ "_dvxDestroyWindow", (void *)shellWrapDestroyWindow },
|
||||
|
||||
// dvxPrefs.h — preferences
|
||||
DXE_EXPORT(prefsGetBool)
|
||||
DXE_EXPORT(prefsGetInt)
|
||||
DXE_EXPORT(prefsGetString)
|
||||
|
||||
// dvxApp.h — direct exports
|
||||
DXE_EXPORT(dvxInit)
|
||||
DXE_EXPORT(dvxSetMouseConfig)
|
||||
DXE_EXPORT(dvxShutdown)
|
||||
DXE_EXPORT(dvxUpdate)
|
||||
{ "_dvxCreateWindowCentered", (void *)shellWrapCreateWindowCentered },
|
||||
DXE_EXPORT(dvxFitWindow)
|
||||
DXE_EXPORT(dvxInvalidateRect)
|
||||
DXE_EXPORT(dvxInvalidateWindow)
|
||||
DXE_EXPORT(dvxMinimizeWindow)
|
||||
DXE_EXPORT(dvxMaximizeWindow)
|
||||
DXE_EXPORT(dvxQuit)
|
||||
DXE_EXPORT(dvxSetTitle)
|
||||
DXE_EXPORT(dvxGetFont)
|
||||
DXE_EXPORT(dvxGetColors)
|
||||
DXE_EXPORT(dvxGetDisplay)
|
||||
DXE_EXPORT(dvxGetBlitOps)
|
||||
DXE_EXPORT(dvxSetWindowIcon)
|
||||
DXE_EXPORT(dvxLoadImage)
|
||||
DXE_EXPORT(dvxFreeImage)
|
||||
DXE_EXPORT(dvxSaveImage)
|
||||
DXE_EXPORT(dvxScreenshot)
|
||||
DXE_EXPORT(dvxWindowScreenshot)
|
||||
DXE_EXPORT(dvxCreateAccelTable)
|
||||
DXE_EXPORT(dvxFreeAccelTable)
|
||||
DXE_EXPORT(dvxAddAccel)
|
||||
DXE_EXPORT(dvxCascadeWindows)
|
||||
DXE_EXPORT(dvxTileWindows)
|
||||
DXE_EXPORT(dvxTileWindowsH)
|
||||
DXE_EXPORT(dvxTileWindowsV)
|
||||
DXE_EXPORT(dvxClipboardCopy)
|
||||
DXE_EXPORT(dvxClipboardGet)
|
||||
|
||||
// dvxDialog.h
|
||||
DXE_EXPORT(dvxMessageBox)
|
||||
DXE_EXPORT(dvxFileDialog)
|
||||
|
||||
// dvxDraw.h
|
||||
DXE_EXPORT(rectFill)
|
||||
DXE_EXPORT(rectCopy)
|
||||
DXE_EXPORT(drawBevel)
|
||||
DXE_EXPORT(drawChar)
|
||||
DXE_EXPORT(drawText)
|
||||
DXE_EXPORT(drawTextN)
|
||||
DXE_EXPORT(textWidth)
|
||||
DXE_EXPORT(drawTextAccel)
|
||||
DXE_EXPORT(textWidthAccel)
|
||||
DXE_EXPORT(drawFocusRect)
|
||||
DXE_EXPORT(drawHLine)
|
||||
DXE_EXPORT(drawVLine)
|
||||
|
||||
// dvxVideo.h
|
||||
DXE_EXPORT(packColor)
|
||||
|
||||
// dvxWm.h
|
||||
DXE_EXPORT(wmAddMenuBar)
|
||||
DXE_EXPORT(wmAddMenu)
|
||||
DXE_EXPORT(wmAddMenuItem)
|
||||
DXE_EXPORT(wmAddMenuCheckItem)
|
||||
DXE_EXPORT(wmAddMenuRadioItem)
|
||||
DXE_EXPORT(wmAddMenuSeparator)
|
||||
DXE_EXPORT(wmAddSubMenu)
|
||||
DXE_EXPORT(wmAddVScrollbar)
|
||||
DXE_EXPORT(wmAddHScrollbar)
|
||||
DXE_EXPORT(wmSetTitle)
|
||||
DXE_EXPORT(wmSetIcon)
|
||||
DXE_EXPORT(wmCreateMenu)
|
||||
DXE_EXPORT(wmFreeMenu)
|
||||
DXE_EXPORT(wmUpdateContentRect)
|
||||
DXE_EXPORT(wmReallocContentBuf)
|
||||
|
||||
// dvxWidget.h — window integration
|
||||
DXE_EXPORT(wgtInitWindow)
|
||||
|
||||
// dvxWidget.h — containers
|
||||
DXE_EXPORT(wgtVBox)
|
||||
DXE_EXPORT(wgtHBox)
|
||||
DXE_EXPORT(wgtFrame)
|
||||
|
||||
// dvxWidget.h — basic widgets
|
||||
DXE_EXPORT(wgtLabel)
|
||||
DXE_EXPORT(wgtButton)
|
||||
DXE_EXPORT(wgtCheckbox)
|
||||
DXE_EXPORT(wgtTextInput)
|
||||
DXE_EXPORT(wgtPasswordInput)
|
||||
DXE_EXPORT(wgtMaskedInput)
|
||||
|
||||
// dvxWidget.h — radio buttons
|
||||
DXE_EXPORT(wgtRadioGroup)
|
||||
DXE_EXPORT(wgtRadio)
|
||||
|
||||
// dvxWidget.h — spacing
|
||||
DXE_EXPORT(wgtSpacer)
|
||||
DXE_EXPORT(wgtHSeparator)
|
||||
DXE_EXPORT(wgtVSeparator)
|
||||
|
||||
// dvxWidget.h — complex widgets
|
||||
DXE_EXPORT(wgtListBox)
|
||||
DXE_EXPORT(wgtTextArea)
|
||||
|
||||
// dvxWidget.h — dropdown/combo
|
||||
DXE_EXPORT(wgtDropdown)
|
||||
DXE_EXPORT(wgtDropdownSetItems)
|
||||
DXE_EXPORT(wgtDropdownGetSelected)
|
||||
DXE_EXPORT(wgtDropdownSetSelected)
|
||||
DXE_EXPORT(wgtComboBox)
|
||||
DXE_EXPORT(wgtComboBoxSetItems)
|
||||
DXE_EXPORT(wgtComboBoxGetSelected)
|
||||
DXE_EXPORT(wgtComboBoxSetSelected)
|
||||
|
||||
// dvxWidget.h — progress bar
|
||||
DXE_EXPORT(wgtProgressBar)
|
||||
DXE_EXPORT(wgtProgressBarV)
|
||||
DXE_EXPORT(wgtProgressBarSetValue)
|
||||
DXE_EXPORT(wgtProgressBarGetValue)
|
||||
|
||||
// dvxWidget.h — slider
|
||||
DXE_EXPORT(wgtSlider)
|
||||
DXE_EXPORT(wgtSliderSetValue)
|
||||
DXE_EXPORT(wgtSliderGetValue)
|
||||
|
||||
// dvxWidget.h — spinner
|
||||
DXE_EXPORT(wgtSpinner)
|
||||
DXE_EXPORT(wgtSpinnerSetValue)
|
||||
DXE_EXPORT(wgtSpinnerGetValue)
|
||||
DXE_EXPORT(wgtSpinnerSetRange)
|
||||
DXE_EXPORT(wgtSpinnerSetStep)
|
||||
|
||||
// dvxWidget.h — tab control
|
||||
DXE_EXPORT(wgtTabControl)
|
||||
DXE_EXPORT(wgtTabPage)
|
||||
DXE_EXPORT(wgtTabControlSetActive)
|
||||
DXE_EXPORT(wgtTabControlGetActive)
|
||||
|
||||
// dvxWidget.h — status bar / toolbar
|
||||
DXE_EXPORT(wgtStatusBar)
|
||||
DXE_EXPORT(wgtToolbar)
|
||||
|
||||
// dvxWidget.h — tree view
|
||||
DXE_EXPORT(wgtTreeView)
|
||||
DXE_EXPORT(wgtTreeViewGetSelected)
|
||||
DXE_EXPORT(wgtTreeViewSetSelected)
|
||||
DXE_EXPORT(wgtTreeViewSetMultiSelect)
|
||||
DXE_EXPORT(wgtTreeViewSetReorderable)
|
||||
DXE_EXPORT(wgtTreeItem)
|
||||
DXE_EXPORT(wgtTreeItemSetExpanded)
|
||||
DXE_EXPORT(wgtTreeItemIsExpanded)
|
||||
DXE_EXPORT(wgtTreeItemIsSelected)
|
||||
DXE_EXPORT(wgtTreeItemSetSelected)
|
||||
|
||||
// dvxWidget.h — list view
|
||||
DXE_EXPORT(wgtListView)
|
||||
DXE_EXPORT(wgtListViewSetColumns)
|
||||
DXE_EXPORT(wgtListViewSetData)
|
||||
DXE_EXPORT(wgtListViewGetSelected)
|
||||
DXE_EXPORT(wgtListViewSetSelected)
|
||||
DXE_EXPORT(wgtListViewSetSort)
|
||||
DXE_EXPORT(wgtListViewSetHeaderClickCallback)
|
||||
DXE_EXPORT(wgtListViewSetMultiSelect)
|
||||
DXE_EXPORT(wgtListViewIsItemSelected)
|
||||
DXE_EXPORT(wgtListViewSetItemSelected)
|
||||
DXE_EXPORT(wgtListViewSelectAll)
|
||||
DXE_EXPORT(wgtListViewClearSelection)
|
||||
DXE_EXPORT(wgtListViewSetReorderable)
|
||||
|
||||
// dvxWidget.h — scroll pane / splitter
|
||||
DXE_EXPORT(wgtScrollPane)
|
||||
DXE_EXPORT(wgtSplitter)
|
||||
DXE_EXPORT(wgtSplitterSetPos)
|
||||
DXE_EXPORT(wgtSplitterGetPos)
|
||||
|
||||
// dvxWidget.h — image / image button
|
||||
DXE_EXPORT(wgtImageButton)
|
||||
DXE_EXPORT(wgtImageButtonFromFile)
|
||||
DXE_EXPORT(wgtImageButtonSetData)
|
||||
DXE_EXPORT(wgtImage)
|
||||
DXE_EXPORT(wgtImageFromFile)
|
||||
DXE_EXPORT(wgtImageSetData)
|
||||
|
||||
// dvxWidget.h — canvas
|
||||
DXE_EXPORT(wgtCanvas)
|
||||
DXE_EXPORT(wgtCanvasClear)
|
||||
DXE_EXPORT(wgtCanvasSetMouseCallback)
|
||||
DXE_EXPORT(wgtCanvasSetPenColor)
|
||||
DXE_EXPORT(wgtCanvasSetPenSize)
|
||||
DXE_EXPORT(wgtCanvasSave)
|
||||
DXE_EXPORT(wgtCanvasLoad)
|
||||
DXE_EXPORT(wgtCanvasDrawLine)
|
||||
DXE_EXPORT(wgtCanvasDrawRect)
|
||||
DXE_EXPORT(wgtCanvasFillRect)
|
||||
DXE_EXPORT(wgtCanvasFillCircle)
|
||||
DXE_EXPORT(wgtCanvasSetPixel)
|
||||
DXE_EXPORT(wgtCanvasGetPixel)
|
||||
|
||||
// dvxWidget.h — ANSI terminal
|
||||
DXE_EXPORT(wgtAnsiTerm)
|
||||
DXE_EXPORT(wgtAnsiTermWrite)
|
||||
DXE_EXPORT(wgtAnsiTermClear)
|
||||
DXE_EXPORT(wgtAnsiTermSetComm)
|
||||
DXE_EXPORT(wgtAnsiTermSetScrollback)
|
||||
DXE_EXPORT(wgtAnsiTermPoll)
|
||||
DXE_EXPORT(wgtAnsiTermRepaint)
|
||||
|
||||
// dvxWidget.h — operations
|
||||
DXE_EXPORT(wgtInvalidate)
|
||||
DXE_EXPORT(wgtInvalidatePaint)
|
||||
DXE_EXPORT(wgtSetDebugLayout)
|
||||
DXE_EXPORT(wgtSetText)
|
||||
DXE_EXPORT(wgtSetTooltip)
|
||||
DXE_EXPORT(wgtGetText)
|
||||
DXE_EXPORT(wgtGetFocused)
|
||||
DXE_EXPORT(wgtSetEnabled)
|
||||
DXE_EXPORT(wgtSetFocused)
|
||||
DXE_EXPORT(wgtSetReadOnly)
|
||||
DXE_EXPORT(wgtSetVisible)
|
||||
DXE_EXPORT(wgtGetContext)
|
||||
DXE_EXPORT(wgtSetName)
|
||||
DXE_EXPORT(wgtFind)
|
||||
DXE_EXPORT(wgtDestroy)
|
||||
|
||||
// dvxWidget.h — list box ops
|
||||
DXE_EXPORT(wgtListBoxSetItems)
|
||||
DXE_EXPORT(wgtListBoxGetSelected)
|
||||
DXE_EXPORT(wgtListBoxSetSelected)
|
||||
DXE_EXPORT(wgtListBoxSetMultiSelect)
|
||||
DXE_EXPORT(wgtListBoxIsItemSelected)
|
||||
DXE_EXPORT(wgtListBoxSetItemSelected)
|
||||
DXE_EXPORT(wgtListBoxSelectAll)
|
||||
DXE_EXPORT(wgtListBoxClearSelection)
|
||||
DXE_EXPORT(wgtListBoxSetReorderable)
|
||||
|
||||
// dvxWidget.h — layout
|
||||
DXE_EXPORT(wgtResolveSize)
|
||||
DXE_EXPORT(wgtLayout)
|
||||
DXE_EXPORT(wgtPaint)
|
||||
|
||||
// taskswitch.h — only yield and query functions are exported.
|
||||
// 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)
|
||||
|
||||
// dvxWm.h — direct window management
|
||||
DXE_EXPORT(wmRaiseWindow)
|
||||
DXE_EXPORT(wmSetFocus)
|
||||
DXE_EXPORT(wmRestoreMinimized)
|
||||
|
||||
// Shell API
|
||||
DXE_EXPORT(shellLog)
|
||||
DXE_EXPORT(shellLoadApp)
|
||||
DXE_EXPORT(shellGetApp)
|
||||
DXE_EXPORT(shellForceKillApp)
|
||||
DXE_EXPORT(shellRunningAppCount)
|
||||
DXE_EXPORT(shellRegisterDesktopUpdate)
|
||||
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 — memory
|
||||
DXE_EXPORT(malloc)
|
||||
DXE_EXPORT(free)
|
||||
DXE_EXPORT(calloc)
|
||||
DXE_EXPORT(realloc)
|
||||
|
||||
// libc — string
|
||||
DXE_EXPORT(memcpy)
|
||||
DXE_EXPORT(memset)
|
||||
DXE_EXPORT(memmove)
|
||||
DXE_EXPORT(memcmp)
|
||||
DXE_EXPORT(strlen)
|
||||
DXE_EXPORT(strcmp)
|
||||
DXE_EXPORT(strncmp)
|
||||
DXE_EXPORT(strcpy)
|
||||
DXE_EXPORT(strncpy)
|
||||
DXE_EXPORT(strcat)
|
||||
DXE_EXPORT(strncat)
|
||||
DXE_EXPORT(strchr)
|
||||
DXE_EXPORT(strrchr)
|
||||
DXE_EXPORT(strstr)
|
||||
DXE_EXPORT(strtol)
|
||||
|
||||
// libc — I/O
|
||||
DXE_EXPORT(printf)
|
||||
DXE_EXPORT(fprintf)
|
||||
DXE_EXPORT(sprintf)
|
||||
DXE_EXPORT(snprintf)
|
||||
DXE_EXPORT(puts)
|
||||
DXE_EXPORT(fopen)
|
||||
DXE_EXPORT(fclose)
|
||||
DXE_EXPORT(fread)
|
||||
DXE_EXPORT(fwrite)
|
||||
DXE_EXPORT(fgets)
|
||||
DXE_EXPORT(fseek)
|
||||
DXE_EXPORT(ftell)
|
||||
DXE_EXPORT(feof)
|
||||
DXE_EXPORT(ferror)
|
||||
|
||||
// libc — math
|
||||
DXE_EXPORT(sin)
|
||||
DXE_EXPORT(cos)
|
||||
DXE_EXPORT(sqrt)
|
||||
|
||||
// libc — time
|
||||
DXE_EXPORT(clock)
|
||||
DXE_EXPORT(time)
|
||||
DXE_EXPORT(localtime)
|
||||
|
||||
// libc — directory
|
||||
DXE_EXPORT(opendir)
|
||||
DXE_EXPORT(readdir)
|
||||
DXE_EXPORT(closedir)
|
||||
|
||||
// libc — filesystem
|
||||
DXE_EXPORT(stat)
|
||||
|
||||
// libc — misc
|
||||
DXE_EXPORT(qsort)
|
||||
DXE_EXPORT(rand)
|
||||
DXE_EXPORT(srand)
|
||||
DXE_EXPORT(abs)
|
||||
DXE_EXPORT(atoi)
|
||||
DXE_EXPORT_END
|
||||
|
||||
|
||||
// ============================================================
|
||||
// shellRegisterExports
|
||||
// ============================================================
|
||||
|
||||
// dlregsym registers our export table with DJGPP's DXE3 runtime.
|
||||
// Must be called once before any dlopen — subsequent dlopen calls
|
||||
// will search this table to resolve DXE symbol references.
|
||||
static void shellRegisterExports(void) {
|
||||
dlregsym(shellExportTable);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Public init function
|
||||
// ============================================================
|
||||
|
||||
void shellExportInit(void) {
|
||||
shellRegisterExports();
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
// shellInfo.c — System information wrapper for DVX Shell
|
||||
//
|
||||
// Delegates hardware detection to the platform layer via
|
||||
// platformGetSystemInfo(), then logs the result line-by-line
|
||||
// to DVX.LOG. The result pointer is cached so subsequent calls
|
||||
// to shellGetSystemInfo() return instantly without re-probing.
|
||||
|
||||
#include "shellInfo.h"
|
||||
#include "shellApp.h"
|
||||
#include "platform/dvxPlatform.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
// ============================================================
|
||||
// Module state
|
||||
// ============================================================
|
||||
|
||||
static const char *sCachedInfo = NULL;
|
||||
|
||||
// ============================================================
|
||||
// shellGetSystemInfo — return the cached info text
|
||||
// ============================================================
|
||||
|
||||
const char *shellGetSystemInfo(void) {
|
||||
return sCachedInfo ? sCachedInfo : "";
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// shellInfoInit — gather info and log it
|
||||
// ============================================================
|
||||
|
||||
void shellInfoInit(AppContextT *ctx) {
|
||||
sCachedInfo = platformGetSystemInfo(&ctx->display);
|
||||
|
||||
// Log each line individually so the log file is readable
|
||||
shellLog("=== System Information ===");
|
||||
|
||||
const char *line = sCachedInfo;
|
||||
|
||||
while (*line) {
|
||||
const char *eol = strchr(line, '\n');
|
||||
|
||||
if (!eol) {
|
||||
shellLog("%s", line);
|
||||
break;
|
||||
}
|
||||
|
||||
int32_t len = (int32_t)(eol - line);
|
||||
char tmp[256];
|
||||
|
||||
if (len >= (int32_t)sizeof(tmp)) {
|
||||
len = sizeof(tmp) - 1;
|
||||
}
|
||||
|
||||
memcpy(tmp, line, len);
|
||||
tmp[len] = '\0';
|
||||
shellLog("%s", tmp);
|
||||
line = eol + 1;
|
||||
}
|
||||
|
||||
shellLog("=== End System Information ===");
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// shellInfo.h — System information display for DVX Shell
|
||||
//
|
||||
// Thin wrapper around platformGetSystemInfo(). Calls the platform
|
||||
// layer to gather hardware info, logs each line to DVX.LOG, and
|
||||
// caches the result for the Program Manager's "System Information"
|
||||
// dialog.
|
||||
#ifndef SHELL_INFO_H
|
||||
#define SHELL_INFO_H
|
||||
|
||||
#include "dvxApp.h"
|
||||
|
||||
// Gather all hardware information via the platform layer, log it,
|
||||
// and store for later retrieval. Call once after dvxInit.
|
||||
void shellInfoInit(AppContextT *ctx);
|
||||
|
||||
// Return the formatted system information text. The pointer is valid
|
||||
// for the lifetime of the process (static buffer in the platform layer).
|
||||
const char *shellGetSystemInfo(void);
|
||||
|
||||
#endif // SHELL_INFO_H
|
||||
|
|
@ -1,422 +0,0 @@
|
|||
// shellMain.c — DVX Shell entry point and main loop
|
||||
//
|
||||
// Initializes the GUI, task system, DXE export table, and loads
|
||||
// the desktop app. Runs the cooperative main loop, yielding to
|
||||
// app tasks and reaping terminated apps each frame.
|
||||
//
|
||||
// The main loop design (dvxUpdate + tsYield + reap + notify):
|
||||
// Each iteration does four things:
|
||||
// 1. dvxUpdate: processes input events, dispatches callbacks, composites
|
||||
// dirty rects, flushes to the LFB. This is the shell's primary job.
|
||||
// 2. tsYield: gives CPU time to app tasks. Without this, main-loop apps
|
||||
// would never run because the shell task would monopolize the CPU.
|
||||
// 3. shellReapApps: cleans up any apps that terminated during this frame
|
||||
// (either their task returned or their last window was closed).
|
||||
// 4. desktopUpdate: notifies the desktop app (Program Manager) if any
|
||||
// apps were reaped, so it can refresh its task list.
|
||||
//
|
||||
// Crash recovery uses setjmp/longjmp:
|
||||
// The shell installs signal handlers for SIGSEGV, SIGFPE, SIGILL. If a
|
||||
// crash occurs in an app task, the handler longjmps back to the setjmp
|
||||
// point in main(). This works because longjmp restores the main task's
|
||||
// stack frame regardless of which task was running. tsRecoverToMain()
|
||||
// then fixes the scheduler's bookkeeping, and the crashed app is killed.
|
||||
// This gives the shell Windows 3.1-style fault tolerance — one bad app
|
||||
// doesn't take down the whole system.
|
||||
|
||||
#include "shellApp.h"
|
||||
#include "shellInfo.h"
|
||||
#include "dvxDialog.h"
|
||||
#include "dvxPrefs.h"
|
||||
#include "platform/dvxPlatform.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <setjmp.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
// DJGPP-specific: provides __djgpp_exception_state_ptr for accessing
|
||||
// CPU register state at the point of the exception
|
||||
#include <sys/exceptn.h>
|
||||
|
||||
// ============================================================
|
||||
// Module state
|
||||
// ============================================================
|
||||
|
||||
static AppContextT sCtx;
|
||||
// setjmp buffer for crash recovery. The crash handler longjmps here to
|
||||
// return control to the shell's main loop after an app crashes.
|
||||
static jmp_buf sCrashJmp;
|
||||
// Volatile because it's written from a signal handler context. Tells
|
||||
// the recovery code which signal fired (for logging/diagnostics).
|
||||
static volatile int sCrashSignal = 0;
|
||||
static FILE *sLogFile = NULL;
|
||||
static void (*sDesktopUpdateFn)(void) = NULL;
|
||||
|
||||
// ============================================================
|
||||
// Prototypes
|
||||
// ============================================================
|
||||
|
||||
static void crashHandler(int sig);
|
||||
static void idleYield(void *ctx);
|
||||
static void installCrashHandler(void);
|
||||
static void logCrash(int sig);
|
||||
static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData);
|
||||
|
||||
// ============================================================
|
||||
// crashHandler — catch page faults and other fatal signals
|
||||
// ============================================================
|
||||
|
||||
// Signal handler for fatal exceptions. DJGPP uses System V signal
|
||||
// semantics where the handler is reset to SIG_DFL after each delivery,
|
||||
// so we must re-install it before doing anything else.
|
||||
//
|
||||
// The longjmp is the key to crash recovery: it unwinds whatever stack
|
||||
// we're on (potentially a crashed app's task stack) and restores the
|
||||
// main task's stack frame to the setjmp point in main(). This is safe
|
||||
// because cooperative switching means the main task's stack is always
|
||||
// intact — it was cleanly suspended at a yield point. The crashed
|
||||
// task's stack is abandoned (and later freed by tsKill).
|
||||
static void crashHandler(int sig) {
|
||||
logCrash(sig);
|
||||
|
||||
// Re-install handler (DJGPP resets to SIG_DFL after delivery)
|
||||
signal(sig, crashHandler);
|
||||
|
||||
sCrashSignal = sig;
|
||||
longjmp(sCrashJmp, 1);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// shellDesktopUpdate — notify desktop app of state change
|
||||
// ============================================================
|
||||
|
||||
void shellDesktopUpdate(void) {
|
||||
if (sDesktopUpdateFn) {
|
||||
sDesktopUpdateFn();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// idleYield — called when no dirty rects need compositing
|
||||
// ============================================================
|
||||
|
||||
// Registered as sCtx.idleCallback. dvxUpdate calls this when it has
|
||||
// processed all pending events and there are no dirty rects to composite.
|
||||
// Instead of busy-spinning, we yield to app tasks — this is where most
|
||||
// of the CPU time for main-loop apps comes from when the UI is idle.
|
||||
// The tsActiveCount > 1 check avoids the overhead of a tsYield call
|
||||
// (which would do a scheduler scan) when the shell is the only task.
|
||||
static void idleYield(void *ctx) {
|
||||
(void)ctx;
|
||||
|
||||
if (tsActiveCount() > 1) {
|
||||
tsYield();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// installCrashHandler
|
||||
// ============================================================
|
||||
|
||||
static void installCrashHandler(void) {
|
||||
signal(SIGSEGV, crashHandler);
|
||||
signal(SIGFPE, crashHandler);
|
||||
signal(SIGILL, crashHandler);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// logCrash — write exception details to the log
|
||||
// ============================================================
|
||||
|
||||
// Dump as much diagnostic info as possible before longjmp destroys the
|
||||
// crash context. This runs inside the signal handler, so only
|
||||
// async-signal-safe functions should be used — but since we're in
|
||||
// DJGPP (single-threaded DOS), reentrancy isn't a practical concern
|
||||
// and vfprintf/fflush are safe to call here.
|
||||
static void logCrash(int sig) {
|
||||
const char *sigName = "UNKNOWN";
|
||||
|
||||
if (sig == SIGSEGV) {
|
||||
sigName = "SIGSEGV (page fault)";
|
||||
} else if (sig == SIGFPE) {
|
||||
sigName = "SIGFPE (floating point exception)";
|
||||
} else if (sig == SIGILL) {
|
||||
sigName = "SIGILL (illegal instruction)";
|
||||
}
|
||||
|
||||
shellLog("=== CRASH ===");
|
||||
shellLog("Signal: %d (%s)", sig, sigName);
|
||||
shellLog("Current app ID: %ld", (long)sCurrentAppId);
|
||||
|
||||
if (sCurrentAppId > 0) {
|
||||
ShellAppT *app = shellGetApp(sCurrentAppId);
|
||||
|
||||
if (app) {
|
||||
shellLog("App name: %s", app->name);
|
||||
shellLog("App path: %s", app->path);
|
||||
shellLog("Has main loop: %s", app->hasMainLoop ? "yes" : "no");
|
||||
shellLog("Task ID: %lu", (unsigned long)app->mainTaskId);
|
||||
}
|
||||
} else {
|
||||
shellLog("Crashed in shell (task 0)");
|
||||
}
|
||||
|
||||
// __djgpp_exception_state_ptr is a DJGPP extension that captures the
|
||||
// full CPU register state at the point of the exception. This gives
|
||||
// us the faulting EIP, stack pointer, and all GPRs — invaluable for
|
||||
// post-mortem debugging of app crashes from the log file.
|
||||
jmp_buf *estate = __djgpp_exception_state_ptr;
|
||||
|
||||
if (estate) {
|
||||
struct __jmp_buf *regs = &(*estate)[0];
|
||||
shellLog("EIP: 0x%08lx CS: 0x%04x", regs->__eip, regs->__cs);
|
||||
shellLog("EAX: 0x%08lx EBX: 0x%08lx ECX: 0x%08lx EDX: 0x%08lx", regs->__eax, regs->__ebx, regs->__ecx, regs->__edx);
|
||||
shellLog("ESI: 0x%08lx EDI: 0x%08lx EBP: 0x%08lx ESP: 0x%08lx", regs->__esi, regs->__edi, regs->__ebp, regs->__esp);
|
||||
shellLog("DS: 0x%04x ES: 0x%04x FS: 0x%04x GS: 0x%04x SS: 0x%04x", regs->__ds, regs->__es, regs->__fs, regs->__gs, regs->__ss);
|
||||
shellLog("EFLAGS: 0x%08lx", regs->__eflags);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData) {
|
||||
(void)userData;
|
||||
shellLog(" %ldx%ld %ldbpp", (long)w, (long)h, (long)bpp);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// shellLog — append a line to DVX.LOG
|
||||
// ============================================================
|
||||
|
||||
void shellLog(const char *fmt, ...) {
|
||||
if (!sLogFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vfprintf(sLogFile, fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
fprintf(sLogFile, "\n");
|
||||
fflush(sLogFile);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// shellRegisterDesktopUpdate
|
||||
// ============================================================
|
||||
|
||||
void shellRegisterDesktopUpdate(void (*updateFn)(void)) {
|
||||
sDesktopUpdateFn = updateFn;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// main
|
||||
// ============================================================
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void)argc;
|
||||
|
||||
// Change to the directory containing the executable so that relative
|
||||
// paths (CONFIG/, APPS/, etc.) resolve correctly regardless of where
|
||||
// the user launched from.
|
||||
char exeDir[260];
|
||||
strncpy(exeDir, argv[0], sizeof(exeDir) - 1);
|
||||
exeDir[sizeof(exeDir) - 1] = '\0';
|
||||
|
||||
char *sep = platformPathDirEnd(exeDir);
|
||||
|
||||
if (sep) {
|
||||
*sep = '\0';
|
||||
platformChdir(exeDir);
|
||||
}
|
||||
|
||||
sLogFile = fopen("dvx.log", "w");
|
||||
shellLog("DVX Shell starting...");
|
||||
|
||||
// Load preferences (missing file or keys silently use defaults)
|
||||
prefsLoad("CONFIG/DVX.INI");
|
||||
|
||||
int32_t videoW = prefsGetInt("video", "width", 640);
|
||||
int32_t videoH = prefsGetInt("video", "height", 480);
|
||||
int32_t videoBpp = prefsGetInt("video", "bpp", 32);
|
||||
shellLog("Preferences: video %ldx%ld %ldbpp", (long)videoW, (long)videoH, (long)videoBpp);
|
||||
|
||||
// Initialize GUI
|
||||
int32_t result = dvxInit(&sCtx, videoW, videoH, videoBpp);
|
||||
|
||||
if (result == 0) {
|
||||
// Apply mouse preferences
|
||||
const char *wheelStr = prefsGetString("mouse", "wheel", "normal");
|
||||
int32_t wheelDir = (strcmp(wheelStr, "reversed") == 0) ? -1 : 1;
|
||||
int32_t dblClick = prefsGetInt("mouse", "doubleclick", 500);
|
||||
|
||||
// Map acceleration name to double-speed threshold (mickeys/sec).
|
||||
// "off" sets a very high threshold so acceleration never triggers.
|
||||
const char *accelStr = prefsGetString("mouse", "acceleration", "medium");
|
||||
int32_t accelVal = 0;
|
||||
|
||||
if (strcmp(accelStr, "off") == 0) {
|
||||
accelVal = 10000;
|
||||
} else if (strcmp(accelStr, "low") == 0) {
|
||||
accelVal = 100;
|
||||
} else if (strcmp(accelStr, "medium") == 0) {
|
||||
accelVal = 64;
|
||||
} else if (strcmp(accelStr, "high") == 0) {
|
||||
accelVal = 32;
|
||||
}
|
||||
|
||||
dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal);
|
||||
shellLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s", wheelStr, (long)dblClick, accelStr);
|
||||
}
|
||||
|
||||
if (result != 0) {
|
||||
shellLog("Failed to initialize DVX GUI (error %ld)", (long)result);
|
||||
|
||||
if (sLogFile) {
|
||||
fclose(sLogFile);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
shellLog("Available video modes:");
|
||||
platformVideoEnumModes(logVideoMode, NULL);
|
||||
shellLog("Selected: %ldx%ld %ldbpp (pitch %ld)", (long)sCtx.display.width, (long)sCtx.display.height, (long)sCtx.display.format.bitsPerPixel, (long)sCtx.display.pitch);
|
||||
|
||||
// Initialize task system
|
||||
if (tsInit() != TS_OK) {
|
||||
shellLog("Failed to initialize task system");
|
||||
dvxShutdown(&sCtx);
|
||||
|
||||
if (sLogFile) {
|
||||
fclose(sLogFile);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Shell task (task 0) gets high priority so the UI remains responsive
|
||||
// even when app tasks are CPU-hungry. With HIGH priority (11 credits
|
||||
// per epoch) vs app tasks at NORMAL (6 credits), the shell gets
|
||||
// roughly twice as many scheduling turns as any single app.
|
||||
tsSetPriority(0, TS_PRIORITY_HIGH);
|
||||
|
||||
// Gather system information (CPU, memory, drives, etc.)
|
||||
shellInfoInit(&sCtx);
|
||||
|
||||
// Register DXE export table
|
||||
shellExportInit();
|
||||
|
||||
// Initialize app slot table
|
||||
shellAppInit();
|
||||
|
||||
// Set up idle callback for cooperative yielding. When dvxUpdate has
|
||||
// no work to do (no input events, no dirty rects), it calls this
|
||||
// instead of busy-looping. This is the main mechanism for giving
|
||||
// app tasks CPU time during quiet periods.
|
||||
sCtx.idleCallback = idleYield;
|
||||
sCtx.idleCtx = &sCtx;
|
||||
|
||||
// Load the desktop app
|
||||
int32_t desktopId = shellLoadApp(&sCtx, SHELL_DESKTOP_APP);
|
||||
|
||||
if (desktopId < 0) {
|
||||
shellLog("Failed to load desktop app '%s'", SHELL_DESKTOP_APP);
|
||||
tsShutdown();
|
||||
dvxShutdown(&sCtx);
|
||||
|
||||
if (sLogFile) {
|
||||
fclose(sLogFile);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Install crash handler after everything is initialized — if
|
||||
// initialization itself crashes, we want the default DJGPP behavior
|
||||
// (abort with register dump) rather than our recovery path, because
|
||||
// the system isn't in a recoverable state yet.
|
||||
installCrashHandler();
|
||||
|
||||
shellLog("DVX Shell ready.");
|
||||
|
||||
// Set recovery point for crash handler. setjmp returns 0 on initial
|
||||
// call (falls through to the main loop). On a crash, longjmp makes
|
||||
// setjmp return non-zero, entering this recovery block. The recovery
|
||||
// code runs on the main task's stack (restored by longjmp) so it's
|
||||
// safe to call any shell function.
|
||||
if (setjmp(sCrashJmp) != 0) {
|
||||
// Returned here from crash handler via longjmp.
|
||||
// The task switcher's currentIdx still points to the crashed task.
|
||||
// Fix it before doing anything else so the scheduler is consistent.
|
||||
tsRecoverToMain();
|
||||
|
||||
shellLog("Recovering from crash, killing app %ld", (long)sCurrentAppId);
|
||||
|
||||
if (sCurrentAppId > 0) {
|
||||
ShellAppT *app = shellGetApp(sCurrentAppId);
|
||||
|
||||
if (app) {
|
||||
char msg[256];
|
||||
snprintf(msg, sizeof(msg), "'%s' has caused a fault and will be terminated.", app->name);
|
||||
shellForceKillApp(&sCtx, app);
|
||||
sCurrentAppId = 0;
|
||||
dvxMessageBox(&sCtx, "Application Error", msg, MB_OK | MB_ICONERROR);
|
||||
}
|
||||
}
|
||||
|
||||
sCurrentAppId = 0;
|
||||
sCrashSignal = 0;
|
||||
shellDesktopUpdate();
|
||||
}
|
||||
|
||||
// Main loop — runs until dvxQuit() sets sCtx.running = false.
|
||||
// Two yield points per iteration: one explicit (below) and one via
|
||||
// the idle callback inside dvxUpdate. The explicit yield here ensures
|
||||
// app tasks get CPU time even during busy frames (lots of repaints).
|
||||
// Without it, a flurry of mouse-move events could starve app tasks
|
||||
// because dvxUpdate would keep finding work to do and never call idle.
|
||||
while (sCtx.running) {
|
||||
dvxUpdate(&sCtx);
|
||||
|
||||
// Give app tasks CPU time even during active frames
|
||||
if (tsActiveCount() > 1) {
|
||||
tsYield();
|
||||
}
|
||||
|
||||
// Reap terminated apps and notify desktop if anything changed.
|
||||
// This is the safe point for cleanup — we're at the top of the
|
||||
// main loop, not inside any callback or compositor operation.
|
||||
if (shellReapApps(&sCtx)) {
|
||||
shellDesktopUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
shellLog("DVX Shell shutting down...");
|
||||
|
||||
// Clean shutdown: terminate all apps first (destroys windows, kills
|
||||
// tasks, closes DXE handles), then tear down the task system and GUI
|
||||
// in reverse initialization order.
|
||||
shellTerminateAllApps(&sCtx);
|
||||
|
||||
tsShutdown();
|
||||
dvxShutdown(&sCtx);
|
||||
prefsFree();
|
||||
|
||||
shellLog("DVX Shell exited.");
|
||||
|
||||
if (sLogFile) {
|
||||
fclose(sLogFile);
|
||||
sLogFile = NULL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
82
mkcd.sh
Executable file
82
mkcd.sh
Executable file
|
|
@ -0,0 +1,82 @@
|
|||
#!/bin/bash
|
||||
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (C) 2026 Scott Duensing
|
||||
#
|
||||
# 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.
|
||||
|
||||
# mkcd.sh -- Build DVX and create a CD-ROM ISO image for the target emulator
|
||||
#
|
||||
# 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 the emulator'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 core build output exists
|
||||
for f in dvx.exe libs/kpunch/libtasks/libtasks.lib libs/kpunch/libdvx/libdvx.lib libs/kpunch/dvxshell/dvxshell.lib; do
|
||||
if [ ! -f "$SCRIPT_DIR/bin/$f" ]; then
|
||||
echo "ERROR: bin/$f not found -- build failed?"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Verify widget DXEs exist
|
||||
WGT_COUNT=0
|
||||
while IFS= read -r f; do
|
||||
WGT_COUNT=$((WGT_COUNT + 1))
|
||||
done < <(find "$SCRIPT_DIR/bin/widgets/" -name "*.wgt" -type f 2>/dev/null)
|
||||
echo "$WGT_COUNT widget modules found in bin/widgets/."
|
||||
|
||||
# Clean emulator local-filesystem artifacts
|
||||
find "$SCRIPT_DIR/bin/" -name ".DBLOCALFILE*" -delete 2>/dev/null
|
||||
|
||||
# Create the ISO image
|
||||
# -iso-level 1: strict 8.3 filenames (DOS compatibility)
|
||||
# -J: Joliet extensions (long names for Windows/Linux)
|
||||
# -V: volume label
|
||||
# Note: -R (Rock Ridge) is omitted because DOS surfaces its
|
||||
# attribute sidecar files (._ATR_) as visible junk files.
|
||||
echo "Creating ISO image..."
|
||||
mkisofs \
|
||||
-iso-level 1 \
|
||||
-J \
|
||||
-V "DVX" \
|
||||
-o "$ISO_PATH" \
|
||||
"$SCRIPT_DIR/bin/"
|
||||
|
||||
echo "ISO created: $ISO_PATH"
|
||||
echo "Size: $(du -h "$ISO_PATH" | cut -f1)"
|
||||
echo ""
|
||||
echo "Mount $ISO_PATH as a CD-ROM drive in the target emulator."
|
||||
echo "Then from DOS: D:\\DVX.EXE (or whatever drive letter)"
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# Packet Serial Transport Makefile for DJGPP cross-compilation
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
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
|
||||
|
||||
OBJDIR = ../obj/packet
|
||||
LIBDIR = ../lib
|
||||
|
||||
SRCS = packet.c
|
||||
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
||||
TARGET = $(LIBDIR)/libpacket.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) | $(LIBDIR)
|
||||
$(AR) rcs $@ $(OBJS)
|
||||
$(RANLIB) $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(LIBDIR):
|
||||
mkdir -p $(LIBDIR)
|
||||
|
||||
# Dependencies
|
||||
$(OBJDIR)/packet.o: packet.c packet.h ../rs232/rs232.h
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
215
packet/README.md
215
packet/README.md
|
|
@ -1,215 +0,0 @@
|
|||
# Packet — Reliable Serial Transport
|
||||
|
||||
Packetized serial transport with HDLC-style framing, CRC-16 error
|
||||
detection, and a Go-Back-N sliding window protocol for reliable,
|
||||
ordered delivery over an unreliable serial link.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Application
|
||||
|
|
||||
[packet] framing, CRC, retransmit, ordering
|
||||
|
|
||||
[rs232] raw byte I/O
|
||||
```
|
||||
|
||||
The packet layer sits on top of an already-open rs232 COM port. It
|
||||
does not open or close the serial port itself.
|
||||
|
||||
## Frame Format
|
||||
|
||||
Before byte stuffing:
|
||||
|
||||
```
|
||||
[0x7E] [SEQ] [TYPE] [LEN] [PAYLOAD...] [CRC_LO] [CRC_HI]
|
||||
```
|
||||
|
||||
| Field | Size | Description |
|
||||
|---------|---------|--------------------------------------|
|
||||
| `0x7E` | 1 byte | Frame delimiter (flag byte) |
|
||||
| `SEQ` | 1 byte | Sequence number (wrapping uint8) |
|
||||
| `TYPE` | 1 byte | Frame type (see below) |
|
||||
| `LEN` | 1 byte | Payload length (0-255) |
|
||||
| Payload | 0-255 | Application data |
|
||||
| `CRC` | 2 bytes | CRC-16-CCITT over SEQ+TYPE+LEN+payload |
|
||||
|
||||
### Frame Types
|
||||
|
||||
| Type | Value | Description |
|
||||
|--------|-------|----------------------------------------------|
|
||||
| `DATA` | 0x00 | Data frame carrying application payload |
|
||||
| `ACK` | 0x01 | Cumulative acknowledgment (next expected seq) |
|
||||
| `NAK` | 0x02 | Negative ack (request retransmit from seq) |
|
||||
| `RST` | 0x03 | Connection reset |
|
||||
|
||||
### Byte Stuffing
|
||||
|
||||
The flag byte (`0x7E`) and escape byte (`0x7D`) are escaped within
|
||||
frame data:
|
||||
|
||||
- `0x7E` becomes `0x7D 0x5E`
|
||||
- `0x7D` becomes `0x7D 0x5D`
|
||||
|
||||
## Reliability
|
||||
|
||||
The protocol uses Go-Back-N with a configurable sliding window
|
||||
(1-8 slots, default 4):
|
||||
|
||||
- **Sender** assigns sequential numbers to each DATA frame and retains
|
||||
a copy in the retransmit buffer until acknowledged.
|
||||
- **Receiver** delivers frames in order. Out-of-order frames trigger a
|
||||
NAK for the expected sequence number.
|
||||
- **ACK** carries the next expected sequence number (cumulative).
|
||||
- **NAK** triggers retransmission of the requested frame and all
|
||||
subsequent unacknowledged frames.
|
||||
- **Timer-based retransmit** fires after 500 poll cycles if no ACK or
|
||||
NAK has been received.
|
||||
|
||||
## API Reference
|
||||
|
||||
### Types
|
||||
|
||||
```c
|
||||
// Receive callback — called for each verified, in-order packet
|
||||
typedef void (*PktRecvCallbackT)(void *ctx, const uint8_t *data, int len);
|
||||
|
||||
// Opaque connection handle
|
||||
typedef struct PktConnS PktConnT;
|
||||
```
|
||||
|
||||
### Constants
|
||||
|
||||
| Name | Value | Description |
|
||||
|-----------------------|-------|-------------------------------------|
|
||||
| `PKT_MAX_PAYLOAD` | 255 | Max payload bytes per packet |
|
||||
| `PKT_DEFAULT_WINDOW` | 4 | Default sliding window size |
|
||||
| `PKT_MAX_WINDOW` | 8 | Maximum sliding window size |
|
||||
| `PKT_SUCCESS` | 0 | Success |
|
||||
| `PKT_ERR_INVALID_PORT`| -1 | Invalid COM port |
|
||||
| `PKT_ERR_NOT_OPEN` | -2 | Connection not open |
|
||||
| `PKT_ERR_ALREADY_OPEN`| -3 | Connection already open |
|
||||
| `PKT_ERR_WOULD_BLOCK` | -4 | Operation would block |
|
||||
| `PKT_ERR_OVERFLOW` | -5 | Buffer overflow |
|
||||
| `PKT_ERR_INVALID_PARAM`| -6 | Invalid parameter |
|
||||
| `PKT_ERR_TX_FULL` | -7 | Transmit window full |
|
||||
| `PKT_ERR_NO_DATA` | -8 | No data available |
|
||||
|
||||
### Functions
|
||||
|
||||
#### pktOpen
|
||||
|
||||
```c
|
||||
PktConnT *pktOpen(int com, int windowSize,
|
||||
PktRecvCallbackT callback, void *callbackCtx);
|
||||
```
|
||||
|
||||
Creates a packetized connection over an already-open COM port.
|
||||
|
||||
- `com` — RS232 port index (`RS232_COM1`..`RS232_COM4`)
|
||||
- `windowSize` — sliding window size (1-8), 0 for default (4)
|
||||
- `callback` — called from `pktPoll()` for each received packet
|
||||
- `callbackCtx` — user pointer passed to callback
|
||||
|
||||
Returns a connection handle, or `NULL` on failure.
|
||||
|
||||
#### pktClose
|
||||
|
||||
```c
|
||||
void pktClose(PktConnT *conn);
|
||||
```
|
||||
|
||||
Frees the connection state. Does **not** close the underlying COM port.
|
||||
|
||||
#### pktSend
|
||||
|
||||
```c
|
||||
int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block);
|
||||
```
|
||||
|
||||
Sends a packet. `len` must be 1..`PKT_MAX_PAYLOAD`.
|
||||
|
||||
- `block = true` — waits for window space, polling for ACKs internally
|
||||
- `block = false` — returns `PKT_ERR_TX_FULL` if the window is full
|
||||
|
||||
The packet is stored in the retransmit buffer until acknowledged.
|
||||
|
||||
#### pktPoll
|
||||
|
||||
```c
|
||||
int pktPoll(PktConnT *conn);
|
||||
```
|
||||
|
||||
Reads available serial data, processes received frames, sends ACKs and
|
||||
NAKs, and checks retransmit timers. Returns the number of DATA packets
|
||||
delivered to the callback.
|
||||
|
||||
Must be called frequently (e.g. in your main loop).
|
||||
|
||||
#### pktReset
|
||||
|
||||
```c
|
||||
int pktReset(PktConnT *conn);
|
||||
```
|
||||
|
||||
Resets all sequence numbers and buffers to zero. Sends a RST frame to
|
||||
the remote side so it resets as well.
|
||||
|
||||
#### pktGetPending
|
||||
|
||||
```c
|
||||
int pktGetPending(PktConnT *conn);
|
||||
```
|
||||
|
||||
Returns the number of unacknowledged packets currently in the transmit
|
||||
window. Useful for throttling sends in non-blocking mode.
|
||||
|
||||
## Example
|
||||
|
||||
```c
|
||||
#include "packet.h"
|
||||
#include "../rs232/rs232.h"
|
||||
|
||||
void onPacket(void *ctx, const uint8_t *data, int len) {
|
||||
// process received packet
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
// Open serial port first
|
||||
rs232Open(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_NONE);
|
||||
|
||||
// Create packet connection with default window size
|
||||
PktConnT *conn = pktOpen(RS232_COM1, 0, onPacket, NULL);
|
||||
|
||||
// Send a packet (blocking)
|
||||
uint8_t msg[] = "Hello, packets!";
|
||||
pktSend(conn, msg, sizeof(msg), true);
|
||||
|
||||
// Main loop
|
||||
while (1) {
|
||||
int delivered = pktPoll(conn);
|
||||
// delivered = number of packets received this iteration
|
||||
}
|
||||
|
||||
pktClose(conn);
|
||||
rs232Close(RS232_COM1);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## CRC
|
||||
|
||||
CRC-16-CCITT (polynomial 0x1021, init 0xFFFF) computed via a 256-entry
|
||||
lookup table (512 bytes). The CRC covers the SEQ, TYPE, LEN, and
|
||||
payload fields.
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
make # builds ../lib/libpacket.a
|
||||
make clean # removes objects and library
|
||||
```
|
||||
|
||||
Requires `librs232.a` at link time.
|
||||
|
||||
Target: DJGPP cross-compiler, 486+ CPU.
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
# SecLink Proxy — Linux build
|
||||
# Compiles the packet, security, and secLink layers against a socket
|
||||
# shim instead of the DJGPP rs232 driver.
|
||||
|
||||
CC = gcc
|
||||
CFLAGS = -O2 -Wall -Wextra
|
||||
|
||||
OBJDIR = ../obj/proxy
|
||||
BINDIR = ../bin
|
||||
|
||||
TARGET = $(BINDIR)/secproxy
|
||||
|
||||
OBJS = $(OBJDIR)/sockShim.o $(OBJDIR)/packet.o $(OBJDIR)/security.o \
|
||||
$(OBJDIR)/secLink.o $(OBJDIR)/proxy.o
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) | $(BINDIR)
|
||||
$(CC) -o $@ $(OBJS)
|
||||
|
||||
# Local sources
|
||||
$(OBJDIR)/sockShim.o: sockShim.c sockShim.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/proxy.o: proxy.c sockShim.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
# Packet layer — block real rs232.h, inject socket shim
|
||||
$(OBJDIR)/packet.o: ../packet/packet.c sockShim.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -I. -Istubs/ -include sockShim.h -c -o $@ $<
|
||||
|
||||
# Security layer — stub DOS-specific headers
|
||||
$(OBJDIR)/security.o: ../security/security.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -Istubs/ -c -o $@ $<
|
||||
|
||||
# SecLink layer — block real rs232.h, inject socket shim
|
||||
$(OBJDIR)/secLink.o: ../seclink/secLink.c sockShim.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -I. -include sockShim.h -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(BINDIR):
|
||||
mkdir -p $(BINDIR)
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
132
proxy/README.md
132
proxy/README.md
|
|
@ -1,132 +0,0 @@
|
|||
# SecLink Proxy
|
||||
|
||||
Linux-hosted proxy that bridges an 86Box emulated serial port to a
|
||||
remote telnet BBS. The 86Box side communicates using the secLink
|
||||
protocol (packet framing, DH key exchange, XTEA encryption). The BBS
|
||||
side is plain telnet over TCP.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
86Box (DOS terminal) Remote BBS
|
||||
| |
|
||||
emulated modem telnet:23
|
||||
| |
|
||||
TCP:2323 TCP:23
|
||||
| |
|
||||
+--- secproxy ----------------------------------+
|
||||
secLink ←→ plaintext
|
||||
(encrypted, reliable)
|
||||
```
|
||||
|
||||
The proxy accepts a single TCP connection from 86Box, performs the
|
||||
secLink handshake (DH key exchange), then connects to the BBS. All
|
||||
traffic between 86Box and the proxy is encrypted via XTEA-CTR on
|
||||
channel 0. Traffic between the proxy and the BBS is unencrypted
|
||||
telnet.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
secproxy [listen_port] [bbs_host] [bbs_port]
|
||||
```
|
||||
|
||||
| Argument | Default | Description |
|
||||
|---------------|------------------------|---------------------------------|
|
||||
| `listen_port` | 2323 | TCP port for 86Box connection |
|
||||
| `bbs_host` | bbs.duensing.digital | BBS hostname |
|
||||
| `bbs_port` | 23 | BBS TCP port |
|
||||
|
||||
```
|
||||
secproxy # all defaults
|
||||
secproxy 5000 # listen on port 5000
|
||||
secproxy 2323 bbs.example.com 23 # different BBS
|
||||
secproxy --help # show usage
|
||||
```
|
||||
|
||||
## Startup Sequence
|
||||
|
||||
1. Listen on the configured TCP port
|
||||
2. Wait for 86Box to connect (blocks on accept)
|
||||
3. Connect to the remote BBS
|
||||
4. Seed the RNG from `/dev/urandom`
|
||||
5. Open secLink and perform the DH handshake (blocks until the DOS
|
||||
side completes its handshake)
|
||||
6. Enter the proxy loop
|
||||
|
||||
## Proxy Loop
|
||||
|
||||
The main loop uses `poll()` with a 10ms timeout to multiplex between
|
||||
the two TCP connections:
|
||||
|
||||
- **86Box → BBS**: `secLinkPoll()` reads from the 86Box socket via
|
||||
the socket shim, decrypts incoming packets, and the receive callback
|
||||
writes plaintext to the BBS socket.
|
||||
- **BBS → 86Box**: `read()` from the BBS socket, then
|
||||
`secLinkSend()` encrypts and sends to 86Box via the socket shim.
|
||||
- **Maintenance**: `secLinkPoll()` also handles packet-layer retransmit
|
||||
timers on every iteration.
|
||||
|
||||
The proxy exits cleanly on Ctrl+C (SIGINT), SIGTERM, or when either
|
||||
side disconnects.
|
||||
|
||||
## 86Box Configuration
|
||||
|
||||
Configure the 86Box serial port to use a telnet connection:
|
||||
|
||||
1. In 86Box settings, set a COM port to "TCP (server)" or
|
||||
"TCP (client)" mode pointing at the proxy's listen port
|
||||
2. Enable "No telnet negotiation" to send raw bytes
|
||||
3. The DOS terminal application running inside 86Box uses secLink
|
||||
over this serial port
|
||||
|
||||
## Socket Shim
|
||||
|
||||
The proxy reuses the same packet, security, and secLink source code
|
||||
as the DOS build. A socket shim (`sockShim.h`/`sockShim.c`) provides
|
||||
rs232-compatible `rs232Read()`/`rs232Write()` functions backed by TCP
|
||||
sockets instead of UART hardware:
|
||||
|
||||
| rs232 function | Socket shim behavior |
|
||||
|----------------|-----------------------------------------------|
|
||||
| `rs232Open()` | No-op (socket already connected) |
|
||||
| `rs232Close()` | Marks port closed (socket managed by caller) |
|
||||
| `rs232Read()` | Non-blocking `recv()` with `MSG_DONTWAIT` |
|
||||
| `rs232Write()` | Blocking `send()` loop with `MSG_NOSIGNAL` |
|
||||
|
||||
The shim maps COM port indices (0-3) to socket file descriptors via
|
||||
`sockShimSetFd()`, which must be called before opening the secLink
|
||||
layer.
|
||||
|
||||
DOS-specific headers (`<pc.h>`, `<go32.h>`, `<sys/farptr.h>`) are
|
||||
replaced by minimal stubs in `stubs/` that provide no-op
|
||||
implementations. The security library's hardware entropy function
|
||||
returns zeros on Linux, which is harmless since the proxy seeds the
|
||||
RNG from `/dev/urandom` before the handshake.
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
make # builds ../bin/secproxy
|
||||
make clean # removes objects and binary
|
||||
```
|
||||
|
||||
Objects are placed in `../obj/proxy/`, the binary in `../bin/`.
|
||||
|
||||
Requires only a standard Linux C toolchain (gcc, libc). No external
|
||||
dependencies.
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
proxy/
|
||||
proxy.c main proxy program
|
||||
sockShim.h rs232-compatible socket API (header)
|
||||
sockShim.c socket shim implementation
|
||||
Makefile Linux build
|
||||
stubs/
|
||||
pc.h stub for DJGPP <pc.h>
|
||||
go32.h stub for DJGPP <go32.h>
|
||||
sys/
|
||||
farptr.h stub for DJGPP <sys/farptr.h>
|
||||
```
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
// Stub for DJGPP <go32.h> — Linux proxy build
|
||||
#ifndef GO32_H_STUB
|
||||
#define GO32_H_STUB
|
||||
|
||||
#define _dos_ds 0
|
||||
|
||||
#endif
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
// Stub for DJGPP <pc.h> — Linux proxy build
|
||||
#ifndef PC_H_STUB
|
||||
#define PC_H_STUB
|
||||
|
||||
static inline void outportb(unsigned short port, unsigned char val) { (void)port; (void)val; }
|
||||
static inline unsigned char inportb(unsigned short port) { (void)port; return 0; }
|
||||
|
||||
#endif
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
// Stub for DJGPP <sys/farptr.h> — Linux proxy build
|
||||
#ifndef FARPTR_H_STUB
|
||||
#define FARPTR_H_STUB
|
||||
|
||||
static inline unsigned long _farpeekl(unsigned short sel, unsigned long ofs) { (void)sel; (void)ofs; return 0; }
|
||||
|
||||
#endif
|
||||
BIN
releases/dvx-a1.zip
(Stored with Git LFS)
Normal file
BIN
releases/dvx-a1.zip
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
releases/dvx-a2.zip
(Stored with Git LFS)
Normal file
BIN
releases/dvx-a2.zip
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
releases/dvx-a3.zip
(Stored with Git LFS)
Normal file
BIN
releases/dvx-a3.zip
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -1,38 +0,0 @@
|
|||
# RS-232 Serial Library Makefile for DJGPP cross-compilation
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
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
|
||||
|
||||
OBJDIR = ../obj/rs232
|
||||
LIBDIR = ../lib
|
||||
|
||||
SRCS = rs232.c
|
||||
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
||||
TARGET = $(LIBDIR)/librs232.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) | $(LIBDIR)
|
||||
$(AR) rcs $@ $(OBJS)
|
||||
$(RANLIB) $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(LIBDIR):
|
||||
mkdir -p $(LIBDIR)
|
||||
|
||||
# Dependencies
|
||||
$(OBJDIR)/rs232.o: rs232.c rs232.h
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
237
rs232/README.md
237
rs232/README.md
|
|
@ -1,237 +0,0 @@
|
|||
# RS232 — Serial Port Library for DJGPP
|
||||
|
||||
ISR-driven UART communication library supporting up to 4 simultaneous
|
||||
COM ports with ring buffers and hardware/software flow control.
|
||||
|
||||
Ported from the DOS Serial Library 1.4 by Karl Stenerud (MIT License),
|
||||
stripped to DJGPP-only codepaths and restyled.
|
||||
|
||||
## Features
|
||||
|
||||
- ISR-driven receive and transmit with 2048-byte ring buffers
|
||||
- Auto-detected IRQ from BIOS data area
|
||||
- UART type detection (8250, 16450, 16550, 16550A)
|
||||
- 16550 FIFO detection and configurable trigger threshold
|
||||
- XON/XOFF, RTS/CTS, and DTR/DSR flow control
|
||||
- DPMI memory locking for ISR safety
|
||||
- Speeds from 50 to 115200 bps
|
||||
- 5-8 data bits, N/O/E/M/S parity, 1-2 stop bits
|
||||
|
||||
## API Reference
|
||||
|
||||
### Types
|
||||
|
||||
All functions take a COM port index (`int com`) as their first argument:
|
||||
|
||||
| Constant | Value | Description |
|
||||
|--------------|-------|-------------|
|
||||
| `RS232_COM1` | 0 | COM1 |
|
||||
| `RS232_COM2` | 1 | COM2 |
|
||||
| `RS232_COM3` | 2 | COM3 |
|
||||
| `RS232_COM4` | 3 | COM4 |
|
||||
|
||||
### UART Types
|
||||
|
||||
| Constant | Value | Description |
|
||||
|---------------------|-------|--------------------------------------|
|
||||
| `RS232_UART_UNKNOWN`| 0 | Unknown or undetected |
|
||||
| `RS232_UART_8250` | 1 | 8250 — no FIFO, no scratch register |
|
||||
| `RS232_UART_16450` | 2 | 16450 — scratch register, no FIFO |
|
||||
| `RS232_UART_16550` | 3 | 16550 — broken FIFO (unusable) |
|
||||
| `RS232_UART_16550A` | 4 | 16550A — working 16-byte FIFO |
|
||||
|
||||
### Handshaking Modes
|
||||
|
||||
| Constant | Value | Description |
|
||||
|--------------------------|-------|---------------------------|
|
||||
| `RS232_HANDSHAKE_NONE` | 0 | No flow control |
|
||||
| `RS232_HANDSHAKE_XONXOFF`| 1 | Software (XON/XOFF) |
|
||||
| `RS232_HANDSHAKE_RTSCTS` | 2 | Hardware (RTS/CTS) |
|
||||
| `RS232_HANDSHAKE_DTRDSR` | 3 | Hardware (DTR/DSR) |
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Constant | Value | Description |
|
||||
|-------------------------------|-------|---------------------------|
|
||||
| `RS232_SUCCESS` | 0 | Success |
|
||||
| `RS232_ERR_UNKNOWN` | -1 | Unknown error |
|
||||
| `RS232_ERR_NOT_OPEN` | -2 | Port not open |
|
||||
| `RS232_ERR_ALREADY_OPEN` | -3 | Port already open |
|
||||
| `RS232_ERR_NO_UART` | -4 | No UART detected |
|
||||
| `RS232_ERR_INVALID_PORT` | -5 | Bad port index |
|
||||
| `RS232_ERR_INVALID_BASE` | -6 | Bad I/O base address |
|
||||
| `RS232_ERR_INVALID_IRQ` | -7 | Bad IRQ number |
|
||||
| `RS232_ERR_INVALID_BPS` | -8 | Unsupported baud rate |
|
||||
| `RS232_ERR_INVALID_DATA` | -9 | Bad data bits (not 5-8) |
|
||||
| `RS232_ERR_INVALID_PARITY` | -10 | Bad parity character |
|
||||
| `RS232_ERR_INVALID_STOP` | -11 | Bad stop bits (not 1-2) |
|
||||
| `RS232_ERR_INVALID_HANDSHAKE` | -12 | Bad handshaking mode |
|
||||
| `RS232_ERR_INVALID_FIFO` | -13 | Bad FIFO threshold |
|
||||
| `RS232_ERR_NULL_PTR` | -14 | NULL pointer argument |
|
||||
| `RS232_ERR_IRQ_NOT_FOUND` | -15 | Could not detect IRQ |
|
||||
| `RS232_ERR_LOCK_MEM` | -16 | DPMI memory lock failed |
|
||||
|
||||
### Functions
|
||||
|
||||
#### Open / Close
|
||||
|
||||
```c
|
||||
int rs232Open(int com, int32_t bps, int dataBits, char parity,
|
||||
int stopBits, int handshake);
|
||||
```
|
||||
|
||||
Opens a COM port. Detects the UART base address from the BIOS data
|
||||
area, auto-detects the IRQ, installs the ISR, and configures the port.
|
||||
|
||||
- `bps` — baud rate (50, 75, 110, 150, 300, 600, 1200, 1800, 2400,
|
||||
3800, 4800, 7200, 9600, 19200, 38400, 57600, 115200)
|
||||
- `dataBits` — 5, 6, 7, or 8
|
||||
- `parity` — `'N'` (none), `'O'` (odd), `'E'` (even), `'M'` (mark),
|
||||
`'S'` (space)
|
||||
- `stopBits` — 1 or 2
|
||||
- `handshake` — `RS232_HANDSHAKE_*` constant
|
||||
|
||||
```c
|
||||
int rs232Close(int com);
|
||||
```
|
||||
|
||||
Closes the port, removes the ISR, and restores the original interrupt
|
||||
vector.
|
||||
|
||||
#### Read / Write
|
||||
|
||||
```c
|
||||
int rs232Read(int com, char *data, int len);
|
||||
```
|
||||
|
||||
Reads up to `len` bytes from the receive buffer. Returns the number of
|
||||
bytes actually read (0 if the buffer is empty).
|
||||
|
||||
```c
|
||||
int rs232Write(int com, const char *data, int len);
|
||||
```
|
||||
|
||||
Blocking write. Sends `len` bytes, waiting for transmit buffer space
|
||||
as needed. Returns `RS232_SUCCESS` or an error code.
|
||||
|
||||
```c
|
||||
int rs232WriteBuf(int com, const char *data, int len);
|
||||
```
|
||||
|
||||
Non-blocking write. Copies as many bytes as will fit into the transmit
|
||||
buffer. Returns the number of bytes actually queued.
|
||||
|
||||
#### Buffer Management
|
||||
|
||||
```c
|
||||
int rs232ClearRxBuffer(int com);
|
||||
int rs232ClearTxBuffer(int com);
|
||||
```
|
||||
|
||||
Discard all data in the receive or transmit ring buffer.
|
||||
|
||||
#### Getters
|
||||
|
||||
```c
|
||||
int rs232GetBase(int com); // UART I/O base address
|
||||
int32_t rs232GetBps(int com); // Current baud rate
|
||||
int rs232GetCts(int com); // CTS line state (0 or 1)
|
||||
int rs232GetData(int com); // Data bits setting
|
||||
int rs232GetDsr(int com); // DSR line state (0 or 1)
|
||||
int rs232GetDtr(int com); // DTR line state (0 or 1)
|
||||
int rs232GetHandshake(int com); // Handshaking mode
|
||||
int rs232GetIrq(int com); // IRQ number
|
||||
int rs232GetLsr(int com); // Line status register
|
||||
int rs232GetMcr(int com); // Modem control register
|
||||
int rs232GetMsr(int com); // Modem status register
|
||||
char rs232GetParity(int com); // Parity setting ('N','O','E','M','S')
|
||||
int rs232GetRts(int com); // RTS line state (0 or 1)
|
||||
int rs232GetRxBuffered(int com); // Bytes in receive buffer
|
||||
int rs232GetStop(int com); // Stop bits setting
|
||||
int rs232GetTxBuffered(int com); // Bytes in transmit buffer
|
||||
int rs232GetUartType(int com); // UART type (RS232_UART_* constant)
|
||||
```
|
||||
|
||||
`rs232GetUartType` probes the UART hardware to identify the chip:
|
||||
|
||||
1. **Scratch register test** — writes two values to register 7 and
|
||||
reads them back. The 8250 lacks this register, so readback fails.
|
||||
2. **FIFO test** — enables the FIFO via the FCR, then reads IIR bits
|
||||
7:6. `0b11` = 16550A (working FIFO), `0b10` = 16550 (broken FIFO),
|
||||
`0b00` = 16450 (no FIFO). The original FCR value is restored after
|
||||
probing.
|
||||
|
||||
#### Setters
|
||||
|
||||
```c
|
||||
int rs232Set(int com, int32_t bps, int dataBits, char parity,
|
||||
int stopBits, int handshake);
|
||||
```
|
||||
|
||||
Reconfigure all port parameters at once (port must be open).
|
||||
|
||||
```c
|
||||
int rs232SetBase(int com, int base); // Override I/O base address
|
||||
int rs232SetBps(int com, int32_t bps); // Change baud rate
|
||||
int rs232SetData(int com, int dataBits); // Change data bits
|
||||
int rs232SetDtr(int com, bool dtr); // Assert/deassert DTR
|
||||
int rs232SetFifoThreshold(int com, int thr); // FIFO trigger level (1,4,8,14)
|
||||
int rs232SetHandshake(int com, int handshake); // Change flow control mode
|
||||
int rs232SetIrq(int com, int irq); // Override IRQ (before Open)
|
||||
int rs232SetMcr(int com, int mcr); // Write modem control register
|
||||
int rs232SetParity(int com, char parity); // Change parity
|
||||
int rs232SetRts(int com, bool rts); // Assert/deassert RTS
|
||||
int rs232SetStop(int com, int stopBits); // Change stop bits
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```c
|
||||
#include "rs232.h"
|
||||
|
||||
int main(void) {
|
||||
// Open COM1 at 115200 8N1, no flow control
|
||||
int rc = rs232Open(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_NONE);
|
||||
if (rc != RS232_SUCCESS) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Identify UART chip
|
||||
int uartType = rs232GetUartType(RS232_COM1);
|
||||
// uartType == RS232_UART_16550A on most systems
|
||||
|
||||
// Blocking send
|
||||
rs232Write(RS232_COM1, "Hello\r\n", 7);
|
||||
|
||||
// Non-blocking receive
|
||||
char buf[128];
|
||||
int n;
|
||||
while ((n = rs232Read(RS232_COM1, buf, sizeof(buf))) > 0) {
|
||||
// process buf[0..n-1]
|
||||
}
|
||||
|
||||
rs232Close(RS232_COM1);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The ISR handles all four COM ports from a single shared handler.
|
||||
On entry it disables UART interrupts for all open ports, then
|
||||
re-enables CPU interrupts so higher-priority devices are serviced.
|
||||
- Ring buffers use power-of-2 sizes (2048 bytes) with bitmask indexing
|
||||
for zero-branch wraparound.
|
||||
- Flow control watermarks are at 80% (assert) and 20% (deassert) of
|
||||
buffer capacity.
|
||||
- DPMI `__dpmi_lock_linear_region` is used to pin the ISR, ring
|
||||
buffers, and port state in physical memory.
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
make # builds ../lib/librs232.a
|
||||
make clean # removes objects and library
|
||||
```
|
||||
|
||||
Target: DJGPP cross-compiler, 486+ CPU.
|
||||
27
run.sh
27
run.sh
|
|
@ -1,2 +1,27 @@
|
|||
#!/bin/bash
|
||||
flatpak run com.dosbox_x.DOSBox-X -conf dosbox-x.conf
|
||||
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (C) 2026 Scott Duensing
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
flatpak run com.dosbox_x.DOSBox-X -conf dosbox-x-overrides.conf
|
||||
#SDL_VIDEO_X11_VISUALID= ~/bin/dosbox-staging/dosbox -conf dosbox-staging-overrides.conf
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
# SecLink Library Makefile for DJGPP cross-compilation
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
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
|
||||
|
||||
OBJDIR = ../obj/seclink
|
||||
LIBDIR = ../lib
|
||||
|
||||
SRCS = secLink.c
|
||||
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
||||
TARGET = $(LIBDIR)/libseclink.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) | $(LIBDIR)
|
||||
$(AR) rcs $@ $(OBJS)
|
||||
$(RANLIB) $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(LIBDIR):
|
||||
mkdir -p $(LIBDIR)
|
||||
|
||||
# Dependencies
|
||||
$(OBJDIR)/secLink.o: secLink.c secLink.h ../rs232/rs232.h ../packet/packet.h ../security/security.h
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
|
|
@ -1,295 +0,0 @@
|
|||
# SecLink — Secure Serial Link Library
|
||||
|
||||
SecLink is a convenience wrapper that ties together three lower-level
|
||||
libraries into a single API for reliable, optionally encrypted serial
|
||||
communication:
|
||||
|
||||
- **rs232** — ISR-driven UART I/O with ring buffers and flow control
|
||||
- **packet** — HDLC-style framing with CRC-16 and sliding window reliability
|
||||
- **security** — 1024-bit Diffie-Hellman key exchange and XTEA-CTR encryption
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Application
|
||||
|
|
||||
[secLink] channels, optional encryption
|
||||
|
|
||||
[packet] framing, CRC, retransmit, ordering
|
||||
|
|
||||
[rs232] ISR-driven UART, ring buffers, flow control
|
||||
|
|
||||
UART
|
||||
```
|
||||
|
||||
SecLink adds a one-byte header to every packet:
|
||||
|
||||
```
|
||||
Bit 7 Bits 6..0
|
||||
----- ---------
|
||||
Encrypt Channel (0-127)
|
||||
```
|
||||
|
||||
This allows mixing encrypted and cleartext traffic on up to 128
|
||||
independent logical channels over a single serial link.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
```
|
||||
secLinkOpen() Open COM port and packet layer
|
||||
secLinkHandshake() DH key exchange (blocks until complete)
|
||||
secLinkSend() Send a packet (encrypted or clear)
|
||||
secLinkPoll() Receive and deliver packets to callback
|
||||
secLinkClose() Tear down everything
|
||||
```
|
||||
|
||||
The handshake is only required if you intend to send encrypted packets.
|
||||
Cleartext packets can be sent immediately after `secLinkOpen()`.
|
||||
|
||||
## API Reference
|
||||
|
||||
### Types
|
||||
|
||||
```c
|
||||
// Receive callback — called for each incoming packet with plaintext
|
||||
typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel);
|
||||
|
||||
// Opaque connection handle
|
||||
typedef struct SecLinkS SecLinkT;
|
||||
```
|
||||
|
||||
### Constants
|
||||
|
||||
| Name | Value | Description |
|
||||
|-------------------------|-------|----------------------------------------|
|
||||
| `SECLINK_MAX_PAYLOAD` | 254 | Max bytes per `secLinkSend()` call |
|
||||
| `SECLINK_MAX_CHANNEL` | 127 | Highest valid channel number |
|
||||
| `SECLINK_SUCCESS` | 0 | Operation succeeded |
|
||||
| `SECLINK_ERR_PARAM` | -1 | Invalid parameter |
|
||||
| `SECLINK_ERR_SERIAL` | -2 | Serial port error |
|
||||
| `SECLINK_ERR_ALLOC` | -3 | Memory allocation failed |
|
||||
| `SECLINK_ERR_HANDSHAKE` | -4 | Key exchange failed |
|
||||
| `SECLINK_ERR_NOT_READY` | -5 | Encryption requested before handshake |
|
||||
| `SECLINK_ERR_SEND` | -6 | Packet layer send failed |
|
||||
|
||||
### Functions
|
||||
|
||||
#### secLinkOpen
|
||||
|
||||
```c
|
||||
SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity,
|
||||
int stopBits, int handshake,
|
||||
SecLinkRecvT callback, void *ctx);
|
||||
```
|
||||
|
||||
Opens the COM port via rs232, creates the packet layer, and returns a
|
||||
link handle. Returns `NULL` on failure. The callback is invoked from
|
||||
`secLinkPoll()` for each received packet.
|
||||
|
||||
#### secLinkClose
|
||||
|
||||
```c
|
||||
void secLinkClose(SecLinkT *link);
|
||||
```
|
||||
|
||||
Destroys cipher contexts, closes the packet layer and COM port, and
|
||||
frees all memory.
|
||||
|
||||
#### secLinkHandshake
|
||||
|
||||
```c
|
||||
int secLinkHandshake(SecLinkT *link);
|
||||
```
|
||||
|
||||
Performs a Diffie-Hellman key exchange. Blocks until both sides have
|
||||
exchanged public keys and derived cipher keys. The RNG must be seeded
|
||||
(via `secRngSeed()` or `secRngAddEntropy()`) before calling this.
|
||||
|
||||
Each side derives separate TX and RX keys from the shared secret,
|
||||
using public key ordering to determine directionality. This prevents
|
||||
CTR counter collisions.
|
||||
|
||||
#### secLinkGetPending
|
||||
|
||||
```c
|
||||
int secLinkGetPending(SecLinkT *link);
|
||||
```
|
||||
|
||||
Returns the number of unacknowledged packets in the transmit window.
|
||||
Useful for non-blocking send loops to determine if there is room to
|
||||
send more data.
|
||||
|
||||
#### secLinkIsReady
|
||||
|
||||
```c
|
||||
bool secLinkIsReady(SecLinkT *link);
|
||||
```
|
||||
|
||||
Returns `true` if the handshake is complete and the link is ready for
|
||||
encrypted communication.
|
||||
|
||||
#### secLinkPoll
|
||||
|
||||
```c
|
||||
int secLinkPoll(SecLinkT *link);
|
||||
```
|
||||
|
||||
Reads available serial data, processes received frames, handles ACKs
|
||||
and retransmits. Decrypts encrypted packets and delivers plaintext to
|
||||
the receive callback. Returns the number of packets delivered, or
|
||||
negative on error.
|
||||
|
||||
Must be called frequently (e.g. in your main loop).
|
||||
|
||||
#### secLinkSend
|
||||
|
||||
```c
|
||||
int secLinkSend(SecLinkT *link, const uint8_t *data, int len,
|
||||
uint8_t channel, bool encrypt, bool block);
|
||||
```
|
||||
|
||||
Sends up to `SECLINK_MAX_PAYLOAD` (255) bytes on the given channel.
|
||||
|
||||
- `channel` — logical channel number (0-127)
|
||||
- `encrypt` — if `true`, encrypts the payload (requires completed handshake)
|
||||
- `block` — if `true`, waits for transmit window space; if `false`,
|
||||
returns `SECLINK_ERR_SEND` when the window is full
|
||||
|
||||
Cleartext packets (`encrypt = false`) can be sent before the handshake.
|
||||
|
||||
#### secLinkSendBuf
|
||||
|
||||
```c
|
||||
int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len,
|
||||
uint8_t channel, bool encrypt);
|
||||
```
|
||||
|
||||
Sends an arbitrarily large buffer by splitting it into
|
||||
`SECLINK_MAX_PAYLOAD`-byte chunks. Always blocks until all data is
|
||||
sent. Returns `SECLINK_SUCCESS` or the first error encountered.
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic encrypted link
|
||||
|
||||
```c
|
||||
#include "secLink.h"
|
||||
#include "../security/security.h"
|
||||
|
||||
void onRecv(void *ctx, const uint8_t *data, int len, uint8_t channel) {
|
||||
// handle received plaintext on 'channel'
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
// Seed the RNG before handshake
|
||||
uint8_t entropy[16];
|
||||
secRngGatherEntropy(entropy, sizeof(entropy));
|
||||
secRngSeed(entropy, sizeof(entropy));
|
||||
|
||||
// Open link on COM1 at 115200 8N1
|
||||
SecLinkT *link = secLinkOpen(0, 115200, 8, 'N', 1, 0, onRecv, NULL);
|
||||
if (!link) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Key exchange (blocks until both sides complete)
|
||||
if (secLinkHandshake(link) != SECLINK_SUCCESS) {
|
||||
secLinkClose(link);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Send encrypted data on channel 0
|
||||
const char *msg = "Hello, secure world!";
|
||||
secLinkSend(link, (const uint8_t *)msg, strlen(msg), 0, true, true);
|
||||
|
||||
// Main loop
|
||||
while (1) {
|
||||
secLinkPoll(link);
|
||||
}
|
||||
|
||||
secLinkClose(link);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Mixed encrypted and cleartext channels
|
||||
|
||||
```c
|
||||
#define CHAN_CONTROL 0 // cleartext control channel
|
||||
#define CHAN_DATA 1 // encrypted data channel
|
||||
|
||||
// Send a cleartext status message (no handshake needed)
|
||||
secLinkSend(link, statusMsg, statusLen, CHAN_CONTROL, false, true);
|
||||
|
||||
// Send encrypted payload (requires completed handshake)
|
||||
secLinkSend(link, payload, payloadLen, CHAN_DATA, true, true);
|
||||
```
|
||||
|
||||
### Non-blocking file transfer
|
||||
|
||||
```c
|
||||
int sendFile(SecLinkT *link, const uint8_t *fileData, int fileSize,
|
||||
uint8_t channel, bool encrypt, int windowSize) {
|
||||
int offset = 0;
|
||||
int bytesLeft = fileSize;
|
||||
|
||||
while (bytesLeft > 0) {
|
||||
secLinkPoll(link); // process ACKs, free window slots
|
||||
|
||||
if (secLinkGetPending(link) < windowSize) {
|
||||
int chunk = bytesLeft;
|
||||
if (chunk > SECLINK_MAX_PAYLOAD) {
|
||||
chunk = SECLINK_MAX_PAYLOAD;
|
||||
}
|
||||
|
||||
int rc = secLinkSend(link, fileData + offset, chunk,
|
||||
channel, encrypt, false);
|
||||
if (rc == SECLINK_SUCCESS) {
|
||||
offset += chunk;
|
||||
bytesLeft -= chunk;
|
||||
}
|
||||
// SECLINK_ERR_SEND means window full, just retry next iteration
|
||||
}
|
||||
|
||||
// Application can do other work here:
|
||||
// update progress bar, check for cancel, etc.
|
||||
}
|
||||
|
||||
// Drain remaining ACKs
|
||||
while (secLinkGetPending(link) > 0) {
|
||||
secLinkPoll(link);
|
||||
}
|
||||
|
||||
return SECLINK_SUCCESS;
|
||||
}
|
||||
```
|
||||
|
||||
### Blocking bulk transfer
|
||||
|
||||
```c
|
||||
// Send an entire file in one call (blocks until complete)
|
||||
secLinkSendBuf(link, fileData, fileSize, CHAN_DATA, true);
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
make # builds ../lib/libseclink.a
|
||||
make clean # removes objects and library
|
||||
```
|
||||
|
||||
Link against all four libraries:
|
||||
|
||||
```
|
||||
-lseclink -lpacket -lsecurity -lrs232
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
SecLink requires these libraries (all in `../lib/`):
|
||||
|
||||
- `librs232.a` — serial port driver
|
||||
- `libpacket.a` — packet framing and reliability
|
||||
- `libsecurity.a` — DH key exchange and XTEA cipher
|
||||
|
||||
Target: DJGPP cross-compiler, 486+ CPU.
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# Security Library Makefile for DJGPP cross-compilation
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
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
|
||||
|
||||
OBJDIR = ../obj/security
|
||||
LIBDIR = ../lib
|
||||
|
||||
SRCS = security.c
|
||||
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
||||
TARGET = $(LIBDIR)/libsecurity.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) | $(LIBDIR)
|
||||
$(AR) rcs $@ $(OBJS)
|
||||
$(RANLIB) $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(LIBDIR):
|
||||
mkdir -p $(LIBDIR)
|
||||
|
||||
# Dependencies
|
||||
$(OBJDIR)/security.o: security.c security.h
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
|
|
@ -1,259 +0,0 @@
|
|||
# Security — DH Key Exchange and XTEA-CTR Cipher
|
||||
|
||||
Cryptographic library providing Diffie-Hellman key exchange and XTEA
|
||||
symmetric encryption, optimized for 486-class DOS hardware running
|
||||
under DJGPP/DPMI.
|
||||
|
||||
## Components
|
||||
|
||||
### Diffie-Hellman Key Exchange
|
||||
|
||||
- 1024-bit MODP group (RFC 2409 Group 2 safe prime)
|
||||
- 256-bit private exponents for fast computation on 486 CPUs
|
||||
- Montgomery multiplication (CIOS variant) for modular exponentiation
|
||||
- Lazy-initialized Montgomery constants (R^2 mod p, -p0^-1 mod 2^32)
|
||||
|
||||
### XTEA Cipher (CTR Mode)
|
||||
|
||||
- 128-bit key, 64-bit block size, 32 rounds
|
||||
- CTR mode — encrypt and decrypt are the same XOR operation
|
||||
- No lookup tables, no key schedule — just shifts, adds, and XORs
|
||||
- Ideal for constrained environments with small key setup cost
|
||||
|
||||
### Pseudo-Random Number Generator
|
||||
|
||||
- XTEA-CTR based DRBG (deterministic random bit generator)
|
||||
- Hardware entropy from PIT counter (~10 bits) and BIOS tick count
|
||||
- Supports additional entropy injection (keyboard timing, mouse, etc.)
|
||||
- Auto-seeds from hardware on first use if not explicitly seeded
|
||||
|
||||
## Performance
|
||||
|
||||
At serial port speeds, encryption overhead is minimal:
|
||||
|
||||
| Speed | Blocks/sec | CPU cycles/sec | % of 33 MHz 486 |
|
||||
|----------|------------|----------------|------------------|
|
||||
| 9600 | 120 | ~240K | < 1% |
|
||||
| 57600 | 720 | ~1.4M | ~4% |
|
||||
| 115200 | 1440 | ~2.9M | ~9% |
|
||||
|
||||
DH key exchange takes approximately 0.3s at 66 MHz or 0.6s at 33 MHz
|
||||
(256-bit private exponent, 1024-bit modulus).
|
||||
|
||||
## API Reference
|
||||
|
||||
### Constants
|
||||
|
||||
| Name | Value | Description |
|
||||
|---------------------|-------|--------------------------------|
|
||||
| `SEC_DH_KEY_SIZE` | 128 | DH public key size (bytes) |
|
||||
| `SEC_XTEA_KEY_SIZE` | 16 | XTEA key size (bytes) |
|
||||
| `SEC_SUCCESS` | 0 | Success |
|
||||
| `SEC_ERR_PARAM` | -1 | Invalid parameter |
|
||||
| `SEC_ERR_NOT_READY` | -2 | Keys not yet generated/derived |
|
||||
| `SEC_ERR_ALLOC` | -3 | Memory allocation failed |
|
||||
|
||||
### Types
|
||||
|
||||
```c
|
||||
typedef struct SecDhS SecDhT; // Opaque DH context
|
||||
typedef struct SecCipherS SecCipherT; // Opaque cipher context
|
||||
```
|
||||
|
||||
### RNG Functions
|
||||
|
||||
```c
|
||||
int secRngGatherEntropy(uint8_t *buf, int len);
|
||||
```
|
||||
|
||||
Reads hardware entropy sources (PIT counter, BIOS tick count). Returns
|
||||
the number of bytes written. Provides roughly 20 bits of true entropy.
|
||||
|
||||
```c
|
||||
void secRngSeed(const uint8_t *entropy, int len);
|
||||
```
|
||||
|
||||
Initializes the DRBG with the given entropy. XOR-folds the input into
|
||||
the XTEA key, derives the counter, and mixes state by generating and
|
||||
discarding 64 bytes.
|
||||
|
||||
```c
|
||||
void secRngAddEntropy(const uint8_t *data, int len);
|
||||
```
|
||||
|
||||
Mixes additional entropy into the running RNG state without resetting
|
||||
it. Use this to stir in keyboard timing, mouse jitter, or other
|
||||
runtime entropy.
|
||||
|
||||
```c
|
||||
void secRngBytes(uint8_t *buf, int len);
|
||||
```
|
||||
|
||||
Generates `len` pseudo-random bytes. Auto-seeds from hardware if not
|
||||
previously seeded.
|
||||
|
||||
### Diffie-Hellman Functions
|
||||
|
||||
```c
|
||||
SecDhT *secDhCreate(void);
|
||||
```
|
||||
|
||||
Allocates a new DH context. Returns `NULL` on allocation failure.
|
||||
|
||||
```c
|
||||
int secDhGenerateKeys(SecDhT *dh);
|
||||
```
|
||||
|
||||
Generates a 256-bit random private key and computes the corresponding
|
||||
1024-bit public key (g^private mod p). The RNG should be seeded first.
|
||||
|
||||
```c
|
||||
int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
|
||||
```
|
||||
|
||||
Exports the public key into `buf`. On entry, `*len` must be at least
|
||||
`SEC_DH_KEY_SIZE` (128). On return, `*len` is set to 128.
|
||||
|
||||
```c
|
||||
int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
|
||||
```
|
||||
|
||||
Computes the shared secret from the remote side's public key.
|
||||
Validates that the remote key is in range [2, p-2] to prevent
|
||||
small-subgroup attacks.
|
||||
|
||||
```c
|
||||
int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen);
|
||||
```
|
||||
|
||||
Derives a symmetric key by XOR-folding the 128-byte shared secret
|
||||
down to `keyLen` bytes.
|
||||
|
||||
```c
|
||||
void secDhDestroy(SecDhT *dh);
|
||||
```
|
||||
|
||||
Securely zeroes and frees the DH context (private key, shared secret).
|
||||
|
||||
### Cipher Functions
|
||||
|
||||
```c
|
||||
SecCipherT *secCipherCreate(const uint8_t *key);
|
||||
```
|
||||
|
||||
Creates an XTEA-CTR cipher context with the given 16-byte key. Counter
|
||||
starts at zero.
|
||||
|
||||
```c
|
||||
void secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
|
||||
```
|
||||
|
||||
Encrypts or decrypts `data` in place. CTR mode is symmetric — the
|
||||
same operation encrypts and decrypts.
|
||||
|
||||
```c
|
||||
void secCipherSetNonce(SecCipherT *c, uint32_t nonceLo, uint32_t nonceHi);
|
||||
```
|
||||
|
||||
Sets the 64-bit nonce/counter. Call before encrypting if you need a
|
||||
specific starting counter value.
|
||||
|
||||
```c
|
||||
void secCipherDestroy(SecCipherT *c);
|
||||
```
|
||||
|
||||
Securely zeroes and frees the cipher context.
|
||||
|
||||
## Example
|
||||
|
||||
### Full Key Exchange
|
||||
|
||||
```c
|
||||
#include "security.h"
|
||||
#include <string.h>
|
||||
|
||||
// Seed the RNG
|
||||
uint8_t entropy[16];
|
||||
secRngGatherEntropy(entropy, sizeof(entropy));
|
||||
secRngSeed(entropy, sizeof(entropy));
|
||||
|
||||
// Create DH context and generate keys
|
||||
SecDhT *dh = secDhCreate();
|
||||
secDhGenerateKeys(dh);
|
||||
|
||||
// Export public key to send to remote
|
||||
uint8_t myPub[SEC_DH_KEY_SIZE];
|
||||
int pubLen = SEC_DH_KEY_SIZE;
|
||||
secDhGetPublicKey(dh, myPub, &pubLen);
|
||||
// ... send myPub to remote, receive remotePub ...
|
||||
|
||||
// Compute shared secret and derive a 16-byte key
|
||||
secDhComputeSecret(dh, remotePub, SEC_DH_KEY_SIZE);
|
||||
|
||||
uint8_t key[SEC_XTEA_KEY_SIZE];
|
||||
secDhDeriveKey(dh, key, SEC_XTEA_KEY_SIZE);
|
||||
secDhDestroy(dh);
|
||||
|
||||
// Create cipher and encrypt
|
||||
SecCipherT *cipher = secCipherCreate(key);
|
||||
uint8_t message[] = "Secret message";
|
||||
secCipherCrypt(cipher, message, sizeof(message));
|
||||
// message is now encrypted
|
||||
|
||||
// Decrypt (same operation — CTR mode is symmetric)
|
||||
// Reset counter first if using the same cipher context
|
||||
secCipherSetNonce(cipher, 0, 0);
|
||||
secCipherCrypt(cipher, message, sizeof(message));
|
||||
// message is now plaintext again
|
||||
|
||||
secCipherDestroy(cipher);
|
||||
```
|
||||
|
||||
### Standalone Encryption
|
||||
|
||||
```c
|
||||
// XTEA-CTR can be used independently of DH
|
||||
uint8_t key[SEC_XTEA_KEY_SIZE] = { /* your key */ };
|
||||
SecCipherT *c = secCipherCreate(key);
|
||||
|
||||
uint8_t data[1024];
|
||||
// ... fill data ...
|
||||
secCipherCrypt(c, data, sizeof(data)); // encrypt in place
|
||||
|
||||
secCipherDestroy(c);
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### BigNum Arithmetic
|
||||
|
||||
All modular arithmetic uses a 1024-bit big number type (`BigNumT`)
|
||||
stored as 32 x `uint32_t` words in little-endian order. Operations:
|
||||
|
||||
- Add, subtract, compare, shift-left-1, bit test
|
||||
- Montgomery multiplication (CIOS with implicit right-shift)
|
||||
- Modular exponentiation (left-to-right binary square-and-multiply)
|
||||
|
||||
### Montgomery Multiplication
|
||||
|
||||
The CIOS (Coarsely Integrated Operand Scanning) variant computes
|
||||
`a * b * R^-1 mod m` in a single pass with implicit division by the
|
||||
word base. Constants are computed once on first DH use:
|
||||
|
||||
- `R^2 mod p` — via 2048 iterations of shift-and-conditional-subtract
|
||||
- `-p[0]^-1 mod 2^32` — via Newton's method (5 iterations)
|
||||
|
||||
### Secure Zeroing
|
||||
|
||||
Key material is erased using a volatile-pointer loop that the compiler
|
||||
cannot optimize away, preventing sensitive data from lingering in
|
||||
memory.
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
make # builds ../lib/libsecurity.a
|
||||
make clean # removes objects and library
|
||||
```
|
||||
|
||||
Target: DJGPP cross-compiler, 486+ CPU.
|
||||
169
src/apps/kpunch/Makefile
Normal file
169
src/apps/kpunch/Makefile
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (C) 2026 Scott Duensing
|
||||
#
|
||||
# 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.
|
||||
|
||||
# DVX Shell Applications Makefile -- builds DXE3 modules
|
||||
#
|
||||
# Source tree is now one dir per app, mirroring bin/apps/kpunch/.
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
|
||||
CFLAGS = -O2 -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation -march=i486 -mtune=i586 -I../../libs/kpunch/libdvx -I../../libs/kpunch/libdvx/platform -I../../libs/kpunch/libdvx/thirdparty -I../../widgets/kpunch -I../../libs/kpunch/libtasks -I../../libs/kpunch/dvxshell
|
||||
|
||||
OBJDIR = ../../../obj/apps
|
||||
BINDIR = ../../../bin/apps
|
||||
DVXRES = ../../../bin/host/dvxres
|
||||
|
||||
# C apps: one directory, one .c file each
|
||||
C_APPS = progman clock dvxdemo cpanel dvxhelp
|
||||
|
||||
BASCOMP = ../../../bin/host/bascomp
|
||||
# BASIC apps: each is a .dbp project in its own directory.
|
||||
# BASIC-only notepad, imgview, etc. replace the old C versions.
|
||||
BASIC_APPS = iconed notepad imgview helpedit resedit basdemo widshow
|
||||
|
||||
.PHONY: all clean $(C_APPS) dvxbasic $(BASIC_APPS)
|
||||
|
||||
all: $(C_APPS) dvxbasic $(BASIC_APPS)
|
||||
|
||||
dvxbasic:
|
||||
$(MAKE) -C dvxbasic
|
||||
|
||||
cpanel: $(BINDIR)/kpunch/cpanel/cpanel.app
|
||||
progman: $(BINDIR)/kpunch/progman/progman.app
|
||||
clock: $(BINDIR)/kpunch/clock/clock.app
|
||||
dvxdemo: $(BINDIR)/kpunch/dvxdemo/dvxdemo.app
|
||||
dvxhelp: $(BINDIR)/kpunch/dvxhelp/dvxhelp.app
|
||||
|
||||
$(BINDIR)/kpunch/cpanel/cpanel.app: $(OBJDIR)/cpanel.o cpanel/cpanel.res cpanel/icon32.bmp | $(BINDIR)/kpunch/cpanel
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
cd cpanel && ../$(DVXRES) build ../$@ cpanel.res
|
||||
|
||||
$(BINDIR)/kpunch/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/kpunch/progman
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
|
||||
$(BINDIR)/kpunch/clock/clock.app: $(OBJDIR)/clock.o clock/clock.res clock/icon32.bmp | $(BINDIR)/kpunch/clock
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
cd clock && ../$(DVXRES) build ../$@ clock.res
|
||||
|
||||
DVXDEMO_BMPS = logo.bmp new.bmp open.bmp sample.bmp save.bmp
|
||||
|
||||
$(BINDIR)/kpunch/dvxdemo/dvxdemo.app: $(OBJDIR)/dvxdemo.o $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) dvxdemo/dvxdemo.res dvxdemo/icon32.bmp | $(BINDIR)/kpunch/dvxdemo
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
cd dvxdemo && ../$(DVXRES) build ../$@ dvxdemo.res
|
||||
cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/kpunch/dvxdemo/
|
||||
|
||||
$(BINDIR)/kpunch/dvxhelp/dvxhelp.app: $(OBJDIR)/dvxhelp.o dvxhelp/dvxhelp.res dvxhelp/icon32.bmp | $(BINDIR)/kpunch/dvxhelp
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
cd dvxhelp && ../$(DVXRES) build ../$@ dvxhelp.res
|
||||
|
||||
$(OBJDIR)/cpanel.o: cpanel/cpanel.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
$(OBJDIR)/progman.o: progman/progman.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
$(OBJDIR)/clock.o: clock/clock.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
$(OBJDIR)/dvxhelp.o: dvxhelp/dvxhelp.c dvxhelp/hlpformat.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
# BASIC apps (compiled from .dbp projects via bascomp). Source for each app
|
||||
# is under the app's own directory.
|
||||
iconed: $(BINDIR)/kpunch/iconed/iconed.app
|
||||
|
||||
$(BINDIR)/kpunch/iconed/iconed.app: iconed/iconed.dbp iconed/iconed.frm iconed/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/iconed dvxbasic
|
||||
$(BASCOMP) iconed/iconed.dbp -o $@ -release
|
||||
|
||||
notepad: $(BINDIR)/kpunch/notepad/notepad.app
|
||||
|
||||
$(BINDIR)/kpunch/notepad/notepad.app: notepad/notepad.dbp notepad/notepad.frm notepad/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/notepad dvxbasic
|
||||
$(BASCOMP) notepad/notepad.dbp -o $@ -release
|
||||
|
||||
imgview: $(BINDIR)/kpunch/imgview/imgview.app
|
||||
|
||||
$(BINDIR)/kpunch/imgview/imgview.app: imgview/imgview.dbp imgview/imgview.frm imgview/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/imgview dvxbasic
|
||||
$(BASCOMP) imgview/imgview.dbp -o $@ -release
|
||||
|
||||
helpedit: $(BINDIR)/kpunch/dvxhelp/helpedit.app
|
||||
|
||||
$(BINDIR)/kpunch/dvxhelp/helpedit.app: dvxhelp/helpedit/helpedit.dbp dvxhelp/helpedit/helpedit.frm dvxhelp/helpedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/dvxhelp dvxbasic
|
||||
$(BASCOMP) dvxhelp/helpedit/helpedit.dbp -o $@ -release
|
||||
$(DVXRES) add $@ helpfile text "dvxhelp.hlp"
|
||||
|
||||
resedit: $(BINDIR)/kpunch/resedit/resedit.app
|
||||
|
||||
$(BINDIR)/kpunch/resedit/resedit.app: resedit/resedit.dbp resedit/resedit.frm resedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/resedit dvxbasic
|
||||
$(BASCOMP) resedit/resedit.dbp -o $@ -release
|
||||
|
||||
basdemo: $(BINDIR)/kpunch/basdemo/basdemo.app
|
||||
|
||||
$(BINDIR)/kpunch/basdemo/basdemo.app: basdemo/basdemo.dbp basdemo/basdemo.frm basdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basdemo dvxbasic
|
||||
$(BASCOMP) basdemo/basdemo.dbp -o $@ -release
|
||||
|
||||
widshow: $(BINDIR)/kpunch/widshow/widshow.app
|
||||
|
||||
$(BINDIR)/kpunch/widshow/widshow.app: widshow/widshow.dbp widshow/widshow.frm widshow/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/widshow dvxbasic
|
||||
$(BASCOMP) widshow/widshow.dbp -o $@ -release
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(BINDIR)/kpunch/cpanel: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/progman: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/clock: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/dvxdemo: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/dvxhelp: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/iconed: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/notepad: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/imgview: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/resedit: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/basdemo: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/widshow: ; mkdir -p $@
|
||||
|
||||
# Header dependencies
|
||||
COMMON_H = ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxDlg.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxWm.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h
|
||||
# Every widget public header. Apps that pull in widget APIs (e.g. dvxdemo)
|
||||
# must rebuild when these change, otherwise the struct-layout contract
|
||||
# between the widget DXE's sApi and the app's cast to RadioApiT/etc.
|
||||
# silently drifts and produces late-binding crashes.
|
||||
WIDGET_H = $(wildcard ../../widgets/kpunch/*/*.h)
|
||||
$(OBJDIR)/cpanel.o: cpanel/cpanel.c $(COMMON_H) ../../libs/kpunch/libdvx/dvxPrefs.h ../../libs/kpunch/libdvx/platform/dvxPlat.h
|
||||
$(OBJDIR)/progman.o: progman/progman.c $(COMMON_H) ../../libs/kpunch/dvxshell/shellInf.h
|
||||
$(OBJDIR)/clock.o: clock/clock.c ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxDraw.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h ../../libs/kpunch/libtasks/taskSwch.h
|
||||
$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c $(COMMON_H) $(WIDGET_H)
|
||||
|
||||
clean:
|
||||
rm -f $(OBJDIR)/*.o
|
||||
rm -f $(BINDIR)/kpunch/cpanel/cpanel.app
|
||||
rm -f $(BINDIR)/kpunch/progman/progman.app
|
||||
rm -f $(BINDIR)/kpunch/clock/clock.app
|
||||
rm -f $(BINDIR)/kpunch/dvxdemo/dvxdemo.app $(addprefix $(BINDIR)/kpunch/dvxdemo/,$(DVXDEMO_BMPS))
|
||||
rm -f $(BINDIR)/kpunch/dvxhelp/dvxhelp.app
|
||||
rm -f $(BINDIR)/kpunch/iconed/iconed.app
|
||||
rm -f $(BINDIR)/kpunch/notepad/notepad.app
|
||||
rm -f $(BINDIR)/kpunch/imgview/imgview.app
|
||||
rm -f $(BINDIR)/kpunch/dvxhelp/helpedit.app
|
||||
rm -f $(BINDIR)/kpunch/resedit/resedit.app
|
||||
rm -f $(BINDIR)/kpunch/basdemo/basdemo.app
|
||||
rm -f $(BINDIR)/kpunch/widshow/widshow.app
|
||||
$(MAKE) -C dvxbasic clean
|
||||
472
src/apps/kpunch/README.md
Normal file
472
src/apps/kpunch/README.md
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
# DVX Shell Applications
|
||||
|
||||
Every DVX application is a DXE3 module packaged as a `.app` file,
|
||||
installed under `bin/apps/kpunch/<name>/<name>.app`. The Program
|
||||
Manager scans this directory at startup and displays each discovered
|
||||
app as a launchable icon on the desktop.
|
||||
|
||||
This directory holds the source for the bundled applications plus
|
||||
the BASIC-based sample apps. Two flavours of app live side by side:
|
||||
|
||||
* **C apps** -- written directly against the DVX SDK headers,
|
||||
compiled to a DXE3 by `dxe3gen`. See `progman/`, `clock/`,
|
||||
`dvxdemo/`, `cpanel/`, `dvxhelp/`.
|
||||
* **BASIC apps** -- `.dbp` projects compiled to `.app` files by the
|
||||
host-side `bascomp`. The compiler links the BASIC bytecode into a
|
||||
copy of `basstub.app`, which acts as the runtime host. See
|
||||
`iconed/`, `notepad/`, `imgview/`, `basdemo/`, `resedit/`,
|
||||
`dvxhelp/helpedit/`.
|
||||
|
||||
The rest of this document covers writing a C app against the SDK.
|
||||
For BASIC apps, see `dvxbasic/README.md`.
|
||||
|
||||
|
||||
## DXE App Contract
|
||||
|
||||
Every `.app` binary exports two required symbols and one optional
|
||||
symbol:
|
||||
|
||||
```c
|
||||
AppDescriptorT appDescriptor; // required
|
||||
int32_t appMain(DxeAppContextT *ctx); // required
|
||||
void appShutdown(void); // optional
|
||||
```
|
||||
|
||||
### `appDescriptor`
|
||||
|
||||
A statically-initialised struct read by the shell at load time to
|
||||
decide how to launch the app:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
char name[SHELL_APP_NAME_MAX]; // display name (<= 64 chars)
|
||||
bool hasMainLoop; // see below
|
||||
bool multiInstance; // may have multiple copies open
|
||||
int32_t stackSize; // SHELL_STACK_DEFAULT or byte count
|
||||
int32_t priority; // TS_PRIORITY_* for main-loop apps
|
||||
} AppDescriptorT;
|
||||
```
|
||||
|
||||
Example (from `clock/clock.c`):
|
||||
|
||||
```c
|
||||
AppDescriptorT appDescriptor = {
|
||||
.name = "Clock",
|
||||
.hasMainLoop = true,
|
||||
.multiInstance = true,
|
||||
.stackSize = SHELL_STACK_DEFAULT,
|
||||
.priority = TS_PRIORITY_LOW
|
||||
};
|
||||
```
|
||||
|
||||
### `appMain`
|
||||
|
||||
Entry point. The shell calls it with a `DxeAppContextT` that exposes
|
||||
the shell's GUI context, the app's ID, its install directory, a
|
||||
writable config directory, launch arguments, and a help-topic query
|
||||
callback for F1.
|
||||
|
||||
Returning from `appMain` signals that the app has finished; the
|
||||
shell unloads the DXE, frees per-app memory, and reclaims the app
|
||||
slot.
|
||||
|
||||
### `appShutdown`
|
||||
|
||||
Called by the shell when the app is force-killed (by the Task
|
||||
Manager) or when the shell is shutting down with this app still
|
||||
running. Main-loop apps use it to set a flag that their main loop
|
||||
observes, so the loop exits cleanly and `appMain` can return.
|
||||
|
||||
|
||||
## App Types
|
||||
|
||||
### Callback-only apps (`hasMainLoop = false`)
|
||||
|
||||
`appMain` creates windows, registers callbacks, and returns 0. The
|
||||
shell drives all further work by invoking those callbacks in
|
||||
response to user input and timers. No dedicated task is allocated;
|
||||
no per-app stack is consumed. The app's lifetime ends when the last
|
||||
window closes (or when every callback has released all references).
|
||||
|
||||
Use this pattern for event-driven UI: Notepad, Program Manager, DVX
|
||||
Demo, Control Panel, Image Viewer.
|
||||
|
||||
### Main-loop apps (`hasMainLoop = true`)
|
||||
|
||||
The shell creates a cooperative task for the app and calls
|
||||
`appMain` from inside it. `appMain` runs a loop that calls
|
||||
`tsYield()` periodically to share the CPU. Exit by breaking out of
|
||||
the loop and returning.
|
||||
|
||||
Use this pattern when the app needs continuous background work that
|
||||
does not map cleanly onto event callbacks: Clock (polls the system
|
||||
clock every second), terminal emulators, games.
|
||||
|
||||
|
||||
## Resource Conventions
|
||||
|
||||
Apps attach resources to their `.app` binary using `dvxres build`
|
||||
(see `src/tools/README.md`). The standard resource names are:
|
||||
|
||||
| Name | Type | Meaning |
|
||||
|---------------|--------|---------|
|
||||
| `icon32` | icon | 32x32 BMP shown in Program Manager and the Task Manager. |
|
||||
| `name` | text | Human-readable display name (overrides `appDescriptor.name` for UI where available). |
|
||||
| `author` | text | Author / maintainer name. |
|
||||
| `publisher` | text | Publisher or studio. |
|
||||
| `copyright` | text | Copyright notice. |
|
||||
| `version` | text | Version string, free-form. |
|
||||
| `description` | text | One-sentence description shown in About dialogs. |
|
||||
| `helpfile` | text | Optional relative path to a `.hlp` file for F1 context help. |
|
||||
|
||||
Additional application-specific resources may be attached with any
|
||||
unique name. The runtime reads them via
|
||||
`dvxResOpen(appPath)` / `dvxResRead(handle, name, &size)` /
|
||||
`dvxResClose(handle)`.
|
||||
|
||||
Typical `.res` manifest:
|
||||
|
||||
```
|
||||
# clock.res
|
||||
icon32 icon icon32.bmp
|
||||
name text "Clock"
|
||||
author text "Scott Duensing"
|
||||
copyright text "Copyright 2026 Scott Duensing"
|
||||
publisher text "Kangaroo Punch Studios"
|
||||
description text "Digital clock with date display"
|
||||
```
|
||||
|
||||
|
||||
## Bundled Applications
|
||||
|
||||
### Program Manager (progman)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/progman/progman.app` |
|
||||
| Type | Callback-only |
|
||||
| Multi-instance | No |
|
||||
|
||||
The desktop. Recursively scans `apps/` for `.app` files and displays
|
||||
them as a launchable grid with icons, names, and publishers. Double
|
||||
click or Enter to launch. Includes a Help menu with the system help
|
||||
viewer and the About / System Info dialog. Registers with
|
||||
`shellRegisterDesktopUpdate()` so the grid refreshes when apps are
|
||||
loaded, terminated, or crash.
|
||||
|
||||
Widget headers used: `box/box.h`, `button/button.h`, `label/label.h`,
|
||||
`statusBar/statBar.h`, `textInput/textInpt.h`, `imageButton/imgBtn.h`,
|
||||
`scrollPane/scrlPane.h`, `wrapBox/wrapBox.h`.
|
||||
|
||||
### Notepad (notepad, BASIC)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/notepad/notepad.app` |
|
||||
| Type | BASIC (main-loop via basstub) |
|
||||
| Multi-instance | No |
|
||||
|
||||
Plain-text editor with File menu (New, Open, Save, Save As) and Edit
|
||||
menu (Cut, Copy, Paste, Select All). Built from a BASIC project
|
||||
(`notepad.dbp` + `notepad.frm`); demonstrates the form designer,
|
||||
TextInput widget, and common dialog integration.
|
||||
|
||||
### Clock (clock)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/clock/clock.app` |
|
||||
| Type | Main-loop |
|
||||
| Multi-instance | Yes |
|
||||
|
||||
Digital clock showing time and date. Polls the system clock every
|
||||
second and invalidates the window to trigger a repaint. Uses the raw
|
||||
`onPaint` callback (no widgets) to draw centered text. Reference
|
||||
implementation for main-loop apps.
|
||||
|
||||
### DVX Demo (dvxdemo)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/dvxdemo/dvxdemo.app` |
|
||||
| Type | Callback-only |
|
||||
| Multi-instance | No |
|
||||
|
||||
Widget toolkit showcase. Opens multiple windows demonstrating
|
||||
virtually every widget type:
|
||||
|
||||
* Main window -- raw paint callbacks (gradients, patterns, text)
|
||||
* Widget demo -- form widgets (TextInput, Checkbox, Radio, ListBox)
|
||||
* Controls window -- tabbed advanced widgets (Dropdown,
|
||||
ProgressBar, Slider, Spinner, TreeView, ListView, ScrollPane,
|
||||
Toolbar, Canvas, Splitter, Image)
|
||||
* Terminal window -- AnsiTerm widget
|
||||
|
||||
Resources include `logo.bmp`, `new.bmp`, `open.bmp`, `sample.bmp`,
|
||||
`save.bmp`. Reference implementation for widget-based UIs.
|
||||
|
||||
### Control Panel (cpanel)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/cpanel/cpanel.app` |
|
||||
| Type | Callback-only |
|
||||
| Multi-instance | No |
|
||||
|
||||
System configuration with four tabs:
|
||||
|
||||
* **Mouse** -- scroll direction, wheel speed, double-click speed,
|
||||
acceleration, cursor speed
|
||||
* **Colors** -- every system colour with live preview; theme load
|
||||
and save
|
||||
* **Desktop** -- wallpaper image and display mode
|
||||
* **Video** -- resolution and colour depth switching
|
||||
|
||||
Changes preview live. OK writes to `CONFIG/DVX.INI`; Cancel reverts
|
||||
to the snapshot taken when the panel opened.
|
||||
|
||||
### Image Viewer (imgview, BASIC)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/imgview/imgview.app` |
|
||||
| Type | BASIC (main-loop via basstub) |
|
||||
| Multi-instance | No |
|
||||
|
||||
Displays BMP, PNG, JPEG, and GIF images scaled to fit the window
|
||||
with aspect ratio preserved. Resize to zoom. Open via the File menu
|
||||
or via launch arguments. Built from a BASIC project using the
|
||||
Canvas widget.
|
||||
|
||||
### DVX BASIC (dvxbasic)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/dvxbasic/dvxbasic.app` |
|
||||
| Type | Callback-only |
|
||||
| Multi-instance | No |
|
||||
|
||||
Visual Basic 3 compatible IDE: form designer, per-procedure code
|
||||
editor, project manager, compiler, and runtime VM. See
|
||||
`dvxbasic/README.md` for a detailed architecture description.
|
||||
|
||||
`dvxbasic/` also builds:
|
||||
|
||||
* `bin/libs/kpunch/basrt/basrt.lib` -- the BASIC runtime (VM +
|
||||
values) as a separately loadable library, so compiled BASIC apps
|
||||
do not carry their own copy of the runtime.
|
||||
* `bin/apps/kpunch/dvxbasic/basstub.app` -- the stub used as the
|
||||
template for every compiled BASIC app.
|
||||
|
||||
### DVX Help Viewer (dvxhelp)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/dvxhelp/dvxhelp.app` |
|
||||
| Type | Callback-only |
|
||||
| Multi-instance | Yes |
|
||||
|
||||
Renders compiled `.hlp` files produced by `dvxhlpc`. Supports table
|
||||
of contents, keyword index, full-text search (trigram), hyperlinks,
|
||||
and inline images.
|
||||
|
||||
### Icon Editor (iconed, BASIC)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/iconed/iconed.app` |
|
||||
| Type | BASIC (main-loop via basstub) |
|
||||
| Multi-instance | No |
|
||||
|
||||
Paint-program-style editor for creating 32x32 BMP icons. Pen /
|
||||
eraser / fill tools, 16-colour palette, BMP save / load.
|
||||
|
||||
### Help Editor (helpedit, BASIC)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/dvxhelp/helpedit.app` |
|
||||
| Type | BASIC (main-loop via basstub) |
|
||||
| Multi-instance | No |
|
||||
|
||||
Editor for `.dhs` help source files with syntax-aware controls. Runs
|
||||
the compiled system help viewer by default for live preview.
|
||||
|
||||
### Resource Editor (resedit, BASIC)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/resedit/resedit.app` |
|
||||
| Type | BASIC (main-loop via basstub) |
|
||||
| Multi-instance | No |
|
||||
|
||||
GUI wrapper around `dvxres`: open a `.app` / `.wgt` / `.lib`,
|
||||
browse its resources, add, replace, extract, or remove them.
|
||||
|
||||
### BASIC Demo (basdemo, BASIC)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/basdemo/basdemo.app` |
|
||||
| Type | BASIC (main-loop via basstub) |
|
||||
| Multi-instance | No |
|
||||
|
||||
Gallery of short BASIC programs demonstrating the language and the
|
||||
form designer. Intended as sample source for reading.
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
make # build every app (C apps + BASIC apps)
|
||||
make <appname> # build a single app
|
||||
make clean # remove all app build artifacts
|
||||
```
|
||||
|
||||
The `apps/kpunch/Makefile` orchestrates both C and BASIC builds:
|
||||
|
||||
* **C apps** compile each `<app>/<app>.c` to an object file, run
|
||||
`dxe3gen -U` on it to produce `<app>.app`, then attach resources
|
||||
with `dvxres build` if a `.res` manifest exists.
|
||||
* **BASIC apps** run `bascomp <app>.dbp -o <app>.app -release`,
|
||||
which handles project-file parsing, compilation, resource
|
||||
attachment, and linking into `basstub.app`.
|
||||
|
||||
Build output goes to `bin/apps/kpunch/<app>/<app>.app`.
|
||||
|
||||
|
||||
## Writing a New C App
|
||||
|
||||
Minimal skeleton:
|
||||
|
||||
```c
|
||||
#include "dvxApp.h"
|
||||
#include "dvxWgt.h"
|
||||
#include "shellApp.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// Prototypes
|
||||
static int32_t appMainImpl(DxeAppContextT *ctx);
|
||||
static void onClose(WindowT *win);
|
||||
|
||||
// Required descriptor
|
||||
AppDescriptorT appDescriptor = {
|
||||
.name = "My App",
|
||||
.hasMainLoop = false,
|
||||
.multiInstance = false,
|
||||
.stackSize = SHELL_STACK_DEFAULT,
|
||||
.priority = 0
|
||||
};
|
||||
|
||||
|
||||
static void onClose(WindowT *win) {
|
||||
(void)win;
|
||||
// Callback-only apps: nothing to do; the shell reaps us when
|
||||
// the last window closes.
|
||||
}
|
||||
|
||||
|
||||
int32_t appMain(DxeAppContextT *ctx) {
|
||||
return appMainImpl(ctx);
|
||||
}
|
||||
|
||||
|
||||
static int32_t appMainImpl(DxeAppContextT *ctx) {
|
||||
AppContextT *ac = ctx->shellCtx;
|
||||
|
||||
WindowT *win = dvxCreateWindow(ac, appDescriptor.name, 100, 100, 320, 240, true);
|
||||
|
||||
if (!win) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
win->onClose = onClose;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
Add a Makefile target that mirrors the existing apps (see
|
||||
`apps/kpunch/Makefile` for the pattern), drop a `.res` manifest next
|
||||
to the source, and provide a 32x32 `icon32.bmp`. `make <app>` should
|
||||
then produce `bin/apps/kpunch/<app>/<app>.app`.
|
||||
|
||||
|
||||
## Testing Locally
|
||||
|
||||
After `make`, the app is deployed in-tree under `bin/apps/kpunch/`.
|
||||
Launch the full system (`bin/dvx.exe`) and the Program Manager picks
|
||||
up the new app automatically on the next boot.
|
||||
|
||||
To iterate quickly: rebuild the single app, restart DVX, and launch
|
||||
it. The log at `bin/DVX.LOG` records load errors, missing
|
||||
dependencies, and per-app crash diagnostics.
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
apps/kpunch/
|
||||
Makefile top-level app build
|
||||
README.md this file
|
||||
|
||||
# C apps
|
||||
progman/
|
||||
progman.c Program Manager
|
||||
dvxhelp.hcf help-compiler config (system reference)
|
||||
clock/
|
||||
clock.c digital clock (main-loop reference)
|
||||
clock.res
|
||||
icon32.bmp
|
||||
dvxdemo/
|
||||
dvxdemo.c widget showcase
|
||||
dvxdemo.res
|
||||
icon32.bmp
|
||||
logo.bmp / new.bmp / open.bmp / sample.bmp / save.bmp
|
||||
cpanel/
|
||||
cpanel.c Control Panel
|
||||
cpanel.res
|
||||
icon32.bmp
|
||||
dvxhelp/
|
||||
dvxhelp.c help viewer
|
||||
dvxhelp.res
|
||||
help.dhs source for the dvxhelp app's own help
|
||||
hlpformat.h binary format shared with the compiler
|
||||
sample.dhs small sample source
|
||||
icon32.bmp
|
||||
helpedit/ BASIC project: help editor
|
||||
helpedit.dbp
|
||||
helpedit.frm
|
||||
ICON32.BMP
|
||||
|
||||
# BASIC apps
|
||||
iconed/
|
||||
iconed.dbp
|
||||
iconed.frm
|
||||
ICON32.BMP
|
||||
notepad/
|
||||
notepad.dbp
|
||||
notepad.frm
|
||||
ICON32.BMP
|
||||
imgview/
|
||||
imgview.dbp
|
||||
imgview.frm
|
||||
ICON32.BMP
|
||||
basdemo/
|
||||
basdemo.dbp
|
||||
basdemo.frm
|
||||
ICON32.BMP
|
||||
resedit/
|
||||
resedit.dbp
|
||||
resedit.frm
|
||||
ICON32.BMP
|
||||
|
||||
dvxbasic/ see dvxbasic/README.md
|
||||
compiler/ lexer, parser, codegen, strip, obfuscate
|
||||
runtime/ VM + value system
|
||||
formrt/ form runtime (BASIC <-> DVX widgets)
|
||||
ide/ IDE front end
|
||||
stub/ bascomp + basstub
|
||||
...
|
||||
```
|
||||
BIN
src/apps/kpunch/basdemo/ICON32.BMP
(Stored with Git LFS)
Normal file
BIN
src/apps/kpunch/basdemo/ICON32.BMP
(Stored with Git LFS)
Normal file
Binary file not shown.
18
src/apps/kpunch/basdemo/basdemo.dbp
Normal file
18
src/apps/kpunch/basdemo/basdemo.dbp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[Project]
|
||||
Name = BASIC Demo
|
||||
Author = Scott Duensing
|
||||
Publisher = Kangaroo Punch Studios
|
||||
Copyright = Copyright 2026 Scott Duensing
|
||||
Description = Comprehensive tour of DVX BASIC features
|
||||
Icon = ICON32.BMP
|
||||
|
||||
[Modules]
|
||||
File0 = ../../../include/basic/commdlg.bas
|
||||
Count = 1
|
||||
|
||||
[Forms]
|
||||
File0 = basdemo.frm
|
||||
Count = 1
|
||||
|
||||
[Settings]
|
||||
StartupForm = BasicDemo
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue