Build a polished 480x272 color UI on an ESP32 with only 7.6 KB of framework code. No SDL. No framebuffer. No desktop dependencies. Just a driver struct and one header.
- Tiny: GUI framework core is 7,770 bytes ≈ 7.6 KB on ESP32.
- Color + widgets + Chinese: full component is about 557 KB, dominated by the 7445-character font set.
- Portable:
fx_driver_tworks with SPI, parallel, or any display backend. - Zero SDL: runs directly on bare ESP-IDF hardware.
- Hot-path zero allocation: row buffers/band buffers are reused; no per-frame malloc.
- One header to start:
fxtk.his the whole public API. - Dual-core friendly: core 0 runs app logic, core 1 renders UI.
- Fast on a 26 MHz SPI bus: a full 10-page switch repaints in 100–150 ms — see Performance.
| Build | Size |
|---|---|
| Framework core only (no fonts/drivers) | 7,770 bytes ≈ 7.6 KB |
| Full fxtk component with CJK fonts | ≈ 557 KB |
English demo firmware, -DDEMO_EN=1 -DFXTK_USE_CJK=0 |
344,576 bytes ≈ 345 KB |
| Full Chinese demo firmware | ≈ 888 KB |
The full component is dominated by fonts. The framework logic itself is tiny.
Page switching in the 10-page demo, measured on the device (ESP32-WROOM-32E @240 MHz, 480x272 ST6201 on a 26 MHz SPI bus). A "switch" repaints the whole tab content area:
| Release | per switch |
|---|---|
| v1.2 | 440–870 ms |
| v1.3 | 100–150 ms |
What made it fast (details in CHANGELOG.md and docs/internals.md):
- SPI transactions are sent with
spi_device_polling_transmitand the bus is pre-acquired, cutting the per-transaction overhead from ~53 µs to ~25 µs. - Same-row spans are merged into a single transfer when the gap colour is known (the most recent fill covers the current clip) — vector art and waveforms stop issuing one SPI window per run.
fx_draw_vline()is one rectangle fill, not one window per row.- Redundant full-area fills are skipped: the root panel no longer repaints under a tab, and the tab background skips children that paint their own rectangle.
At ~140 K pixels per switch the renderer is now close to the 26 MHz SPI bandwidth floor (~86 ms), so the remaining headroom is in repainting less, not pushing faster.
- Declarative widgets:
fx_button_new(pixel(), title(), call()) - Widgets: button, label, grid, canvas, slider, progress, checkbox, panel, tab, image
- Layouts: pixel, percent, grid
- Immediate-mode canvas: line, circle, ellipse, polygon, arc, round-rect, text, gradients, offscreen buffers
- Text: 16/24/32px Arial grayscale, symbols, and 7445 Chinese characters
- Dual-core rendering: core 1 UI / core 0 logic
- Dirty rect, canvas region clipping, span line buffers, band-merged fonts
- Optional widget trimming:
-DFXTK_WIDGET_XXX=0 - Tab sidebar modes: top / left / right / bottom
- 10-page demo in English and Chinese
. $IDF_PATH/export.sh
idf.py set-target esp32
idf.py build
# English full demo (left tab, translated from the Chinese demo)
idf.py -DDEMO_EN=1 build./tools/flash.sh /dev/ttyUSB0
# or directly:
python -m esptool --chip esp32 -p /dev/ttyUSB0 -b 460800 \
--before default_reset --after hard_reset write_flash \
--flash_mode dio --flash_freq 40m --flash_size 4MB \
0x1000 build/bootloader/bootloader.bin \
0x8000 build/partition_table/partition-table.bin \
0x10000 build/fxtk_esp.binFX_MAX_WIDGETScontrols widget pool RAM: default 256, use 32/16 for very small apps.-DFXTK_WIDGET_BUTTON=0,-DFXTK_WIDGET_LABEL=0, ... remove widget draw support.- Fonts are separate translation units; remove unused font files to save flash.
- English/no-CJK builds:
idf.py -DDEMO_EN=1 -DFXTK_USE_CJK=0 builddrops the 500 KB CJK table. - The framework core is only 7.6 KB; most of the size is the Chinese glyph set.
| Part | Details |
|---|---|
| MCU | ESP32-WROOM-32E, dual-core 240 MHz |
| Display | 4.3" 480x272 ST6201, SPI 4-wire |
| Backlight | GPIO2 LEDC PWM |
| Touch | GT911 I2C |
esp32 embedded-gui gui-framework esp-idf freertos tft spi-display color-lcd st6201 microcontroller low-memory tiny-gui no-sdl c iot
MIT.