ludic/packages/ludic.ui/backend.ludic
Orkuncakilkaya 49b5e80af5 ludic ui-preview: ludic.ui components previewed for a studio over stdio (R5)
A prebuilt tool, bin/ludic-ui-preview (built by ludic-dev build, shipped beside ludic-lsp, run as
`ludic ui-preview [--font DIR]`): ludic.ui with a backend that records every draw call as a line,
and mock component classes the studio describes (load / model / calls). frame t runs ui_show at
interface time t; text is measured on the CPU with the game's font.json metrics; locale goes
through ludic.i18n; tree / box / rules inspect the frame. No game code, no GPU, no network. The wire
protocol is frozen as tools/ui-preview/protocol-v1.md; smoke.txt is a transcript to run.

ludic.ui gains, all additive: UiClass.make_of, UiBackend.said / emitted, UiRule.text / at (with
comment line breaks kept so rule lines count true, and a class's styles read under its .lss path),
and ui_root, ui_find, ui_rules_of, ui_rule_value, ui_building, ui_class, ui_class_load,
ui_file_forget, ui_errors_clear; ui_nine_cuts_into exported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:53:26 +03:00

247 lines
12 KiB
Text

# backend.ludic - the renderer seam: what a renderer draws, and nothing more. Only rect, text and
# measure are needed; the rest improve on a default (a rounded rect falls back to a square one, an
# image to nothing). Input is the runtime's own (Input.*), so no renderer has to supply it.
export property UiBackend {
rect: fn(float, float, float, float, int, float) -> void = null # x y w h rgb alpha
round: fn(float, float, float, float, float, int, float) -> void = null # ... radius rgb alpha
ring: fn(float, float, float, float, float, float, int, float) -> void = null # x y w h radius width rgb alpha
text: fn(float, float, string, float, int, float) -> void = null # x y text size rgb alpha
measure: fn(string, float) -> float = null
image: fn(string, float, float, float, float, int, float) -> void = null # src x y w h tint alpha
image_w: fn(string) -> float = null # a picture's own size, for object-fit
image_h: fn(string) -> float = null
nine: fn(string, float, float, float, float, float, float, int, float) -> void = null # src slice x y w h width tint alpha (ui_nine_cuts)
clip: fn(float, float, float, float) -> void = null
unclip: fn() -> void = null
scale: fn() -> float = null # screen pixels per design pixel
pointer_scale: fn() -> float = null # screen pixels per unit of Input.mouse_x/y (2 on Retina)
translate: fn(string) -> string = null # the program's language, for every text a template shows
sound: fn(string) -> void = null # a control was used: "click", or its sound="..."
read: fn(string) -> string = null # a template file's text (else Fs.read_text)
now: fn() -> float = null # seconds, for animation (else frames at 60 a second)
input: bool = true # false: take no input from Input.* (a test drives it)
said: fn(string) -> void = null # a template error, for a host that reports it itself (else printed)
emitted: fn(string) -> void = null # `emit name` ran, whether or not a handler took it (a host's log)
}
# a renderer a program installs by importing it (render3d.ludic): the first one is the backend
property UiRenderer {
key: string = ""
make: fn() -> UiBackend = null
}
registry UiRenderers of UiRenderer as UIR
function ui_default_backend() -> UiBackend {
if len(UiRenderers) == 0 { return null }
return UiRenderers[0].make()
}
export state UiState {
@max(512) pl_pools: []UiPool = new []UiPool # pool.ludic: each screen's nodes and envs, used again
pl_cur: UiPool = null
uv_on: bool = false # pool.ludic: a build is under way, so expressions' Values are pooled
uv_true: Val = null
uv_false: Val = null
mm: UiMemo = null # memo.ludic: splits, gradients and keys by their text
mm_ints: []string = null
mm_negs: []string = new []string # memo_join.ludic: negative numbers' texts, and the last big ones
mm_big_v: []int = new []int
mm_big_s: []string = new []string
mm_big_at: int = 0
fc_none: Val = Value.null() # focus.ludic: the value a key's down and up carry, made once
mm_ivals: []Val = null
ev_depth: int = 0 # eval.ludic: a call's arguments, in lists kept one a depth
ev_args: []Val = new []Val
@max(64) ev_ring: []Val = ev_ring_new() # eval.ludic: calls' answers while an action runs, EV_RING of them, made with the state
ev_ring_at: int = 0
nt_evs: []Val = nt_evs_new() # native.ludic: fired events' objects, NT_EVS of them, made with the state
nt_ev_at: int = 0
nt_nums: []Val = nt_nums_new() # fired.ludic: the numbers a scroll or a slider reports, a ring
nt_num_at: int = 0
ct_clamp_n: []UiNode = new []UiNode # controls_opts.ludic: selects whose value a build clamped
ct_clamp_i: []int = new []int
hd_ev: Val = hd_ev_new() # hold.ludic: the held control's event, filled in place
cv_texts: []string = new []string # units_pieces.ludic: a value's var() pieces, kept
tx_pieces: [][]string = new [][]string # eval_text.ludic: a text's pieces, a list a depth
tx_depth: int = 0
ca_pool: []CaVal = new []CaVal # calc.ludic: an evaluation's terms, used again by the next
ca_used: int = 0
ca_args: [][]CaVal = new [][]CaVal
ca_depth: int = 0
@max(16) pe_evs: []UiPointer = pe_evs__new() # pointer_events.ludic: an event's record and its Value, a ring
@max(16) pe_vals: []Val = pe_vals__new()
pe_at: int = 0
nth_a: int = 0 # select_nth.ludic: the a and b just read
nth_b: int = 0
tt_src: string = null # tooltip.ludic: the title tt_text was translated from
fit_out: []float = new []float # draw_content.ludic: an image's box, kept
r3n_cut: []float = new []float # render3d_nine.ludic: a nine-slice's edges, kept
r3n_xs: []float = new []float
r3n_ys: []float = new []float
r3n_us: []float = new []float
r3n_vs: []float = new []float
ly_depth: int = 0 # layout_run.ludic: the lists a line lays out with, one a depth
ly_sizes: [][]float = new [][]float
ly_frozen: [][]bool = new [][]bool
cs_rules: []UiRule = new []UiRule # cascade_merge.ludic: a component root's rules, gathered
cs_envs: []UiEnv = new []UiEnv
cs_seen: []UiSheet = new []UiSheet
an_frames: []UiKeyframes = new []UiKeyframes
an_keys: []string = new []string
an_start: []float = new []float
an_frame: int = 0
el_s0: float = 0.0
el_s1: float = 0.0
el_s2: float = 0.0
el_s3: float = 0.0
ui_be: UiBackend = ui_default_backend()
tr_no: bool = false # inside translate="no" while a frame is built
ca_s: string = ""
ca_i: int = 0
ca_bad: string = ""
ca_px: float = 0.0 # the result: its lengths, its percentage, and whether it had units
ca_pct: float = 0.0
ca_num: float = 0.0 # a plain number's own value, and whether it was one
ca_bare: bool = false
cs_pass: int = 0
ua_sh: UiSheet = null
uc_list: []UiClass = new []UiClass # filled by the compiler's registrations, before the program's globals
cl_alpha: float = 1.0
ct_capture: string = ""
ct_capture_at: int = -1 # the frame listening began, whose own key started it
ct_capture_seen: int = -1 # the last frame that began listening, for ui_capturing
ct_num_key: string = ""
ct_num_text: string = ""
dv_on: bool = false
dv_frames: int = 0
dv_dumped: bool = false
fr_ga: float = 1.0 # the opacity of everything around this element, which it multiplies
gr_a: float = 1.0
fr_clips: []float = new []float
ui_view: UiView = null
ui_errs: []string = new []string
ui_at: string = ""
fc_key: string = ""
fc_visible: bool = false # focus came from the keyboard: :focus-visible, and the ring
fc_list: []UiNode = new []UiNode
fb_roots: []UiNode = new []UiNode
hd_key: string = ""
hd_t0: float = 0.0
hd_last: float = 0.0
in_now: UiInput = new UiInput
in_was_down: bool = false
in_last: UiInput = new UiInput
in_rec_a: UiInput = new UiInput
in_rec_b: UiInput = new UiInput
in_given: bool = false
rp_next: []float = [-1.0, -1.0, -1.0, -1.0]
rp_a: bool = false
rp_b: bool = false
rp_act: bool = false
in_act_down: bool = false # A or Enter went down this frame, or came up
in_act_up: bool = false
in_list: []UiInst = new []UiInst
in_free: []UiInst = new []UiInst # unmounted instances, each mounted again as its class's next one
in_build: int = 0
ly_fg_a: float = 1.0
ly_lead: float = 0.0
ly_extra: float = 0.0
lib_files: []UiFile = new []UiFile
lib_screen_names: []string = new []string
lib_screens: []UiTpl = new []UiTpl
lc_keys: []string = new []string
lc_mounts: []UiAct = new []UiAct
lc_unmounts: []UiAct = new []UiAct
lc_envs: []UiEnv = new []UiEnv
lc_sp_keys: []string = new []string # life.ludic: the spare set lc_run fills and swaps in
lc_sp_unmounts: []UiAct = new []UiAct
lc_sp_envs: []UiEnv = new []UiEnv
lc_prev_keys: []string = new []string
lc_prev_unmounts: []UiAct = new []UiAct
lc_prev_envs: []UiEnv = new []UiEnv
nt_list: []UiNative = new []UiNative
nt_sp_nodes: []UiNode = nt_nodes__new() # native.ludic: the queue's spare, swapped while it runs;
nt_sp_names: []string = nt_names__new() # each list is made with room for a busy frame's events
nt_sp_vals: []Val = nt_vals__new()
nt_q_nodes: []UiNode = nt_nodes__new()
nt_q_names: []string = nt_names__new()
nt_q_vals: []Val = nt_vals__new()
fr_hover: []string = new []string
fr_hover_sp: []string = new []string
fr_press_key: string = ""
fr_down_now: bool = false
fr_key_press: string = "" # the control Enter or the pad's A is holding down
pe_key: string = "" # the element the pointer is captured by
pe_btn: int = 0
pop_list: []UiNode = new []UiNode
pop_spare: []UiNode = new []UiNode
pp_bx: float = 0.0
pp_by: float = 0.0
pp_bw: float = 0.0
pp_bh: float = 0.0
r3u_user: float = 1.0
r3u_tex_paths: []string = new []string
r3u_tex_ids: []int = new []int
r3u_tex_sizes: []int = new []int
r3u_tex_seen: []float = new []float
r3u_atlas: []string = new []string
r3u_atlas_tex: []int = new []int
r3u_atlas_cols: []int = new []int
r3u_atlas_rows: []int = new []int
r3u_atlas_names: [][]string = new [][]string
sc_keys: []string = new []string
sc_offs: []float = new []float
sc_drag: string = ""
sc_scope: UiNode = null # the popover that has the pointer this frame, if one is up
sb_x: float = 0.0
sb_w: float = 0.0
sb_ty: float = 0.0
sb_th: float = 0.0
sb_bar: UiNode = null
sc_grab: float = 0.0 # how far below the thumb's top the pointer holds it
sc_want_keys: []string = new []string
sc_want_px: []float = new []float
st_keys: []string = new []string
st_vals: []Val = new []Val
tf_size: float = 0.0 # the size tf_fit drew at
tt_key: string = ""
tt_since: float = 0.0
tt_text: string = ""
tt_x: float = 0.0
tt_y: float = 0.0
tt_by_keys: bool = false # the focus moved by the keyboard since the pointer last moved
tt_focus_seen: string = ""
tp_nl: []int = new []int
tr_keys: []string = new []string
tr_from: []float = new []float
tr_to: []float = new []float
tr_at: []float = new []float
el_cur: UiNode = null
el_key: string = ""
ly_vp_x: float = 0.0
ly_vp_y: float = 0.0
ly_vp_w: float = 0.0
ly_vp_h: float = 0.0
}
export function ui_backend(ui_st: mut UiState, b: UiBackend) -> void { ui_st.ui_be = b }
function ui_px(ui_st: UiState, v: float) -> float {
if ui_st.ui_be != null and ui_st.ui_be.scale != null { return v * ui_st.ui_be.scale() }
return v
}
function ui_tr(ui_st: UiState, s: string) -> string {
if ui_st.tr_no { return s }
if ui_st.ui_be != null and ui_st.ui_be.translate != null { return ui_st.ui_be.translate(s) }
return s
}
function ui_sound(ui_st: UiState, name: string) -> void {
if ui_st.ui_be != null and ui_st.ui_be.sound != null { ui_st.ui_be.sound(name) }
}
# the program's language: every text a template shows goes through `f` before it is measured
export function ui_translator(ui_st: mut UiState, f: fn(string) -> string) -> void {
if ui_st.ui_be != null { ui_st.ui_be.translate = f }
}
# the program's sounds: "click" when a control is used, or the control's own sound="..."
export function ui_sounds(ui_st: mut UiState, f: fn(string) -> void) -> void {
if ui_st.ui_be != null { ui_st.ui_be.sound = f }
}
# the program's clock, in seconds, for animation (a game's own time, which a headless run counts)
export function ui_clock(ui_st: mut UiState, f: fn() -> float) -> void {
if ui_st.ui_be != null { ui_st.ui_be.now = f }
}