Back in my youth I was hugely inspired by the timeline component of Demopaja, Moppi Productions' GUI tool for making demos. It turned out to be a very professional and conceptual product, with a plugin system tightly integrated into the editor. In my humble opinion, it was hardly inferior to the commercial products of its time — frankly, even today, about 25 years later, it still looks remarkably good.
Time went by, and around 2020 I met cool lightweight libraries built on using immediate-mode concept — Nuklear and Dear ImGui. The stars aligned, and I got the idea of showing that complex GUI components can be recreated utilizing these concepts. So I decided to port the timeline component to show that there is no need to rely on heavy frameworks like MFC to implement complex, composite and, most importantly, portable GUI controls nowadays.
Luckily, by that time Memon of Moppi had already released the Demopaja source code. In 2022 I took it on and set out to port it to Lua with the Nuklear library. Within roughly three months of spare-time coding the core code was ported and adapted. But it was full of small and annoying bugs, some of them were not so easy to localize and fix. After several iterations I got tired of it and put the project "on the shelf".
And so, in 2026, I decided to use the power of AI to merge the two sources to finally get the work done. AI fixed all the bugs much better and faster than me :-)
All original copyrights go to Demopaja author Mikko Mononen (https://github.com/memononen). All the graphic resources — the cursors and the interface sprites — are borrowed from Demopaja as well.
I hope someone finds this project useful, feel free to use and modify it according to your own needs.
| Dependency | Purpose | Where to get it |
|---|---|---|
| Lua 5.3 | interpreter | distro package |
| LuaSocket | timers (socket.gettime) |
distro package lua-socket |
| MoonGL | OpenGL bindings | build from source |
| MoonGLFW | window/input bindings | build from source |
| MoonNuklear | Nuklear UI bindings | build from source |
| MoonImage | PNG/JPG loading | build from source |
| GLFW 3, GLEW | native libs behind the bindings | distro packages |
| OpenGL 3.3 + X11 display | rendering | driver / WSLg on Windows |
json.lua (atlas descriptors), fonts, icon atlases and pixel cursors are
bundled in the repository — nothing else to fetch.
Debian/Ubuntu (including WSL2 with WSLg):
sudo apt install lua5.3 liblua5.3-dev lua-socket libglfw3-dev libglew-devThen build the four Moon* bindings. Each one follows the same routine
(the Makefile detects the Lua version and installs into /usr/local):
for lib in moongl moonglfw moonimage moonnuklear; do
git clone --depth 1 https://github.com/stetre/$lib
make -C $lib
sudo make -C $lib install
doneVerify the setup:
lua5.3 -e "require('moongl'); require('moonglfw'); require('moonnuklear'); require('moonimage'); print('ok')"From the repository root (resource paths are relative):
lua5.3 timelinebar.luaConfiguration knobs at the top of timelinebar.lua:
GLOBAL_SCALE— UI scale for HiDPI screens (default2; set1for a 1:1 pixel match with the original).START_THEME—'dark'or'light'; also switchable at runtime with the button in the top-left corner.
- Left click — select rows/keys; drag keys by time, curve points by value and time (the original moved them vertically only); drag the bottom edge of an expanded curve to resize it.
- Double click — add a key on a curve or a segment; add a marker on the waveform strip.
- Right click — context menus: layers/effects (rename, insert, delete), key interpolation and TCB/ease, out-of-range modes, markers, ruler zoom.
- Box select — drag in an empty timeline area; drag any selected key to move the whole selection. Inside expanded curve blocks keys are picked by their curve points; when the box spans several blocks, the block with the most captured keys wins.
- Space + drag — pan; mouse wheel — scroll the list.
- Enter — play/stop; arrows — step frames (with Ctrl: markers, with Shift: beats); Ctrl+Home/End — jump to start/end; Esc — deselect; Ctrl+Q — quit.
lua5.3 tests.lua129 headless checks of the model/document/theme/utility/container layers — no GL or window required. The exit code is the number of failures.
timelinebar.lua entry point: config, GLFW/GL/nuklear init, main loop
tests.lua headless tests (model, doc, theme, util, const)
timeline/
util.lua C integer semantics, CRect, NiceNum, font metrics
const.lua all original constants (flags, hits, tracking)
theme.lua light/dark palettes, in-place theme switching
resources.lua texture atlases (per-theme cache), pixel cursors
model.lua corelib model: keys, segments, TCB controllers, scene
doc.lua CDemopajaDoc-style edit operations, snapshot undo
render.lua CDC replacement: canvas + scissor stack + scaling
widgets.lua timers, popup menus, scrollbars, buttons
container.lua IContainer: pluggable editors for expanded blocks
(CurveContainer, CurveColorContainer, custom ones)
core.lua TimeLineBarC: constructor, layout, themes
draw.lua TimeLineBarC: all drawing
hittest.lua TimeLineBarC: hit tests
input.lua TimeLineBarC: mouse/keyboard/menus/transport
demo_scene.lua demo content
Port sources: Demopaja_050508/demopaja/TimeLineBar.{h,cpp} and
corelib/. Method and field names follow the original C++ so the code can
be compared side by side. Known omissions: type-in dialogs, the full undo
stack (a cancel snapshot is kept instead), clipboard, music waveform.
The expanded area below a parameter row is owned by an IContainer
(timeline/container.lua): it reports its size, draws itself and handles
mouse events, returning control to the bar for anything it does not
consume. The curve editor is CurveContainer; colors use its subclass
CurveColorContainer (gradient background, byte scale). To draw a
completely new block, implement the interface and register it:
local Containers = require('timeline.container')
local MyBlockC = Containers.subclass(Containers.IContainerC)
function MyBlockC:draw(view, render)
-- view carries the doc, the row, geometry and time mapping helpers
end
Containers.register(C.PARAM_TYPE_TEXT, MyBlockC) -- for a parameter type
Containers.set_class(pParam, MyBlockC) -- for a single parameterBottom-edge resizing comes from the base class for free; override
hit_test/on_lbutton_down/get_cursor to add interaction.
