dfx is a simplified second-generation immediate-mode GUI framework built on top of Dear ImGui. It provides a clean, Go-idiomatic API for building desktop applications with a focus on simplicity and ease of use.
dfx is a complete rewrite of the original imapp framework (a personal project, never released), designed to provide the same core functionality with a much simpler and more intuitive API. Key improvements include:
- 50% less code - Eliminated redundant abstractions
- Simpler mental model - Everything is a Component
- Better composition - Components can have children
- Type safety - Structured events instead of raw IO polling
- Conflict detection - Actions prevent key binding conflicts
- Built-in theming - Comprehensive font and theme system
The dfx Roadmap is always up-to-date with the current and planned work on the project.
The fundamental abstraction in dfx is the Component:
type Component interface {
Draw(state *State)
Actions() *ActionRegistry
}Components receive a State containing all drawing context and can define keyboard actions.
The State struct consolidates all drawing parameters:
type State struct {
Size imgui.Vec2 // Available drawing area
Position imgui.Vec2 // Position within parent
IO *imgui.IO // ImGui input/output
App *App // Application reference
Parent Component // Parent component (nil for root)
}The simplest way to create a component:
root := dfx.NewFunc(func(state *dfx.State) {
imgui.Text("Hello World!")
if imgui.Button("Click Me") {
fmt.Println("Button clicked!")
}
})For more complex components with state and children:
type MyComponent struct {
dfx.Container
counter int
}
func NewMyComponent() *MyComponent {
c := &MyComponent{}
c.Visible = true
c.OnDraw = func(state *dfx.State) {
imgui.Text(fmt.Sprintf("Counter: %d", c.counter))
if imgui.Button("Increment") {
c.counter++
}
}
return c
}package main
import "github.com/michaelquigley/dfx"
func main() {
root := dfx.NewFunc(func(state *dfx.State) {
imgui.Text("Hello from dfx!")
if imgui.Button("Click Me") {
// handle button click
}
})
app := dfx.New(root, dfx.Config{
Title: "My App",
Width: 800,
Height: 600,
})
app.Run()
}menuBar := dfx.NewFunc(func(state *dfx.State) {
if imgui.BeginMenu("File") {
if imgui.MenuItemBoolV("New", "Ctrl+N", false, true) {
// handle new
}
if imgui.MenuItemBoolV("Open", "Ctrl+O", false, true) {
// handle open
}
imgui.EndMenu()
}
})
app := dfx.New(root, dfx.Config{
Title: "My App",
MenuBar: menuBar,
})Config Callbacks:
OnSetup(app *App)- Called once after ImGui context is createdOnShutdown(app *App)- Called before shutdownOnTick(app *App)- Called each frame before drawingOnClose(app *App)- Called when window is about to close (can cancel viaSetShouldClose(false))OnSizeChange(width, height int)- Called when window is resized
Config Fields:
Icons []image.Image- Optional window icons for taskbar/title bar
App Methods:
Run() error- Run the application (blocks until closed)Wait() error- Block untilRun()completes (useful whenRun()is called from a goroutine)Stop()- Request application to stopSetRoot(root Component)- Change the root component at runtimeActions() *ActionRegistry- Get global action registrySetWindowTitle(title string)- Update window title dynamicallySetShouldClose(shouldClose bool)- Control window close behaviorGetWindowSize() (int, int)- Get current window dimensionsGetWindowPos() (int, int)- Get current window position
dfx includes a comprehensive theming system with both predefined and customizable themes.
app := dfx.New(root, dfx.Config{
Title: "Themed App",
Theme: dfx.BlueTheme, // or GreenTheme, RedTheme, PurpleTheme, ModernDark
})customTheme := dfx.NewHueColorScheme("Custom", 180, 60, 200)
app := dfx.New(root, dfx.Config{
Title: "Custom Themed App",
Theme: customTheme,
})// Change theme during runtime
dfx.SetTheme(dfx.ModernDark)dfx provides three font constants with Material Icons merged where applicable:
- MainFont (20px) - Gidole Regular with Material Icons
- MonospaceFont (16px) - JetBrains Mono for code
- SmallFont (16px) - Gidole Regular small with Material Icons, for labels/indicators
// Default font (with icons)
imgui.Text("Regular text " + string(fonts.ICON_FAVORITE))
// Monospace font
dfx.PushFont(dfx.MonospaceFont)
imgui.Text("Monospace code text")
dfx.PopFont()
// Small font for labels and indicators
dfx.PushFont(dfx.SmallFont)
imgui.Text("CH1")
dfx.PopFont()app := dfx.New(root, dfx.Config{
Title: "Minimal App",
DisableFonts: true, // Use default ImGui fonts
DisableTheming: true, // Use default ImGui theme
})For trivial ImGui operations (Button, Text, Separator, SameLine, Spacing, TreeNode, TreePop, BeginChild, EndChild, BeginMenu, EndMenu, BeginMenuBar, EndMenuBar, MenuItem), call imgui.* directly. dfx provides value-add wrappers for controls that benefit from a cleaner Go-idiomatic API, returning (newValue, changed) tuples instead of requiring pointers:
// Text input
text, changed := dfx.Input("Label", currentText)
if changed {
// handle text change
}
// Slider
value, changed := dfx.Slider("Volume", currentValue, 0.0, 1.0)
// Checkbox
checked, changed := dfx.Checkbox("Enable feature", isEnabled)
// Button (use imgui directly)
if imgui.Button("Submit") {
// handle button click
}
// Combo/Dropdown
items := []string{"Option 1", "Option 2", "Option 3"}
selected, changed := dfx.Combo("Choose", currentIndex, items)Value-add wrappers (in controls.go): Input, InputMultiline, Checkbox, Slider, SliderInt, Combo, ColorEdit3, ColorEdit4, Toggle, WheelSlider.
Text Utilities (in text.go):
CenterText(text string)- Draws text centered horizontally and vertically in the available content regionCenterTextDisabled(text string)- Draws disabled (dimmed) text centered horizontally and vertically
dfx provides several enhanced controls with additional features beyond standard ImGui widgets:
Toolbar - Full-width header bar for section labels:
// simple toolbar with label
dfx.Toolbar("Settings")
// toolbar with extra controls on the right
dfx.ToolbarEx("Actions", func() {
if imgui.Button("Add") {
// handle add
}
imgui.SameLine()
if imgui.Button("Remove") {
// handle remove
}
})Features:
- Draws full-width background rectangle using
ColHeadercolor - Automatically handles padding and cursor positioning
ToolbarExallows additional controls to the right of the label via callback
Toggle - Boolean toggle button with visual feedback:
// inactive (false): dimmed appearance
// active (true): checkmark color
enabled, changed := dfx.Toggle("Play", playEnabled)WheelSlider - Horizontal slider with mouse wheel support:
// hover and scroll to adjust, Ctrl = 10x faster, Alt = 10x slower
value, changed := dfx.WheelSlider("Volume", volume, 0.0, 1.0, 100, "%.2f", imgui.SliderFlagsNone)Fader - Advanced vertical fader designed for audio mixing applications with support for logarithmic tapers, range limits, and multiple value representations:
FaderN - Normalized fader (0.0 to 1.0):
params := dfx.DefaultFaderParams()
params.Taper = dfx.AudioTaper()
params.Format = func(norm float32) string {
return fmt.Sprintf("%.2f", norm)
}
value, changed := dfx.FaderN("##fader", normalizedValue, params)FaderF - Float fader (arbitrary min/max range):
// Example: -60.0 dB to +12.0 dB with audio taper
params := dfx.DefaultFaderParams()
params.Taper = dfx.AudioTaper()
params.Format = func(norm float32) string {
db := norm*72.0 - 60.0
if db <= -59.9 {
return "-∞ dB"
}
return fmt.Sprintf("%.1f dB", db)
}
dbValue, changed := dfx.FaderF("##db", gainDB, -60.0, 12.0, params)FaderI - Integer fader (arbitrary min/max range):
// Example: 0 to 32767 for hardware control
params := dfx.DefaultFaderParams()
params.MinStop = 0.1 // limit to 10%-90% of range
params.MaxStop = 0.9
hwValue, changed := dfx.FaderI("##hw", hardwareValue, 0, 32767, params)FaderParams provides extensive configuration:
Taper- Response curve (Linear, Log, Audio, or Custom)MinStop/MaxStop- Range limits in normalized 0-1 spaceResetValue- Right-click reset target (normalized 0-1 space)Width/Height- Fader dimensionsFormat- Custom tooltip formatting functionShowTooltip- Enable/disable value tooltip (default: true)WheelSteps- Mouse wheel sensitivity (default: 100.0)
Built-in Tapers:
LinearTaper()- No taper, 1:1 mapping (default)LogTaper(steepness)- Logarithmic curve (steepness: 1.0 = gentle, 3.0 = moderate, 10.0 = steep)AudioTaper()- Standard audio fader curve (gentle bottom, steep top, optimized for dB scales)DecibelTaper(dbRange)- UI position linear with dB; for hardware values proportional to amplitudeCustomTaper(apply, invert)- User-defined taper functions
Multi-Representation Pattern: Advanced faders support maintaining multiple value representations (normalized, hardware, display) synchronized via conversion functions:
type FaderState struct {
normalized float32 // 0.0 - 1.0 (master value)
hardware int // 0 - 32767
decibels float32 // -60.0 to +12.0
}
func updateFromNormalized(state *FaderState, norm float32) {
state.normalized = norm
state.hardware = int(norm * 32767)
state.decibels = norm*72.0 - 60.0
}
// User chooses which API to use based on their needs
// FaderN for normalized, FaderI for hardware, FaderF for display valuesFaders with Scales:
The FaderWithScaleN/F/I functions add tick marks and labels next to faders, perfect for audio applications that need visual reference marks:
// Example: dB fader with scale
params := dfx.DefaultFaderParams()
params.Taper = dfx.AudioTaper()
scale := dfx.DefaultScaleConfig()
scale.Marks = []float32{0.0, 0.417, 0.667, 0.833, 1.0}
scale.Labels = map[float32]string{
0.0: "-60",
0.417: "-30",
0.667: "-12",
0.833: "0",
1.0: "+12",
}
dbValue, changed := dfx.FaderWithScaleF("##gain", gainDB, -60.0, 12.0, params, scale)ScaleConfig provides:
Marks- Array of normalized positions (0-1) for tick marksLabels- Map of position → label text for specific marksTickLength- Tick mark length in pixels (default: 5.0)LabelOffset- Distance from ticks to labels (default: 3.0)Position- "left" or "right" side placement (default: "left")
Key features:
- Taper-aware: Tick marks automatically respect the fader's taper curve for visual accuracy
- Theme integration: Uses colors from the current theme
- Flexible: Add scales to any normalized, float, or integer range fader
See examples/dfx_example_mixer for a complete demonstration with horizontally scrollable mixer interface showcasing all fader types and scales.
VUMeter - Vertical level meter with multi-channel support and three display modes:
// create a stereo meter
meter := dfx.NewVUMeter(2)
meter.SetLabels([]string{"L", "R"})
// set display mode (optional - VUMeterSolid is default)
meter.Mode = dfx.VUMeterSolid // continuous fill
meter.Mode = dfx.VUMeterHighres // 1px segments with 1px gaps
meter.Mode = dfx.VUMeterSegmented // configurable segments
// update levels each frame (0.0 to 1.0)
meter.SetLevels([]float32{leftLevel, rightLevel})
// draw the meter
meter.Draw(state)Configuration:
Mode- Display mode:VUMeterSolid(default),VUMeterHighres,VUMeterSegmentedHeight- Total height in pixels (default: 200)ChannelWidth- Width of each channel meter (default: 12)SegmentCount- Number of vertical segments, applies to VUMeterSegmented mode (default: 20)SegmentGap/ChannelGap- Spacing between segments and channelsPeakHoldMs- Peak hold duration in ms, 0 = disabled (default: 1000)PeakDecayRate- Peak decay rate per second (default: 0.5)ClipHoldMs- Clip indicator hold time in ms (default: 2000)Labels- Custom labels per channel (e.g., "L", "R", "Kick")ColorLow/Mid/High/Off/Peak/Clip- Customizable segment colors
Display Modes:
- VUMeterSolid: Continuous fill with stacked color zones - clean, modern look
- VUMeterHighres: Fixed 1px segments with 1px gaps - high resolution digital look
- VUMeterSegmented: Configurable segments via
SegmentCountandSegmentGap
Features:
- Multi-channel: Supports any number of channels displayed side-by-side
- Color zones: Green (0-60%), yellow (60-80%), red (80-100%)
- Peak hold: Displays peak level with configurable hold and decay
- Clip indicator: Top indicator lights red when signal clips, auto-resets
- Custom labels: Per-channel labels displayed below meters
See examples/dfx_example_vumeter for a complete demonstration.
VUWaterfall - Scrolling history display of VU levels over time:
// create a stereo waterfall
waterfall := dfx.NewVUWaterfall(2)
waterfall.Height = 150
waterfall.HistorySize = 100 // samples to retain
// each frame, add current levels to history
waterfall.SetLevels([]float32{leftLevel, rightLevel})
// draw the waterfall
waterfall.Draw(state)Configuration:
Height- Total height in pixels (default: 200)ChannelWidth- Width per channel (default: 40)ChannelGap- Gap between channels (default: 4)RowHeight- Height of each history row (default: 2)RowGap- Gap between rows (default: 0)HistorySize- Number of samples to retain (default: 100)SampleInterval- Minimum time between samples for throttling (default: 16ms / ~60fps)Highres- When true, alternates row opacity for scanline effectColorLow/Mid/High/Off- Zone colors (same defaults as VUMeter)
Additional Methods:
SetHistorySize(size int)- Change history depth (clears buffer)ChannelCount() int- Get current channel count
Features:
- Vertical scrolling: New data appears at bottom, scrolls upward
- Multi-channel: Channels displayed side-by-side
- Color zones: Green (0-60%), yellow (60-80%), red (80-100%)
- Centered bars: Level represented by bar width, centered in channel
- Sample throttling: Consistent scroll speed via
SampleInterval - Highres mode: Scanline effect with alternating row opacity
See examples/dfx_example_vumeter for a complete demonstration.
LogViewer - Buffered log display with configurable empty-state behavior:
buffer := dfx.NewLogBuffer(1000)
viewer := dfx.NewLogViewer(buffer)
viewer.Visible = true
viewer.ShowDisabledMessage = true
viewer.DisabledMessage = "logging capture disabled"Visibility behavior:
Visible == falserenders nothingVisible == trueandBuffer != nilrenders the log streamVisible == trueandBuffer == nilrendersDisabledMessageonly whenShowDisabledMessage == true
Use NewSlogHandler(...) with a shared LogBuffer to route slog output into the viewer.
FileNode provides a Find method for searching trees, along with predicate constructors for common patterns:
// find all .go files
goFiles := root.Find(dfx.MatchExt(".go"))
// find by name regex
pred, err := dfx.MatchName(`^main\.`)
if err != nil {
// handle invalid regex
}
mains := root.Find(pred)
// find by full path regex
pred, err = dfx.MatchPath(`src/.*\.go$`)
if err != nil {
// handle invalid regex
}
srcGoFiles := root.Find(pred)
// find all directories with an inline predicate
dirs := root.Find(func(n *dfx.FileNode) bool { return n.Dir })Methods:
Find(predicate func(*FileNode) bool) []*FileNode- Returns all matching nodes in depth-first pre-order. Returns nil on a nil receiver.
Predicate Constructors:
MatchExt(ext string) func(*FileNode) bool- Matches non-directory nodes by file extension (case-insensitive). The ext parameter should include the dot (e.g. ".go").MatchName(pattern string) (func(*FileNode) bool, error)- Matches nodes whose Name matches the given regex.MatchPath(pattern string) (func(*FileNode) bool, error)- Matches nodes whose full Path() matches the given regex.
Predicates are composable with FileTree.Filter:
fileTree.Filter = dfx.MatchExt(".go")dfx provides a hierarchical action system with conflict detection:
Register application-wide keyboard shortcuts:
app := dfx.New(root, dfx.Config{
Title: "App with Shortcuts",
OnSetup: func(app *dfx.App) {
// Register global shortcuts
app.Actions().Register("save", "Ctrl+S", func() {
// handle save
})
app.Actions().Register("quit", "Ctrl+Q", func() {
app.Stop()
})
},
})Components can define their own keyboard shortcuts that automatically override global actions:
myComponent := &dfx.Container{
Visible: true,
OnDraw: func(state *dfx.State) {
imgui.Text("Component with local actions")
},
}
// Add component-specific actions
myComponent.Actions().Register("increment", "Up", func() {
// handle up arrow - only when this component has focus
})
myComponent.Actions().Register("decrement", "Down", func() {
// handle down arrow
})The action system provides:
- Automatic conflict detection within components
- Hierarchical override behavior - component actions override global actions
- Simple key binding syntax - "Ctrl+S", "Alt+F4", "Up", etc.
- No boilerplate - just define actions and they work
For custom composite components, implement:
ChildActions() []Componentto expose children for traversalLocalActions() *ActionRegistryto expose local actions
Action precedence is:
- child component actions first
- parent-local actions next
- app-global actions last
For applications with menu bars, dfx provides menu-compatible actions that work both as keyboard shortcuts and menu items:
// create menu actions
fileNew := dfx.NewMenuAction("New", "Ctrl+N", func() {
// handle new file
})
fileSave := dfx.NewMenuAction("Save", "Ctrl+S", func() {
// handle save
})
fileQuit := dfx.NewMenuAction("Quit", "Ctrl+Q", func() {
app.Stop()
})
// create menu bar component
// NOTE: dfx.Config.MenuBar already wraps this in BeginMainMenuBar/EndMainMenuBar
menuBar := dfx.NewFunc(func(state *dfx.State) {
if imgui.BeginMenu("File") {
fileNew.DrawMenuItem() // renders as menu item with shortcut label
imgui.Separator()
fileSave.DrawMenuItem()
imgui.Separator()
fileQuit.DrawMenuItem()
imgui.EndMenu()
}
})
// register for keyboard shortcuts
app.Actions().MustRegisterAction(fileNew)
app.Actions().MustRegisterAction(fileSave)
app.Actions().MustRegisterAction(fileQuit)
// use menu bar in config
app := dfx.New(root, dfx.Config{
MenuBar: menuBar,
})Menu actions provide:
- Dual functionality - work as both menu items and keyboard shortcuts
- Automatic shortcut labels - keyboard shortcuts display in menus
- Single definition - define once, use in both menu and keyboard
- Consistent behavior - clicking menu or pressing keys calls the same handler
See examples/dfx_example_menu for a complete demonstration.
For a comprehensive guide to Dear ImGui's layout system including child windows, sizing semantics, and practical patterns, see docs/LAYOUT_GUIDE.md. The interactive demo in examples/dfx_example_layout demonstrates all concepts with real-time values.
Components can contain children for complex layouts:
container := &dfx.Container{
Visible: true,
Children: []dfx.Component{
header,
content,
footer,
},
OnDraw: func(state *dfx.State) {
// Custom layout logic for this container.
// Children are drawn automatically by Container.Draw().
},
}The HCollapse component provides a horizontal collapsible panel that contains content to its right. When collapsed, only the toggle button is visible. When expanded, it shows a header bar with title and the content below.
// create a collapsible sidebar
sidebar := dfx.NewHCollapse(
sidebarContent,
dfx.HCollapseConfig{
Title: "Sidebar",
ExpandedWidth: 250,
TransitionMs: 100,
Resizable: true,
Expanded: true,
},
)
// optional: add keyboard shortcut for toggle
sidebar.Actions().Register("toggle-sidebar", "[", func() {
sidebar.Toggle()
})
// in Draw, use SameLine() to place content to the right
func (m *MyApp) Draw(state *dfx.State) {
sidebar.Draw(state)
imgui.SameLine()
// main content fills remaining width
remaining := state.Size.X - sidebar.CurrentWidth
imgui.BeginChildStrV("main", imgui.Vec2{X: remaining, Y: state.Size.Y}, 0, 0)
mainContent.Draw(state)
imgui.EndChild()
}Configuration:
Title- displayed in header when expanded (also used for unique imgui ID)ExpandedWidth- width when fully expandedMinWidth- collapsed width (defaults to 36px, toggle button only)MaxWidth- maximum width when resizing (0 = no limit)TransitionMs- animation duration (default: 80ms)Resizable- allow drag-to-resize when expandedExpanded- initial state
Features:
- Animated transitions - smooth expand/collapse animation
- Header bar - toggle button on left, title when expanded, resize handle on right
- Drag-to-resize - adjust width by dragging the right edge
- Collapsed tooltip - hovering over collapsed toggle shows title
- Toggle callback -
OnToggle func(expanded bool)for state change notifications - CurrentWidth - read current width for layout calculations
Note: When using custom-drawn components (like VUMeter, Fader) inside tables within an HCollapse, use imgui.TableFlagsNoClip and imgui.TableColumnFlagsNoClip to prevent cell clipping.
The Workspace component provides high-level management of multiple named views with easy switching. It separates stable identifiers from display names, allowing display names to include icons and formatting without affecting code that switches workspaces.
// create workspaces
editor := dfx.NewFunc(func(state *dfx.State) {
imgui.Text("Editor View")
// editor UI...
})
viewer := dfx.NewFunc(func(state *dfx.State) {
imgui.Text("Viewer")
// viewer UI...
})
// create workspace manager with IDs and display names
ws := dfx.NewWorkspace()
ws.Add("editor", "📝 Editor", editor) // ID, display name, component
ws.Add("viewer", "👁️ Viewer", viewer)
ws.ShowSelector = true // shows combo selector
ws.SelectorLabel = "View"
// callback receives stable IDs
ws.OnSwitch = func(oldID, newID string) {
fmt.Printf("switched from '%s' to '%s'\n", oldID, newID)
}
// add keyboard shortcuts using stable IDs
ws.Actions().MustRegister("Switch to Editor", "Ctrl+1", func() {
ws.Switch("editor") // won't break if display name changes
})
ws.Actions().MustRegister("Switch to Viewer", "Ctrl+2", func() {
ws.Switch("viewer")
})
// change display name without affecting code
ws.SetName("editor", "✏️ Code Editor")
app := dfx.New(ws, dfx.Config{...})API Methods:
NewWorkspace()- create workspace managerAdd(id, name, component)- add/replace workspace with ID and display nameRemove(id)- remove workspace by IDSwitch(id)- switch to workspace by IDSwitchByIndex(index)- switch by indexCurrent()- get current workspace IDCurrentName()- get current display nameCurrentComponent()- get current componentSetName(id, name)- change display name (ID unchanged)GetName(id)- get display name for IDWorkspaceIds()- get list of workspace IDsWorkspaceNames()- get list of display names
Configuration:
ShowSelector- show/hide combo selector (default: true)SelectorLabel- label for combo (default: "Workspace")SelectorWidth- width of selector (default: 200, -1 for auto)OnSwitch- callback when workspace changes (receives IDs)
Benefits of ID/Name Separation:
- Stable IDs for code, config files, keyboard shortcuts
- Display names can include icons, emoji, formatting
- Change display names without breaking code
- Easy localization (same ID, different display names)
See examples/dfx_example_workspace for a complete demonstration.
dfx includes a command-pattern undo/redo system for tracking reversible operations:
// define a command
type SetValueCommand struct {
dfx.BaseCommand
target *int
oldValue int
newValue int
}
func (c *SetValueCommand) Description() string { return fmt.Sprintf("set value to %d", c.newValue) }
func (c *SetValueCommand) Run() { *c.target = c.newValue }
func (c *SetValueCommand) Undo() { *c.target = c.oldValue }
// create and use the undo system
undoSystem := dfx.NewUndoSystem()
undoSystem.Run(&SetValueCommand{target: &myValue, oldValue: myValue, newValue: 42})
undoSystem.Undo() // reverts to old value
undoSystem.Redo() // re-applies new valueAPI:
NewUndoSystem() *UndoSystem- Create undo systemRun(cmd Command)- Execute and track command (supports automatic merging viaMergeableCommand)Undo()/Redo()- Navigate command historyClear()- Remove all commands from both stacksCanUndo() bool/CanRedo() bool- Check availabilityHistoryComponent()- Returns a component displaying undo/redo historyRunF func(Command)- Optional callback invoked whenever a command is executed
Command Interfaces:
Command- Core:Description(),Run(),Undo()MergeableCommand- AddsMerge(other Command) boolfor combining adjacent commandsStampedCommand- AddsStamp() time.Timefor timestamp trackingFullCommand- Combines MergeableCommand and StampedCommand
BaseCommand - Embeddable helper struct providing automatic timestamp via Stamp() and manual override via SetStamp(time.Time).
See examples/dfx_example_undo for a complete demonstration.
SizeDebugger - Visual component that displays the available drawing area size and draws a border with crossing lines. Useful for debugging layout issues.
debugger := dfx.NewSizeDebugger()
debugger.Margin = 8 // inset from edges (default: 4)
// use as any Component - shows size label and border lines
// press Shift+Alt+D to toggle the size labeldfx provides optional utilities for configuration management in config.go. These helpers simplify common patterns like saving/loading JSON configuration, persisting window state, and managing dashboard layouts.
type Config struct {
Window dfx.WindowConfig `json:"window"`
Dashes map[string]dfx.DashConfig `json:"dashes"`
// ... your app-specific settings
}
func main() {
// determine config file path
cfgPath, _ := dfx.ConfigPath("myapp", "config.json")
// load with defaults
cfg := defaultConfig()
dfx.LoadJSON(cfgPath, cfg)
// create app with saved window size and position
app := dfx.New(root, dfx.Config{
Title: "My App",
Width: cfg.Window.Width,
Height: cfg.Window.Height,
X: cfg.Window.X,
Y: cfg.Window.Y,
OnClose: func(app *dfx.App) {
cfg.Window = dfx.CaptureWindowState(app)
dfx.SaveJSON(cfgPath, cfg)
},
OnSizeChange: func(width, height int) {
cfg.Window.Width = width
cfg.Window.Height = height
},
})
app.Run()
}// capture dashboard state
cfg.Dashes = dfx.CaptureDashState(dashMgr)
// save to file
dfx.SaveJSON(cfgPath, cfg)
// later, restore dashboard state
dfx.RestoreDashState(dashMgr, cfg.Dashes)ConfigPath(appName, filename string) (string, error)- Returns standard config file path in user home directory (e.g.,~/.myapp/config.json)SaveJSON(path string, config interface{}) error- Saves struct to JSON file with formattingLoadJSON(path string, config interface{}) error- Loads JSON file into struct (silent if file doesn't exist)CaptureDashState(dm *DashManager) map[string]DashConfig- Extracts dashboard visibility and sizesRestoreDashState(dm *DashManager, config map[string]DashConfig)- Applies configuration to dashboardsCaptureWindowState(app *App) WindowConfig- Gets current window position, size, and state
Note: WindowConfig includes a Maximized field for future compatibility, but maximized state capture/restore is not yet implemented (requires backend enhancements).
See examples/dfx_example_config for a complete demonstration of configuration persistence including window state, dashboard layouts, and application settings.
For larger applications, dfx integrates with the df/da dependency injection container and df/dd data-driven serialization packages. This pattern provides a more scalable architecture with factory-based component creation and automatic lifecycle management.
The container-based pattern uses three packages:
- da.Application[C] - Application lifecycle with typed configuration
- dfx - GUI framework
- dd - Struct-to-YAML/JSON bidirectional binding
The lifecycle flow is: Configure → Build (factories) → Link → Start → (user interaction) → Stop → Save
// config.go - typed configuration
type config struct {
WindowX int
WindowY int
WindowWidth int
WindowHeight int
Counter int
}
func defaultConfig() config {
return config{
WindowX: 100,
WindowY: 100,
WindowWidth: 800,
WindowHeight: 600,
}
}// shellFactory.go - factory creates the main window
type shellFactory struct{}
func (f *shellFactory) Build(a *da.Application[config]) error {
shl := &shell{
cfg: &a.Cfg, // reference to mutable config
workspace: dfx.NewWorkspace(),
}
shl.app = dfx.New(shl.root, dfx.Config{
Title: "my app",
Width: shl.cfg.WindowWidth,
Height: shl.cfg.WindowHeight,
X: shl.cfg.WindowX,
Y: shl.cfg.WindowY,
OnSizeChange: func(w, h int) {
shl.cfg.WindowWidth = w
shl.cfg.WindowHeight = h
},
OnClose: func(app *dfx.App) {
shl.cfg.WindowX, shl.cfg.WindowY = app.GetWindowPos()
},
})
da.Set(a.C, shl) // register in container
return nil
}
// Link wires up tagged workspace components
func (s *shell) Link(c *da.Container) error {
for i, ws := range da.TaggedAsType[dfx.Component](c, "workspaces") {
s.workspace.Add(fmt.Sprintf("ws-%d", i), fmt.Sprintf("workspace %d", i+1), ws)
}
return nil
}
func (s *shell) Start() error {
go s.app.Run()
return nil
}// panelFactory.go - factory registers tagged workspace component
type panelFactory struct{}
func (f *panelFactory) Build(a *da.Application[config]) error {
panel := &myPanel{cfg: &a.Cfg}
da.AddTagged(a.C, "workspaces", panel) // tagged registration
return nil
}// main.go - application lifecycle
func main() {
cfgPath, _ := configPath()
app := da.NewApplication[config](defaultConfig())
app.Factories = append(app.Factories, &panelFactory{})
app.Factories = append(app.Factories, &shellFactory{})
app.InitializeWithPaths(da.OptionalPath(cfgPath))
app.Start()
// wait for GUI to close
if shl, ok := da.Get[*shell](app.C); ok {
shl.app.Wait()
}
// save config
dd.UnbindYAMLFile(app.Cfg, cfgPath)
app.Stop()
}- Factory pattern - Components created via
Build()methods with access to typed config - Tagged components -
da.AddTagged()for modular registration without naming conflicts - Link phase - Wire up dependencies after all factories complete
- Config mutation - Components hold
*configreference for real-time state updates - Automatic persistence -
dd.UnbindYAMLFile()saves config struct to YAML on shutdown
See examples/dfx_example_container for a complete demonstration of container-based architecture with tagged workspace components.
See the examples/ directory for complete working examples:
dfx_example_simple- Basic usagedfx_example_actions- Keyboard shortcutsdfx_example_custom_component- Custom component creationdfx_example_composition- Complex UI with menu barsdfx_example_themes- Theming and font demonstrationdfx_example_filetree- Filesystem tree viewerdfx_example_logviewer- Log viewer with df/dl integrationdfx_example_controls- Control wrappers (Combo, Toggle, WheelSlider)dfx_example_mixer- Advanced fader demonstration with tapers, range limits, and horizontal scrolling mixerdfx_example_vumeter- VU meter and waterfall with display modes and scrolling historydfx_example_hcollapse- Horizontal collapsible panels with faders and metersdfx_example_simple_hcollapse- Minimal HCollapse exampledfx_example_workspace- Workspace switching with multiple viewsdfx_example_lifecycle- Window lifecycle callbacksdfx_example_config- Configuration persistence with window and dashboard statedfx_example_container- Container-based lifecycle with df/da dependency injectiondfx_example_layout- Comprehensive ImGui layout and sizing tutorial (seedocs/LAYOUT_GUIDE.md)dfx_example_multigrid- MultiGrid layout systemdfx_example_dash- DashManager panel systemdfx_example_undo- Undo/redo system demodfx_example_menu- Menu-compatible actionsdfx_example_demo- ImGui demo window
# Build all examples
go build ./examples/dfx_example_simple
go build ./examples/dfx_example_actions
go build ./examples/dfx_example_themes
# Run an example
./dfx_example_themesdfx is designed as a replacement for imapp v1:
- Replace
imapp.Surfaceusage withdfx.Component - Convert
Surface.DrawFfunctions todfx.Funccomponents - Replace action registration with new conflict-detecting system
- Update control usage to new return-value API
The migration should be straightforward due to conceptual similarity, but the new API is much cleaner and more Go-idiomatic.
Components render within an invisible, borderless ImGui window that fills the entire backend window. This matches imapp v1's behavior exactly and provides a transparent "canvas" for drawing.
dfx deliberately does not include a layout system, allowing components to handle their own positioning. This keeps the framework simple while enabling maximum flexibility.
Currently supports only the GLFW backend, matching imapp v1's approach.
Part of the baab project.