Flow provides two complementary tools for runtime observability: debug mode (input and hit-test tracing) and structured logging (leveled, context-scoped output).
Debug mode traces input events: where the pointer lands, what gets hit, and the coordinate conversion steps.
flow.ui.set_debug(self, true)Or pass it during mount:
flow.ui.mount(self, { debug = true })On every action.pressed event:
[flow][DEBUG][ui.input] touch pressed
gui: (234.0, 410.0)
layout: (234.0, 230.0)
window: (960, 640) gui: (960, 640) scale: (1.0, 1.0)
hit: "item_7" bounds: {x=16, y=218, w=928, h=60}
This tells you:
- Input coordinates in GUI space and converted layout space.
- Window and GUI dimensions and the scale factor between them.
- The key of the hit element and its layout rectangle.
flow.ui.set_debug(self, false)Debug mode is stored per renderer instance (self.ui) and is not shared across GUI scripts.
flow.log is the library's runtime observability surface. It uses leveled output with per-context overrides, so you can turn up verbosity for one subsystem without flooding the console.
local flow = require "flow/flow"
local log = flow.log| Level | What it shows |
|---|---|
"none" |
Nothing |
"error" |
Errors only |
"warn" |
Warnings + errors |
"info" |
Info + warn + errors |
"debug" |
Everything |
flow.log.set_level("warn") -- warnings and errors
flow.log.set_level("debug") -- everything
flow.log.set_level("none") -- silence all outputThe library starts with the global level set to "none". Apps opt into visibility by calling set_level(...) or setting per-context overrides.
-- Show debug from input handling only, keep everything else at warn
flow.log.set_level("warn")
flow.log.set_context_level("ui.input", "debug")Restore a context to the global level:
flow.log.clear_context_level("ui.input")| Context | What it covers |
|---|---|
flow |
Top-level facade events |
ui |
Renderer lifecycle |
ui.input |
Input routing, hit testing |
ui.renderer |
Node creation and updates |
ui.scroll |
Scroll physics, bounds, momentum |
nav |
Navigation push/pop/replace/reset, transitions |
nav.messages |
Message-driven navigation dispatch |
nav.proxy |
Collection-proxy preload/enable/disable |
nav.runtime |
Non-GUI runtime bootstrap |
Debug scroll physics:
flow.log.set_level("warn")
flow.log.set_context_level("ui.scroll", "debug")Debug navigation transitions:
flow.log.set_context_level("nav", "debug")Debug hit testing when a button doesn't respond:
flow.log.set_context_level("ui.input", "debug")
flow.ui.set_debug(self, true)Silence everything except errors:
flow.log.set_level("error")By default, log entries go to print(). Provide your own sink for custom output or test capture:
flow.log.set_sink(function(entry)
-- entry.level → "debug" | "info" | "warn" | "error"
-- entry.context → "ui.input" etc.
-- entry.message → formatted string
-- entry.line → source line number
my_logger:write(entry.level, entry.context, entry.message)
end)Restore the default:
flow.log.set_sink(nil)- Enable debug mode and click on the button area.
- Check the printed hit result. If it's
nil, nothing was hit at those coordinates. - Possible causes:
- The button's parent has
height = 0(missing explicit height on a wrapper box). - The button is covered by an invisible overlay (a
Popupwith_visible = trueleft in the tree). - Input focus not acquired — make sure
msg.post(".", "acquire_input_focus")is called ininit()(handled automatically byflow.init).
- The button's parent has
- Print
node.layouton the suspect element after rendering:-- Temporarily in view(): local my_box = Box({ key = "suspect", ... }) -- After flow.update: -- flow.ui.update internally calls layout.compute, so layout is written to the tree print(my_box.layout and my_box.layout.w or "no layout yet")
- Check that every ancestor has an explicit
widthandheight(orflex_grow). A parent with height 0 collapses all children.
- Print
flow.nav.get_scroll_offset("your_scroll_key")each frame. - Verify
_virtual_heightis set correctly on theScrollnode. - Check
first_render/last_rendercalculations — off-by-one errors here shift which items are visible.
- Enable renderer logging:
flow.log.set_context_level("ui.renderer", "debug")
- Look for keys that appear in creation logs but not in deletion logs.
- Cause: a key that was in the tree before is no longer in the new tree but wasn't cleaned up — usually because the key changed (random key bug).
Scroll state is saved in current.params.scroll_state by the navigation GUI adapter. It is restored each time the screen's view() is called. If the scroll position resets:
- Ensure you are not calling
flow.nav.reset(...)unintentionally. - Check if the screen is being replaced instead of remaining on the stack.
- If you clear
paramsmanually, make sure to preserveparams.scroll_state.
Default format:
[flow][WARN][nav] screen id not found: inventory
[flow][DEBUG][ui.input] hit "btn_ok" at layout {x=400, y=280, w=120, h=44}
Fields: [flow] prefix, level in brackets, context in brackets, then the message.