wip(0.S3): packages and examples migrated in one run - ludic migrate state packages <every example program> packages/ludic.lab/example/plate.ludic

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 15:02:45 +03:00
parent d490d4f9f1
commit 3eda72e8c2
172 changed files with 3094 additions and 2966 deletions

View file

@ -20,8 +20,10 @@ enum RoomStyle { Pillars, Columns, CenterSlab, Bars, Scatter }
const DUNGEON_MARGIN: int = 2
var dungeon_wall: int = '#'
var dungeon_floor: int = '.'
state DungeonState {
dungeon_wall: int = '#'
dungeon_floor: int = '.'
}
function dungeon_columns() -> int { return Map.width() }
function dungeon_rows() -> int { return Map.height() }
@ -29,62 +31,62 @@ function dungeon_door_column() -> int { return Map.width() / 2 - 1 } # the l
function dungeon_door_row() -> int { return Map.height() / 2 } # the top of the two door cells
# a wall, but never in the margin so rooms stay traversable
function dungeon_place(x: int, y: int) -> void {
function dungeon_place(dungeon_st: DungeonState, x: int, y: int) -> void {
if (x < DUNGEON_MARGIN) or (x > dungeon_columns() - 1 - DUNGEON_MARGIN) { return }
if (y < DUNGEON_MARGIN) or (y > dungeon_rows() - 1 - DUNGEON_MARGIN) { return }
Map.set(x: x, y: y, glyph: dungeon_wall)
Map.set(x: x, y: y, glyph: dungeon_st.dungeon_wall)
}
function dungeon_mirrored(x: int, y: int) -> void {
dungeon_place(x, y)
dungeon_place(dungeon_columns() - 1 - x, y)
dungeon_place(x, dungeon_rows() - 1 - y)
dungeon_place(dungeon_columns() - 1 - x, dungeon_rows() - 1 - y)
function dungeon_mirrored(dungeon_st: DungeonState, x: int, y: int) -> void {
dungeon_place(dungeon_st, x, y)
dungeon_place(dungeon_st, dungeon_columns() - 1 - x, y)
dungeon_place(dungeon_st, x, dungeon_rows() - 1 - y)
dungeon_place(dungeon_st, dungeon_columns() - 1 - x, dungeon_rows() - 1 - y)
}
function dungeon_pillar(x: int, y: int) -> void { # 2x2, mirrored four ways
dungeon_mirrored(x, y)
dungeon_mirrored(x + 1, y)
dungeon_mirrored(x, y + 1)
dungeon_mirrored(x + 1, y + 1)
function dungeon_pillar(dungeon_st: DungeonState, x: int, y: int) -> void { # 2x2, mirrored four ways
dungeon_mirrored(dungeon_st, x, y)
dungeon_mirrored(dungeon_st, x + 1, y)
dungeon_mirrored(dungeon_st, x, y + 1)
dungeon_mirrored(dungeon_st, x + 1, y + 1)
}
function dungeon_style(style: int) -> void {
function dungeon_style(dungeon_st: DungeonState, style: int) -> void {
let cols = dungeon_columns()
let rows = dungeon_rows()
match style {
RoomStyle.Pillars => {
let x = Random.range(low: 3, high: 5)
let y = Random.range(low: 3, high: 4)
dungeon_pillar(x, y)
if Random.chance(percent: 60) { dungeon_pillar(x + 3, y + 2) }
dungeon_pillar(dungeon_st, x, y)
if Random.chance(percent: 60) { dungeon_pillar(dungeon_st, x + 3, y + 2) }
}
RoomStyle.Columns => {
let x = Random.range(low: 4, high: 6)
for y in 3 .. rows - 3 { dungeon_mirrored(x, y) }
for y in 3 .. rows - 3 { dungeon_mirrored(dungeon_st, x, y) }
}
RoomStyle.CenterSlab => { Map.rect(x: 6, y: 5, width: cols - 12, height: rows - 10, glyph: dungeon_wall) }
RoomStyle.CenterSlab => { Map.rect(x: 6, y: 5, width: cols - 12, height: rows - 10, glyph: dungeon_st.dungeon_wall) }
RoomStyle.Bars => {
let y = Random.range(low: 3, high: 4)
for x in 4 .. cols - 4 { dungeon_mirrored(x, y) }
for x in 4 .. cols - 4 { dungeon_mirrored(dungeon_st, x, y) }
}
_ => {
for i in 0 .. Random.range(low: 3, high: 5) {
let x = Random.range(low: 3, high: cols / 2 - 1)
dungeon_place(x, Random.range(low: 3, high: rows - 4))
dungeon_place(cols - 1 - x, Random.range(low: 3, high: rows - 4))
dungeon_place(dungeon_st, x, Random.range(low: 3, high: rows - 4))
dungeon_place(dungeon_st, cols - 1 - x, Random.range(low: 3, high: rows - 4))
}
}
}
}
@Namespace(Dungeon) function dungeon_open_arena(wall: int, floor: int) -> void {
dungeon_wall = wall
dungeon_floor = floor
@Namespace(Dungeon) function dungeon_open_arena(dungeon_st: mut DungeonState, wall: int, floor: int) -> void {
dungeon_st.dungeon_wall = wall
dungeon_st.dungeon_floor = floor
Map.fill(glyph: floor)
Map.border(glyph: wall)
}
@Namespace(Dungeon) function dungeon_arena(style: int, wall: int, floor: int) -> void {
dungeon_open_arena(wall, floor)
dungeon_style(style)
@Namespace(Dungeon) function dungeon_arena(dungeon_st: mut DungeonState, style: int, wall: int, floor: int) -> void {
dungeon_open_arena(dungeon_st, wall, floor)
dungeon_style(dungeon_st, style)
Map.rect(x: dungeon_door_column(), y: 1, width: 2, height: dungeon_rows() - 2, glyph: floor) # the door lanes
Map.rect(x: 1, y: dungeon_door_row(), width: dungeon_columns() - 2, height: 2, glyph: floor)
}

View file

@ -23,13 +23,16 @@ event Died { e: int = 0, src: int = 0 }
event Healed { e: int = 0, amount: int = 0 }
event Crit { target: int = 0, src: int = 0 } # the attacker's crit_pct fired
var combat_reflecting: bool = false # thorns never reflect thorns
state GameplayState {
combat_reflecting: bool = false # thorns never reflect thorns
combat_pending: int = 0
fac_tbl: words = null
}
# the mutable channel for lever 6: listeners rewrite the pending damage here.
var combat_pending: int = 0
@Namespace(Combat) function combat_amount() -> int { return combat_pending }
@Namespace(Combat) function combat_set_amount(n: int) -> void { combat_pending = n }
@Namespace(Combat) function combat_amount(gameplay_st: GameplayState) -> int { return gameplay_st.combat_pending }
@Namespace(Combat) function combat_set_amount(gameplay_st: mut GameplayState, n: int) -> void { gameplay_st.combat_pending = n }
# Combat.mitigate(target, raw) — raw minus effective defence, floored at 1 so any
# landed hit chips at least a point (a common, tunable default). A game that wants
@ -43,7 +46,7 @@ var combat_pending: int = 0
# Combat.damage(target, src, amount) — the full pipeline. Returns the damage
# actually applied (0 when vetoed or the target has no Stats/hp).
@Namespace(Combat) function combat_damage(target: int, src: int, amount: int) -> int {
@Namespace(Combat) function combat_damage(gameplay_st: mut GameplayState, target: int, src: int, amount: int) -> int {
let P = World.prop_id("Stats")
if P < 0 { return 0 }
if World.has(target, P) == 0 { return 0 }
@ -54,9 +57,9 @@ var combat_pending: int = 0
var raw = amount * stats_field(src, "damage_pct", 100) / 100
let crit = stats_field(src, "crit_pct", 0)
if (crit > 0) and Random.chance(percent: crit) { raw *= 2; emit Crit(target: target, src: src) }
combat_pending = combat_mitigate(target, raw)
if emit DamageAboutToApply(target: target, src: src, amount: combat_pending) != 0 { return 0 }
var dmg = combat_pending
gameplay_st.combat_pending = combat_mitigate(target, raw)
if emit DamageAboutToApply(target: target, src: src, amount: gameplay_st.combat_pending) != 0 { return 0 }
var dmg = gameplay_st.combat_pending
if dmg < 0 { dmg = 0 }
var hp = World.get(target, P, fhp) - dmg
@ -69,10 +72,10 @@ var combat_pending: int = 0
let leech = stats_field(src, "leech_pct", 0)
if (leech > 0) and Random.chance(percent: leech) { combat_heal(src, stats_field(src, "leech_hp", 1)) }
let thorns = stats_field(target, "thorns", 0)
if (thorns > 0) and (src != target) and (not combat_reflecting) and (World.has(src, P) != 0) {
combat_reflecting = true
combat_damage(src, target, thorns)
combat_reflecting = false
if (thorns > 0) and (src != target) and (not gameplay_st.combat_reflecting) and (World.has(src, P) != 0) {
gameplay_st.combat_reflecting = true
combat_damage(gameplay_st, src, target, thorns)
gameplay_st.combat_reflecting = false
}
}
return dmg

View file

@ -16,26 +16,25 @@ const REL_HOSTILE: int = 2
# relationships stored as (rel + 1); a zero slot means "unset -> use the default".
# A global var initialiser must be a literal, so it is allocated lazily.
var fac_tbl: words = null
function fac_ensure() -> void {
if fac_tbl == null { fac_tbl = words(FAC_MAX * FAC_MAX) }
function fac_ensure(gameplay_st: mut GameplayState) -> void {
if gameplay_st.fac_tbl == null { gameplay_st.fac_tbl = words(FAC_MAX * FAC_MAX) }
}
# Faction.set(a, b, rel) — record a symmetric relationship between two factions.
@Namespace(Faction) function faction_set(a: int, b: int, rel: int) -> void {
fac_ensure()
@Namespace(Faction) function faction_set(gameplay_st: mut GameplayState, a: int, b: int, rel: int) -> void {
fac_ensure(gameplay_st)
if (a < 0) or (a >= FAC_MAX) { return }
if (b < 0) or (b >= FAC_MAX) { return }
fac_tbl[a * FAC_MAX + b] = rel + 1
fac_tbl[b * FAC_MAX + a] = rel + 1
gameplay_st.fac_tbl[a * FAC_MAX + b] = rel + 1
gameplay_st.fac_tbl[b * FAC_MAX + a] = rel + 1
}
# Faction.rel(a, b) — the relationship code (0 friend, 1 neutral, 2 hostile).
@Namespace(Faction) function faction_rel(a: int, b: int) -> int {
fac_ensure()
@Namespace(Faction) function faction_rel(gameplay_st: mut GameplayState, a: int, b: int) -> int {
fac_ensure(gameplay_st)
if (a >= 0) and (a < FAC_MAX) and (b >= 0) and (b < FAC_MAX) {
let v = fac_tbl[a * FAC_MAX + b]
let v = gameplay_st.fac_tbl[a * FAC_MAX + b]
if v != 0 { return v - 1 }
}
if a == b { return REL_FRIEND }
@ -43,12 +42,12 @@ function fac_ensure() -> void {
}
# Faction.hostile(a, b) / Faction.friendly(a, b) — the common yes/no queries.
@Namespace(Faction) function faction_hostile(a: int, b: int) -> int {
if faction_rel(a, b) == REL_HOSTILE { return 1 }
@Namespace(Faction) function faction_hostile(gameplay_st: mut GameplayState, a: int, b: int) -> int {
if faction_rel(gameplay_st, a, b) == REL_HOSTILE { return 1 }
return 0
}
@Namespace(Faction) function faction_friendly(a: int, b: int) -> int {
if faction_rel(a, b) == REL_FRIEND { return 1 }
@Namespace(Faction) function faction_friendly(gameplay_st: mut GameplayState, a: int, b: int) -> int {
if faction_rel(gameplay_st, a, b) == REL_FRIEND { return 1 }
return 0
}
@ -65,6 +64,6 @@ function fac_ensure() -> void {
# Faction.enemies(a, b) — true when the two entities' factions are hostile; the
# one-call test the shooter and AI reach for. Missing factions read as hostile
# (an unaligned hazard hits anyone) unless the ids happen to match.
@Namespace(Faction) function faction_enemies(ea: int, eb: int) -> int {
return faction_hostile(faction_id_of(ea), faction_id_of(eb))
@Namespace(Faction) function faction_enemies(gameplay_st: mut GameplayState, ea: int, eb: int) -> int {
return faction_hostile(gameplay_st, faction_id_of(ea), faction_id_of(eb))
}

View file

@ -6,15 +6,15 @@
import "ludic.lab"
program LabPlate {
numbers float
function probe_setup() -> void {
lab_model(lab_plate_dir(), "probe.gltf", "probe", 0.0, 0.0, 0.5)
lab_note("a box and a ball, in the plate's light")
lab_shot_at("front", 0.0, 0.7, 0.0, 4.0, 0.0, 12.0)
lab_shot_at("side", 0.0, 0.7, 0.0, 4.0, 90.0, 12.0)
lab_shot_at("above", 0.0, 0.5, 0.0, 7.0, 30.0, 55.0)
function probe_setup(lab_st: mut LabState, render3d_st: mut Render3dState) -> void {
lab_model(render3d_st, lab_plate_dir(lab_st), "probe.gltf", "probe", 0.0, 0.0, 0.5)
lab_note(lab_st, "a box and a ball, in the plate's light")
lab_shot_at(lab_st, "front", 0.0, 0.7, 0.0, 4.0, 0.0, 12.0)
lab_shot_at(lab_st, "side", 0.0, 0.7, 0.0, 4.0, 90.0, 12.0)
lab_shot_at(lab_st, "above", 0.0, 0.5, 0.0, 7.0, 30.0, 55.0)
}
def LabScenes probe { setup: fn probe_setup }
handler Boot phase Start { lab_boot() }
handler Look phase Input { lab_input() }
handler Draw phase Render { lab_render() }
handler Boot(lab_st: mut LabState, render3d_st: mut Render3dState) phase Start { lab_boot(lab_st, render3d_st) }
handler Look(lab_st: mut LabState, render3d_st: mut Render3dState) phase Input { lab_input(lab_st, render3d_st) }
handler Draw(lab_st: mut LabState, render3d_st: mut Render3dState) phase Render { lab_render(lab_st, render3d_st) }
}

View file

@ -17,7 +17,7 @@ export function np_of_pid(net_st: NetState, pid: int) -> int {
return -1
}
function net__open(net_st: NetState, slot: int, ip: int, port: int, pid: int) -> void {
function net__open(net_st: mut NetState, slot: int, ip: int, port: int, pid: int) -> void {
let p = net_st.net__peers[slot]
p.on = true
p.ip = ip
@ -32,7 +32,7 @@ function net__open(net_st: NetState, slot: int, ip: int, port: int, pid: int) ->
p.closing = false
}
export function np_close(net_st: NetState, slot: int) -> void {
export function np_close(net_st: mut NetState, slot: int) -> void {
if net_st.net__peers == null or slot < 0 or slot >= NET_PEERS { return }
let p = net_st.net__peers[slot]
p.on = false
@ -41,7 +41,7 @@ export function np_close(net_st: NetState, slot: int) -> void {
}
# the written message, for one peer (a reliable one is kept until it is taken)
export function np_queue(net_st: NetState, slot: int, kind: int, rel: bool) -> void {
export function np_queue(net_st: mut NetState, slot: int, kind: int, rel: bool) -> void {
if net_st.net__peers == null or slot < 0 or slot >= NET_PEERS { return }
let p = net_st.net__peers[slot]
if not p.on { return }
@ -63,7 +63,7 @@ export function np_queue(net_st: NetState, slot: int, kind: int, rel: bool) -> v
}
# to everyone this machine talks to (a host: every guest but `except`; a guest: the host)
export function np_queue_all(net_st: NetState, kind: int, rel: bool, except: int) -> void {
export function np_queue_all(net_st: mut NetState, kind: int, rel: bool, except: int) -> void {
if net_st.net__peers == null { return }
for i in 0 .. NET_PEERS { if net_st.net__peers[i].on and i != except { np_queue(net_st, i, kind, rel) } }
}
@ -94,7 +94,7 @@ export function net_peer_heard(net_st: NetState, slot: int) -> float {
}
# let the slot go once it has taken everything queued for it so far, or after `secs`
export function net_close_when_sent(net_st: NetState, slot: int, secs: float) -> void {
export function net_close_when_sent(net_st: mut NetState, slot: int, secs: float) -> void {
if not net_peer_on(net_st, slot) { return }
let p = net_st.net__peers[slot]
p.closing = true

View file

@ -29,7 +29,7 @@ function net__packet(base_st: mut BaseState, net_st: mut NetState, n: int, ip: i
}
# the known peer at that address, a stranger's new slot (a host, and only with a hello), or -1
function net__slot_for(net_st: NetState, n: int, ip: int, port: int) -> int {
function net__slot_for(net_st: mut NetState, n: int, ip: int, port: int) -> int {
let token = nb_get32(net_st.net__in, 4)
var slot = net__find(net_st, ip, port)
if slot >= 0 {

View file

@ -9,49 +9,51 @@
const PREFS_MAX: int = 64
var prefs_path: pointer = null
var prefs_names: pointers = null
var prefs_vals: words = null
var prefs_n: int = 0
state PrefsState {
prefs_path: pointer = null
prefs_names: pointers = null
prefs_vals: words = null
prefs_n: int = 0
}
namespace Prefs {
internal function init() -> void {
if prefs_names != null { return }
prefs_names = pointers(PREFS_MAX)
prefs_vals = words(PREFS_MAX)
prefs_n = 0
internal function init(prefs_st: mut PrefsState) -> void {
if prefs_st.prefs_names != null { return }
prefs_st.prefs_names = pointers(PREFS_MAX)
prefs_st.prefs_vals = words(PREFS_MAX)
prefs_st.prefs_n = 0
}
internal function find(key: pointer) -> int {
internal function find(prefs_st: PrefsState, key: pointer) -> int {
var i = 0
while i < prefs_n { if prefs_names[i] == key { return i }; i += 1 }
while i < prefs_st.prefs_n { if prefs_st.prefs_names[i] == key { return i }; i += 1 }
return -1
}
export function get(key: pointer, fallback: int) -> int {
init()
let i = find(key)
export function get(prefs_st: mut PrefsState, key: pointer, fallback: int) -> int {
init(prefs_st)
let i = find(prefs_st, key)
if i < 0 { return fallback }
return prefs_vals[i]
return prefs_st.prefs_vals[i]
}
# keep the larger of the stored value and `value`; true when `value` is a new record
export function max(key: pointer, value: int) -> bool {
if value <= get(key, 0) { return false }
set(key, value)
export function max(prefs_st: mut PrefsState, key: pointer, value: int) -> bool {
if value <= get(prefs_st, key, 0) { return false }
set(prefs_st, key, value)
return true
}
export function set(key: pointer, value: int) -> void {
init()
var i = find(key)
export function set(prefs_st: mut PrefsState, key: pointer, value: int) -> void {
init(prefs_st)
var i = find(prefs_st, key)
if i < 0 {
if prefs_n >= PREFS_MAX { return }
i = prefs_n
prefs_names[i] = key
prefs_n += 1
if prefs_st.prefs_n >= PREFS_MAX { return }
i = prefs_st.prefs_n
prefs_st.prefs_names[i] = key
prefs_st.prefs_n += 1
}
prefs_vals[i] = value
prefs_st.prefs_vals[i] = value
}
# the text of a whole file, or null when it does not exist
@ -69,7 +71,7 @@ namespace Prefs {
}
# digits after `=` on one line -> an int (a leading '-' negates)
internal function parse_line(buf: pointer, from: int, to: int) -> void {
internal function parse_line(prefs_st: mut PrefsState, buf: pointer, from: int, to: int) -> void {
var eq = from
while (eq < to) and (buf[eq] != '=') { eq += 1 } # '='
if eq >= to { return }
@ -83,21 +85,21 @@ namespace Prefs {
if (p < to) and (buf[p] == '-') { neg = true; p += 1 } # '-'
while (p < to) and (buf[p] >= '0') and (buf[p] <= '9') { v = v * 10 + (buf[p] - 48); p += 1 }
if neg { v = -v }
set(key, v)
set(prefs_st, key, v)
}
export function load(path: pointer) -> void {
init()
prefs_path = path
export function load(prefs_st: mut PrefsState, path: pointer) -> void {
init(prefs_st)
prefs_st.prefs_path = path
let buf = read_all(path)
if buf == null { return }
var start = 0
var i = 0
while buf[i] != 0 {
if buf[i] == '\n' { parse_line(buf, start, i); start = i + 1 } # newline
if buf[i] == '\n' { parse_line(prefs_st, buf, start, i); start = i + 1 } # newline
i += 1
}
if i > start { parse_line(buf, start, i) }
if i > start { parse_line(prefs_st, buf, start, i) }
}
internal function write_text(f: pointer, s: pointer) -> void {
@ -106,14 +108,14 @@ namespace Prefs {
file_write(f, s, n)
}
export function save() -> void {
init()
if prefs_path == null { return }
let f = file_open(prefs_path, "wb")
export function save(prefs_st: mut PrefsState) -> void {
init(prefs_st)
if prefs_st.prefs_path == null { return }
let f = file_open(prefs_st.prefs_path, "wb")
if f == null { return }
var i = 0
while i < prefs_n {
write_text(f, `{prefs_names[i]}={string(prefs_vals[i])}\n`)
while i < prefs_st.prefs_n {
write_text(f, `{prefs_st.prefs_names[i]}={string(prefs_st.prefs_vals[i])}\n`)
i += 1
}
file_close(f)

View file

@ -9,20 +9,12 @@ import "ludic.rpg/inventory.ludic"
const CRAFT_MAX: int = 128
const CRAFT_ING_MAX: int = 512
var recipe_reg: Dict = null # recipe name -> id
var recipe_n: int = 0
var recipe_out: pointers = null # output item name
var recipe_outqty: words = null
var ing_n: int = 0
var ing_rid: words = null # owning recipe id
var ing_name: pointers = null # ingredient item name
var ing_qty: words = null
function craft_ensure() -> void {
if recipe_reg == null {
recipe_reg = Dict.new()
recipe_out = pointers(CRAFT_MAX); recipe_outqty = words(CRAFT_MAX)
ing_rid = words(CRAFT_ING_MAX); ing_name = pointers(CRAFT_ING_MAX); ing_qty = words(CRAFT_ING_MAX)
function craft_ensure(rpg_st: mut RpgState) -> void {
if rpg_st.recipe_reg == null {
rpg_st.recipe_reg = Dict.new()
rpg_st.recipe_out = pointers(CRAFT_MAX); rpg_st.recipe_outqty = words(CRAFT_MAX)
rpg_st.ing_rid = words(CRAFT_ING_MAX); rpg_st.ing_name = pointers(CRAFT_ING_MAX); rpg_st.ing_qty = words(CRAFT_ING_MAX)
}
}
@ -30,35 +22,35 @@ event cancellable CraftRequested { e: int = 0, recipe: int = 0 }
event CraftCompleted { e: int = 0, recipe: int = 0 }
# Craft.recipe(name, out_item, out_qty) — declare a recipe; returns its id.
@Namespace(Craft) function craft_recipe(name: pointer, out_item: pointer, out_qty: int) -> int {
craft_ensure()
if recipe_n >= CRAFT_MAX { return -1 }
let id = recipe_n
recipe_out[id] = out_item; recipe_outqty[id] = out_qty
recipe_n += 1
Dict.set(recipe_reg, name, id)
@Namespace(Craft) function craft_recipe(rpg_st: mut RpgState, name: pointer, out_item: pointer, out_qty: int) -> int {
craft_ensure(rpg_st)
if rpg_st.recipe_n >= CRAFT_MAX { return -1 }
let id = rpg_st.recipe_n
rpg_st.recipe_out[id] = out_item; rpg_st.recipe_outqty[id] = out_qty
rpg_st.recipe_n += 1
Dict.set(rpg_st.recipe_reg, name, id)
return id
}
@Namespace(Craft) function craft_id(name: pointer) -> int { craft_ensure(); return Dict.get_or(recipe_reg, name, -1) }
@Namespace(Craft) function craft_id(rpg_st: mut RpgState, name: pointer) -> int { craft_ensure(rpg_st); return Dict.get_or(rpg_st.recipe_reg, name, -1) }
# Craft.ingredient(recipe_name, item, qty) — add an ingredient requirement.
@Namespace(Craft) function craft_ingredient(recipe_name: pointer, item: pointer, qty: int) -> void {
craft_ensure()
let rid = craft_id(recipe_name)
@Namespace(Craft) function craft_ingredient(rpg_st: mut RpgState, recipe_name: pointer, item: pointer, qty: int) -> void {
craft_ensure(rpg_st)
let rid = craft_id(rpg_st, recipe_name)
if rid < 0 { return }
if ing_n >= CRAFT_ING_MAX { return }
ing_rid[ing_n] = rid; ing_name[ing_n] = item; ing_qty[ing_n] = qty
ing_n += 1
if rpg_st.ing_n >= CRAFT_ING_MAX { return }
rpg_st.ing_rid[rpg_st.ing_n] = rid; rpg_st.ing_name[rpg_st.ing_n] = item; rpg_st.ing_qty[rpg_st.ing_n] = qty
rpg_st.ing_n += 1
}
# Craft.can(e, recipe_name) — does the owner hold every ingredient?
@Namespace(Craft) function craft_can(e: int, recipe_name: pointer) -> int {
craft_ensure()
let rid = craft_id(recipe_name)
@Namespace(Craft) function craft_can(rpg_st: mut RpgState, e: int, recipe_name: pointer) -> int {
craft_ensure(rpg_st)
let rid = craft_id(rpg_st, recipe_name)
if rid < 0 { return 0 }
var i = 0
while i < ing_n {
if ing_rid[i] == rid {
if Inventory.count(e, ing_name[i]) < ing_qty[i] { return 0 }
while i < rpg_st.ing_n {
if rpg_st.ing_rid[i] == rid {
if Inventory.count(e, rpg_st.ing_name[i]) < rpg_st.ing_qty[i] { return 0 }
}
i += 1
}
@ -66,18 +58,18 @@ event CraftCompleted { e: int = 0, recipe: int = 0 }
}
# Craft.make(e, recipe_name) — consume ingredients + produce output; 1 on success.
@Namespace(Craft) function craft_make(e: int, recipe_name: pointer) -> int {
craft_ensure()
let rid = craft_id(recipe_name)
@Namespace(Craft) function craft_make(rpg_st: mut RpgState, e: int, recipe_name: pointer) -> int {
craft_ensure(rpg_st)
let rid = craft_id(rpg_st, recipe_name)
if rid < 0 { return 0 }
if craft_can(e, recipe_name) == 0 { return 0 }
if craft_can(rpg_st, e, recipe_name) == 0 { return 0 }
if emit CraftRequested(e: e, recipe: rid) != 0 { return 0 }
var i = 0
while i < ing_n {
if ing_rid[i] == rid { Inventory.remove(e, ing_name[i], ing_qty[i]) }
while i < rpg_st.ing_n {
if rpg_st.ing_rid[i] == rid { Inventory.remove(e, rpg_st.ing_name[i], rpg_st.ing_qty[i]) }
i += 1
}
Inventory.add(e, recipe_out[rid], recipe_outqty[rid])
Inventory.add(e, rpg_st.recipe_out[rid], rpg_st.recipe_outqty[rid])
emit CraftCompleted(e: e, recipe: rid)
return 1
}

View file

@ -8,23 +8,13 @@ const DLG_TREE_MAX: int = 64
const DLG_NODE_MAX: int = 512
const DLG_CHOICE_MAX: int = 1024
var dlg_reg: Dict = null # tree name -> tree id
var dlg_tree_n: int = 0
var dlg_tree_start: words = null # first node id per tree
var dlg_node_n: int = 0
var dlg_node_tree: words = null
var dlg_node_text: pointers = null
var dlg_choice_n: int = 0
var dlg_choice_node: words = null # owning node
var dlg_choice_label: pointers = null
var dlg_choice_next: words = null # next node id, or -1 to end
function dlg_ensure() -> void {
if dlg_reg == null {
dlg_reg = Dict.new()
dlg_tree_start = words(DLG_TREE_MAX)
dlg_node_tree = words(DLG_NODE_MAX); dlg_node_text = pointers(DLG_NODE_MAX)
dlg_choice_node = words(DLG_CHOICE_MAX); dlg_choice_label = pointers(DLG_CHOICE_MAX); dlg_choice_next = words(DLG_CHOICE_MAX)
function dlg_ensure(rpg_st: mut RpgState) -> void {
if rpg_st.dlg_reg == null {
rpg_st.dlg_reg = Dict.new()
rpg_st.dlg_tree_start = words(DLG_TREE_MAX)
rpg_st.dlg_node_tree = words(DLG_NODE_MAX); rpg_st.dlg_node_text = pointers(DLG_NODE_MAX)
rpg_st.dlg_choice_node = words(DLG_CHOICE_MAX); rpg_st.dlg_choice_label = pointers(DLG_CHOICE_MAX); rpg_st.dlg_choice_next = words(DLG_CHOICE_MAX)
}
}
@ -36,37 +26,37 @@ event cancellable DialogChoice { e: int = 0, node: int = 0, choice: int = 0 }
event DialogEnded { e: int = 0 }
# Dialog.tree(name) — register (or fetch) a tree id.
@Namespace(Dialog) function dialog_tree(name: pointer) -> int {
dlg_ensure()
let ex = Dict.get_or(dlg_reg, name, -1)
@Namespace(Dialog) function dialog_tree(rpg_st: mut RpgState, name: pointer) -> int {
dlg_ensure(rpg_st)
let ex = Dict.get_or(rpg_st.dlg_reg, name, -1)
if ex >= 0 { return ex }
if dlg_tree_n >= DLG_TREE_MAX { return -1 }
let id = dlg_tree_n
dlg_tree_start[id] = -1
dlg_tree_n += 1
Dict.set(dlg_reg, name, id)
if rpg_st.dlg_tree_n >= DLG_TREE_MAX { return -1 }
let id = rpg_st.dlg_tree_n
rpg_st.dlg_tree_start[id] = -1
rpg_st.dlg_tree_n += 1
Dict.set(rpg_st.dlg_reg, name, id)
return id
}
# Dialog.node(tree_name, text) — add a line node; the first node added to a tree is
# its entry point. Returns the node id.
@Namespace(Dialog) function dialog_node(tree_name: pointer, text: pointer) -> int {
dlg_ensure()
let tid = dialog_tree(tree_name)
@Namespace(Dialog) function dialog_node(rpg_st: mut RpgState, tree_name: pointer, text: pointer) -> int {
dlg_ensure(rpg_st)
let tid = dialog_tree(rpg_st, tree_name)
if tid < 0 { return -1 }
if dlg_node_n >= DLG_NODE_MAX { return -1 }
let id = dlg_node_n
dlg_node_tree[id] = tid; dlg_node_text[id] = text
dlg_node_n += 1
if dlg_tree_start[tid] < 0 { dlg_tree_start[tid] = id }
if rpg_st.dlg_node_n >= DLG_NODE_MAX { return -1 }
let id = rpg_st.dlg_node_n
rpg_st.dlg_node_tree[id] = tid; rpg_st.dlg_node_text[id] = text
rpg_st.dlg_node_n += 1
if rpg_st.dlg_tree_start[tid] < 0 { rpg_st.dlg_tree_start[tid] = id }
return id
}
# Dialog.choice(node, label, next_node) — add a branch (next_node -1 ends).
@Namespace(Dialog) function dialog_choice(node: int, label: pointer, next_node: int) -> int {
dlg_ensure()
if dlg_choice_n >= DLG_CHOICE_MAX { return -1 }
let id = dlg_choice_n
dlg_choice_node[id] = node; dlg_choice_label[id] = label; dlg_choice_next[id] = next_node
dlg_choice_n += 1
@Namespace(Dialog) function dialog_choice(rpg_st: mut RpgState, node: int, label: pointer, next_node: int) -> int {
dlg_ensure(rpg_st)
if rpg_st.dlg_choice_n >= DLG_CHOICE_MAX { return -1 }
let id = rpg_st.dlg_choice_n
rpg_st.dlg_choice_node[id] = node; rpg_st.dlg_choice_label[id] = label; rpg_st.dlg_choice_next[id] = next_node
rpg_st.dlg_choice_n += 1
return id
}
@ -80,45 +70,45 @@ function dlg_set(e: int, name: pointer, v: int) -> void {
}
# Dialog.start(e, tree_name) — open the tree on a speaker (DialogStarted + Line).
@Namespace(Dialog) function dialog_start(e: int, tree_name: pointer) -> void {
dlg_ensure()
let tid = dialog_tree(tree_name)
@Namespace(Dialog) function dialog_start(rpg_st: mut RpgState, e: int, tree_name: pointer) -> void {
dlg_ensure(rpg_st)
let tid = dialog_tree(rpg_st, tree_name)
if tid < 0 { return }
dlg_set(e, "tree", tid)
dlg_set(e, "node", dlg_tree_start[tid])
dlg_set(e, "node", rpg_st.dlg_tree_start[tid])
dlg_set(e, "active", 1)
emit DialogStarted(e: e, tree: tid)
emit DialogLine(e: e, node: dlg_tree_start[tid])
emit DialogLine(e: e, node: rpg_st.dlg_tree_start[tid])
}
# Dialog.line(e) — text of the current node (empty when inactive).
@Namespace(Dialog) function dialog_line(e: int) -> pointer {
@Namespace(Dialog) function dialog_line(rpg_st: RpgState, e: int) -> pointer {
if dlg_get(e, "active") == 0 { return "" }
return dlg_node_text[dlg_get(e, "node")]
return rpg_st.dlg_node_text[dlg_get(e, "node")]
}
# Dialog.count(e) — number of choices at the current node.
@Namespace(Dialog) function dialog_count(e: int) -> int {
dlg_ensure()
@Namespace(Dialog) function dialog_count(rpg_st: mut RpgState, e: int) -> int {
dlg_ensure(rpg_st)
if dlg_get(e, "active") == 0 { return 0 }
let node = dlg_get(e, "node")
var n = 0; var i = 0
while i < dlg_choice_n { if dlg_choice_node[i] == node { n += 1 }; i += 1 }
while i < rpg_st.dlg_choice_n { if rpg_st.dlg_choice_node[i] == node { n += 1 }; i += 1 }
return n
}
# Dialog.choose(e, i) — take the i-th choice (veto-able); advance or end.
@Namespace(Dialog) function dialog_choose(e: int, idx: int) -> int {
dlg_ensure()
@Namespace(Dialog) function dialog_choose(rpg_st: mut RpgState, e: int, idx: int) -> int {
dlg_ensure(rpg_st)
if dlg_get(e, "active") == 0 { return 0 }
let node = dlg_get(e, "node")
var seen = 0; var i = 0; var pick = -1
while i < dlg_choice_n {
if dlg_choice_node[i] == node {
if seen == idx { pick = i; i = dlg_choice_n } else { seen += 1 }
while i < rpg_st.dlg_choice_n {
if rpg_st.dlg_choice_node[i] == node {
if seen == idx { pick = i; i = rpg_st.dlg_choice_n } else { seen += 1 }
}
i += 1
}
if pick < 0 { return 0 }
if emit DialogChoice(e: e, node: node, choice: idx) != 0 { return 0 }
let nxt = dlg_choice_next[pick]
let nxt = rpg_st.dlg_choice_next[pick]
if nxt < 0 {
dlg_set(e, "active", 0)
emit DialogEnded(e: e)

View file

@ -10,40 +10,70 @@ import "ludic.gameplay/stats.ludic"
const INV_MAX: int = 256
# ---- item registry --------------------------------------------------------
var item_reg: Dict = null
var item_n: int = 0
var item_stackmax: words = null
var item_weight: words = null
var item_value: words = null
state RpgState {
recipe_reg: Dict = null # recipe name -> id
recipe_n: int = 0
recipe_out: pointers = null # output item name
recipe_outqty: words = null
ing_n: int = 0
ing_rid: words = null # owning recipe id
ing_name: pointers = null # ingredient item name
ing_qty: words = null
dlg_reg: Dict = null # tree name -> tree id
dlg_tree_n: int = 0
dlg_tree_start: words = null # first node id per tree
dlg_node_n: int = 0
dlg_node_tree: words = null
dlg_node_text: pointers = null
dlg_choice_n: int = 0
dlg_choice_node: words = null # owning node
dlg_choice_label: pointers = null
dlg_choice_next: words = null # next node id, or -1 to end
item_reg: Dict = null
item_n: int = 0
item_stackmax: words = null
item_weight: words = null
item_value: words = null
inv_counts: Dict = null
quest_reg: Dict = null # quest name -> id
quest_n: int = 0
quest_state: words = null
obj_n: int = 0
obj_qid: words = null # owning quest id
obj_kind: words = null
obj_target: words = null # a target id/tag (kind-specific; 0 = any)
obj_need: words = null
obj_have: words = null
quest_flags: Dict = null # global flag store
}
function item_ensure() -> void {
if item_reg == null {
item_reg = Dict.new()
item_stackmax = words(INV_MAX); item_weight = words(INV_MAX); item_value = words(INV_MAX)
function item_ensure(rpg_st: mut RpgState) -> void {
if rpg_st.item_reg == null {
rpg_st.item_reg = Dict.new()
rpg_st.item_stackmax = words(INV_MAX); rpg_st.item_weight = words(INV_MAX); rpg_st.item_value = words(INV_MAX)
}
}
# Item.def(name, stack_max, weight, value) — register an item template. Returns id.
@Namespace(Item) function item_def(name: pointer, stack_max: int, weight: int, value: int) -> int {
item_ensure()
if item_n >= INV_MAX { return -1 }
let id = item_n
item_stackmax[id] = stack_max; item_weight[id] = weight; item_value[id] = value
item_n += 1
Dict.set(item_reg, name, id)
@Namespace(Item) function item_def(rpg_st: mut RpgState, name: pointer, stack_max: int, weight: int, value: int) -> int {
item_ensure(rpg_st)
if rpg_st.item_n >= INV_MAX { return -1 }
let id = rpg_st.item_n
rpg_st.item_stackmax[id] = stack_max; rpg_st.item_weight[id] = weight; rpg_st.item_value[id] = value
rpg_st.item_n += 1
Dict.set(rpg_st.item_reg, name, id)
return id
}
@Namespace(Item) function item_id(name: pointer) -> int { item_ensure(); return Dict.get_or(item_reg, name, -1) }
@Namespace(Item) function item_value_of(name: pointer) -> int {
let id = item_id(name); if id < 0 { return 0 }
return item_value[id]
@Namespace(Item) function item_id(rpg_st: mut RpgState, name: pointer) -> int { item_ensure(rpg_st); return Dict.get_or(rpg_st.item_reg, name, -1) }
@Namespace(Item) function item_value_of(rpg_st: mut RpgState, name: pointer) -> int {
let id = item_id(rpg_st, name); if id < 0 { return 0 }
return rpg_st.item_value[id]
}
# ---- per-owner counts -----------------------------------------------------
property Inventory { gold: int = 0, weight_max: int = 0, slots: int = 0 }
var inv_counts: Dict = null
function inv_ensure() -> void { if inv_counts == null { inv_counts = Dict.new() } }
function inv_ensure(rpg_st: mut RpgState) -> void { if rpg_st.inv_counts == null { rpg_st.inv_counts = Dict.new() } }
function inv_key(e: int, name: pointer) -> pointer { return `{e}/{name}` }
event ItemAdded { e: int = 0, item: int = 0, qty: int = 0 } # item = registry id
@ -51,36 +81,36 @@ event ItemRemoved { e: int = 0, item: int = 0, qty: int = 0 }
event cancellable ItemUse { e: int = 0, item: int = 0 } # veto a use (e.g. cursed / requirement)
# Inventory.add(e, name, n) — add n of an item; emits ItemAdded.
@Namespace(Inventory) function inventory_add(e: int, name: pointer, n: int) -> void {
inv_ensure()
@Namespace(Inventory) function inventory_add(rpg_st: mut RpgState, e: int, name: pointer, n: int) -> void {
inv_ensure(rpg_st)
let k = inv_key(e, name)
Dict.set(inv_counts, k, Dict.get_or(inv_counts, k, 0) + n)
emit ItemAdded(e: e, item: item_id(name), qty: n)
Dict.set(rpg_st.inv_counts, k, Dict.get_or(rpg_st.inv_counts, k, 0) + n)
emit ItemAdded(e: e, item: item_id(rpg_st, name), qty: n)
}
# Inventory.count(e, name) — how many the owner holds.
@Namespace(Inventory) function inventory_count(e: int, name: pointer) -> int {
inv_ensure()
return Dict.get_or(inv_counts, inv_key(e, name), 0)
@Namespace(Inventory) function inventory_count(rpg_st: mut RpgState, e: int, name: pointer) -> int {
inv_ensure(rpg_st)
return Dict.get_or(rpg_st.inv_counts, inv_key(e, name), 0)
}
@Namespace(Inventory) function inventory_has(e: int, name: pointer) -> int {
if inventory_count(e, name) > 0 { return 1 }
@Namespace(Inventory) function inventory_has(rpg_st: mut RpgState, e: int, name: pointer) -> int {
if inventory_count(rpg_st, e, name) > 0 { return 1 }
return 0
}
# Inventory.remove(e, name, n) — remove up to n; returns 1 if it had enough.
@Namespace(Inventory) function inventory_remove(e: int, name: pointer, n: int) -> int {
inv_ensure()
@Namespace(Inventory) function inventory_remove(rpg_st: mut RpgState, e: int, name: pointer, n: int) -> int {
inv_ensure(rpg_st)
let k = inv_key(e, name)
let have = Dict.get_or(inv_counts, k, 0)
let have = Dict.get_or(rpg_st.inv_counts, k, 0)
if have < n { return 0 }
Dict.set(inv_counts, k, have - n)
emit ItemRemoved(e: e, item: item_id(name), qty: n)
Dict.set(rpg_st.inv_counts, k, have - n)
emit ItemRemoved(e: e, item: item_id(rpg_st, name), qty: n)
return 1
}
# Inventory.use(e, name) — consume one item unless a listener vetoes ItemUse.
@Namespace(Inventory) function inventory_use(e: int, name: pointer) -> int {
if inventory_count(e, name) <= 0 { return 0 }
if emit ItemUse(e: e, item: item_id(name)) != 0 { return 0 }
return inventory_remove(e, name, 1)
@Namespace(Inventory) function inventory_use(rpg_st: mut RpgState, e: int, name: pointer) -> int {
if inventory_count(rpg_st, e, name) <= 0 { return 0 }
if emit ItemUse(e: e, item: item_id(rpg_st, name)) != 0 { return 0 }
return inventory_remove(rpg_st, e, name, 1)
}
# Inventory.gold / add_gold / spend_gold
@Namespace(Inventory) function inventory_gold(e: int) -> int {

View file

@ -14,23 +14,13 @@
const QUEST_MAX: int = 128
const OBJ_MAX: int = 512
var quest_reg: Dict = null # quest name -> id
var quest_n: int = 0
var quest_state: words = null
var obj_n: int = 0
var obj_qid: words = null # owning quest id
var obj_kind: words = null
var obj_target: words = null # a target id/tag (kind-specific; 0 = any)
var obj_need: words = null
var obj_have: words = null
var quest_flags: Dict = null # global flag store
function quest_ensure() -> void {
if quest_reg == null {
quest_reg = Dict.new(); quest_flags = Dict.new()
quest_state = words(QUEST_MAX)
obj_qid = words(OBJ_MAX); obj_kind = words(OBJ_MAX); obj_target = words(OBJ_MAX)
obj_need = words(OBJ_MAX); obj_have = words(OBJ_MAX)
function quest_ensure(rpg_st: mut RpgState) -> void {
if rpg_st.quest_reg == null {
rpg_st.quest_reg = Dict.new(); rpg_st.quest_flags = Dict.new()
rpg_st.quest_state = words(QUEST_MAX)
rpg_st.obj_qid = words(OBJ_MAX); rpg_st.obj_kind = words(OBJ_MAX); rpg_st.obj_target = words(OBJ_MAX)
rpg_st.obj_need = words(OBJ_MAX); rpg_st.obj_have = words(OBJ_MAX)
}
}
@ -39,48 +29,48 @@ event QuestCompleted { quest: int = 0 }
event ObjectiveProgressed { quest: int = 0, objective: int = 0, have: int = 0, need: int = 0 }
# Quest.define(name) — register a quest (state hidden). Returns id.
@Namespace(Quest) function quest_define(name: pointer) -> int {
quest_ensure()
if quest_n >= QUEST_MAX { return -1 }
let id = quest_n
quest_state[id] = 0
quest_n += 1
Dict.set(quest_reg, name, id)
@Namespace(Quest) function quest_define(rpg_st: mut RpgState, name: pointer) -> int {
quest_ensure(rpg_st)
if rpg_st.quest_n >= QUEST_MAX { return -1 }
let id = rpg_st.quest_n
rpg_st.quest_state[id] = 0
rpg_st.quest_n += 1
Dict.set(rpg_st.quest_reg, name, id)
return id
}
@Namespace(Quest) function quest_id(name: pointer) -> int { quest_ensure(); return Dict.get_or(quest_reg, name, -1) }
@Namespace(Quest) function quest_id(rpg_st: mut RpgState, name: pointer) -> int { quest_ensure(rpg_st); return Dict.get_or(rpg_st.quest_reg, name, -1) }
# Quest.objective(quest_name, kind, target, need) — add an objective.
@Namespace(Quest) function quest_objective(quest_name: pointer, kind: int, target: int, need: int) -> int {
quest_ensure()
let qid = quest_id(quest_name)
@Namespace(Quest) function quest_objective(rpg_st: mut RpgState, quest_name: pointer, kind: int, target: int, need: int) -> int {
quest_ensure(rpg_st)
let qid = quest_id(rpg_st, quest_name)
if qid < 0 { return -1 }
if obj_n >= OBJ_MAX { return -1 }
let id = obj_n
obj_qid[id] = qid; obj_kind[id] = kind; obj_target[id] = target
obj_need[id] = need; obj_have[id] = 0
obj_n += 1
if rpg_st.obj_n >= OBJ_MAX { return -1 }
let id = rpg_st.obj_n
rpg_st.obj_qid[id] = qid; rpg_st.obj_kind[id] = kind; rpg_st.obj_target[id] = target
rpg_st.obj_need[id] = need; rpg_st.obj_have[id] = 0
rpg_st.obj_n += 1
return id
}
# Quest.start(name) — activate a quest (emits QuestStarted).
@Namespace(Quest) function quest_start(name: pointer) -> void {
quest_ensure()
let qid = quest_id(name)
@Namespace(Quest) function quest_start(rpg_st: mut RpgState, name: pointer) -> void {
quest_ensure(rpg_st)
let qid = quest_id(rpg_st, name)
if qid < 0 { return }
quest_state[qid] = 2
rpg_st.quest_state[qid] = 2
emit QuestStarted(quest: qid)
}
@Namespace(Quest) function quest_state_of(name: pointer) -> int {
quest_ensure()
let qid = quest_id(name)
@Namespace(Quest) function quest_state_of(rpg_st: mut RpgState, name: pointer) -> int {
quest_ensure(rpg_st)
let qid = quest_id(rpg_st, name)
if qid < 0 { return -1 }
return quest_state[qid]
return rpg_st.quest_state[qid]
}
# is every objective of a quest satisfied?
function quest_all_done(qid: int) -> int {
function quest_all_done(rpg_st: RpgState, qid: int) -> int {
var i = 0
while i < obj_n {
if obj_qid[i] == qid { if obj_have[i] < obj_need[i] { return 0 } }
while i < rpg_st.obj_n {
if rpg_st.obj_qid[i] == qid { if rpg_st.obj_have[i] < rpg_st.obj_need[i] { return 0 } }
i += 1
}
return 1
@ -89,20 +79,20 @@ function quest_all_done(qid: int) -> int {
# Quest.notify(kind, target, amount) — report a gameplay signal. Every ACTIVE
# quest's matching objectives tick up (target 0 on the objective = "any"); a quest
# whose objectives are all met auto-completes.
@Namespace(Quest) function quest_notify(kind: int, target: int, amount: int) -> void {
quest_ensure()
@Namespace(Quest) function quest_notify(rpg_st: mut RpgState, kind: int, target: int, amount: int) -> void {
quest_ensure(rpg_st)
var i = 0
while i < obj_n {
let qid = obj_qid[i]
if quest_state[qid] == 2 {
if obj_kind[i] == kind {
if (obj_target[i] == 0) or (obj_target[i] == target) {
if obj_have[i] < obj_need[i] {
obj_have[i] += amount
if obj_have[i] > obj_need[i] { obj_have[i] = obj_need[i] }
emit ObjectiveProgressed(quest: qid, objective: i, have: obj_have[i], need: obj_need[i])
if quest_all_done(qid) == 1 {
quest_state[qid] = 3
while i < rpg_st.obj_n {
let qid = rpg_st.obj_qid[i]
if rpg_st.quest_state[qid] == 2 {
if rpg_st.obj_kind[i] == kind {
if (rpg_st.obj_target[i] == 0) or (rpg_st.obj_target[i] == target) {
if rpg_st.obj_have[i] < rpg_st.obj_need[i] {
rpg_st.obj_have[i] += amount
if rpg_st.obj_have[i] > rpg_st.obj_need[i] { rpg_st.obj_have[i] = rpg_st.obj_need[i] }
emit ObjectiveProgressed(quest: qid, objective: i, have: rpg_st.obj_have[i], need: rpg_st.obj_need[i])
if quest_all_done(rpg_st, qid) == 1 {
rpg_st.quest_state[qid] = 3
emit QuestCompleted(quest: qid)
}
}
@ -114,5 +104,5 @@ function quest_all_done(qid: int) -> int {
}
# ---- global flag store (branching / world state) --------------------------
@Namespace(Quest) function quest_flag(name: pointer, value: int) -> void { quest_ensure(); Dict.set(quest_flags, name, value) }
@Namespace(Quest) function quest_flag_get(name: pointer) -> int { quest_ensure(); return Dict.get_or(quest_flags, name, 0) }
@Namespace(Quest) function quest_flag(rpg_st: mut RpgState, name: pointer, value: int) -> void { quest_ensure(rpg_st); Dict.set(rpg_st.quest_flags, name, value) }
@Namespace(Quest) function quest_flag_get(rpg_st: mut RpgState, name: pointer) -> int { quest_ensure(rpg_st); return Dict.get_or(rpg_st.quest_flags, name, 0) }

View file

@ -54,7 +54,7 @@ export function ply_leave(base_st: mut BaseState, session_st: mut SessionState,
export function ply_leave_all(base_st: mut BaseState, session_st: mut SessionState) -> void { for p in 1 .. PLY_MAX { ply_leave(base_st, session_st, p) } }
# what a remote player's machine reported this tick
export function ply_report(session_st: SessionState, p: int, x: float, y: float, z: float, yaw: float, speed: float) -> void {
export function ply_report(session_st: mut SessionState, p: int, x: float, y: float, z: float, yaw: float, speed: float) -> void {
if p <= 0 or not ply_on(session_st, p) { return }
let r = session_st.session__slot[p]
r.x = x

View file

@ -69,76 +69,79 @@ event WeaponFired { owner: int = 0, def_id: int = 0 }
# ---------------------------------------------------------------------------
# Weapon registry (name-keyed, mod-friendly — add a gun with no code)
# ---------------------------------------------------------------------------
var wpn_reg: Dict = null
var wpn_n: int = 0
var wpn_rate: words = null
var wpn_dmg: words = null
var wpn_speed: words = null
var wpn_spread: words = null
var wpn_pellets: words = null
var wpn_pattern: words = null
var wpn_ttl: words = null
var wpn_pierce: words = null
var wpn_homing: words = null
var wpn_color: words = null
state ShooterState {
wpn_reg: Dict = null
wpn_n: int = 0
wpn_rate: words = null
wpn_dmg: words = null
wpn_speed: words = null
wpn_spread: words = null
wpn_pellets: words = null
wpn_pattern: words = null
wpn_ttl: words = null
wpn_pierce: words = null
wpn_homing: words = null
wpn_color: words = null
wpn_rate_base: words = null
wpn_fire_color: int = 0xffffff # the colour of the weapon being fired (sh_fire)
}
function wpn_ensure() -> void {
if wpn_reg == null {
wpn_reg = Dict.new()
wpn_rate = words(WPN_MAX); wpn_dmg = words(WPN_MAX); wpn_speed = words(WPN_MAX)
wpn_spread = words(WPN_MAX); wpn_pellets = words(WPN_MAX); wpn_pattern = words(WPN_MAX)
wpn_ttl = words(WPN_MAX); wpn_pierce = words(WPN_MAX); wpn_homing = words(WPN_MAX)
wpn_color = words(WPN_MAX); wpn_rate_base = words(WPN_MAX)
function wpn_ensure(shooter_st: mut ShooterState) -> void {
if shooter_st.wpn_reg == null {
shooter_st.wpn_reg = Dict.new()
shooter_st.wpn_rate = words(WPN_MAX); shooter_st.wpn_dmg = words(WPN_MAX); shooter_st.wpn_speed = words(WPN_MAX)
shooter_st.wpn_spread = words(WPN_MAX); shooter_st.wpn_pellets = words(WPN_MAX); shooter_st.wpn_pattern = words(WPN_MAX)
shooter_st.wpn_ttl = words(WPN_MAX); shooter_st.wpn_pierce = words(WPN_MAX); shooter_st.wpn_homing = words(WPN_MAX)
shooter_st.wpn_color = words(WPN_MAX); shooter_st.wpn_rate_base = words(WPN_MAX)
}
}
# Weapon.def(name, fire_rate, damage, speed, spread, pellets, pattern) — register a
# gun. pattern: 0 single/aimed, 1 spread cone, 2 ring, 3 spiral. Returns the def id.
@Namespace(Weapon) function weapon_def(name: pointer, fire_rate: int, damage: int, speed: int, spread: int, pellets: int, pattern: int) -> int {
wpn_ensure()
if wpn_n >= WPN_MAX { return -1 }
let id = wpn_n
wpn_rate[id] = fire_rate; wpn_dmg[id] = damage; wpn_speed[id] = speed
wpn_spread[id] = spread; wpn_pellets[id] = pellets; wpn_pattern[id] = pattern
wpn_ttl[id] = 120
wpn_color[id] = 0xffffff
wpn_rate_base[id] = fire_rate
wpn_n += 1
Dict.set(wpn_reg, name, id)
@Namespace(Weapon) function weapon_def(shooter_st: mut ShooterState, name: pointer, fire_rate: int, damage: int, speed: int, spread: int, pellets: int, pattern: int) -> int {
wpn_ensure(shooter_st)
if shooter_st.wpn_n >= WPN_MAX { return -1 }
let id = shooter_st.wpn_n
shooter_st.wpn_rate[id] = fire_rate; shooter_st.wpn_dmg[id] = damage; shooter_st.wpn_speed[id] = speed
shooter_st.wpn_spread[id] = spread; shooter_st.wpn_pellets[id] = pellets; shooter_st.wpn_pattern[id] = pattern
shooter_st.wpn_ttl[id] = 120
shooter_st.wpn_color[id] = 0xffffff
shooter_st.wpn_rate_base[id] = fire_rate
shooter_st.wpn_n += 1
Dict.set(shooter_st.wpn_reg, name, id)
return id
}
@Namespace(Weapon) function weapon_id(name: pointer) -> int {
wpn_ensure()
return Dict.get_or(wpn_reg, name, -1)
@Namespace(Weapon) function weapon_id(shooter_st: mut ShooterState, name: pointer) -> int {
wpn_ensure(shooter_st)
return Dict.get_or(shooter_st.wpn_reg, name, -1)
}
# Weapon.set_rate(id, frames) — frames between shots (a fire-rate upgrade changes it in place).
@Namespace(Weapon) function weapon_set_rate(id: int, frames: int) -> void {
wpn_ensure(); if (id >= 0) and (id < WPN_MAX) { wpn_rate[id] = frames }
@Namespace(Weapon) function weapon_set_rate(shooter_st: mut ShooterState, id: int, frames: int) -> void {
wpn_ensure(shooter_st); if (id >= 0) and (id < WPN_MAX) { shooter_st.wpn_rate[id] = frames }
}
@Namespace(Weapon) function weapon_rate(id: int) -> int {
wpn_ensure(); if (id >= 0) and (id < WPN_MAX) { return wpn_rate[id] }; return 0
@Namespace(Weapon) function weapon_rate(shooter_st: mut ShooterState, id: int) -> int {
wpn_ensure(shooter_st); if (id >= 0) and (id < WPN_MAX) { return shooter_st.wpn_rate[id] }; return 0
}
# Weapon.reset(id) — back to the definition: no pierce, no homing, the defined fire rate.
var wpn_rate_base: words = null
@Namespace(Weapon) function weapon_reset(id: int) -> void {
wpn_ensure(); if (id < 0) or (id >= WPN_MAX) { return }
wpn_pierce[id] = 0; wpn_homing[id] = 0; wpn_rate[id] = wpn_rate_base[id]
@Namespace(Weapon) function weapon_reset(shooter_st: mut ShooterState, id: int) -> void {
wpn_ensure(shooter_st); if (id < 0) or (id >= WPN_MAX) { return }
shooter_st.wpn_pierce[id] = 0; shooter_st.wpn_homing[id] = 0; shooter_st.wpn_rate[id] = shooter_st.wpn_rate_base[id]
}
# Weapon.set_color(id, color) — the colour the engine draws this weapon's shots in.
@Namespace(Weapon) function weapon_set_color(id: int, color: int) -> void {
wpn_ensure(); if (id >= 0) and (id < WPN_MAX) { wpn_color[id] = color }
@Namespace(Weapon) function weapon_set_color(shooter_st: mut ShooterState, id: int, color: int) -> void {
wpn_ensure(shooter_st); if (id >= 0) and (id < WPN_MAX) { shooter_st.wpn_color[id] = color }
}
# Weapon.set_ttl(id, frames) — projectile lifetime for a weapon (range control).
@Namespace(Weapon) function weapon_set_ttl(id: int, frames: int) -> void {
wpn_ensure(); if (id >= 0) and (id < WPN_MAX) { wpn_ttl[id] = frames }
@Namespace(Weapon) function weapon_set_ttl(shooter_st: mut ShooterState, id: int, frames: int) -> void {
wpn_ensure(shooter_st); if (id >= 0) and (id < WPN_MAX) { shooter_st.wpn_ttl[id] = frames }
}
# Weapon.set_pierce(id, n) — a shot passes through n targets before it dies.
@Namespace(Weapon) function weapon_set_pierce(id: int, n: int) -> void {
wpn_ensure(); if (id >= 0) and (id < WPN_MAX) { wpn_pierce[id] = n }
@Namespace(Weapon) function weapon_set_pierce(shooter_st: mut ShooterState, id: int, n: int) -> void {
wpn_ensure(shooter_st); if (id >= 0) and (id < WPN_MAX) { shooter_st.wpn_pierce[id] = n }
}
# Weapon.set_homing(id, deg) — max degrees/frame a shot turns toward an enemy.
@Namespace(Weapon) function weapon_set_homing(id: int, deg: int) -> void {
wpn_ensure(); if (id >= 0) and (id < WPN_MAX) { wpn_homing[id] = deg }
@Namespace(Weapon) function weapon_set_homing(shooter_st: mut ShooterState, id: int, deg: int) -> void {
wpn_ensure(shooter_st); if (id >= 0) and (id < WPN_MAX) { shooter_st.wpn_homing[id] = deg }
}
# ---------------------------------------------------------------------------
@ -493,7 +496,7 @@ function sh_angle_diff(from: int, to: int) -> int {
}
# spawn one projectile travelling at `deg` degrees from (px,py).
function sh_spawn_bullet(owner: int, fac: int, px: int, py: int, deg: int, speed: int, dmg: int, ttl: int, pierce: int, homing: int) -> int {
function sh_spawn_bullet(shooter_st: ShooterState, owner: int, fac: int, px: int, py: int, deg: int, speed: int, dmg: int, ttl: int, pierce: int, homing: int) -> int {
let m = World.model_id("Bullet")
if m < 0 { return -1 }
let b = World.spawn(m)
@ -518,21 +521,20 @@ function sh_spawn_bullet(owner: int, fac: int, px: int, py: int, deg: int, speed
World.set(b, PC, World.field_id(PC, "h"), 4)
}
# announce it: a listener may veto (cancel -> despawn) or tweak the Projectile.
pr_set(b, "color", wpn_fire_color)
pr_set(b, "color", shooter_st.wpn_fire_color)
if emit ProjectileSpawned(proj: b, owner: owner, damage: dmg) != 0 { despawn b; return -1 }
return b
}
var wpn_fire_color: int = 0xffffff # the colour of the weapon being fired (sh_fire)
# ===========================================================================
# esys_weapon (Update) — fire-rate gating + pattern emission. Reads the owner's
# TopDown.aim_angle + Weapon.want-fire-equivalent (TopDown.want_fire); one impl
# serves player and NPC.
# ===========================================================================
@EngineSystem(Weapon, Update) function esys_weapon() -> void {
@EngineSystem(Weapon, Update) function esys_weapon(shooter_st: mut ShooterState) -> void {
let P = World.prop_id("Weapon")
if P < 0 { return }
wpn_ensure()
wpn_ensure(shooter_st)
var e = World.query_next(P, 0)
while e >= 0 {
let cd = World.get(e, P, World.field_id(P, "cooldown"))
@ -540,34 +542,34 @@ var wpn_fire_color: int = 0xffffff # the colour of the weapon being
let fire = td_get(e, "want_fire")
if (fire == 1) and (cd <= 0) {
let id = World.get(e, P, World.field_id(P, "def_id"))
if (id >= 0) and (id < wpn_n) {
sh_fire(e, id)
World.set(e, P, World.field_id(P, "cooldown"), wpn_rate[id] * stats_field(e, "fire_rate_pct", 100) / 100)
if (id >= 0) and (id < shooter_st.wpn_n) {
sh_fire(shooter_st, e, id)
World.set(e, P, World.field_id(P, "cooldown"), shooter_st.wpn_rate[id] * stats_field(e, "fire_rate_pct", 100) / 100)
}
}
e = World.query_next(P, e + 1)
}
}
function sh_fire(owner: int, id: int) -> void {
function sh_fire(shooter_st: mut ShooterState, owner: int, id: int) -> void {
let px = sh_cx(owner)
let py = sh_cy(owner)
let base = td_get(owner, "aim_angle")
let fac = Faction.id_of(owner)
let speed = wpn_speed[id]
let dmg = wpn_dmg[id]
let spread = wpn_spread[id]
let pellets0 = wpn_pellets[id]
let pattern = wpn_pattern[id]
let ttl = wpn_ttl[id]
let pierce = wpn_pierce[id]
let homing = wpn_homing[id]
wpn_fire_color = wpn_color[id]
let speed = shooter_st.wpn_speed[id]
let dmg = shooter_st.wpn_dmg[id]
let spread = shooter_st.wpn_spread[id]
let pellets0 = shooter_st.wpn_pellets[id]
let pattern = shooter_st.wpn_pattern[id]
let ttl = shooter_st.wpn_ttl[id]
let pierce = shooter_st.wpn_pierce[id]
let homing = shooter_st.wpn_homing[id]
shooter_st.wpn_fire_color = shooter_st.wpn_color[id]
var pellets = pellets0; if pellets < 1 { pellets = 1 }
emit WeaponFired(owner: owner, def_id: id)
if pattern == WeaponPattern.Ring { # pellets around 360
var i = 0
while i < pellets { sh_spawn_bullet(owner, fac, px, py, sh_wrap_deg(base + i * 360 / pellets), speed, dmg, ttl, pierce, homing); i += 1 }
while i < pellets { sh_spawn_bullet(shooter_st, owner, fac, px, py, sh_wrap_deg(base + i * 360 / pellets), speed, dmg, ttl, pierce, homing); i += 1 }
return
}
if pattern == WeaponPattern.Spiral { # ring + a rotating phase
@ -575,18 +577,18 @@ function sh_fire(owner: int, id: int) -> void {
let fph = World.field_id(PW, "phase")
var ph = 0; if fph >= 0 { ph = World.get(owner, PW, fph) }
var i = 0
while i < pellets { sh_spawn_bullet(owner, fac, px, py, sh_wrap_deg(base + ph + i * 360 / pellets), speed, dmg, ttl, pierce, homing); i += 1 }
while i < pellets { sh_spawn_bullet(shooter_st, owner, fac, px, py, sh_wrap_deg(base + ph + i * 360 / pellets), speed, dmg, ttl, pierce, homing); i += 1 }
if fph >= 0 { World.set(owner, PW, fph, sh_wrap_deg(ph + 15)) }
return
}
# single / spread cone centred on the aim
if pellets == 1 {
sh_spawn_bullet(owner, fac, px, py, base, speed, dmg, ttl, pierce, homing)
sh_spawn_bullet(shooter_st, owner, fac, px, py, base, speed, dmg, ttl, pierce, homing)
} else {
var i = 0
while i < pellets {
let off = -(spread / 2) + i * spread / (pellets - 1)
sh_spawn_bullet(owner, fac, px, py, sh_wrap_deg(base + off), speed, dmg, ttl, pierce, homing)
sh_spawn_bullet(shooter_st, owner, fac, px, py, sh_wrap_deg(base + off), speed, dmg, ttl, pierce, homing)
i += 1
}
}
@ -727,8 +729,8 @@ event WaveCleared { spawner: int = 0, wave: int = 0 }
@Namespace(TopDown) function topdown_aim(e: int, deg: int) -> void { td_set(e, "aim_angle", deg) }
@Namespace(TopDown) function topdown_fire(e: int, on: int) -> void { td_set(e, "want_fire", on) }
# equip a weapon by registry name.
@Namespace(Weapon) function weapon_equip(e: int, name: pointer) -> void {
@Namespace(Weapon) function weapon_equip(shooter_st: mut ShooterState, e: int, name: pointer) -> void {
let P = World.prop_id("Weapon")
if P < 0 { return }
World.set(e, P, World.field_id(P, "def_id"), weapon_id(name))
World.set(e, P, World.field_id(P, "def_id"), weapon_id(shooter_st, name))
}

View file

@ -51,20 +51,20 @@ function act_keyword(r: UiRd, word: string) -> bool {
}
return true
}
function act_run(a: UiAct, e: UiEnv) -> void {
function act_run(ui_st: mut UiState, a: UiAct, e: UiEnv) -> void {
for i in 0 .. len(a.kinds) {
if a.kinds[i] == A_SET { env_set(e, a.names[i], ui_eval(a.exprs[i], e)) }
if a.kinds[i] == A_CALL { ui_eval(a.exprs[i], e) }
if a.kinds[i] == A_EMIT { act_emit(a.names[i], e) }
if a.kinds[i] == A_SET { env_set(ui_st, e, a.names[i], ui_eval(ui_st, a.exprs[i], e)) }
if a.kinds[i] == A_CALL { ui_eval(ui_st, a.exprs[i], e) }
if a.kinds[i] == A_EMIT { act_emit(ui_st, a.names[i], e) }
}
}
function act_emit(name: string, e: UiEnv) -> void {
function act_emit(ui_st: mut UiState, name: string, e: UiEnv) -> void {
var at = e
while at != null {
if at.on_names != null {
for i in 0 .. len(at.on_names) {
if at.on_names[i] == name {
act_run(at.on_acts[i], at.on_envs[i])
act_run(ui_st, at.on_acts[i], at.on_envs[i])
return
}
}

View file

@ -6,17 +6,13 @@ export property UiKeyframes {
stops: []float = null
rules: []UiRule = null
}
var an_frames: []UiKeyframes = new []UiKeyframes
var an_keys: []string = new []string
var an_start: []float = new []float
var an_frame: int = 0
# the renderer's clock in seconds, or the frames shown at sixty a second (a test, a still picture)
function an_now() -> float {
if ui_be != null and ui_be.now != null { return ui_be.now() }
return float(an_frame) / 60.0
function an_now(ui_st: UiState) -> float {
if ui_st.ui_be != null and ui_st.ui_be.now != null { return ui_st.ui_be.now() }
return float(ui_st.an_frame) / 60.0
}
# @keyframes NAME { from {...} 50% {...} to {...} }
function an_read(name: string, body: string, err: []string) -> void {
function an_read(ui_st: mut UiState, name: string, body: string, err: []string) -> void {
let k = new UiKeyframes
k.name = tpl_words(name)
k.stops = new []float
@ -34,40 +30,40 @@ function an_read(name: string, body: string, err: []string) -> void {
push(k.stops, at)
push(k.rules, decls)
}
push(an_frames, k)
push(ui_st.an_frames, k)
}
function an_find(name: string) -> UiKeyframes {
for i in 0 .. len(an_frames) {
if an_frames[i].name == name { return an_frames[i] }
function an_find(ui_st: UiState, name: string) -> UiKeyframes {
for i in 0 .. len(ui_st.an_frames) {
if ui_st.an_frames[i].name == name { return ui_st.an_frames[i] }
}
return null
}
# when this element's animation began: the first frame it was seen with it
function an_began(key: string) -> float {
for i in 0 .. len(an_keys) {
if an_keys[i] == key { return an_start[i] }
function an_began(ui_st: mut UiState, key: string) -> float {
for i in 0 .. len(ui_st.an_keys) {
if ui_st.an_keys[i] == key { return ui_st.an_start[i] }
}
push(an_keys, key)
push(an_start, an_now())
return an_now()
push(ui_st.an_keys, key)
push(ui_st.an_start, an_now(ui_st))
return an_now(ui_st)
}
# the element's animation at this moment: each property between the stops around it
function an_apply(n: UiNode) -> void {
function an_apply(ui_st: mut UiState, n: UiNode) -> void {
if n.anim == "" or n.anim_dur <= 0.0 { return }
let k = an_find(n.anim)
let k = an_find(ui_st, n.anim)
if k == null {
ui_err(`animation {n.anim}: there is no @keyframes of that name`)
ui_err(ui_st, `animation {n.anim}: there is no @keyframes of that name`)
return
}
var t = (an_now() - an_began(`{n.key}@{n.anim}`)) / n.anim_dur
var t = (an_now(ui_st) - an_began(ui_st, `{n.key}@{n.anim}`)) / n.anim_dur
if n.anim_loop {
let lap = int(t)
t = t - float(lap)
if n.anim_alt and lap % 2 == 1 { t = 1.0 - t }
} else { t = Math.clamp(t, 0.0, 1.0) }
an_at(n, k, t)
an_at(ui_st, n, k, t)
}
function an_at(n: UiNode, k: UiKeyframes, t: float) -> void {
function an_at(ui_st: mut UiState, n: UiNode, k: UiKeyframes, t: float) -> void {
var lo = 0
var hi = len(k.stops) - 1
for i in 0 .. len(k.stops) {
@ -79,11 +75,11 @@ function an_at(n: UiNode, k: UiKeyframes, t: float) -> void {
let a = k.rules[lo]
let b = k.rules[hi]
for i in 0 .. len(a.keys) {
let va = el_num(vl_text(ui_eval(a.vals[i], n.env)))
let va = el_num(vl_text(ui_eval(ui_st, a.vals[i], n.env)))
var vb = va
for j in 0 .. len(b.keys) {
if b.keys[j] == a.keys[i] { vb = el_num(vl_text(ui_eval(b.vals[j], n.env))) }
if b.keys[j] == a.keys[i] { vb = el_num(vl_text(ui_eval(ui_st, b.vals[j], n.env))) }
}
el_apply(n, a.keys[i], Value.float(va + (vb - va) * f))
el_apply(ui_st, n, a.keys[i], Value.float(va + (vb - va) * f))
}
}

View file

@ -3,21 +3,21 @@
const AT_ALIAS: string = " width=w height=h min-width=min-w max-width=max-w min-height=min-h max-height=max-h padding=pad background=bg background-color=bg font-size=size opacity=alpha flex-grow=grow flex-shrink=shrink flex-direction=dir flex-wrap=wrap justify-content=justify align-items=align align-self=self row-gap=gap column-gap=gap text-align=talign "
const AT_UNDRAWN: string = " font-weight font-family font-style box-shadow cursor transition animation letter-spacing text-decoration "
# a stylesheet's property: one that means nothing is said
function el_attr(n: UiNode, key: string, v: Val) -> void {
if el_apply(n, key, v) { return }
if lk_in(AT_UNDRAWN, at_alias(key)) { ui_err(`{key}: this renderer does not draw it`) } else { ui_err(`{key}: there is no such property`) }
function el_attr(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> void {
if el_apply(ui_st, n, key, v) { return }
if lk_in(AT_UNDRAWN, at_alias(key)) { ui_err(ui_st, `{key}: this renderer does not draw it`) } else { ui_err(ui_st, `{key}: there is no such property`) }
}
# an element's attribute: a property when it names one, otherwise only there for [selectors]
function el_apply(n: UiNode, key: string, v0: Val) -> bool {
function el_apply(ui_st: mut UiState, n: UiNode, key: string, v0: Val) -> bool {
let k = at_alias(key)
el_cur = n
el_key = k
ui_st.el_cur = n
ui_st.el_key = k
if len(k) > 2 and k[0..2] == "--" {
cs_var_set(n, k, vl_text(v0))
return true
}
let v = cs_vars_in(n, v0)
return el_size(n, k, v) or el_box(n, k, v) or el_flex(n, k, v) or el_look(n, k, v) or el_place_prop(n, k, v) or el_edge(n, k, v) or el_more(n, k, v)
let v = cs_vars_in(ui_st, n, v0)
return el_size(ui_st, n, k, v) or el_box(ui_st, n, k, v) or el_flex(n, k, v) or el_look(ui_st, n, k, v) or el_place_prop(ui_st, n, k, v) or el_edge(ui_st, n, k, v) or el_more(ui_st, n, k, v)
}
function at_alias(key: string) -> string {
let pat = ` {key}=`
@ -31,8 +31,8 @@ function at_alias(key: string) -> string {
}
return key
}
function el_size(n: UiNode, key: string, v: Val) -> bool {
if key == "w" or key == "h" { el_dim(n, key == "w", v) } else if key == "min-w" { n.min_w = el_len(v) } else if key == "max-w" { n.max_w = el_len(v) } else if key == "min-h" { n.min_h = el_len(v) } else if key == "max-h" { n.max_h = el_len(v) } else if key == "gap" { n.gap = el_len(v) } else { return false }
function el_size(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
if key == "w" or key == "h" { el_dim(ui_st, n, key == "w", v) } else if key == "min-w" { n.min_w = el_len(ui_st, v) } else if key == "max-w" { n.max_w = el_len(ui_st, v) } else if key == "min-h" { n.min_h = el_len(ui_st, v) } else if key == "max-h" { n.max_h = el_len(ui_st, v) } else if key == "gap" { n.gap = el_len(ui_st, v) } else { return false }
return true
}
function el_flex(n: UiNode, key: string, v: Val) -> bool {
@ -43,33 +43,33 @@ function el_flex(n: UiNode, key: string, v: Val) -> bool {
} else if key == "grow" { n.grow = el_num(s) } else if key == "flex" { el_flex_short(n, s) } else if key == "wrap" { n.wrap = s == "wrap" or (vl_truthy(v) and s != "false" and s != "nowrap") } else if key == "align" { n.align = el_place(v) } else if key == "self" { n.self = el_place(v) } else if key == "justify" { n.justify = el_place(v) } else if key == "talign" { n.talign = el_place(v) } else if key == "display" { n.hidden = s == "none" } else { return false }
return true
}
function el_look(n: UiNode, key: string, v: Val) -> bool {
function el_look(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
if key == "size" { n.size = el_len(v) } else if key == "color" {
n.fg = ui_color(s)
n.fg_a = cl_alpha
if key == "size" { n.size = el_len(ui_st, v) } else if key == "color" {
n.fg = ui_color(ui_st, s)
n.fg_a = ui_st.cl_alpha
} else if key == "bg" {
if gr_is(tpl_words(s)) { nx(n).grad = gr_parse(s) } else if len(s) > 4 and s[0..4] == "url(" { n.bg_img = el_url(s) } else if tpl_words(s) == "transparent" {
if gr_is(tpl_words(s)) { nx(n).grad = gr_parse(ui_st, s) } else if len(s) > 4 and s[0..4] == "url(" { n.bg_img = el_url(s) } else if tpl_words(s) == "transparent" {
n.bg = 0
n.bg_a = 0.0
} else {
n.bg = ui_color(s)
n.bg_a = cl_alpha
n.bg = ui_color(ui_st, s)
n.bg_a = ui_st.cl_alpha
}
} else if key == "alpha" { n.alpha = el_num(s) } else if key == "enabled" { n.enabled = vl_truthy(v) and s != "false" } else if key == "border-color" {
n.border_c = ui_color(s)
n.border_a = cl_alpha
} else if key == "border-width" { el_sides_border(n, el_len(v)) } else if key == "border" { el_border(n, s) } else if key == "border-top" or key == "border-right" or key == "border-bottom" or key == "border-left" { el_border_side(n, key, s) } else { return false }
n.border_c = ui_color(ui_st, s)
n.border_a = ui_st.cl_alpha
} else if key == "border-width" { el_sides_border(n, el_len(ui_st, v)) } else if key == "border" { el_border(ui_st, n, s) } else if key == "border-top" or key == "border-right" or key == "border-bottom" or key == "border-left" { el_border_side(ui_st, n, key, s) } else { return false }
return true
}
# border: 1 #ffcc00, or 1px solid accent
function el_border(n: UiNode, s: string) -> void {
function el_border(ui_st: mut UiState, n: UiNode, s: string) -> void {
let parts = lss_split(s, 32)
for i in 0 .. len(parts) {
let p = parts[i]
if len(p) > 0 and ((p[0] >= 48 and p[0] <= 57) or p[0] == 46) { el_sides_border(n, el_unit(p)) } else if p == "none" { el_sides_border(n, 0.0) } else if len(p) > 0 and p != "solid" {
n.border_c = ui_color(p)
n.border_a = cl_alpha
if len(p) > 0 and ((p[0] >= 48 and p[0] <= 57) or p[0] == 46) { el_sides_border(n, el_unit(ui_st, p)) } else if p == "none" { el_sides_border(n, 0.0) } else if len(p) > 0 and p != "solid" {
n.border_c = ui_color(ui_st, p)
n.border_a = ui_st.cl_alpha
}
}
if n.border_c == UI_NONE { n.border_c = n.rfg }

View file

@ -8,12 +8,12 @@ function el_sides_border(n: UiNode, w: float) -> void {
n.bl = w
}
# border-bottom: 3px solid var(--accent) - one side's width, and the colour for the border
function el_border_side(n: UiNode, key: string, s: string) -> void {
function el_border_side(ui_st: mut UiState, n: UiNode, key: string, s: string) -> void {
let keep_t = n.bt
let keep_r = n.br
let keep_b = n.bb
let keep_l = n.bl
el_border(n, s)
el_border(ui_st, n, s)
let w = n.bt
n.bt = keep_t
n.br = keep_r

View file

@ -1,50 +1,46 @@
# attr_box.ludic - padding and margin (one to four lengths, as CSS writes them, or one side by
# name), lengths with px or %, and the words that place things
function el_box(n: UiNode, key: string, v: Val) -> bool {
function el_box(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
if key == "pad" {
el_sides(v)
n.pt = el_s0
n.pr = el_s1
n.pb = el_s2
n.pl = el_s3
el_sides(ui_st, v)
n.pt = ui_st.el_s0
n.pr = ui_st.el_s1
n.pb = ui_st.el_s2
n.pl = ui_st.el_s3
} else if key == "margin" {
el_sides(v)
n.mt = el_s0
n.mr = el_s1
n.mb = el_s2
n.ml = el_s3
} else if key == "padding-top" { n.pt = el_len(v) } else if key == "padding-right" { n.pr = el_len(v) } else if key == "padding-bottom" { n.pb = el_len(v) } else if key == "padding-left" { n.pl = el_len(v) } else if key == "margin-top" { n.mt = el_len(v) } else if key == "margin-right" { n.mr = el_len(v) } else if key == "margin-bottom" { n.mb = el_len(v) } else if key == "margin-left" { n.ml = el_len(v) } else { return false }
el_sides(ui_st, v)
n.mt = ui_st.el_s0
n.mr = ui_st.el_s1
n.mb = ui_st.el_s2
n.ml = ui_st.el_s3
} else if key == "padding-top" { n.pt = el_len(ui_st, v) } else if key == "padding-right" { n.pr = el_len(ui_st, v) } else if key == "padding-bottom" { n.pb = el_len(ui_st, v) } else if key == "padding-left" { n.pl = el_len(ui_st, v) } else if key == "margin-top" { n.mt = el_len(ui_st, v) } else if key == "margin-right" { n.mr = el_len(ui_st, v) } else if key == "margin-bottom" { n.mb = el_len(ui_st, v) } else if key == "margin-left" { n.ml = el_len(ui_st, v) } else { return false }
return true
}
var el_s0: float = 0.0
var el_s1: float = 0.0
var el_s2: float = 0.0
var el_s3: float = 0.0
# top right bottom left out of 1 to 4 lengths
function el_sides(v: Val) -> void {
function el_sides(ui_st: mut UiState, v: Val) -> void {
let parts = new []float
if vl_num(v) { push(parts, el_px(Value.as_float(v))) } else {
if vl_num(v) { push(parts, el_px(ui_st, Value.as_float(v))) } else {
let words = el_words(vl_text(v))
for i in 0 .. len(words) {
if len(words[i]) > 0 { push(parts, el_word_len(words[i])) }
if len(words[i]) > 0 { push(parts, el_word_len(ui_st, words[i])) }
}
}
while len(parts) < 4 {
if len(parts) == 0 { push(parts, 0.0) } else if len(parts) == 1 { push(parts, parts[0]) } else if len(parts) == 2 { push(parts, parts[0]) } else { push(parts, parts[1]) }
}
el_s0 = parts[0]
el_s1 = parts[1]
el_s2 = parts[2]
el_s3 = parts[3]
ui_st.el_s0 = parts[0]
ui_st.el_s1 = parts[1]
ui_st.el_s2 = parts[2]
ui_st.el_s3 = parts[3]
}
# a length: a number, 10px, 1.5em, 2rem, 50vw, calc(...), fill, fit or auto (a percentage is el_pct's)
function el_len(v: Val) -> float {
if vl_num(v) { return el_px(Value.as_float(v)) }
function el_len(ui_st: mut UiState, v: Val) -> float {
if vl_num(v) { return el_px(ui_st, Value.as_float(v)) }
let s = vl_text(v)
if s == "fill" { return UI_FILL }
if ca_is(s) { return el_calc_len(s) }
if ca_is(s) { return el_calc_len(ui_st, s) }
if s == "fit" or s == "auto" or (len(s) > 0 and s[len(s) - 1] == 37) { return 0.0 }
return el_unit(s)
return el_unit(ui_st, s)
}
function el_pct(v: Val) -> float {
let s = vl_text(v)

View file

@ -1,14 +1,14 @@
# attr_edge.ludic - border-radius, outline (and its width, colour and offset), scrollbar-color
function el_edge(n: UiNode, key: string, v: Val) -> bool {
function el_edge(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
if key == "border-radius" { n.radius = el_len(v) } else if key == "outline" { el_outline(n, s) } else if key == "outline-width" { n.outline_w = el_len(v) } else if key == "outline-offset" { n.outline_off = el_len(v) } else if key == "outline-color" {
n.outline_c = ui_color(s)
n.outline_a = cl_alpha
if key == "border-radius" { n.radius = el_len(ui_st, v) } else if key == "outline" { el_outline(ui_st, n, s) } else if key == "outline-width" { n.outline_w = el_len(ui_st, v) } else if key == "outline-offset" { n.outline_off = el_len(ui_st, v) } else if key == "outline-color" {
n.outline_c = ui_color(ui_st, s)
n.outline_a = ui_st.cl_alpha
} else if key == "scrollbar-color" {
let parts = lss_split(s, 32)
if len(parts) > 0 { n.bar_c = ui_color(parts[0]) }
if len(parts) > 1 { n.bar_track = ui_color(parts[1]) }
} else if key == "background-image" and gr_is(tpl_words(s)) { nx(n).grad = gr_parse(s) } else if key == "background-image" { n.bg_img = el_url(s) } else if key == "border-image" { el_border_image(n, s) } else if key == "box-shadow" { el_shadow(n, s) } else if key == "animation" { el_animation(n, s) } else if key == "transition" { n.tr_dur = el_secs(s) } else { return false }
if len(parts) > 0 { n.bar_c = ui_color(ui_st, parts[0]) }
if len(parts) > 1 { n.bar_track = ui_color(ui_st, parts[1]) }
} else if key == "background-image" and gr_is(tpl_words(s)) { nx(n).grad = gr_parse(ui_st, s) } else if key == "background-image" { n.bg_img = el_url(s) } else if key == "border-image" { el_border_image(ui_st, n, s) } else if key == "box-shadow" { el_shadow(ui_st, n, s) } else if key == "animation" { el_animation(n, s) } else if key == "transition" { n.tr_dur = el_secs(s) } else { return false }
return true
}
# url(path) or url("path") -> path
@ -20,17 +20,17 @@ function el_url(s: string) -> string {
return t
}
# border-image: url(chip.png) 14 / 12px - the source's corner size, then how wide they are drawn
function el_border_image(n: UiNode, s: string) -> void {
function el_border_image(ui_st: UiState, n: UiNode, s: string) -> void {
var close = 0
while close < len(s) and s[close] != 41 { close += 1 }
n.bi_src = el_url(s[0..Math.min(close + 1, len(s))])
let rest = lss_split(s[Math.min(close + 1, len(s))..len(s)], 47)
n.bi_slice = el_num(tpl_words(rest[0]))
n.bi_width = el_px(n.bi_slice)
if len(rest) > 1 { n.bi_width = el_unit(tpl_words(rest[1])) }
n.bi_width = el_px(ui_st, n.bi_slice)
if len(rest) > 1 { n.bi_width = el_unit(ui_st, tpl_words(rest[1])) }
}
# box-shadow: 6px 10px 0 rgba(0, 0, 0, 0.45) - x, y, a blur this renderer draws sharp, the colour
function el_shadow(n: UiNode, s: string) -> void {
function el_shadow(ui_st: mut UiState, n: UiNode, s: string) -> void {
if tpl_words(s) == "none" {
n.sh_c = -2
return
@ -42,24 +42,24 @@ function el_shadow(n: UiNode, s: string) -> void {
let p = parts[i]
if len(p) == 0 { continue }
if col == "" and ((p[0] >= 48 and p[0] <= 57) or p[0] == 45 or p[0] == 46) {
if nums == 0 { n.sh_x = el_unit(p) }
if nums == 1 { n.sh_y = el_unit(p) }
if nums == 0 { n.sh_x = el_unit(ui_st, p) }
if nums == 1 { n.sh_y = el_unit(ui_st, p) }
nums += 1
} else if col == "" { col = p } else { col = col + " " + p }
}
if col == "" { col = "rgba(0,0,0,0.5)" }
n.sh_c = ui_color(col)
n.sh_a = cl_alpha
n.sh_c = ui_color(ui_st, col)
n.sh_a = ui_st.cl_alpha
}
# outline: 2px solid #ffcc00, or none
function el_outline(n: UiNode, s: string) -> void {
function el_outline(ui_st: mut UiState, n: UiNode, s: string) -> void {
let parts = lss_split(s, 32)
n.outline_w = 0.0
for i in 0 .. len(parts) {
let p = parts[i]
if len(p) > 0 and ((p[0] >= 48 and p[0] <= 57) or p[0] == 46) { n.outline_w = el_unit(p) } else if len(p) > 0 and p != "solid" and p != "none" {
n.outline_c = ui_color(p)
n.outline_a = cl_alpha
if len(p) > 0 and ((p[0] >= 48 and p[0] <= 57) or p[0] == 46) { n.outline_w = el_unit(ui_st, p) } else if len(p) > 0 and p != "solid" and p != "none" {
n.outline_c = ui_color(ui_st, p)
n.outline_a = ui_st.cl_alpha
}
}
}

View file

@ -1,6 +1,6 @@
# attr_fit.ludic - aspect-ratio (16 / 9, or 1.5), object-fit (fill, contain, cover, none, scale-down)
# for pictures and natives, zoom (zoom.ludic), and a list split at its top-level commas
function el_fit_props(n: UiNode, key: string, s: string) -> bool {
function el_fit_props(ui_st: mut UiState, n: UiNode, key: string, s: string) -> bool {
if key == "aspect-ratio" {
let parts = lss_split(s, 47)
var r = el_num(tpl_words(parts[0]))
@ -12,7 +12,7 @@ function el_fit_props(n: UiNode, key: string, s: string) -> bool {
var k = 0
if w == "contain" { k = 1 } else if w == "cover" { k = 2 } else if w == "none" { k = 3 } else if w == "scale-down" { k = 4 }
nx(n).fit_obj = k
} else if key == "zoom" { el_zoom(n, s) } else { return false }
} else if key == "zoom" { el_zoom(ui_st, n, s) } else { return false }
return true
}
# "a, rgba(1, 2, 3, 0.5) 40%, b" -> its three parts

View file

@ -1,21 +1,21 @@
# attr_inset.ludic - top, right, bottom and left as lengths, percentages or calc() of both: the
# percentage is kept apart and taken of the containing block's width (left, right) or height (top,
# bottom) when the element is placed
function el_inset(n: UiNode, side: int, v: Val) -> float {
function el_inset(ui_st: mut UiState, n: UiNode, side: int, v: Val) -> float {
let s = vl_text(v)
var pct = 0.0
var px = UI_AUTO
if s != "auto" {
if ca_is(s) {
px = 0.0
if ca_eval(s) {
pct = ca_pct
px = ca_px
if ca_eval(ui_st, s) {
pct = ui_st.ca_pct
px = ui_st.ca_px
}
} else if len(s) > 1 and s[len(s) - 1] == 37 {
pct = el_num(s)
px = 0.0
} else { px = el_len(v) }
} else { px = el_len(ui_st, v) }
}
if pct != 0.0 or n.ex != null {
let x = nx(n)

View file

@ -1,26 +1,26 @@
# attr_more.ludic - width and height (where 0 is 0 and calc() may hold a percentage), the flex
# shorthand, flex-shrink, order, text-shadow, pointer-events, text-fit and line-height (text_fit.ludic)
function el_more(n: UiNode, key: string, v: Val) -> bool {
function el_more(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
if key == "shrink" { nx(n).shrink = Math.max(el_num(s), 0.0) } else if key == "order" { nx(n).order = int(el_num(s)) } else if key == "text-shadow" { el_text_shadow(n, s) } else if key == "pointer-events" { nx(n).no_pointer = tpl_words(s) == "none" } else if key == "text-fit" { el_text_fit(n, s) } else if key == "line-height" { el_line_h(n, s) } else { return el_fit_props(n, key, s) }
if key == "shrink" { nx(n).shrink = Math.max(el_num(s), 0.0) } else if key == "order" { nx(n).order = int(el_num(s)) } else if key == "text-shadow" { el_text_shadow(ui_st, n, s) } else if key == "pointer-events" { nx(n).no_pointer = tpl_words(s) == "none" } else if key == "text-fit" { el_text_fit(ui_st, n, s) } else if key == "line-height" { el_line_h(ui_st, n, s) } else { return el_fit_props(ui_st, n, key, s) }
return true
}
# width or height: a length (0 included), fill, a percentage, calc(), or auto/fit (none)
function el_dim(n: UiNode, wide: bool, v: Val) -> void {
function el_dim(ui_st: mut UiState, n: UiNode, wide: bool, v: Val) -> void {
let s = vl_text(v)
var fixed = UI_AUTO
var pct = 0.0
var off = 0.0
if ca_is(s) {
if ca_eval(s) {
pct = ca_pct
off = ca_px
if pct <= 0.0 { fixed = Math.max(ca_px, 0.0) }
if ca_eval(ui_st, s) {
pct = ui_st.ca_pct
off = ui_st.ca_px
if pct <= 0.0 { fixed = Math.max(ui_st.ca_px, 0.0) }
}
} else if s == "fill" { fixed = UI_FILL } else if len(s) > 1 and s[len(s) - 1] == 37 {
pct = el_num(s)
if pct <= 0.0 { fixed = 0.0 }
} else if s != "auto" and s != "fit" and s != "" { fixed = el_len(v) }
} else if s != "auto" and s != "fit" and s != "" { fixed = el_len(ui_st, v) }
if pct <= 0.0 { off = 0.0 }
if n.ex != null or off != 0.0 {
if wide { nx(n).w_off = off } else { nx(n).h_off = off }
@ -34,10 +34,10 @@ function el_dim(n: UiNode, wide: bool, v: Val) -> void {
}
}
# a length out of calc(); a percentage has nothing to be a share of here
function el_calc_len(s: string) -> float {
if not ca_eval(s) { return 0.0 }
if ca_pct != 0.0 { ui_err(`{el_key}: {s} - a percentage in calc() is for a width or a height`) }
return ca_px
function el_calc_len(ui_st: mut UiState, s: string) -> float {
if not ca_eval(ui_st, s) { return 0.0 }
if ui_st.ca_pct != 0.0 { ui_err(ui_st, `{ui_st.el_key}: {s} - a percentage in calc() is for a width or a height`) }
return ui_st.ca_px
}
# flex: 1 | flex: 1 0 | flex: 2 1 auto | flex: none - the grow, then the shrink
function el_flex_short(n: UiNode, s: string) -> void {
@ -67,7 +67,7 @@ function el_words(s: string) -> []string {
return out
}
# text-shadow: 1px 2px 3px rgba(0, 0, 0, 0.6) - x, y, a blur drawn sharp, the colour; or none
function el_text_shadow(n: UiNode, s: string) -> void {
function el_text_shadow(ui_st: mut UiState, n: UiNode, s: string) -> void {
let x = nx(n)
x.tsh_set = true
x.tsh_c = -2
@ -78,17 +78,17 @@ function el_text_shadow(n: UiNode, s: string) -> void {
for i in 0 .. len(parts) {
let p = parts[i]
if col == "" and ((p[0] >= 48 and p[0] <= 57) or p[0] == 45 or p[0] == 46) {
if nums == 0 { x.tsh_x = el_unit(p) }
if nums == 1 { x.tsh_y = el_unit(p) }
if nums == 0 { x.tsh_x = el_unit(ui_st, p) }
if nums == 1 { x.tsh_y = el_unit(ui_st, p) }
nums += 1
} else if col == "" { col = p } else { col = col + " " + p }
}
if col == "" { col = "rgba(0,0,0,0.6)" }
x.tsh_c = ui_color(col)
x.tsh_a = cl_alpha
x.tsh_c = ui_color(ui_st, col)
x.tsh_a = ui_st.cl_alpha
}
# one word of a list of lengths (padding: calc(1em / 4) 2px)
function el_word_len(w: string) -> float {
if ca_is(w) { return el_calc_len(w) }
return el_unit(w)
function el_word_len(ui_st: mut UiState, w: string) -> float {
if ca_is(w) { return el_calc_len(ui_st, w) }
return el_unit(ui_st, w)
}

View file

@ -1,17 +1,17 @@
# attr_place.ludic - position and its insets, z-index, overflow, white-space and text-overflow
function el_place_prop(n: UiNode, key: string, v: Val) -> bool {
function el_place_prop(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
if key == "position" {
n.pos = 0
if s == "relative" { n.pos = 1 }
if s == "absolute" { n.pos = 2 }
if s == "fixed" { n.pos = 3 }
} else if key == "top" { n.top = el_inset(n, 0, v) } else if key == "right" { n.right = el_inset(n, 1, v) } else if key == "bottom" { n.bottom = el_inset(n, 2, v) } else if key == "left" { n.left = el_inset(n, 3, v) } else if key == "inset" {
el_sides(v)
n.top = el_s0
n.right = el_s1
n.bottom = el_s2
n.left = el_s3
} else if key == "top" { n.top = el_inset(ui_st, n, 0, v) } else if key == "right" { n.right = el_inset(ui_st, n, 1, v) } else if key == "bottom" { n.bottom = el_inset(ui_st, n, 2, v) } else if key == "left" { n.left = el_inset(ui_st, n, 3, v) } else if key == "inset" {
el_sides(ui_st, v)
n.top = ui_st.el_s0
n.right = ui_st.el_s1
n.bottom = ui_st.el_s2
n.left = ui_st.el_s3
} else if key == "z-index" { n.z = int(el_num(s)) } else if key == "overflow" or key == "overflow-y" {
if (s == "auto" or s == "scroll" or s == "hidden") and n.kind == UI_BOX { n.kind = UI_SCROLL }
} else if key == "white-space" { n.nowrap = s == "nowrap" or s == "pre" } else if key == "text-overflow" { n.ellipsis = s == "ellipsis" } else { return false }

View file

@ -30,30 +30,155 @@ function ui_default_backend() -> UiBackend {
if len(UiRenderers) == 0 { return null }
return UiRenderers[0].make()
}
var ui_be: UiBackend = ui_default_backend()
export function ui_backend(b: UiBackend) -> void { ui_be = b }
function ui_px(v: float) -> float {
if ui_be != null and ui_be.scale != null { return v * ui_be.scale() }
export state UiState {
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 = null # 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_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_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_prev_keys: []string = new []string
lc_prev_unmounts: []UiAct = new []UiAct
lc_prev_envs: []UiEnv = new []UiEnv
nt_list: []UiNative = new []UiNative
nt_q_nodes: []UiNode = new []UiNode
nt_q_names: []string = new []string
nt_q_vals: []Val = new []Val
fr_hover: []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
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
}
var tr_no: bool = false # inside translate="no" while a frame is built
function ui_tr(s: string) -> string {
if tr_no { return s }
if ui_be != null and ui_be.translate != null { return ui_be.translate(s) }
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(name: string) -> void {
if ui_be != null and ui_be.sound != null { ui_be.sound(name) }
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(f: fn(string) -> string) -> void {
if ui_be != null { ui_be.translate = f }
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(f: fn(string) -> void) -> void {
if ui_be != null { ui_be.sound = f }
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(f: fn() -> float) -> void {
if ui_be != null { ui_be.now = f }
export function ui_clock(ui_st: mut UiState, f: fn() -> float) -> void {
if ui_st.ui_be != null { ui_st.ui_be.now = f }
}

View file

@ -2,47 +2,47 @@
# <each in= as= index=> are the template's own; a tag its file linked to a component is that one,
# given its attributes as props (over the defaults its own tag gives: <component name="Card"
# sub="">), its on-* as what `emit` runs, and its content as <slot/>.
function bd_kids(kids: []UiTpl, e0: UiEnv, key: string, out: []UiNode) -> void {
function bd_kids(ui_st: mut UiState, kids: []UiTpl, e0: UiEnv, key: string, out: []UiNode) -> void {
var last_if = true
var e = e0
for i in 0 .. len(kids) {
let k = kids[i]
ui_at = k.where
ui_st.ui_at = k.where
let ck = `{key}/{i}`
if k.tag == "state" or k.tag == "style" { continue }
if k.tag == "let" {
e = bd_let(k, e)
e = bd_let(ui_st, k, e)
continue
}
if k.tag == "fragment" or k.tag == "#text" or (k.tag == "template" and tpl_attr(k, "slot") == null) { bd_group(k, e, ck, out) } else if k.tag == "provide" { bd_provide(k, e, ck, out) } else if k.tag == "template" { continue } else if k.tag == "if" {
last_if = bd_test(k, e)
if last_if { bd_kids(k.kids, e, ck, out) }
if k.tag == "fragment" or k.tag == "#text" or (k.tag == "template" and tpl_attr(k, "slot") == null) { bd_group(ui_st, k, e, ck, out) } else if k.tag == "provide" { bd_provide(ui_st, k, e, ck, out) } else if k.tag == "template" { continue } else if k.tag == "if" {
last_if = bd_test(ui_st, k, e)
if last_if { bd_kids(ui_st, k.kids, e, ck, out) }
} else if k.tag == "else" {
if not last_if { bd_kids(k.kids, e, ck, out) }
if not last_if { bd_kids(ui_st, k.kids, e, ck, out) }
last_if = true
} else if k.tag == "each" { bd_each(k, e, ck, out) } else if k.tag == "slot" { bd_slot(k, e, ck, out) } else if not bd_comp(k, e, ck, out) { push(out, bd_elem(k, e, ck)) }
} else if k.tag == "each" { bd_each(ui_st, k, e, ck, out) } else if k.tag == "slot" { bd_slot(ui_st, k, e, ck, out) } else if not bd_comp(ui_st, k, e, ck, out) { push(out, bd_elem(ui_st, k, e, ck)) }
}
}
function bd_test(k: UiTpl, e: UiEnv) -> bool {
function bd_test(ui_st: mut UiState, k: UiTpl, e: UiEnv) -> bool {
let t = tpl_attr(k, "test")
if t == null {
ui_err("<if> needs a test=\"{...}\"")
ui_err(ui_st, "<if> needs a test=\"{...}\"")
return false
}
return ui_eval_bool(t, e)
return ui_eval_bool(ui_st, t, e)
}
function bd_name(k: UiTpl, attr: string, dflt: string) -> string {
let x = tpl_attr(k, attr)
if x == null or x.op != X_LIT { return dflt }
return Value.as_str(x.v)
}
function bd_each(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
function bd_each(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
let src = tpl_attr(k, "in")
if src == null {
ui_err("<each> needs an in=\"{...}\"")
ui_err(ui_st, "<each> needs an in=\"{...}\"")
return
}
let list = ui_eval(src, e)
let list = ui_eval(ui_st, src, e)
let as_name = bd_name(k, "as", "item")
let at_name = bd_name(k, "index", "index")
let by = tpl_attr(k, "key")
@ -51,28 +51,28 @@ function bd_each(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
env_bind(le, as_name, Value.at(list, j))
env_bind(le, at_name, Value.int(j))
var id = string(j)
if by != null { id = ui_eval_text(by, le) }
bd_kids(k.kids, le, `{key}#{id}`, out)
if by != null { id = ui_eval_text(ui_st, by, le) }
bd_kids(ui_st, k.kids, le, `{key}#{id}`, out)
}
}
# <slot/> is what the component's user put inside it; <slot name="x"/> their <template slot="x">
function bd_slot(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
function bd_slot(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
var at = e
while at != null and at.slot == null { at = at.parent }
if at == null { return }
let name = bd_name(k, "name", "")
if name == "" {
bd_kids(at.slot.kids, at.slot_env, key, out)
bd_kids(ui_st, at.slot.kids, at.slot_env, key, out)
return
}
for i in 0 .. len(at.slot.kids) {
let t = at.slot.kids[i]
if t.tag == "template" and bd_name(t, "slot", "") == name { bd_group(t, at.slot_env, key, out) }
if t.tag == "template" and bd_name(t, "slot", "") == name { bd_group(ui_st, t, at.slot_env, key, out) }
}
}
function bd_comp(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> bool {
function bd_comp(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> bool {
if k.cls != null {
bd_class(k, e, key, out)
bd_class(ui_st, k, e, key, out)
return true
}
if k.comp == null { return false }
@ -81,9 +81,9 @@ function bd_comp(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> bool {
ce.model = e.model
ce.ctx = e.ctx
for i in 0 .. len(c.keys) {
if c.keys[i] != "name" and c.keys[i] != "export" { env_bind(ce, c.keys[i], ui_eval(c.vals[i], ce)) }
if c.keys[i] != "name" and c.keys[i] != "export" { env_bind(ce, c.keys[i], ui_eval(ui_st, c.vals[i], ce)) }
}
for i in 0 .. len(k.keys) { env_bind(ce, k.keys[i], ui_eval(k.vals[i], e)) }
for i in 0 .. len(k.keys) { env_bind(ce, k.keys[i], ui_eval(ui_st, k.vals[i], e)) }
ce.on_names = k.on_keys
ce.on_acts = k.on_acts
ce.on_envs = new []UiEnv
@ -91,8 +91,8 @@ function bd_comp(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> bool {
ce.slot = k
ce.slot_env = e
ce.ctx = e.ctx
ce.state = st_for(key, c, ce)
lc_note(key, k.on_keys, k.on_acts, e)
bd_kids(c.kids, ce, key, out)
ce.state = st_for(ui_st, key, c, ce)
lc_note(ui_st, key, k.on_keys, k.on_acts, e)
bd_kids(ui_st, c.kids, ce, key, out)
return true
}

View file

@ -1,14 +1,14 @@
# build_class.ludic - a Ludic component's instance built: the parent's attributes are its props (id,
# class and style go on its root element instead, styled by the parent's sheet as well as its own),
# its model is its props, state and fields, and its template reads nothing else
function bd_class(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
function bd_class(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
let cls = k.cls
uc_load(cls)
let it = in_get(key, cls)
uc_load(ui_st, cls)
let it = in_get(ui_st, key, cls)
let props = Value.object()
for i in 0 .. len(k.keys) {
let a = k.keys[i]
if a != "class" and a != "style" and a != "id" { Value.put(props, a, ui_eval(k.vals[i], e)) }
if a != "class" and a != "style" and a != "id" { Value.put(props, a, ui_eval(ui_st, k.vals[i], e)) }
}
cls.props(it.ptr, props)
let ce = env_new(null)
@ -21,25 +21,25 @@ function bd_class(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
ce.on_acts = k.on_acts
ce.on_envs = new []UiEnv
for i in 0 .. len(k.on_keys) { push(ce.on_envs, e) }
lc_note(key, k.on_keys, k.on_acts, e)
lc_note(ui_st, key, k.on_keys, k.on_acts, e)
let at = len(out)
let root = new []UiTpl
push(root, cls.root)
bd_kids(root, ce, key, out)
if len(out) > at { bd_pass_down(out[at], k, e) }
bd_kids(ui_st, root, ce, key, out)
if len(out) > at { bd_pass_down(ui_st, out[at], k, e) }
}
# <Card class="wide" style="margin: 4px" id="c"> puts those on the card's own root element; when that
# root is itself a component, each user in turn adds theirs (the outermost's id wins)
function bd_pass_down(n: UiNode, k: UiTpl, e: UiEnv) -> void {
function bd_pass_down(ui_st: mut UiState, n: UiNode, k: UiTpl, e: UiEnv) -> void {
let c = tpl_attr(k, "class")
if c != null {
let parts = lss_split(ui_eval_text(c, e), 32)
let parts = lss_split(ui_eval_text(ui_st, c, e), 32)
for i in 0 .. len(parts) {
if len(parts[i]) > 0 { push(n.classes, parts[i]) }
}
}
let id = tpl_attr(k, "id")
if id != null { n.id = ui_eval_text(id, e) }
if id != null { n.id = ui_eval_text(ui_st, id, e) }
let up = new UiPass
up.sheet = k.sheet
up.style = k.style

View file

@ -1,27 +1,27 @@
# build_more.ludic - <let name="{value}"/> names a value for the siblings after it, and
# <provide name="{value}"> gives it to everything inside, components included (React's context)
function bd_let(k: UiTpl, e: UiEnv) -> UiEnv {
function bd_let(ui_st: mut UiState, k: UiTpl, e: UiEnv) -> UiEnv {
let le = env_new(e)
for i in 0 .. len(k.keys) { env_bind(le, k.keys[i], ui_eval(k.vals[i], e)) }
for i in 0 .. len(k.keys) { env_bind(le, k.keys[i], ui_eval(ui_st, k.vals[i], e)) }
return le
}
function bd_provide(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
function bd_provide(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
let cx = env_new(e.ctx)
for i in 0 .. len(k.keys) { env_bind(cx, k.keys[i], ui_eval(k.vals[i], e)) }
for i in 0 .. len(k.keys) { env_bind(cx, k.keys[i], ui_eval(ui_st, k.vals[i], e)) }
let pe = env_new(e)
pe.ctx = cx
bd_kids(k.kids, pe, key, out)
bd_kids(ui_st, k.kids, pe, key, out)
}
# a fragment's (or a template's) content: its elements, or its words as a text of their own
function bd_group(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
function bd_group(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
if k.text == null {
bd_kids(k.kids, e, key, out)
bd_kids(ui_st, k.kids, e, key, out)
return
}
let t = new UiNode
t.kind = UI_TEXT
t.key = key
t.text = ui_tr(ui_eval_text(k.text, e))
t.text = ui_tr(ui_st, ui_eval_text(ui_st, k.text, e))
t.env = e
t.children = new []UiNode
push(out, t)

View file

@ -1,31 +1,24 @@
# calc.ludic - CSS's calc(), min(), max() and clamp(): + - * / and brackets over lengths (px, em, rem,
# vw, vh, a bare number is px), percentages and plain numbers. A width, height or inset keeps its
# percentage apart, to be taken of the room it is given; elsewhere a percentage has nothing to share.
var ca_s: string = ""
var ca_i: int = 0
var ca_bad: string = ""
var ca_px: float = 0.0 # the result: its lengths, its percentage, and whether it had units
var ca_pct: float = 0.0
var ca_num: float = 0.0 # a plain number's own value, and whether it was one
var ca_bare: bool = false
function ca_is(s: string) -> bool { return ca_fn_at(s, 0) != "" }
# calc(...) worked out into ca_px and ca_pct; false (with the reason said) when it cannot be
function ca_eval(s: string) -> bool {
ca_s = s
ca_i = 0
ca_bad = ""
let v = ca_factor()
ca_ws()
if ca_bad == "" and ca_i < len(ca_s) { ca_bad = "nothing after its closing )" }
if ca_bad != "" {
ui_err(`{s}: calc() expected {ca_bad}`)
function ca_eval(ui_st: mut UiState, s: string) -> bool {
ui_st.ca_s = s
ui_st.ca_i = 0
ui_st.ca_bad = ""
let v = ca_factor(ui_st)
ca_ws(ui_st)
if ui_st.ca_bad == "" and ui_st.ca_i < len(ui_st.ca_s) { ui_st.ca_bad = "nothing after its closing )" }
if ui_st.ca_bad != "" {
ui_err(ui_st, `{s}: calc() expected {ui_st.ca_bad}`)
return false
}
ca_px = v.px
ca_pct = v.pct
ca_num = v.n
ca_bare = v.bare
if v.bare { ca_px = el_px(v.n) }
ui_st.ca_px = v.px
ui_st.ca_pct = v.pct
ui_st.ca_num = v.n
ui_st.ca_bare = v.bare
if v.bare { ui_st.ca_px = el_px(ui_st, v.n) }
return true
}
property CaVal {
@ -34,22 +27,22 @@ property CaVal {
n: float = 0.0
bare: bool = true # a plain number, which may scale a length
}
function ca_ws() -> void {
while ca_i < len(ca_s) and (ca_s[ca_i] == 32 or ca_s[ca_i] == 9 or ca_s[ca_i] == 10) { ca_i += 1 }
function ca_ws(ui_st: mut UiState) -> void {
while ui_st.ca_i < len(ui_st.ca_s) and (ui_st.ca_s[ui_st.ca_i] == 32 or ui_st.ca_s[ui_st.ca_i] == 9 or ui_st.ca_s[ui_st.ca_i] == 10) { ui_st.ca_i += 1 }
}
function ca_sum() -> CaVal {
var a = ca_product()
while ca_bad == "" {
ca_ws()
if ca_i >= len(ca_s) or (ca_s[ca_i] != 43 and ca_s[ca_i] != 45) { return a }
let minus = ca_s[ca_i] == 45
ca_i += 1
let b = ca_product()
a = ca_add(a, b, minus)
function ca_sum(ui_st: mut UiState) -> CaVal {
var a = ca_product(ui_st)
while ui_st.ca_bad == "" {
ca_ws(ui_st)
if ui_st.ca_i >= len(ui_st.ca_s) or (ui_st.ca_s[ui_st.ca_i] != 43 and ui_st.ca_s[ui_st.ca_i] != 45) { return a }
let minus = ui_st.ca_s[ui_st.ca_i] == 45
ui_st.ca_i += 1
let b = ca_product(ui_st)
a = ca_add(ui_st, a, b, minus)
}
return a
}
function ca_add(a: CaVal, b: CaVal, minus: bool) -> CaVal {
function ca_add(ui_st: UiState, a: CaVal, b: CaVal, minus: bool) -> CaVal {
var sign = 1.0
if minus { sign = -1.0 }
let r = new CaVal
@ -58,15 +51,15 @@ function ca_add(a: CaVal, b: CaVal, minus: bool) -> CaVal {
return r
}
r.bare = false
r.px = ca_len(a).px + sign * ca_len(b).px
r.px = ca_len(ui_st, a).px + sign * ca_len(ui_st, b).px
r.pct = a.pct + sign * b.pct
return r
}
# a bare number beside a length is a length in px
function ca_len(a: CaVal) -> CaVal {
function ca_len(ui_st: UiState, a: CaVal) -> CaVal {
if not a.bare { return a }
let r = new CaVal
r.bare = false
r.px = el_px(a.n)
r.px = el_px(ui_st, a.n)
return r
}

View file

@ -9,40 +9,40 @@ function ca_fn_at(s: string, at: int) -> string {
return ""
}
function ca_word(s: string, at: int, w: string) -> bool { return at + len(w) <= len(s) and s[at..at + len(w)] == w }
function ca_fn(f: string) -> CaVal {
ca_i = ca_i + len(f) + 1
function ca_fn(ui_st: mut UiState, f: string) -> CaVal {
ui_st.ca_i = ui_st.ca_i + len(f) + 1
let args = new []CaVal
while ca_bad == "" {
push(args, ca_sum())
ca_ws()
if ca_i < len(ca_s) and ca_s[ca_i] == 44 { ca_i += 1 } else { break }
while ui_st.ca_bad == "" {
push(args, ca_sum(ui_st))
ca_ws(ui_st)
if ui_st.ca_i < len(ui_st.ca_s) and ui_st.ca_s[ui_st.ca_i] == 44 { ui_st.ca_i += 1 } else { break }
}
if ca_bad == "" and (ca_i >= len(ca_s) or ca_s[ca_i] != 41) { ca_bad = "a closing )" }
ca_i += 1
if ca_bad != "" { return new CaVal }
if ui_st.ca_bad == "" and (ui_st.ca_i >= len(ui_st.ca_s) or ui_st.ca_s[ui_st.ca_i] != 41) { ui_st.ca_bad = "a closing )" }
ui_st.ca_i += 1
if ui_st.ca_bad != "" { return new CaVal }
if f == "clamp" and len(args) != 3 {
ca_bad = "clamp(least, want, most)"
ui_st.ca_bad = "clamp(least, want, most)"
return new CaVal
}
for i in 0 .. len(args) {
if args[i].pct != 0.0 {
ca_bad = `lengths in {f}(), not a percentage`
ui_st.ca_bad = `lengths in {f}(), not a percentage`
return new CaVal
}
}
if f == "clamp" { return ca_pick(ca_pick(args[1], args[0], false), args[2], true) }
if f == "clamp" { return ca_pick(ui_st, ca_pick(ui_st, args[1], args[0], false), args[2], true) }
var r = args[0]
for i in 1 .. len(args) { r = ca_pick(r, args[i], f == "min") }
for i in 1 .. len(args) { r = ca_pick(ui_st, r, args[i], f == "min") }
return r
}
# the smaller (or the larger) of two, a bare number beside a length being px
function ca_pick(a: CaVal, b: CaVal, smaller: bool) -> CaVal {
function ca_pick(ui_st: UiState, a: CaVal, b: CaVal, smaller: bool) -> CaVal {
if a.bare and b.bare {
if (b.n < a.n) == smaller and b.n != a.n { return b }
return a
}
let la = ca_len(a)
let lb = ca_len(b)
let la = ca_len(ui_st, a)
let lb = ca_len(ui_st, b)
if (lb.px < la.px) == smaller and lb.px != la.px { return lb }
return la
}

View file

@ -1,28 +1,28 @@
# calc_terms.ludic - calc()'s products and its terms: a number with its unit, a bracket, a nested
# calc(), a minus in front; a product needs a plain number on one side, and a quotient a plain number
# or a length on its right (a length over a length is a plain number, as CSS's typed division makes)
function ca_product() -> CaVal {
var a = ca_factor()
while ca_bad == "" {
ca_ws()
if ca_i >= len(ca_s) or (ca_s[ca_i] != 42 and ca_s[ca_i] != 47) { return a }
let div = ca_s[ca_i] == 47
ca_i += 1
let b = ca_factor()
function ca_product(ui_st: mut UiState) -> CaVal {
var a = ca_factor(ui_st)
while ui_st.ca_bad == "" {
ca_ws(ui_st)
if ui_st.ca_i >= len(ui_st.ca_s) or (ui_st.ca_s[ui_st.ca_i] != 42 and ui_st.ca_s[ui_st.ca_i] != 47) { return a }
let div = ui_st.ca_s[ui_st.ca_i] == 47
ui_st.ca_i += 1
let b = ca_factor(ui_st)
if div and not b.bare and not a.bare and a.pct == 0.0 and b.pct == 0.0 and b.px != 0.0 {
a = ca_ratio(a.px / b.px)
continue
}
if div and not b.bare {
ca_bad = "a plain number, or a length under a length, after /"
ui_st.ca_bad = "a plain number, or a length under a length, after /"
return a
}
if div and b.n == 0.0 {
ca_bad = "a number that is not 0 after /"
ui_st.ca_bad = "a number that is not 0 after /"
return a
}
if div { a = ca_scale(a, 1.0 / b.n) } else if a.bare { a = ca_scale(b, a.n) } else if b.bare { a = ca_scale(a, b.n) } else {
ca_bad = "a plain number on one side of *"
ui_st.ca_bad = "a plain number on one side of *"
return a
}
}
@ -42,47 +42,47 @@ function ca_scale(a: CaVal, k: float) -> CaVal {
r.pct = a.pct * k
return r
}
function ca_factor() -> CaVal {
ca_ws()
if ca_i >= len(ca_s) {
ca_bad = "a value"
function ca_factor(ui_st: mut UiState) -> CaVal {
ca_ws(ui_st)
if ui_st.ca_i >= len(ui_st.ca_s) {
ui_st.ca_bad = "a value"
return new CaVal
}
let c = ca_s[ca_i]
let c = ui_st.ca_s[ui_st.ca_i]
if c == 45 {
ca_i += 1
return ca_scale(ca_factor(), -1.0)
ui_st.ca_i += 1
return ca_scale(ca_factor(ui_st), -1.0)
}
let f = ca_fn_at(ca_s, ca_i)
if f == "min" or f == "max" or f == "clamp" { return ca_fn(f) }
let f = ca_fn_at(ui_st.ca_s, ui_st.ca_i)
if f == "min" or f == "max" or f == "clamp" { return ca_fn(ui_st, f) }
if c == 40 or f == "calc" {
while ca_s[ca_i] != 40 { ca_i += 1 }
ca_i += 1
let v = ca_sum()
ca_ws()
if ca_i < len(ca_s) and ca_s[ca_i] == 41 { ca_i += 1 } else if ca_bad == "" { ca_bad = "a closing )" }
while ui_st.ca_s[ui_st.ca_i] != 40 { ui_st.ca_i += 1 }
ui_st.ca_i += 1
let v = ca_sum(ui_st)
ca_ws(ui_st)
if ui_st.ca_i < len(ui_st.ca_s) and ui_st.ca_s[ui_st.ca_i] == 41 { ui_st.ca_i += 1 } else if ui_st.ca_bad == "" { ui_st.ca_bad = "a closing )" }
return v
}
return ca_number()
return ca_number(ui_st)
}
# 12, 1.5em, 50%, 20vw
function ca_number() -> CaVal {
let at = ca_i
while ca_i < len(ca_s) and ((ca_s[ca_i] >= 48 and ca_s[ca_i] <= 57) or ca_s[ca_i] == 46) { ca_i += 1 }
let num_end = ca_i
while ca_i < len(ca_s) and ((ca_s[ca_i] >= 97 and ca_s[ca_i] <= 122) or ca_s[ca_i] == 37) { ca_i += 1 }
function ca_number(ui_st: mut UiState) -> CaVal {
let at = ui_st.ca_i
while ui_st.ca_i < len(ui_st.ca_s) and ((ui_st.ca_s[ui_st.ca_i] >= 48 and ui_st.ca_s[ui_st.ca_i] <= 57) or ui_st.ca_s[ui_st.ca_i] == 46) { ui_st.ca_i += 1 }
let num_end = ui_st.ca_i
while ui_st.ca_i < len(ui_st.ca_s) and ((ui_st.ca_s[ui_st.ca_i] >= 97 and ui_st.ca_s[ui_st.ca_i] <= 122) or ui_st.ca_s[ui_st.ca_i] == 37) { ui_st.ca_i += 1 }
let r = new CaVal
if num_end == at {
ca_bad = "a number"
ui_st.ca_bad = "a number"
return r
}
let word: string = ca_s[at..ca_i]
let unit: string = ca_s[num_end..ca_i]
let word: string = ui_st.ca_s[at..ui_st.ca_i]
let unit: string = ui_st.ca_s[num_end..ui_st.ca_i]
if unit == "" {
r.n = el_num(word)
return r
}
r.bare = false
if unit == "%" { r.pct = el_num(word) } else if unit == "px" or unit == "em" or unit == "rem" or unit == "vw" or unit == "vh" { r.px = el_unit(word) } else { ca_bad = `a unit calc() knows, not {unit}` }
if unit == "%" { r.pct = el_num(word) } else if unit == "px" or unit == "em" or unit == "rem" or unit == "vw" or unit == "vh" { r.px = el_unit(ui_st, word) } else { ui_st.ca_bad = `a unit calc() knows, not {unit}` }
return r
}

View file

@ -1,14 +1,14 @@
# cascade.ludic - once the frame's tree is built, every element is styled as a browser would:
# the default sheet, then the file's rules from least to most specific, then its style="...", then
# its own attributes. Then what is display: none leaves the tree.
function cs_tree(n: UiNode) -> void {
function cs_tree(ui_st: mut UiState, n: UiNode) -> void {
for i in 0 .. len(n.children) {
let c = n.children[i]
c.parent = n
c.index = i
}
if n.tag != "" { cs_node(n) }
for i in 0 .. len(n.children) { cs_tree(n.children[i]) }
if n.tag != "" { cs_node(ui_st, n) }
for i in 0 .. len(n.children) { cs_tree(ui_st, n.children[i]) }
var shown = new []UiNode
for i in 0 .. len(n.children) {
if not n.children[i].hidden { push(shown, n.children[i]) }
@ -18,54 +18,52 @@ function cs_tree(n: UiNode) -> void {
}
# twice: the font size (and zoom) first, from every rule, so an em anywhere reads the size the
# element ends with (again when it zooms, so its own lengths are zoomed)
function cs_node(n: UiNode) -> void {
cs_pass = 1
cs_styles(n)
if zm_own(n) != 1.0 { cs_styles(n) }
cs_pass = 0
cs_styles(n)
an_apply(n)
tr_apply(n)
function cs_node(ui_st: mut UiState, n: UiNode) -> void {
ui_st.cs_pass = 1
cs_styles(ui_st, n)
if zm_own(n) != 1.0 { cs_styles(ui_st, n) }
ui_st.cs_pass = 0
cs_styles(ui_st, n)
an_apply(ui_st, n)
tr_apply(ui_st, n)
}
function cs_styles(n: UiNode) -> void {
cs_sheet(n, ua_sheet())
if n.ups != null { cs_merged(n) } else if n.sheet != null { cs_sheet(n, n.sheet) }
if n.style != null { cs_rule(n, n.style) }
function cs_styles(ui_st: mut UiState, n: UiNode) -> void {
cs_sheet(ui_st, n, ua_sheet(ui_st))
if n.ups != null { cs_merged(ui_st, n) } else if n.sheet != null { cs_sheet(ui_st, n, n.sheet) }
if n.style != null { cs_rule(ui_st, n, n.style) }
if n.ups != null {
for i in 0 .. len(n.ups) {
if n.ups[i].style != null { cs_rule_in(n, n.ups[i].style, n.ups[i].env) }
if n.ups[i].style != null { cs_rule_in(ui_st, n, n.ups[i].style, n.ups[i].env) }
}
}
for i in 0 .. len(n.akeys) {
if cs_pass_ok(n.akeys[i]) { el_apply(n, n.akeys[i], n.vals[i]) }
if cs_pass_ok(ui_st, n.akeys[i]) { el_apply(ui_st, n, n.akeys[i], n.vals[i]) }
}
}
var cs_pass: int = 0
function cs_pass_ok(key: string) -> bool { return cs_pass == 0 or at_alias(key) == "size" or key == "zoom" }
function cs_sheet(n: UiNode, sh: UiSheet) -> void {
function cs_pass_ok(ui_st: UiState, key: string) -> bool { return ui_st.cs_pass == 0 or at_alias(key) == "size" or key == "zoom" }
function cs_sheet(ui_st: mut UiState, n: UiNode, sh: UiSheet) -> void {
for i in 0 .. len(sh.rules) {
let r = sh.rules[i]
if md_ok(r) and sel_match(r.sel, n) { cs_rule(n, r) }
if md_ok(ui_st, r) and sel_match(ui_st, r.sel, n) { cs_rule(ui_st, n, r) }
}
}
function cs_rule(n: UiNode, r: UiRule) -> void { cs_rule_in(n, r, n.env) }
function cs_rule(ui_st: mut UiState, n: UiNode, r: UiRule) -> void { cs_rule_in(ui_st, n, r, n.env) }
# a rule's custom properties first, so its other declarations can read them; values read in `e`
function cs_rule_in(n: UiNode, r: UiRule, e: UiEnv) -> void {
function cs_rule_in(ui_st: mut UiState, n: UiNode, r: UiRule, e: UiEnv) -> void {
for j in 0 .. len(r.keys) {
if len(r.keys[j]) > 2 and r.keys[j][0..2] == "--" { el_attr(n, r.keys[j], ui_eval(r.vals[j], e)) }
if len(r.keys[j]) > 2 and r.keys[j][0..2] == "--" { el_attr(ui_st, n, r.keys[j], ui_eval(ui_st, r.vals[j], e)) }
}
for j in 0 .. len(r.keys) {
if not (len(r.keys[j]) > 2 and r.keys[j][0..2] == "--") and cs_pass_ok(r.keys[j]) { el_attr(n, r.keys[j], ui_eval(r.vals[j], e)) }
if not (len(r.keys[j]) > 2 and r.keys[j][0..2] == "--") and cs_pass_ok(ui_st, r.keys[j]) { el_attr(ui_st, n, r.keys[j], ui_eval(ui_st, r.vals[j], e)) }
}
}
var ua_sh: UiSheet = null
function ua_sheet() -> UiSheet {
if ua_sh != null { return ua_sh }
function ua_sheet(ui_st: mut UiState) -> UiSheet {
if ui_st.ua_sh != null { return ui_st.ua_sh }
let f = new UiFile
f.path = "the default sheet"
f.imp = new []UiFile
f.own = new []UiRule
lss_text(f, UA_LSS)
ua_sh = sheet_of(f)
return ua_sh
lss_text(ui_st, f, UA_LSS)
ui_st.ua_sh = sheet_of(f)
return ui_st.ua_sh
}

View file

@ -2,25 +2,25 @@
# that passed a class down to it, all at once: the rules that match, from all of them, ordered by
# specificity as one sheet's are. On a tie the user's rule wins (the outermost user's last), as a
# later rule does. Then `order`, which rearranges a flex box's children without touching the tree.
function cs_merged(n: UiNode) -> void {
function cs_merged(ui_st: mut UiState, n: UiNode) -> void {
let rules = new []UiRule
let envs = new []UiEnv
let seen = new []UiSheet
if n.sheet != null { cs_gather(n, n.sheet, n.env, rules, envs, seen) }
if n.sheet != null { cs_gather(ui_st, n, n.sheet, n.env, rules, envs, seen) }
for i in 0 .. len(n.ups) {
if n.ups[i].sheet != null { cs_gather(n, n.ups[i].sheet, n.ups[i].env, rules, envs, seen) }
if n.ups[i].sheet != null { cs_gather(ui_st, n, n.ups[i].sheet, n.ups[i].env, rules, envs, seen) }
}
for i in 0 .. len(rules) { cs_rule_in(n, rules[i], envs[i]) }
for i in 0 .. len(rules) { cs_rule_in(ui_st, n, rules[i], envs[i]) }
}
# a sheet's matching rules into the list, each after every rule at most as specific (stable)
function cs_gather(n: UiNode, sh: UiSheet, e: UiEnv, rules: []UiRule, envs: []UiEnv, seen: []UiSheet) -> void {
function cs_gather(ui_st: mut UiState, n: UiNode, sh: UiSheet, e: UiEnv, rules: []UiRule, envs: []UiEnv, seen: []UiSheet) -> void {
for i in 0 .. len(seen) {
if seen[i] == sh { return }
}
push(seen, sh)
for i in 0 .. len(sh.rules) {
let r = sh.rules[i]
if md_ok(r) and sel_match(r.sel, n) {
if md_ok(ui_st, r) and sel_match(ui_st, r.sel, n) {
push(rules, r)
push(envs, e)
var j = len(rules) - 1

View file

@ -16,43 +16,42 @@ export property UiClass {
seen_xml: string = "" # the files as a developer's build last found them on disk
seen_lss: string = ""
}
var uc_list: []UiClass = null # filled by the compiler's registrations, before the program's globals
export function ui_register(k: UiClass) -> int {
if uc_list == null { uc_list = new []UiClass }
push(uc_list, k)
return len(uc_list)
export function ui_register(ui_st: mut UiState, k: UiClass) -> int {
if ui_st.uc_list == null { ui_st.uc_list = new []UiClass }
push(ui_st.uc_list, k)
return len(ui_st.uc_list)
}
function uc_find(name: string) -> UiClass {
if uc_list == null { return null }
for i in 0 .. len(uc_list) {
if uc_list[i].name == name { return uc_list[i] }
function uc_find(ui_st: UiState, name: string) -> UiClass {
if ui_st.uc_list == null { return null }
for i in 0 .. len(ui_st.uc_list) {
if ui_st.uc_list[i].name == name { return ui_st.uc_list[i] }
}
return null
}
# the class's template and styles, read the first time it is shown
function uc_load(k: UiClass) -> void {
function uc_load(ui_st: mut UiState, k: UiClass) -> void {
if k.root != null { return }
let f = lib_blank(k.xml_path)
f.src = k.xml
if k.lss != "" { lss_text(f, k.lss) }
if k.lss != "" { lss_text(ui_st, f, k.lss) }
f.sheet = sheet_of(f)
tp_lines(k.xml)
let root = tpl_read(Xml.parse(k.xml), k.xml_path)
tp_lines(ui_st, k.xml)
let root = tpl_read(ui_st, Xml.parse(k.xml), k.xml_path)
k.sheet = f.sheet
lib_link(f, root)
lib_link(ui_st, f, root)
k.root = root
}
# a developer's build reads a class's files again when they change on disk: the first look is what
# they are, and a later one that differs is taken (an @import in the styles is read from disk too)
function uc_reload() -> bool {
if uc_list == null { return false }
function uc_reload(ui_st: mut UiState) -> bool {
if ui_st.uc_list == null { return false }
var changed = false
for i in 0 .. len(uc_list) {
let k = uc_list[i]
for i in 0 .. len(ui_st.uc_list) {
let k = ui_st.uc_list[i]
if k.xml_path != "" and Fs.exists(k.xml_path) {
let x = lib_text(k.xml_path)
let x = lib_text(ui_st, k.xml_path)
var l = ""
if k.lss_path != "" and Fs.exists(k.lss_path) { l = lib_text(k.lss_path) }
if k.lss_path != "" and Fs.exists(k.lss_path) { l = lib_text(ui_st, k.lss_path) }
if k.seen_xml != "" and (x != k.seen_xml or l != k.seen_lss) {
k.xml = x
k.lss = l
@ -66,7 +65,7 @@ function uc_reload() -> bool {
return changed
}
# every class reads its template and styles again, as when a stylesheet they import has changed
function uc_invalidate() -> void {
if uc_list == null { return }
for i in 0 .. len(uc_list) { uc_list[i].root = null }
function uc_invalidate(ui_st: mut UiState) -> void {
if ui_st.uc_list == null { return }
for i in 0 .. len(ui_st.uc_list) { ui_st.uc_list[i].root = null }
}

View file

@ -1,19 +1,18 @@
# color.ludic - CSS colours: #rgb, #rgba, #rrggbb, #rrggbbaa, rgb(r, g, b), rgba(r, g, b, a), the
# basic names, transparent and none. A colour is 0xRRGGBB; its alpha is left in cl_alpha.
var cl_alpha: float = 1.0
const CL_NAMES: string = " black=000000 white=ffffff red=ff0000 green=008000 lime=00ff00 blue=0000ff yellow=ffff00 cyan=00ffff aqua=00ffff magenta=ff00ff fuchsia=ff00ff gray=808080 grey=808080 silver=c0c0c0 maroon=800000 olive=808000 navy=000080 purple=800080 teal=008080 orange=ffa500 "
function ui_color(name0: string) -> int {
function ui_color(ui_st: mut UiState, name0: string) -> int {
let name = tpl_words(name0)
cl_alpha = 1.0
ui_st.cl_alpha = 1.0
if name == "none" or name == "" or name == "transparent" { return UI_NONE }
if name[0] == 35 { return cl_hex(name[1..len(name)]) }
if len(name) > 4 and (name[0..4] == "rgb(" or name[0..5] == "rgba(") { return cl_rgb(name) }
if name[0] == 35 { return cl_hex(ui_st, name[1..len(name)]) }
if len(name) > 4 and (name[0..4] == "rgb(" or name[0..5] == "rgba(") { return cl_rgb(ui_st, name) }
let pat = ` {name}=`
let n = len(pat)
for i in 0 .. len(CL_NAMES) - n + 1 {
if CL_NAMES[i..i + n] == pat { return cl_hex(CL_NAMES[i + n..i + n + 6]) }
if CL_NAMES[i..i + n] == pat { return cl_hex(ui_st, CL_NAMES[i + n..i + n + 6]) }
}
ui_err(`{name}: not a colour - #rrggbb, rgb(), rgba(), a name, or var(--a-colour)`)
ui_err(ui_st, `{name}: not a colour - #rrggbb, rgb(), rgba(), a name, or var(--a-colour)`)
return 15658734
}
function cl_digit(c: int) -> int {
@ -21,26 +20,26 @@ function cl_digit(c: int) -> int {
if c >= 65 { return c - 55 }
return c - 48
}
function cl_hex(h: string) -> int {
function cl_hex(ui_st: mut UiState, h: string) -> int {
var c = 0
let n = len(h)
if n == 3 or n == 4 {
for i in 0 .. 3 { c = c * 256 + cl_digit(h[i]) * 17 }
if n == 4 { cl_alpha = float(cl_digit(h[3]) * 17) / 255.0 }
if n == 4 { ui_st.cl_alpha = float(cl_digit(h[3]) * 17) / 255.0 }
return c
}
for i in 0 .. 3 { c = c * 256 + cl_digit(h[i * 2]) * 16 + cl_digit(h[i * 2 + 1]) }
if n == 8 { cl_alpha = float(cl_digit(h[6]) * 16 + cl_digit(h[7])) / 255.0 }
if n == 8 { ui_st.cl_alpha = float(cl_digit(h[6]) * 16 + cl_digit(h[7])) / 255.0 }
return c
}
# rgb(217, 165, 74) or rgba(0, 0, 0, 0.55)
function cl_rgb(s: string) -> int {
function cl_rgb(ui_st: mut UiState, s: string) -> int {
var open = 0
while open < len(s) and s[open] != 40 { open += 1 }
let parts = lss_split(s[open + 1..len(s) - 1], 44)
var c = 0
for i in 0 .. Math.min(3, len(parts)) { c = c * 256 + Math.clamp(int(el_num(tpl_words(parts[i]))), 0, 255) }
if len(parts) > 3 { cl_alpha = el_num(tpl_words(parts[3])) }
if len(parts) > 3 { ui_st.cl_alpha = el_num(tpl_words(parts[3])) }
return c
}
# a colour as #rrggbb, for a dump or a test

View file

@ -11,45 +11,45 @@ function ct_type(n: UiNode) -> string {
}
function ct_takes_arrows(n: UiNode) -> bool { return ct_type(n) == "range" or ct_type(n) == "select" or ct_type(n) == "number" }
function ct_takes_space(n: UiNode) -> bool { return ct_type(n) == "text" }
function ct_change(n: UiNode, v: Val) -> void {
ui_sound(ct_sound(n))
ui_fire(n, "change", v)
function ct_change(ui_st: mut UiState, n: UiNode, v: Val) -> void {
ui_sound(ui_st, ct_sound(n))
ui_fire(ui_st, n, "change", v)
}
function ct_sound(n: UiNode) -> string {
let s = ui_attr_text(n, "sound")
if s == "" { return "click" }
return s
}
function ct_activate(n: UiNode) -> void {
function ct_activate(ui_st: mut UiState, n: UiNode) -> void {
if not n.enabled { return }
let t = ct_type(n)
if t == "checkbox" { ct_change(n, vl_bool(not ui_attr_on(n, "checked"))) } else if t == "radio" { ct_change(n, ui_attr(n, "value")) } else if t == "select" { ct_step(n, 1) } else if t == "key" { ct_key_start(n) } else if t == "number" { ct_num_commit(n) } else if t == "text" { ct_submit(n) } else if t == "" {
ui_sound(ct_sound(n))
ui_fire(n, "press", Value.null())
if t == "checkbox" { ct_change(ui_st, n, vl_bool(not ui_attr_on(n, "checked"))) } else if t == "radio" { ct_change(ui_st, n, ui_attr(n, "value")) } else if t == "select" { ct_step(ui_st, n, 1) } else if t == "key" { ct_key_start(ui_st, n) } else if t == "number" { ct_num_commit(ui_st, n) } else if t == "text" { ct_submit(ui_st, n) } else if t == "" {
ui_sound(ui_st, ct_sound(n))
ui_fire(ui_st, n, "press", Value.null())
}
}
function ct_step(n: UiNode, d: int) -> void {
function ct_step(ui_st: mut UiState, n: UiNode, d: int) -> void {
let t = ct_type(n)
if t == "select" and n.opts != null and len(n.opts) > 0 {
let at = (ct_option_at(n, n.opts) + d + len(n.opts)) % len(n.opts)
let ov = ui_attr(n.opts[at], "value")
if Value.kind(ov) == 0 { ct_change(n, Value.int(at)) } else { ct_change(n, ov) }
if Value.kind(ov) == 0 { ct_change(ui_st, n, Value.int(at)) } else { ct_change(ui_st, n, ov) }
}
if t == "number" { ct_num_step(n, d) }
if t == "number" { ct_num_step(ui_st, n, d) }
if t == "range" {
var step = Value.as_float(ui_attr(n, "step"))
if step <= 0.0 { step = 1.0 }
ct_range_set(n, Value.as_float(ui_attr(n, "value")) + step * float(d))
ct_range_set(ui_st, n, Value.as_float(ui_attr(n, "value")) + step * float(d))
}
}
function ct_range_set(n: UiNode, v0: float) -> void {
function ct_range_set(ui_st: mut UiState, n: UiNode, v0: float) -> void {
let lo = Value.as_float(ui_attr(n, "min"))
let hi = Value.as_float(ui_attr(n, "max"))
let v = Math.clamp(v0, lo, Math.max(hi, lo))
if v != Value.as_float(ui_attr(n, "value")) { ui_fire(n, "change", Value.float(v)) }
if v != Value.as_float(ui_attr(n, "value")) { ui_fire(ui_st, n, "change", Value.float(v)) }
}
# a range's track pressed or dragged: the value where the pointer is
function ct_press(n: UiNode, x: float, y: float) -> void {
function ct_press(ui_st: mut UiState, n: UiNode, x: float, y: float) -> void {
if ct_type(n) != "range" { return }
for i in 0 .. len(n.children) {
let t = n.children[i]
@ -59,26 +59,26 @@ function ct_press(n: UiNode, x: float, y: float) -> void {
var step = Value.as_float(ui_attr(n, "step"))
if step <= 0.0 { step = 1.0 }
let raw = lo + Math.clamp((x - t.x) / t.cw, 0.0, 1.0) * (hi - lo)
ct_range_set(n, lo + float(int((raw - lo) / step + 0.5)) * step)
ct_range_set(ui_st, n, lo + float(int((raw - lo) / step + 0.5)) * step)
}
}
}
function ct_drag(root: UiNode, key: string, x: float, y: float) -> void {
function ct_drag(ui_st: mut UiState, root: UiNode, key: string, x: float, y: float) -> void {
let n = fr_node(root, key)
if n != null { ct_press(n, x, y) }
if n != null { ct_press(ui_st, n, x, y) }
}
# typing into a focused text field, or the key a key field is waiting for
function ct_typed(n: UiNode) -> void {
function ct_typed(ui_st: mut UiState, n: UiNode) -> void {
let t = ct_type(n)
if t == "key" { ct_key_typed(n) }
if t == "number" { ct_num_typed(n) }
if t != "text" or (in_now.typed == "" and not in_now.backspace) { return }
if t == "key" { ct_key_typed(ui_st, n) }
if t == "number" { ct_num_typed(ui_st, n) }
if t != "text" or (ui_st.in_now.typed == "" and not ui_st.in_now.backspace) { return }
var s = ui_attr_text(n, "value")
if in_now.backspace and len(s) > 0 { s = s[0..ct_char_start(s)] }
if ui_st.in_now.backspace and len(s) > 0 { s = s[0..ct_char_start(s)] }
var max = Value.as_int(ui_attr(n, "maxlength"))
if max <= 0 { max = 1000 }
if len(in_now.typed) > 0 and len(s) < max { s = s + in_now.typed[0..Math.min(len(in_now.typed), max - len(s))] }
ui_fire(n, "change", Value.str(s))
if len(ui_st.in_now.typed) > 0 and len(s) < max { s = s + ui_st.in_now.typed[0..Math.min(len(ui_st.in_now.typed), max - len(s))] }
ui_fire(ui_st, n, "change", Value.str(s))
}
# where the last UTF-8 character starts, so Backspace takes a whole letter
function ct_char_start(s: string) -> int {

View file

@ -32,7 +32,7 @@ function ct_build_bar(n: UiNode) -> void {
fill.w_pct = Math.clamp((Value.as_float(ui_attr(n, "value")) - lo) / (hi - lo), 0.0, 1.0) * 100.0
push(n.children, fill)
}
function ct_build(n: UiNode) -> void {
function ct_build(ui_st: UiState, n: UiNode) -> void {
if n.tag == "progress" or n.tag == "meter" {
ct_build_bar(n)
return
@ -41,10 +41,10 @@ function ct_build(n: UiNode) -> void {
n.focusable = true
let t = ui_attr_text(n, "type")
if n.tag == "select" {
ct_build_select(n)
ct_build_select(ui_st, n)
return
}
ct_label(n)
ct_label(ui_st, n)
if t == "checkbox" or t == "radio" {
let track = ct_part(n, "ui-track", "")
push(track.children, ct_part(n, "ui-knob", ""))
@ -53,8 +53,8 @@ function ct_build(n: UiNode) -> void {
ct_build_range(n)
} else {
let field = ct_part(n, "ui-field", "")
push(field.children, ct_part(n, "ui-value", ct_field_text(n)))
if fc_key == n.key and t != "key" { push(field.children, ct_part(n, "ui-caret", "")) }
push(field.children, ct_part(n, "ui-value", ct_field_text(ui_st, n)))
if ui_st.fc_key == n.key and t != "key" { push(field.children, ct_part(n, "ui-caret", "")) }
push(n.children, field)
}
}
@ -71,10 +71,10 @@ function ct_build_range(n: UiNode) -> void {
push(n.children, track)
push(n.children, ct_part(n, "ui-value", ct_value_text(n)))
}
function ct_build_select(n: UiNode) -> void {
function ct_build_select(ui_st: UiState, n: UiNode) -> void {
let opts = n.children
n.children = new []UiNode
ct_label(n)
ct_label(ui_st, n)
var shown = ""
let at = ct_option_at(n, opts)
if at >= 0 { shown = opts[at].text }
@ -92,9 +92,9 @@ function ct_option_at(n: UiNode, opts: []UiNode) -> int {
}
return -1
}
function ct_field_text(n: UiNode) -> string {
function ct_field_text(ui_st: UiState, n: UiNode) -> string {
let t = ui_attr_text(n, "type")
if t == "key" { return ct_key_text(n) }
if t == "number" { return ct_num_shown(n) }
if t == "key" { return ct_key_text(ui_st, n) }
if t == "number" { return ct_num_shown(ui_st, n) }
return ui_attr_text(n, "value")
}

View file

@ -2,54 +2,51 @@
# the next key pressed is its value - Tab, the arrows and Enter included, which move nothing while it
# listens. Esc stops listening and keeps the value; Backspace, when it is not listening, clears it (0).
# `shown` is what it says for its value (a host's own name for the key), else the keyboard's label.
var ct_capture: string = ""
var ct_capture_at: int = -1 # the frame listening began, whose own key started it
var ct_capture_seen: int = -1 # the last frame that began listening, for ui_capturing
function ct_key_start(n: UiNode) -> void {
ct_capture = n.key
ct_capture_at = an_frame
ct_capture_seen = an_frame
function ct_key_start(ui_st: mut UiState, n: UiNode) -> void {
ui_st.ct_capture = n.key
ui_st.ct_capture_at = ui_st.an_frame
ui_st.ct_capture_seen = ui_st.an_frame
}
# while it listens, the frame's keys are the field's: nothing else reads them (fc_keys)
function ct_key_listening() -> bool { return ct_capture != "" and ct_capture_at != an_frame }
function ct_key_listening(ui_st: UiState) -> bool { return ui_st.ct_capture != "" and ui_st.ct_capture_at != ui_st.an_frame }
# the field listening, given this frame's key; false when there is none to give it to
function ct_key_capture(list: []UiNode) -> bool {
if ct_capture == "" { return false }
ct_capture_seen = an_frame
function ct_key_capture(ui_st: mut UiState, list: []UiNode) -> bool {
if ui_st.ct_capture == "" { return false }
ui_st.ct_capture_seen = ui_st.an_frame
var n: UiNode = null
for i in 0 .. len(list) {
if list[i].key == ct_capture { n = list[i] }
if list[i].key == ui_st.ct_capture { n = list[i] }
}
if n == null {
ct_capture = ""
ui_st.ct_capture = ""
return false
}
if ct_capture_at == an_frame { return true }
if in_now.escape {
ct_capture = ""
if ui_st.ct_capture_at == ui_st.an_frame { return true }
if ui_st.in_now.escape {
ui_st.ct_capture = ""
return true
}
if in_now.key != 0 {
ct_capture = ""
ct_change(n, Value.int(in_now.key))
if ui_st.in_now.key != 0 {
ui_st.ct_capture = ""
ct_change(ui_st, n, Value.int(ui_st.in_now.key))
}
return true
}
# focused, not listening: Backspace unbinds it
function ct_key_typed(n: UiNode) -> void {
if ct_capture != n.key and in_now.backspace { ct_change(n, Value.int(0)) }
function ct_key_typed(ui_st: mut UiState, n: UiNode) -> void {
if ui_st.ct_capture != n.key and ui_st.in_now.backspace { ct_change(ui_st, n, Value.int(0)) }
}
# a press anywhere but the field stops it listening
function ct_key_pressed_away(hit: UiNode) -> void {
if ct_capture != "" and (hit == null or hit.key != ct_capture) { ct_capture = "" }
function ct_key_pressed_away(ui_st: mut UiState, hit: UiNode) -> void {
if ui_st.ct_capture != "" and (hit == null or hit.key != ui_st.ct_capture) { ui_st.ct_capture = "" }
}
function ct_key_text(n: UiNode) -> string {
if ct_capture == n.key { return ui_tr("press a key...") }
if Value.kind(ui_attr(n, "shown")) != 0 { return ui_tr(ui_attr_text(n, "shown")) }
function ct_key_text(ui_st: UiState, n: UiNode) -> string {
if ui_st.ct_capture == n.key { return ui_tr(ui_st, "press a key...") }
if Value.kind(ui_attr(n, "shown")) != 0 { return ui_tr(ui_st, ui_attr_text(n, "shown")) }
let v = Value.as_int(ui_attr(n, "value"))
if v >= UI_MOUSE_LEFT and v <= UI_MOUSE_MIDDLE { return ct_mouse_name(v) }
if v >= UI_MOUSE_LEFT and v <= UI_MOUSE_MIDDLE { return ct_mouse_name(ui_st, v) }
return Input.key_label(v)
}
# a key field is listening: the host should leave the keys alone until it is done (it stays true
# for the frame in which Esc or a key ended it, so that key is not read twice)
export function ui_capturing() -> bool { return ct_capture != "" or ct_capture_seen == an_frame }
export function ui_capturing(ui_st: UiState) -> bool { return ui_st.ct_capture != "" or ui_st.ct_capture_seen == ui_st.an_frame }

View file

@ -4,19 +4,19 @@
export const UI_MOUSE_LEFT: int = 256
export const UI_MOUSE_RIGHT: int = 257
export const UI_MOUSE_MIDDLE: int = 258
function ct_key_mouse(root: UiNode) -> bool {
if ct_capture == "" or ct_capture_at == an_frame { return false }
let b = pe_pressed(in_now, in_last)
function ct_key_mouse(ui_st: mut UiState, root: UiNode) -> bool {
if ui_st.ct_capture == "" or ui_st.ct_capture_at == ui_st.an_frame { return false }
let b = pe_pressed(ui_st.in_now, ui_st.in_last)
if b < 0 { return false }
let n = fr_node(root, ct_capture)
ct_capture = ""
ct_capture_seen = an_frame
fr_press_key = ""
if n != null { ct_change(n, Value.int(UI_MOUSE_LEFT + b)) }
let n = fr_node(root, ui_st.ct_capture)
ui_st.ct_capture = ""
ui_st.ct_capture_seen = ui_st.an_frame
ui_st.fr_press_key = ""
if n != null { ct_change(ui_st, n, Value.int(UI_MOUSE_LEFT + b)) }
return true
}
function ct_mouse_name(v: int) -> string {
if v == UI_MOUSE_RIGHT { return ui_tr("Right click") }
if v == UI_MOUSE_MIDDLE { return ui_tr("Middle click") }
return ui_tr("Left click")
function ct_mouse_name(ui_st: UiState, v: int) -> string {
if v == UI_MOUSE_RIGHT { return ui_tr(ui_st, "Right click") }
if v == UI_MOUSE_MIDDLE { return ui_tr(ui_st, "Middle click") }
return ui_tr(ui_st, "Left click")
}

View file

@ -3,8 +3,6 @@
# the field commits, held between min and max; Backspace takes a digit back (of the value, when it is
# the first key), and the left and right arrows step it. Each commit is one on-change with the
# number: an int when step and the value are whole.
var ct_num_key: string = ""
var ct_num_text: string = ""
function ct_num_bound(n: UiNode, key: string) -> bool { return Value.kind(ui_attr(n, key)) != 0 }
function ct_num_step_of(n: UiNode) -> float {
let s = Value.as_float(ui_attr(n, "step"))
@ -17,57 +15,57 @@ function ct_num_clamp(n: UiNode, v0: float) -> float {
if ct_num_bound(n, "min") { v = Math.max(v, Value.as_float(ui_attr(n, "min"))) }
return v
}
function ct_num_set(n: UiNode, v0: float) -> void {
function ct_num_set(ui_st: mut UiState, n: UiNode, v0: float) -> void {
let v = ct_num_clamp(n, v0)
let step = ct_num_step_of(n)
let was = ui_attr(n, "value")
if Value.kind(was) != 0 and Value.as_float(was) == v { return }
if step == float(int(step)) and v == float(int(v)) { ct_change(n, Value.int(int(v))) } else { ct_change(n, Value.float(v)) }
if step == float(int(step)) and v == float(int(v)) { ct_change(ui_st, n, Value.int(int(v))) } else { ct_change(ui_st, n, Value.float(v)) }
}
# Enter, or the focus leaving it: the draft becomes the value (a draft of nothing changes nothing)
function ct_num_commit(n: UiNode) -> void {
if ct_num_key != n.key { return }
let s = ct_num_text
ct_num_key = ""
function ct_num_commit(ui_st: mut UiState, n: UiNode) -> void {
if ui_st.ct_num_key != n.key { return }
let s = ui_st.ct_num_text
ui_st.ct_num_key = ""
if s == "" or s == "-" or s == "." or s == "-." { return }
ct_num_set(n, el_num(s))
ct_num_set(ui_st, n, el_num(s))
}
function ct_num_step(n: UiNode, d: int) -> void {
function ct_num_step(ui_st: mut UiState, n: UiNode, d: int) -> void {
var base = Value.as_float(ui_attr(n, "value"))
if ct_num_key == n.key and ct_num_text != "" and ct_num_text != "-" { base = el_num(ct_num_text) }
ct_num_key = ""
ct_num_set(n, base + ct_num_step_of(n) * float(d))
if ui_st.ct_num_key == n.key and ui_st.ct_num_text != "" and ui_st.ct_num_text != "-" { base = el_num(ui_st.ct_num_text) }
ui_st.ct_num_key = ""
ct_num_set(ui_st, n, base + ct_num_step_of(n) * float(d))
}
# what was typed this frame, the characters a number can hold; Backspace takes one back
function ct_num_typed(n: UiNode) -> void {
if fc_key != n.key or (in_now.typed == "" and not in_now.backspace) { return }
if ct_num_key != n.key {
ct_num_key = n.key
ct_num_text = ""
if in_now.typed == "" { ct_num_text = ui_attr_text(n, "value") }
function ct_num_typed(ui_st: mut UiState, n: UiNode) -> void {
if ui_st.fc_key != n.key or (ui_st.in_now.typed == "" and not ui_st.in_now.backspace) { return }
if ui_st.ct_num_key != n.key {
ui_st.ct_num_key = n.key
ui_st.ct_num_text = ""
if ui_st.in_now.typed == "" { ui_st.ct_num_text = ui_attr_text(n, "value") }
}
if in_now.backspace and len(ct_num_text) > 0 { ct_num_text = ct_num_text[0..len(ct_num_text) - 1] }
if ui_st.in_now.backspace and len(ui_st.ct_num_text) > 0 { ui_st.ct_num_text = ui_st.ct_num_text[0..len(ui_st.ct_num_text) - 1] }
let step = ct_num_step_of(n)
let lo = Value.as_float(ui_attr(n, "min"))
for i in 0 .. len(in_now.typed) {
let c = in_now.typed[i]
for i in 0 .. len(ui_st.in_now.typed) {
let c = ui_st.in_now.typed[i]
let digit = c >= 48 and c <= 57
let minus = c == 45 and ct_num_text == "" and (not ct_num_bound(n, "min") or lo < 0.0)
let point = c == 46 and step != float(int(step)) and not ui_has(ct_num_text, ".")
if (digit or minus or point) and len(ct_num_text) < 24 { ct_num_text = ct_num_text + in_now.typed[i..i + 1] }
let minus = c == 45 and ui_st.ct_num_text == "" and (not ct_num_bound(n, "min") or lo < 0.0)
let point = c == 46 and step != float(int(step)) and not ui_has(ui_st.ct_num_text, ".")
if (digit or minus or point) and len(ui_st.ct_num_text) < 24 { ui_st.ct_num_text = ui_st.ct_num_text + ui_st.in_now.typed[i..i + 1] }
}
}
# the focus has gone from the field being typed in: what was typed is kept
function ct_num_blur(root: UiNode) -> void {
if ct_num_key == "" or ct_num_key == fc_key { return }
let n = fr_node(root, ct_num_key)
function ct_num_blur(ui_st: mut UiState, root: UiNode) -> void {
if ui_st.ct_num_key == "" or ui_st.ct_num_key == ui_st.fc_key { return }
let n = fr_node(root, ui_st.ct_num_key)
if n == null {
ct_num_key = ""
ui_st.ct_num_key = ""
return
}
ct_num_commit(n)
ct_num_commit(ui_st, n)
}
function ct_num_shown(n: UiNode) -> string {
if ct_num_key == n.key { return ct_num_text }
function ct_num_shown(ui_st: UiState, n: UiNode) -> string {
if ui_st.ct_num_key == n.key { return ui_st.ct_num_text }
return ct_value_text(n)
}

View file

@ -1,16 +1,16 @@
# controls_parts.ludic - what a control shows besides its own parts: its label, with a `note` (a line
# of help under it) in .ui-labels > .ui-label + .ui-note, and a range's or a number's value as text:
# `decimals="1"` places, `format="percent"` its place between min and max, and a `unit` after it
function ct_label(n: UiNode) -> void {
function ct_label(ui_st: UiState, n: UiNode) -> void {
let label = ui_attr_text(n, "label")
let note = ui_attr_text(n, "note")
if note == "" {
if label != "" { push(n.children, ct_part(n, "ui-label", ui_tr(label))) }
if label != "" { push(n.children, ct_part(n, "ui-label", ui_tr(ui_st, label))) }
return
}
let both = ct_part(n, "ui-labels", "")
if label != "" { push(both.children, ct_part(n, "ui-label", ui_tr(label))) }
push(both.children, ct_part(n, "ui-note", ui_tr(note)))
if label != "" { push(both.children, ct_part(n, "ui-label", ui_tr(ui_st, label))) }
push(both.children, ct_part(n, "ui-note", ui_tr(ui_st, note)))
push(n.children, both)
}
function ct_value_text(n: UiNode) -> string {
@ -46,7 +46,7 @@ function ct_fixed(f: float, places: int) -> string {
}
# Enter (or A) on a text field: on-submit with what it holds; the focus stays, and the text too
# unless it says clear-on-submit
function ct_submit(n: UiNode) -> void {
ui_fire(n, "submit", ui_attr(n, "value"))
if ui_attr_on(n, "clear-on-submit") { ui_fire(n, "change", Value.str("")) }
function ct_submit(ui_st: mut UiState, n: UiNode) -> void {
ui_fire(ui_st, n, "submit", ui_attr(n, "value"))
if ui_attr_on(n, "clear-on-submit") { ui_fire(ui_st, n, "change", Value.str("")) }
}

View file

@ -1,24 +1,21 @@
# dev.ludic - a developer's build: templates and stylesheets read again when they change (once a
# second), template errors drawn over the screen, and LUDIC_UI_DUMP=<screen> printing that screen's
# tree once, with every element's box and computed style
var dv_on: bool = false
var dv_frames: int = 0
var dv_dumped: bool = false
export function ui_dev(on: bool) -> void { dv_on = on }
function dv_before() -> void {
if not dv_on { return }
dv_frames += 1
if dv_frames % 60 == 0 { ui_reload() }
export function ui_dev(ui_st: mut UiState, on: bool) -> void { ui_st.dv_on = on }
function dv_before(ui_st: mut UiState) -> void {
if not ui_st.dv_on { return }
ui_st.dv_frames += 1
if ui_st.dv_frames % 60 == 0 { ui_reload(ui_st) }
}
function dv_after(screen: string, root: UiNode) -> void {
if not dv_on { return }
if not dv_dumped and Os.has_env("LUDIC_UI_DUMP") and Os.env("LUDIC_UI_DUMP") == screen {
dv_dumped = true
function dv_after(ui_st: mut UiState, screen: string, root: UiNode) -> void {
if not ui_st.dv_on { return }
if not ui_st.dv_dumped and Os.has_env("LUDIC_UI_DUMP") and Os.env("LUDIC_UI_DUMP") == screen {
ui_st.dv_dumped = true
print(ui_dump(root, 0))
}
if len(ui_errs) == 0 or ui_be == null or ui_be.rect == null { return }
let n = Math.min(len(ui_errs), 8)
let line = ui_px(26.0)
ui_be.rect(0.0, 0.0, 4000.0, line * float(n) + ui_px(8.0), 4194304, 0.85)
for i in 0 .. n { ui_be.text(ui_px(12.0), ui_px(6.0) + line * float(i), ui_errs[len(ui_errs) - n + i], ui_px(18.0), 16767449, 1.0) }
if len(ui_st.ui_errs) == 0 or ui_st.ui_be == null or ui_st.ui_be.rect == null { return }
let n = Math.min(len(ui_st.ui_errs), 8)
let line = ui_px(ui_st, 26.0)
ui_st.ui_be.rect(0.0, 0.0, 4000.0, line * float(n) + ui_px(ui_st, 8.0), 4194304, 0.85)
for i in 0 .. n { ui_st.ui_be.text(ui_px(ui_st, 12.0), ui_px(ui_st, 6.0) + line * float(i), ui_st.ui_errs[len(ui_st.ui_errs) - n + i], ui_px(ui_st, 18.0), 16767449, 1.0) }
}

View file

@ -1,84 +1,83 @@
# draw.ludic - the laid-out nodes through the renderer: the background (rounded by border-radius),
# the border, the content, the children in painting order (a scroll box's clipped to it, with its
# bar), then the outline, which is where a focus ring is drawn
var fr_ga: float = 1.0 # the opacity of everything around this element, which it multiplies
function fr_draw(n: UiNode) -> void {
let outer = fr_ga
fr_ga = outer * n.alpha
fr_draw_in(n)
fr_ga = outer
function fr_draw(ui_st: mut UiState, n: UiNode) -> void {
let outer = ui_st.fr_ga
ui_st.fr_ga = outer * n.alpha
fr_draw_in(ui_st, n)
ui_st.fr_ga = outer
}
function fr_draw_in(n: UiNode) -> void {
function fr_draw_in(ui_st: mut UiState, n: UiNode) -> void {
if n.kind == UI_NATIVE {
n.native.draw(n)
return
}
fr_background(n)
fr_border(n)
if n.kind == UI_TEXT { fr_text(n) }
if n.kind == UI_IMAGE and ui_be.image != null { fr_image(n) }
let clipped = n.kind == UI_SCROLL and ui_be.clip != null
if clipped { fr_clip_push(n.x, n.y, n.cw, n.ch) }
if n.children != null { fr_children(n) }
if clipped { fr_clip_pop() }
if n.kind == UI_SCROLL { sc_bar(n) }
if n.outline_w > 0.0 { fr_outline(n) }
fr_background(ui_st, n)
fr_border(ui_st, n)
if n.kind == UI_TEXT { fr_text(ui_st, n) }
if n.kind == UI_IMAGE and ui_st.ui_be.image != null { fr_image(ui_st, n) }
let clipped = n.kind == UI_SCROLL and ui_st.ui_be.clip != null
if clipped { fr_clip_push(ui_st, n.x, n.y, n.cw, n.ch) }
if n.children != null { fr_children(ui_st, n) }
if clipped { fr_clip_pop(ui_st) }
if n.kind == UI_SCROLL { sc_bar(ui_st, n) }
if n.outline_w > 0.0 { fr_outline(ui_st, n) }
}
# the shadow, then the background: the colour (rounded), or a border-image's nine-slice - drawn as
# painted, as CSS draws one, or tinted by a background colour when there is one (so `transparent`,
# or any colour with no alpha, leaves nothing to see) - then a picture over it
function fr_background(n: UiNode) -> void {
if n.sh_c >= 0 { fr_fill(n, n.x + n.sh_x, n.y + n.sh_y, n.sh_c, fr_ga * n.sh_a) }
function fr_background(ui_st: mut UiState, n: UiNode) -> void {
if n.sh_c >= 0 { fr_fill(ui_st, n, n.x + n.sh_x, n.y + n.sh_y, n.sh_c, ui_st.fr_ga * n.sh_a) }
if n.bg >= 0 or n.bi_src != "" {
var c = n.bg
var a = fr_ga * n.bg_a
var a = ui_st.fr_ga * n.bg_a
if c < 0 {
c = 16777215
a = fr_ga
a = ui_st.fr_ga
}
if a > 0.0 { fr_fill(n, n.x, n.y, c, a) }
if a > 0.0 { fr_fill(ui_st, n, n.x, n.y, c, a) }
}
if n.ex != null and n.ex.grad != null { gr_draw(n.ex.grad, n.x, n.y, n.cw, n.ch, fr_ga) }
if n.bg_img != "" and ui_be.image != null { ui_be.image(n.bg_img, n.x, n.y, n.cw, n.ch, 16777215, fr_ga) }
if n.ex != null and n.ex.grad != null { gr_draw(ui_st, n.ex.grad, n.x, n.y, n.cw, n.ch, ui_st.fr_ga) }
if n.bg_img != "" and ui_st.ui_be.image != null { ui_st.ui_be.image(n.bg_img, n.x, n.y, n.cw, n.ch, 16777215, ui_st.fr_ga) }
}
function fr_fill(n: UiNode, x: float, y: float, c: int, a: float) -> void {
if n.bi_src != "" and ui_be.nine != null {
ui_be.nine(n.bi_src, n.bi_slice, x, y, n.cw, n.ch, n.bi_width, c, a)
function fr_fill(ui_st: UiState, n: UiNode, x: float, y: float, c: int, a: float) -> void {
if n.bi_src != "" and ui_st.ui_be.nine != null {
ui_st.ui_be.nine(n.bi_src, n.bi_slice, x, y, n.cw, n.ch, n.bi_width, c, a)
return
}
fr_box(x, y, n.cw, n.ch, n.radius, c, a)
fr_box(ui_st, x, y, n.cw, n.ch, n.radius, c, a)
}
function fr_box(x: float, y: float, w: float, h: float, r: float, c: int, a: float) -> void {
if r > 0.0 and ui_be.round != null { ui_be.round(x, y, w, h, r, c, a) } else if ui_be.rect != null { ui_be.rect(x, y, w, h, c, a) }
function fr_box(ui_st: UiState, x: float, y: float, w: float, h: float, r: float, c: int, a: float) -> void {
if r > 0.0 and ui_st.ui_be.round != null { ui_st.ui_be.round(x, y, w, h, r, c, a) } else if ui_st.ui_be.rect != null { ui_st.ui_be.rect(x, y, w, h, c, a) }
}
function fr_border(n: UiNode) -> void {
if n.bt + n.br + n.bb + n.bl <= 0.0 or ui_be.rect == null { return }
function fr_border(ui_st: UiState, n: UiNode) -> void {
if n.bt + n.br + n.bb + n.bl <= 0.0 or ui_st.ui_be.rect == null { return }
var c = n.border_c
var ca = fr_ga * n.border_a
var ca = ui_st.fr_ga * n.border_a
if c < 0 {
c = n.rfg
ca = fr_ga * n.rfg_a
ca = ui_st.fr_ga * n.rfg_a
}
if n.bt > 0.0 { ui_be.rect(n.x, n.y, n.cw, n.bt, c, ca) }
if n.bb > 0.0 { ui_be.rect(n.x, n.y + n.ch - n.bb, n.cw, n.bb, c, ca) }
if n.bl > 0.0 { ui_be.rect(n.x, n.y + n.bt, n.bl, n.ch - n.bt - n.bb, c, ca) }
if n.br > 0.0 { ui_be.rect(n.x + n.cw - n.br, n.y + n.bt, n.br, n.ch - n.bt - n.bb, c, ca) }
if n.bt > 0.0 { ui_st.ui_be.rect(n.x, n.y, n.cw, n.bt, c, ca) }
if n.bb > 0.0 { ui_st.ui_be.rect(n.x, n.y + n.ch - n.bb, n.cw, n.bb, c, ca) }
if n.bl > 0.0 { ui_st.ui_be.rect(n.x, n.y + n.bt, n.bl, n.ch - n.bt - n.bb, c, ca) }
if n.br > 0.0 { ui_st.ui_be.rect(n.x + n.cw - n.br, n.y + n.bt, n.br, n.ch - n.bt - n.bb, c, ca) }
}
function fr_frame(x: float, y: float, w: float, h: float, b: float, c: int, a: float) -> void {
ui_be.rect(x, y, w, b, c, a)
ui_be.rect(x, y + h - b, w, b, c, a)
ui_be.rect(x, y + b, b, h - b * 2.0, c, a)
ui_be.rect(x + w - b, y + b, b, h - b * 2.0, c, a)
function fr_frame(ui_st: UiState, x: float, y: float, w: float, h: float, b: float, c: int, a: float) -> void {
ui_st.ui_be.rect(x, y, w, b, c, a)
ui_st.ui_be.rect(x, y + h - b, w, b, c, a)
ui_st.ui_be.rect(x, y + b, b, h - b * 2.0, c, a)
ui_st.ui_be.rect(x + w - b, y + b, b, h - b * 2.0, c, a)
}
# an outline follows the box's corners, as CSS's does: rounded by border-radius, grown by its offset
function fr_outline(n: UiNode) -> void {
function fr_outline(ui_st: UiState, n: UiNode) -> void {
var c = n.outline_c
if c < 0 { c = n.rfg }
let o = n.outline_off + n.outline_w
let a = fr_ga * n.outline_a
if n.radius > 0.0 and ui_be.ring != null {
ui_be.ring(n.x - o, n.y - o, n.cw + o * 2.0, n.ch + o * 2.0, n.radius + o, n.outline_w, c, a)
let a = ui_st.fr_ga * n.outline_a
if n.radius > 0.0 and ui_st.ui_be.ring != null {
ui_st.ui_be.ring(n.x - o, n.y - o, n.cw + o * 2.0, n.ch + o * 2.0, n.radius + o, n.outline_w, c, a)
return
}
if ui_be.rect != null { fr_frame(n.x - o, n.y - o, n.cw + o * 2.0, n.ch + o * 2.0, n.outline_w, c, a) }
if ui_st.ui_be.rect != null { fr_frame(ui_st, n.x - o, n.y - o, n.cw + o * 2.0, n.ch + o * 2.0, n.outline_w, c, a) }
}

View file

@ -1,23 +1,23 @@
# draw_content.ludic - what is inside a box drawn: a text's lines, placed by text-align, each over its
# text-shadow, and a picture
# a text's lines (one, or the wrapped ones), each placed by text-align, the last cut by an ellipsis
function fr_text(n: UiNode) -> void {
function fr_text(ui_st: mut UiState, n: UiNode) -> void {
let lh = ly_line_h(n)
let y = n.y + n.pt + n.bt + (lh - n.rsize * 1.25) / 2.0
let a = n.rfg_a * fr_ga
let a = n.rfg_a * ui_st.fr_ga
let sh = fr_text_shadow(n)
if n.lines == null {
var s = tf_fit(n, n.text)
if n.ellipsis { s = ly_ellipsis(s, tf_size, n.cw - ly_hin(n)) }
fr_line(n, sh, fr_line_x(n, s, tf_size), y, s, a, tf_size)
var s = tf_fit(ui_st, n, n.text)
if n.ellipsis { s = ly_ellipsis(ui_st, s, ui_st.tf_size, n.cw - ly_hin(n)) }
fr_line(ui_st, n, sh, fr_line_x(ui_st, n, s, ui_st.tf_size), y, s, a, ui_st.tf_size)
return
}
for i in 0 .. len(n.lines) { fr_line(n, sh, fr_line_x(n, n.lines[i], n.rsize), y + float(i) * lh, n.lines[i], a, n.rsize) }
for i in 0 .. len(n.lines) { fr_line(ui_st, n, sh, fr_line_x(ui_st, n, n.lines[i], n.rsize), y + float(i) * lh, n.lines[i], a, n.rsize) }
}
# one line, over its shadow when it has one
function fr_line(n: UiNode, sh: UiNodeX, x: float, y: float, s: string, a: float, size: float) -> void {
if sh != null { ui_be.text(x + sh.tsh_x, y + sh.tsh_y, s, size, sh.tsh_c, sh.tsh_a * fr_ga) }
ui_be.text(x, y, s, size, n.rfg, a)
function fr_line(ui_st: UiState, n: UiNode, sh: UiNodeX, x: float, y: float, s: string, a: float, size: float) -> void {
if sh != null { ui_st.ui_be.text(x + sh.tsh_x, y + sh.tsh_y, s, size, sh.tsh_c, sh.tsh_a * ui_st.fr_ga) }
ui_st.ui_be.text(x, y, s, size, n.rfg, a)
}
# text-shadow is inherited: the nearest element that says it, or null (none)
function fr_text_shadow(n: UiNode) -> UiNodeX {
@ -32,35 +32,35 @@ function fr_text_shadow(n: UiNode) -> UiNodeX {
return null
}
# the opacity a native's draw is at: every group opacity around it, its own included
export function ui_opacity() -> float { return fr_ga }
export function ui_opacity(ui_st: UiState) -> float { return ui_st.fr_ga }
# where a line starts: text-align within the room the box gives it
function fr_line_x(n: UiNode, s: string, size: float) -> float {
function fr_line_x(ui_st: UiState, n: UiNode, s: string, size: float) -> float {
let x = n.x + n.pl + n.bl
if n.talign == UI_START { return x }
let spare = n.cw - ly_hin(n) - ly_text_w(s, size)
let spare = n.cw - ly_hin(n) - ly_text_w(ui_st, s, size)
if n.talign == UI_CENTER { return x + spare / 2.0 }
return x + spare
}
# an <img>: a picture file drawn as it is, as a browser draws one (unless the img sets its own
# color); a cell of an atlas ("icon:tent") is a glyph, and takes the color around it as text does
function fr_image(n: UiNode) -> void {
function fr_image(ui_st: mut UiState, n: UiNode) -> void {
var c = 16777215
var a = fr_ga
var a = ui_st.fr_ga
if n.fg != UI_INHERIT or fr_is_cell(n.src) {
c = n.rfg
a = fr_ga * n.rfg_a
a = ui_st.fr_ga * n.rfg_a
}
var nat_w = 0.0
var nat_h = 0.0
if ui_be.image_w != null and ui_be.image_h != null {
nat_w = ui_be.image_w(n.src)
nat_h = ui_be.image_h(n.src)
if ui_st.ui_be.image_w != null and ui_st.ui_be.image_h != null {
nat_w = ui_st.ui_be.image_w(n.src)
nat_h = ui_st.ui_be.image_h(n.src)
}
let r = ui_object_fit(n, nat_w, nat_h)
let clipped = ui_be.clip != null and (r[2] > n.cw - ly_hin(n) or r[3] > n.ch - ly_vin(n))
if clipped { fr_clip_push(n.x + n.pl + n.bl, n.y + n.pt + n.bt, n.cw - ly_hin(n), n.ch - ly_vin(n)) }
ui_be.image(n.src, r[0], r[1], r[2], r[3], c, a)
if clipped { fr_clip_pop() }
let clipped = ui_st.ui_be.clip != null and (r[2] > n.cw - ly_hin(n) or r[3] > n.ch - ly_vin(n))
if clipped { fr_clip_push(ui_st, n.x + n.pl + n.bl, n.y + n.pt + n.bt, n.cw - ly_hin(n), n.ch - ly_vin(n)) }
ui_st.ui_be.image(n.src, r[0], r[1], r[2], r[3], c, a)
if clipped { fr_clip_pop(ui_st) }
}
# prefix:name, and not a path (C:/... is a drive, not an atlas)
function fr_is_cell(src: string) -> bool {

View file

@ -8,7 +8,7 @@ export property UiGradient {
at: []float = null # each stop's place, 0 to 1
}
function gr_is(s: string) -> bool { return len(s) > 16 and s[0..16] == "linear-gradient(" }
function gr_parse(s0: string) -> UiGradient {
function gr_parse(ui_st: mut UiState, s0: string) -> UiGradient {
let s = tpl_words(s0)
let parts = el_commas(s[16..len(s) - 1])
let g = new UiGradient
@ -26,8 +26,8 @@ function gr_parse(s0: string) -> UiGradient {
at = el_num(last) / 100.0
col = tpl_words(col[0..len(col) - len(last)])
}
push(g.cols, ui_color(col))
push(g.alphas, cl_alpha)
push(g.cols, ui_color(ui_st, col))
push(g.alphas, ui_st.cl_alpha)
push(g.at, at)
}
for i in 0 .. len(g.at) {
@ -46,8 +46,8 @@ function gr_dir(g: UiGradient, w: string) -> bool {
return true
}
# the gradient over the box, a band every two pixels at most
function gr_draw(g: UiGradient, x: float, y: float, w: float, h: float, a: float) -> void {
if ui_be.rect == null or len(g.cols) == 0 { return }
function gr_draw(ui_st: mut UiState, g: UiGradient, x: float, y: float, w: float, h: float, a: float) -> void {
if ui_st.ui_be.rect == null or len(g.cols) == 0 { return }
let across = g.dir >= 2
var span = h
if across { span = w }
@ -57,20 +57,19 @@ function gr_draw(g: UiGradient, x: float, y: float, w: float, h: float, a: float
let e1 = Math.floor(span * float(i + 1) / float(bands) + 0.5)
var t = (e0 + e1) / 2.0 / span
if g.dir == 1 or g.dir == 3 { t = 1.0 - t }
let c = gr_at(g, t)
if across { ui_be.rect(x + e0, y, e1 - e0, h, c, gr_a * a) } else { ui_be.rect(x, y + e0, w, e1 - e0, c, gr_a * a) }
let c = gr_at(ui_st, g, t)
if across { ui_st.ui_be.rect(x + e0, y, e1 - e0, h, c, ui_st.gr_a * a) } else { ui_st.ui_be.rect(x, y + e0, w, e1 - e0, c, ui_st.gr_a * a) }
}
}
var gr_a: float = 1.0
# the colour at `t` between the stops around it, its alpha in gr_a
function gr_at(g: UiGradient, t: float) -> int {
function gr_at(ui_st: mut UiState, g: UiGradient, t: float) -> int {
var k = 0
while k + 1 < len(g.at) and g.at[k + 1] < t { k += 1 }
let j = Math.min(k + 1, len(g.at) - 1)
var f = 0.0
if g.at[j] > g.at[k] { f = Math.clamp((t - g.at[k]) / (g.at[j] - g.at[k]), 0.0, 1.0) }
if t <= g.at[0] { f = 0.0 }
gr_a = g.alphas[k] + (g.alphas[j] - g.alphas[k]) * f
ui_st.gr_a = g.alphas[k] + (g.alphas[j] - g.alphas[k]) * f
return cl_mix(g.cols[k], g.cols[j], f)
}
function cl_mix(a: int, b: int, f: float) -> int {

View file

@ -1,35 +1,34 @@
# draw_order.ludic - painting order, and clipping that nests
var fr_clips: []float = new []float
function fr_clip_push(x: float, y: float, w: float, h: float) -> void {
function fr_clip_push(ui_st: mut UiState, x: float, y: float, w: float, h: float) -> void {
var x0 = x
var y0 = y
var x1 = x + w
var y1 = y + h
let n = len(fr_clips)
let n = len(ui_st.fr_clips)
if n >= 4 {
x0 = Math.max(x0, fr_clips[n - 4])
y0 = Math.max(y0, fr_clips[n - 3])
x1 = Math.min(x1, fr_clips[n - 2])
y1 = Math.min(y1, fr_clips[n - 1])
x0 = Math.max(x0, ui_st.fr_clips[n - 4])
y0 = Math.max(y0, ui_st.fr_clips[n - 3])
x1 = Math.min(x1, ui_st.fr_clips[n - 2])
y1 = Math.min(y1, ui_st.fr_clips[n - 1])
}
push(fr_clips, x0)
push(fr_clips, y0)
push(fr_clips, x1)
push(fr_clips, y1)
ui_be.clip(x0, y0, Math.max(x1 - x0, 0.0), Math.max(y1 - y0, 0.0))
push(ui_st.fr_clips, x0)
push(ui_st.fr_clips, y0)
push(ui_st.fr_clips, x1)
push(ui_st.fr_clips, y1)
ui_st.ui_be.clip(x0, y0, Math.max(x1 - x0, 0.0), Math.max(y1 - y0, 0.0))
}
function fr_clip_pop() -> void {
function fr_clip_pop(ui_st: mut UiState) -> void {
var keep = new []float
for i in 0 .. len(fr_clips) - 4 { push(keep, fr_clips[i]) }
fr_clips = keep
let n = len(fr_clips)
if n >= 4 { ui_be.clip(fr_clips[n - 4], fr_clips[n - 3], fr_clips[n - 2] - fr_clips[n - 4], fr_clips[n - 1] - fr_clips[n - 3]) } else if ui_be.unclip != null { ui_be.unclip() }
for i in 0 .. len(ui_st.fr_clips) - 4 { push(keep, ui_st.fr_clips[i]) }
ui_st.fr_clips = keep
let n = len(ui_st.fr_clips)
if n >= 4 { ui_st.ui_be.clip(ui_st.fr_clips[n - 4], ui_st.fr_clips[n - 3], ui_st.fr_clips[n - 2] - ui_st.fr_clips[n - 4], ui_st.fr_clips[n - 1] - ui_st.fr_clips[n - 3]) } else if ui_st.ui_be.unclip != null { ui_st.ui_be.unclip() }
}
# the children in painting order: those in the flow first, then positioned ones by z-index
function fr_children(n: UiNode) -> void {
function fr_children(ui_st: mut UiState, n: UiNode) -> void {
var positioned = 0
for i in 0 .. len(n.children) {
if n.children[i].popover { push(pop_list, n.children[i]) } else if n.children[i].pos == 0 { fr_draw(n.children[i]) } else { positioned += 1 }
if n.children[i].popover { push(ui_st.pop_list, n.children[i]) } else if n.children[i].pos == 0 { fr_draw(ui_st, n.children[i]) } else { positioned += 1 }
}
if positioned == 0 { return }
var lo = 1000000
@ -42,7 +41,7 @@ function fr_children(n: UiNode) -> void {
}
for z in lo .. hi + 1 {
for i in 0 .. len(n.children) {
if n.children[i].pos != 0 and not n.children[i].popover and n.children[i].z == z { fr_draw(n.children[i]) }
if n.children[i].pos != 0 and not n.children[i].popover and n.children[i].z == z { fr_draw(ui_st, n.children[i]) }
}
}
}

View file

@ -2,8 +2,8 @@
# and attributes (kept for selectors, applied after every rule), its text, and its children
const EL_BOXES: string = " col div progress meter section header footer nav main article aside ul ol li form box "
const EL_TEXTS: string = " text span p label h1 h2 h3 h4 h5 h6 strong em small b i a option "
function bd_elem(k: UiTpl, e: UiEnv, key: string) -> UiNode {
ui_at = k.where
function bd_elem(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string) -> UiNode {
ui_st.ui_at = k.where
let n = new UiNode
n.key = key
n.tag = k.tag
@ -15,34 +15,34 @@ function bd_elem(k: UiTpl, e: UiEnv, key: string) -> UiNode {
n.akeys = new []string
n.avals = new []string
n.vals = new []Val
el_kind(n, k.tag)
for i in 0 .. len(k.keys) { el_keep(n, k.keys[i], ui_eval(k.vals[i], e)) }
n.hovered = fr_was_hovered(key)
el_kind(ui_st, n, k.tag)
for i in 0 .. len(k.keys) { el_keep(n, k.keys[i], ui_eval(ui_st, k.vals[i], e)) }
n.hovered = fr_was_hovered(ui_st, key)
n.on_names = k.on_keys
n.on_acts = k.on_acts
for i in 0 .. len(k.on_keys) {
if k.on_keys[i] == "press" { n.act = k.on_acts[i] }
}
lc_note(key, k.on_keys, k.on_acts, e)
let was_no = tr_no
lc_note(ui_st, key, k.on_keys, k.on_acts, e)
let was_no = ui_st.tr_no
let say = ui_attr_text(n, "translate")
if say == "no" { tr_no = true } else if say == "yes" { tr_no = false }
bd_kids(k.kids, e, key, n.children)
if k.text != null { el_text(n, ui_tr(ui_eval_text(k.text, e))) }
if say == "no" { ui_st.tr_no = true } else if say == "yes" { ui_st.tr_no = false }
bd_kids(ui_st, k.kids, e, key, n.children)
if k.text != null { el_text(n, ui_tr(ui_st, ui_eval_text(ui_st, k.text, e))) }
el_inline(n)
if k.tag == "input" or k.tag == "select" or k.tag == "progress" or k.tag == "meter" { ct_build(n) }
tr_no = was_no
if k.tag == "input" or k.tag == "select" or k.tag == "progress" or k.tag == "meter" { ct_build(ui_st, n) }
ui_st.tr_no = was_no
return n
}
function el_kind(n: UiNode, tag: string) -> void {
let nv = nt_find(tag)
function el_kind(ui_st: mut UiState, n: UiNode, tag: string) -> void {
let nv = nt_find(ui_st, tag)
if nv != null {
n.kind = UI_NATIVE
n.native = nv
} else if lk_in(EL_TEXTS, tag) { n.kind = UI_TEXT } else if tag == "row" { n.dir = UI_ROW } else if tag == "button" {
n.dir = UI_ROW
n.focusable = true
} else if tag == "input" or tag == "select" { n.focusable = true } else if tag == "spacer" { n.kind = UI_SPACER } else if tag == "scroll" { n.kind = UI_SCROLL } else if tag == "img" { n.kind = UI_IMAGE } else if tag == "hr" { n.kind = UI_RULE } else if not lk_in(EL_BOXES, tag) { ui_err(`<{tag}>: there is no such element`) }
} else if tag == "input" or tag == "select" { n.focusable = true } else if tag == "spacer" { n.kind = UI_SPACER } else if tag == "scroll" { n.kind = UI_SCROLL } else if tag == "img" { n.kind = UI_IMAGE } else if tag == "hr" { n.kind = UI_RULE } else if not lk_in(EL_BOXES, tag) { ui_err(ui_st, `<{tag}>: there is no such element`) }
}
# a box with words in it holds them as a text of its own, as <div>words</div> does
function el_text(n: UiNode, s: string) -> void {

View file

@ -15,8 +15,6 @@ export property UiEnv {
ctx: UiEnv = null # what a <provide> above gave, seen through components too
inst: UiInst = null # the Ludic component whose template this is
}
var ui_view: UiView = null
var ui_errs: []string = new []string
function env_new(parent: UiEnv) -> UiEnv {
let e = new UiEnv
@ -33,7 +31,7 @@ function env_bind(e: UiEnv, name: string, v: Val) -> void {
push(e.names, name)
push(e.vals, v)
}
function env_get(e: UiEnv, name: string) -> Val {
function env_get(ui_st: mut UiState, e: UiEnv, name: string) -> Val {
var at = e
while at != null {
var i = len(at.names) - 1
@ -52,11 +50,11 @@ function env_get(e: UiEnv, name: string) -> Val {
cx = cx.parent
}
if e.model != null and Value.has(e.model, name) != 0 { return Value.get(e.model, name) }
ui_err(`no {name} here: not a loop's, a prop, a state or in the view`)
ui_err(ui_st, `no {name} here: not a loop's, a prop, a state or in the view`)
return Value.null()
}
# `set name = value`: the nearest component's state of that name, or <state> that holds it
function env_set(e: UiEnv, name: string, v: Val) -> void {
function env_set(ui_st: mut UiState, e: UiEnv, name: string, v: Val) -> void {
var at = e
while at != null {
if at.inst != null {
@ -70,18 +68,17 @@ function env_set(e: UiEnv, name: string, v: Val) -> void {
}
at = at.parent
}
ui_err(`set {name}: no state here holds it - declare it with <state {name}="..."/>`)
ui_err(ui_st, `set {name}: no state here holds it - declare it with <state {name}="..."/>`)
}
# an error is said once, however many frames make it, and names the template line being built
var ui_at: string = ""
# said by the runtime, or by a host about something a template asked for
export function ui_err(msg0: string) -> void {
export function ui_err(ui_st: mut UiState, msg0: string) -> void {
var msg = msg0
if ui_at != "" and not ui_has(msg, ".xml:") { msg = `{ui_at}: {msg}` }
for i in 0 .. len(ui_errs) {
if ui_errs[i] == msg { return }
if ui_st.ui_at != "" and not ui_has(msg, ".xml:") { msg = `{ui_st.ui_at}: {msg}` }
for i in 0 .. len(ui_st.ui_errs) {
if ui_st.ui_errs[i] == msg { return }
}
push(ui_errs, msg)
push(ui_st.ui_errs, msg)
print(`ui: {msg}`)
}
function ui_has(s: string, part: string) -> bool {
@ -92,4 +89,4 @@ function ui_has(s: string, part: string) -> bool {
return false
}
# every error said so far - for a host to show them where a developer will see them
export function ui_errors() -> []string { return ui_errs }
export function ui_errors(ui_st: UiState) -> []string { return ui_st.ui_errs }

View file

@ -1,41 +1,41 @@
# eval.ludic - an expression's value, here
function ui_eval(x: UiExpr, e: UiEnv) -> Val {
function ui_eval(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> Val {
if x.op == X_LIT { return x.v }
if x.op == X_NAME { return env_get(e, x.s) }
if x.op == X_NAME { return env_get(ui_st, e, x.s) }
if x.op == X_MEMBER {
let o = ui_eval(x.a, e)
let o = ui_eval(ui_st, x.a, e)
if Value.kind(o) == 6 and Value.has(o, x.s) != 0 { return Value.get(o, x.s) }
if Value.kind(o) == 5 and x.s == "length" { return Value.int(Value.count(o)) }
ui_err(`.{x.s}: there is no such field`)
ui_err(ui_st, `.{x.s}: there is no such field`)
return Value.null()
}
if x.op == X_INDEX { return Value.at(ui_eval(x.a, e), Value.as_int(ui_eval(x.b, e))) }
if x.op == X_CALL { return ev_call(x, e) }
if x.op == X_NOT { return vl_bool(not vl_truthy(ui_eval(x.a, e))) }
if x.op == X_NEG { return vl_arith("-", Value.int(0), ui_eval(x.a, e)) }
if x.op == X_INDEX { return Value.at(ui_eval(ui_st, x.a, e), Value.as_int(ui_eval(ui_st, x.b, e))) }
if x.op == X_CALL { return ev_call(ui_st, x, e) }
if x.op == X_NOT { return vl_bool(not vl_truthy(ui_eval(ui_st, x.a, e))) }
if x.op == X_NEG { return vl_arith("-", Value.int(0), ui_eval(ui_st, x.a, e)) }
if x.op == X_AND {
if not vl_truthy(ui_eval(x.a, e)) { return vl_bool(false) }
return vl_bool(vl_truthy(ui_eval(x.b, e)))
if not vl_truthy(ui_eval(ui_st, x.a, e)) { return vl_bool(false) }
return vl_bool(vl_truthy(ui_eval(ui_st, x.b, e)))
}
if x.op == X_OR {
if vl_truthy(ui_eval(x.a, e)) { return vl_bool(true) }
return vl_bool(vl_truthy(ui_eval(x.b, e)))
if vl_truthy(ui_eval(ui_st, x.a, e)) { return vl_bool(true) }
return vl_bool(vl_truthy(ui_eval(ui_st, x.b, e)))
}
if x.op == X_COND {
if vl_truthy(ui_eval(x.a, e)) { return ui_eval(x.b, e) }
return ui_eval(x.c, e)
if vl_truthy(ui_eval(ui_st, x.a, e)) { return ui_eval(ui_st, x.b, e) }
return ui_eval(ui_st, x.c, e)
}
if x.op == X_TEXT {
var out = ""
for i in 0 .. len(x.args) { out = out + vl_text(ui_eval(x.args[i], e)) }
for i in 0 .. len(x.args) { out = out + vl_text(ui_eval(ui_st, x.args[i], e)) }
return Value.str(out)
}
return vl_binary(x.s, ui_eval(x.a, e), ui_eval(x.b, e))
return vl_binary(x.s, ui_eval(ui_st, x.a, e), ui_eval(ui_st, x.b, e))
}
# a call: len() and range() are the template's own, anything else is the view's
function ev_call(x: UiExpr, e: UiEnv) -> Val {
function ev_call(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> Val {
let args = Value.list()
for i in 0 .. len(x.args) { Value.add(args, ui_eval(x.args[i], e)) }
for i in 0 .. len(x.args) { Value.add(args, ui_eval(ui_st, x.args[i], e)) }
if x.s == "len" and len(x.args) == 1 { return Value.int(Value.count(Value.at(args, 0))) }
if x.s == "range" and len(x.args) == 1 {
let out = Value.list()
@ -50,11 +50,11 @@ function ev_call(x: UiExpr, e: UiEnv) -> Val {
}
at = at.parent
}
if ui_view == null or ui_view.call == null {
ui_err(`{x.s}(): there is no function or event of that name here`)
if ui_st.ui_view == null or ui_st.ui_view.call == null {
ui_err(ui_st, `{x.s}(): there is no function or event of that name here`)
return Value.null()
}
return ui_view.call(x.s, args)
return ui_st.ui_view.call(x.s, args)
}
function ui_eval_text(x: UiExpr, e: UiEnv) -> string { return vl_text(ui_eval(x, e)) }
function ui_eval_bool(x: UiExpr, e: UiEnv) -> bool { return vl_truthy(ui_eval(x, e)) }
function ui_eval_text(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> string { return vl_text(ui_eval(ui_st, x, e)) }
function ui_eval_bool(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> bool { return vl_truthy(ui_eval(ui_st, x, e)) }

View file

@ -1,75 +1,72 @@
# focus.ludic - the keyboard's place: focus moves through the controls in document order (Tab and
# Shift+Tab, or the arrows), Enter and Space activate what has it, and a pointer press focuses what
# it presses; a key field that is listening has every key to itself. A key that is gone from the screen leaves nothing focused.
var fc_key: string = ""
var fc_visible: bool = false # focus came from the keyboard: :focus-visible, and the ring
var fc_list: []UiNode = new []UiNode
function fc_collect(n: UiNode) -> void {
if n.focusable and n.enabled { push(fc_list, n) }
for i in 0 .. len(n.children) { fc_collect(n.children[i]) }
function fc_collect(ui_st: mut UiState, n: UiNode) -> void {
if n.focusable and n.enabled { push(ui_st.fc_list, n) }
for i in 0 .. len(n.children) { fc_collect(ui_st, n.children[i]) }
}
function fc_find() -> int {
for i in 0 .. len(fc_list) {
if fc_list[i].key == fc_key { return i }
function fc_find(ui_st: UiState) -> int {
for i in 0 .. len(ui_st.fc_list) {
if ui_st.fc_list[i].key == ui_st.fc_key { return i }
}
return -1
}
function fc_move(step: int) -> void {
if len(fc_list) == 0 { return }
var at = fc_find()
function fc_move(ui_st: mut UiState, step: int) -> void {
if len(ui_st.fc_list) == 0 { return }
var at = fc_find(ui_st)
if at < 0 {
at = 0
if step < 0 { at = len(fc_list) - 1 }
} else { at = (at + step + len(fc_list)) % len(fc_list) }
fc_key = fc_list[at].key
fc_visible = true
if step < 0 { at = len(ui_st.fc_list) - 1 }
} else { at = (at + step + len(ui_st.fc_list)) % len(ui_st.fc_list) }
ui_st.fc_key = ui_st.fc_list[at].key
ui_st.fc_visible = true
}
# the keys, after the tree is laid out: moving, then acting on what has the focus
function fc_keys(root: UiNode) -> void {
fc_list = new []UiNode
function fc_keys(ui_st: mut UiState, root: UiNode) -> void {
ui_st.fc_list = new []UiNode
let top = pp_top(root)
if top != null { fc_collect(top) } else { fc_collect(root) }
if ct_key_capture(fc_list) { return }
if top != null and in_now.escape { ui_fire(top, "close", Value.null()) }
let i = in_now
if fc_find() < 0 { fc_autofocus() }
let at = fc_find()
if top != null { fc_collect(ui_st, top) } else { fc_collect(ui_st, root) }
if ct_key_capture(ui_st, ui_st.fc_list) { return }
if top != null and ui_st.in_now.escape { ui_fire(ui_st, top, "close", Value.null()) }
let i = ui_st.in_now
if fc_find(ui_st) < 0 { fc_autofocus(ui_st) }
let at = fc_find(ui_st)
var n: UiNode = null
if at >= 0 { n = fc_list[at] }
if i.tab and i.shift { fc_move(-1) } else if i.tab { fc_move(1) }
if at >= 0 { n = ui_st.fc_list[at] }
if i.tab and i.shift { fc_move(ui_st, -1) } else if i.tab { fc_move(ui_st, 1) }
let horizontal = n != null and ct_takes_arrows(n)
if i.up or (i.left and not horizontal) { fc_move(-1) }
if i.down_key or (i.right and not horizontal) { fc_move(1) }
ct_num_blur(root)
if i.up or (i.left and not horizontal) { fc_move(ui_st, -1) }
if i.down_key or (i.right and not horizontal) { fc_move(ui_st, 1) }
ct_num_blur(ui_st, root)
if n == null { return }
fc_hold(root, n)
if i.enter or (i.space and not ct_takes_space(n)) { ct_activate(n) }
if horizontal and i.left { ct_step(n, -1) }
if horizontal and i.right { ct_step(n, 1) }
ct_typed(n)
fc_hold(ui_st, root, n)
if i.enter or (i.space and not ct_takes_space(n)) { ct_activate(ui_st, n) }
if horizontal and i.left { ct_step(ui_st, n, -1) }
if horizontal and i.right { ct_step(ui_st, n, 1) }
ct_typed(ui_st, n)
}
# Enter or A held on a control: its on-down, and on-up when let go (or the focus moves away)
function fc_hold(root: UiNode, n: UiNode) -> void {
if fr_key_press != "" and (in_act_up or n.key != fr_key_press) {
let was = fr_node(root, fr_key_press)
if was != null { ui_fire(was, "up", Value.null()) }
fr_key_press = ""
function fc_hold(ui_st: mut UiState, root: UiNode, n: UiNode) -> void {
if ui_st.fr_key_press != "" and (ui_st.in_act_up or n.key != ui_st.fr_key_press) {
let was = fr_node(root, ui_st.fr_key_press)
if was != null { ui_fire(ui_st, was, "up", Value.null()) }
ui_st.fr_key_press = ""
}
if in_act_down and n.enabled {
fr_key_press = n.key
ui_fire(n, "down", Value.null())
if ui_st.in_act_down and n.enabled {
ui_st.fr_key_press = n.key
ui_fire(ui_st, n, "down", Value.null())
}
}
export function ui_focus(key: string) -> void {
fc_key = key
fc_visible = false
export function ui_focus(ui_st: mut UiState, key: string) -> void {
ui_st.fc_key = key
ui_st.fc_visible = false
}
# nothing focused (a screen just opened): the control that says autofocus, as HTML's does
function fc_autofocus() -> void {
for i in 0 .. len(fc_list) {
if ui_attr_on(fc_list[i], "autofocus") {
fc_key = fc_list[i].key
fc_visible = false
function fc_autofocus(ui_st: mut UiState) -> void {
for i in 0 .. len(ui_st.fc_list) {
if ui_attr_on(ui_st.fc_list[i], "autofocus") {
ui_st.fc_key = ui_st.fc_list[i].key
ui_st.fc_visible = false
return
}
}

View file

@ -1,46 +1,46 @@
# frame.ludic - a screen shown: built from the view's model, laid out, drawn, and its presses
# run once the frame is drawn, so nothing the view does can change a frame half way through it
export function ui_show(screen: string, view: UiView, x: float, y: float, w: float, h: float) -> void {
in_read()
dv_before()
an_frame += 1
ui_viewport(w, h)
let root = ui_nodes(screen, view)
export function ui_show(ui_st: mut UiState, screen: string, view: UiView, x: float, y: float, w: float, h: float) -> void {
in_read(ui_st)
dv_before(ui_st)
ui_st.an_frame += 1
ui_viewport(ui_st, w, h)
let root = ui_nodes(ui_st, screen, view)
if root == null { return }
ui_place(root, x, y, w, h)
nt_fired_clear()
ui_place(ui_st, root, x, y, w, h)
nt_fired_clear(ui_st)
sc_undo(root)
sc_update(root)
fr_pointer(root)
fc_keys(root)
hd_tick(root)
tt_track(root, in_now.x, in_now.y)
fb_keep(screen, root)
if ui_be != null and ui_be.text != null {
fr_draw(root)
pp_draw_top()
tt_draw(root)
sc_update(ui_st, root)
fr_pointer(ui_st, root)
fc_keys(ui_st, root)
hd_tick(ui_st, root)
tt_track(ui_st, root, ui_st.in_now.x, ui_st.in_now.y)
fb_keep(ui_st, screen, root)
if ui_st.ui_be != null and ui_st.ui_be.text != null {
fr_draw(ui_st, root)
pp_draw_top(ui_st)
tt_draw(ui_st, root)
}
dv_after(screen, root)
lc_run(screen)
nt_fired_run()
dv_after(ui_st, screen, root)
lc_run(ui_st, screen)
nt_fired_run(ui_st)
}
# the screen's nodes for this frame, not yet laid out - for a test, or a host that places them
export function ui_nodes(screen: string, view: UiView) -> UiNode {
let si = lib_find(lib_screen_names, screen)
let cls = uc_find(screen)
if si < 0 and cls != null { return fr_class_nodes(cls) }
export function ui_nodes(ui_st: mut UiState, screen: string, view: UiView) -> UiNode {
let si = lib_find(ui_st.lib_screen_names, screen)
let cls = uc_find(ui_st, screen)
if si < 0 and cls != null { return fr_class_nodes(ui_st, cls) }
if si < 0 {
ui_err(`there is no screen {screen} - ui_load the file that has it`)
ui_err(ui_st, `there is no screen {screen} - ui_load the file that has it`)
return null
}
let t = lib_screens[si]
lc_begin()
ui_view = view
let t = ui_st.lib_screens[si]
lc_begin(ui_st)
ui_st.ui_view = view
let e = env_new(null)
if view != null and view.model != null { e.model = view.model() } else { e.model = Value.object() }
e.state = st_for(screen, t, e)
e.state = st_for(ui_st, screen, t, e)
let root = new UiNode
root.key = screen
root.children = new []UiNode
@ -52,32 +52,32 @@ export function ui_nodes(screen: string, view: UiView) -> UiNode {
root.vals = new []Val
root.tag = "screen"
for i in 0 .. len(t.keys) {
if t.keys[i] != "name" { el_keep(root, t.keys[i], ui_eval(t.vals[i], e)) }
if t.keys[i] != "name" { el_keep(root, t.keys[i], ui_eval(ui_st, t.vals[i], e)) }
}
bd_kids(t.kids, e, screen, root.children)
cs_tree(root)
bd_kids(ui_st, t.kids, e, screen, root.children)
cs_tree(ui_st, root)
return root
}
# the room vw, vh and @media measure against; ui_show sets it to the screen's rectangle
export function ui_viewport(w: float, h: float) -> void {
ly_vp_w = w
ly_vp_h = h
export function ui_viewport(ui_st: mut UiState, w: float, h: float) -> void {
ui_st.ly_vp_w = w
ui_st.ly_vp_h = h
}
export function ui_place(root: UiNode, x: float, y: float, w: float, h: float) -> void {
ly_vp_x = x
ly_vp_y = y
export function ui_place(ui_st: mut UiState, root: UiNode, x: float, y: float, w: float, h: float) -> void {
ui_st.ly_vp_x = x
ui_st.ly_vp_y = y
root.w = UI_FILL
root.h = UI_FILL
let fg = 15658734
ly_fg_a = 1.0
ly_measure(root, w, ui_px(16.0), fg)
ly_place(root, x, y, w, h)
sc_apply(root)
pp_anchor_all(root)
ui_st.ly_fg_a = 1.0
ly_measure(ui_st, root, w, ui_px(ui_st, 16.0), fg)
ly_place(ui_st, root, x, y, w, h)
sc_apply(ui_st, root)
pp_anchor_all(ui_st, root)
}
# a press, as if the pointer had made it
export function ui_press(n: UiNode) -> void {
if n.act != null and n.enabled { act_run(n.act, n.env) }
export function ui_press(ui_st: mut UiState, n: UiNode) -> void {
if n.act != null and n.enabled { act_run(ui_st, n.act, n.env) }
}
# any event, as if the element had fired it - for a test
export function ui_event(n: UiNode, name: string, value: Val) -> void { nt_run(n, name, value) }
export function ui_event(ui_st: mut UiState, n: UiNode, name: string, value: Val) -> void { nt_run(ui_st, n, name, value) }

View file

@ -6,22 +6,21 @@ export property UiBox {
w: float = 0.0
h: float = 0.0
}
var fb_roots: []UiNode = new []UiNode
# the screen pixels per design pixel: the renderer's scale (render3d's includes ui_render3d_scale)
export function ui_scale() -> float { return ui_px(1.0) }
function fb_keep(screen: string, root: UiNode) -> void {
for i in 0 .. len(fb_roots) {
if fb_roots[i].key == screen {
fb_roots[i] = root
export function ui_scale(ui_st: UiState) -> float { return ui_px(ui_st, 1.0) }
function fb_keep(ui_st: mut UiState, screen: string, root: UiNode) -> void {
for i in 0 .. len(ui_st.fb_roots) {
if ui_st.fb_roots[i].key == screen {
ui_st.fb_roots[i] = root
return
}
}
push(fb_roots, root)
push(ui_st.fb_roots, root)
}
# the box of the element with that id (or key) on any screen shown, in screen pixels; null if none
export function ui_box(id: string) -> UiBox {
for i in 0 .. len(fb_roots) {
let n = fb_find(fb_roots[i], id)
export function ui_box(ui_st: UiState, id: string) -> UiBox {
for i in 0 .. len(ui_st.fb_roots) {
let n = fb_find(ui_st.fb_roots[i], id)
if n != null {
let b = new UiBox
b.x = n.x

View file

@ -1,28 +1,25 @@
# hold.ludic - on-hold: every frame a control is held down - by the pointer (wherever it goes until it
# is let go), or by Enter or the pad's A on the focus - with event.dt, the seconds since the last
# frame, and event.t, how long it has been held, on the ui clock
var hd_key: string = ""
var hd_t0: float = 0.0
var hd_last: float = 0.0
function hd_tick(root: UiNode) -> void {
var key = fr_key_press
if fr_press_key != "" and in_now.down { key = fr_press_key }
function hd_tick(ui_st: mut UiState, root: UiNode) -> void {
var key = ui_st.fr_key_press
if ui_st.fr_press_key != "" and ui_st.in_now.down { key = ui_st.fr_press_key }
if key == "" {
hd_key = ""
ui_st.hd_key = ""
return
}
let now = an_now()
if key != hd_key {
hd_key = key
hd_t0 = now
hd_last = now
let now = an_now(ui_st)
if key != ui_st.hd_key {
ui_st.hd_key = key
ui_st.hd_t0 = now
ui_st.hd_last = now
}
let n = fr_node(root, key)
if n != null {
let v = Value.object()
Value.put(v, "dt", Value.float(now - hd_last))
Value.put(v, "t", Value.float(now - hd_t0))
ui_fire(n, "hold", v)
Value.put(v, "dt", Value.float(now - ui_st.hd_last))
Value.put(v, "t", Value.float(now - ui_st.hd_t0))
ui_fire(ui_st, n, "hold", v)
}
hd_last = now
ui_st.hd_last = now
}

View file

@ -28,19 +28,15 @@ export property UiInput {
down_right: bool = false # the other mouse buttons held
down_middle: bool = false
}
var in_now: UiInput = new UiInput
var in_was_down: bool = false
var in_last: UiInput = new UiInput
var in_given: bool = false
function in_read() -> void {
if in_given {
in_given = false
function in_read(ui_st: mut UiState) -> void {
if ui_st.in_given {
ui_st.in_given = false
return
}
in_was_down = in_now.down
in_last = in_now
ui_st.in_was_down = ui_st.in_now.down
ui_st.in_last = ui_st.in_now
let i = new UiInput
if ui_be != null and ui_be.input {
if ui_st.ui_be != null and ui_st.ui_be.input {
i.x = float(Input.mouse_x())
i.y = float(Input.mouse_y())
i.down = Input.mouse_down(0)
@ -61,8 +57,8 @@ function in_read() -> void {
i.key = in_any_key()
in_held(i)
}
in_repeat(i)
in_now = i
in_repeat(ui_st, i)
ui_st.in_now = i
}
function in_any_key() -> int {
for k in 8 .. 167 {
@ -71,10 +67,10 @@ function in_any_key() -> int {
return 0
}
# this frame's input, given by a test or a host that reads its own
export function ui_input(i: UiInput) -> void {
in_was_down = in_now.down
in_last = in_now
in_repeat(i)
in_now = i
in_given = true
export function ui_input(ui_st: mut UiState, i: UiInput) -> void {
ui_st.in_was_down = ui_st.in_now.down
ui_st.in_last = ui_st.in_now
in_repeat(ui_st, i)
ui_st.in_now = i
ui_st.in_given = true
}

View file

@ -4,12 +4,6 @@
const RP_FIRST: float = 0.42
const RP_EVERY: float = 0.11
const PAD_STICK: float = 0.5
var rp_next: []float = [-1.0, -1.0, -1.0, -1.0]
var rp_a: bool = false
var rp_b: bool = false
var rp_act: bool = false
var in_act_down: bool = false # A or Enter went down this frame, or came up
var in_act_up: bool = false
# what is held this frame: the arrow keys, the d-pad (SDL's order: 11 up, 12 down, 13 left, 14
# right) and the stick past half way along its larger axis
function in_held(i: UiInput) -> void {
@ -30,32 +24,32 @@ function in_held(i: UiInput) -> void {
i.pad_b = Input.pad_button(0, 1)
}
# the held directions and buttons as this frame's presses
function in_repeat(i: UiInput) -> void {
let now = an_now()
if rp_fire(0, i.held_up, now) { i.up = true }
if rp_fire(1, i.held_down, now) { i.down_key = true }
if rp_fire(2, i.held_left, now) { i.left = true }
if rp_fire(3, i.held_right, now) { i.right = true }
if i.pad_a and not rp_a { i.enter = true }
if i.pad_b and not rp_b { i.escape = true }
rp_a = i.pad_a
rp_b = i.pad_b
function in_repeat(ui_st: mut UiState, i: UiInput) -> void {
let now = an_now(ui_st)
if rp_fire(ui_st, 0, i.held_up, now) { i.up = true }
if rp_fire(ui_st, 1, i.held_down, now) { i.down_key = true }
if rp_fire(ui_st, 2, i.held_left, now) { i.left = true }
if rp_fire(ui_st, 3, i.held_right, now) { i.right = true }
if i.pad_a and not ui_st.rp_a { i.enter = true }
if i.pad_b and not ui_st.rp_b { i.escape = true }
ui_st.rp_a = i.pad_a
ui_st.rp_b = i.pad_b
let act = i.pad_a or i.held_enter
in_act_down = act and not rp_act
in_act_up = rp_act and not act
rp_act = act
ui_st.in_act_down = act and not ui_st.rp_act
ui_st.in_act_up = ui_st.rp_act and not act
ui_st.rp_act = act
}
function rp_fire(k: int, held: bool, now: float) -> bool {
function rp_fire(ui_st: mut UiState, k: int, held: bool, now: float) -> bool {
if not held {
rp_next[k] = -1.0
ui_st.rp_next[k] = -1.0
return false
}
if rp_next[k] < 0.0 {
rp_next[k] = now + RP_FIRST
if ui_st.rp_next[k] < 0.0 {
ui_st.rp_next[k] = now + RP_FIRST
return true
}
if now + 0.000001 < rp_next[k] { return false }
rp_next[k] = rp_next[k] + RP_EVERY
if rp_next[k] <= now { rp_next[k] = now + RP_EVERY }
if now + 0.000001 < ui_st.rp_next[k] { return false }
ui_st.rp_next[k] = ui_st.rp_next[k] + RP_EVERY
if ui_st.rp_next[k] <= now { ui_st.rp_next[k] = now + RP_EVERY }
return true
}

View file

@ -6,13 +6,11 @@ property UiInst {
ptr: pointer = null
seen: int = 0
}
var in_list: []UiInst = new []UiInst
var in_build: int = 0
function in_get(key: string, k: UiClass) -> UiInst {
for i in 0 .. len(in_list) {
let it = in_list[i]
function in_get(ui_st: mut UiState, key: string, k: UiClass) -> UiInst {
for i in 0 .. len(ui_st.in_list) {
let it = ui_st.in_list[i]
if it.key == key and it.cls.name == k.name {
it.seen = in_build
it.seen = ui_st.in_build
return it
}
}
@ -20,16 +18,16 @@ function in_get(key: string, k: UiClass) -> UiInst {
it.key = key
it.cls = k
it.ptr = k.make()
it.seen = in_build
push(in_list, it)
it.seen = ui_st.in_build
push(ui_st.in_list, it)
return it
}
# after a build of `screen`: its instances the build did not reach are unmounted
function in_sweep(screen: string) -> void {
function in_sweep(ui_st: mut UiState, screen: string) -> void {
let keep = new []UiInst
for i in 0 .. len(in_list) {
let it = in_list[i]
if it.seen == in_build or not lc_mine(it.key, screen) { push(keep, it) }
for i in 0 .. len(ui_st.in_list) {
let it = ui_st.in_list[i]
if it.seen == ui_st.in_build or not lc_mine(it.key, screen) { push(keep, it) }
}
in_list = keep
ui_st.in_list = keep
}

View file

@ -1,19 +1,19 @@
# layout.ludic - flex layout on the CSS box model, top-down: a box is told how wide it may be,
# measures its content at that width (so a wrapping row knows how tall it is), then places each
# child. A size is the border box: padding and border inside it, margin outside it.
function ly_measure(n: UiNode, avail: float, size: float, fg: int) -> void {
function ly_measure(ui_st: mut UiState, n: UiNode, avail: float, size: float, fg: int) -> void {
n.rsize = size * zm_own(n)
if n.size > 0.0 { n.rsize = n.size }
n.rfg = fg
n.rfg_a = ly_fg_a
n.rfg_a = ui_st.ly_fg_a
if n.fg != UI_INHERIT {
n.rfg = n.fg
n.rfg_a = n.fg_a
}
ly_fg_a = n.rfg_a
ui_st.ly_fg_a = n.rfg_a
n.mw = ly_hin(n)
n.mh = ly_vin(n)
if n.kind == UI_TEXT { ly_measure_text(n, avail) }
if n.kind == UI_TEXT { ly_measure_text(ui_st, n, avail) }
if n.kind == UI_NATIVE and n.native.measure != null {
let base_w = n.mw
let base_h = n.mh
@ -33,7 +33,7 @@ function ly_measure(n: UiNode, avail: float, size: float, fg: int) -> void {
var inner = avail - n.ml - n.mr
if fixed_w >= 0.0 { inner = fixed_w }
inner = ly_clamp(inner, n.min_w, n.max_w) - ly_hin(n)
if n.dir == UI_ROW and n.wrap { ly_measure_wrap(n, inner) } else { ly_measure_box(n, inner) }
if n.dir == UI_ROW and n.wrap { ly_measure_wrap(ui_st, n, inner) } else { ly_measure_box(ui_st, n, inner) }
}
n.content_h = n.mh
if fixed_w >= 0.0 { n.mw = fixed_w }
@ -43,7 +43,6 @@ function ly_measure(n: UiNode, avail: float, size: float, fg: int) -> void {
n.mh = ly_clamp(n.mh, n.min_h, n.max_h)
ly_min_note(n)
}
var ly_fg_a: float = 1.0
function ly_hin(n: UiNode) -> float { return n.pl + n.pr + n.bl + n.br }
function ly_vin(n: UiNode) -> float { return n.pt + n.pb + n.bt + n.bb }
function ly_clamp(v: float, lo: float, hi: float) -> float {

View file

@ -1,11 +1,11 @@
# layout_box.ludic - a box's content measured: its children in a line along its direction (a
# row's text wrapped into the room its siblings leave), or a wrapping row's lines
function ly_measure_box(n: UiNode, inner: float) -> void {
function ly_measure_box(ui_st: mut UiState, n: UiNode, inner: float) -> void {
for i in 0 .. len(n.children) {
ly_fg_a = n.rfg_a
ly_measure(n.children[i], inner, n.rsize, n.rfg)
ui_st.ly_fg_a = n.rfg_a
ly_measure(ui_st, n.children[i], inner, n.rsize, n.rfg)
}
if n.dir == UI_ROW and inner > 0.0 { ly_row_fit(n, inner) }
if n.dir == UI_ROW and inner > 0.0 { ly_row_fit(ui_st, n, inner) }
var main = 0.0
var cross = 0.0
var count = 0
@ -28,7 +28,7 @@ function ly_measure_box(n: UiNode, inner: float) -> void {
}
# a row wider than its room: its texts (and boxes of their own width) measured again in the room the
# rest leave them, each giving up a share of the excess as wide as it is, so a line wraps beside an icon
function ly_row_fit(n: UiNode, inner: float) -> void {
function ly_row_fit(ui_st: mut UiState, n: UiNode, inner: float) -> void {
var used = 0.0
var flex = 0.0
var count = 0
@ -46,8 +46,8 @@ function ly_row_fit(n: UiNode, inner: float) -> void {
let c = n.children[i]
let share = ly_main(c, UI_ROW) - over * ly_main(c, UI_ROW) / flex
if ly_flexes(c) and share > 0.0 {
ly_fg_a = n.rfg_a
ly_measure(c, share, n.rsize, n.rfg)
ui_st.ly_fg_a = n.rfg_a
ly_measure(ui_st, c, share, n.rsize, n.rfg)
}
}
}
@ -57,7 +57,7 @@ function ly_flexes(c: UiNode) -> bool {
return c.kind == UI_BOX and c.children != null and len(c.children) > 0
}
# a wrapping row: children in lines no wider than `inner`, the lines stacked `gap` apart
function ly_measure_wrap(n: UiNode, inner: float) -> void {
function ly_measure_wrap(ui_st: mut UiState, n: UiNode, inner: float) -> void {
var line_w = 0.0
var line_h = 0.0
var widest = 0.0
@ -65,8 +65,8 @@ function ly_measure_wrap(n: UiNode, inner: float) -> void {
var count = 0
for i in 0 .. len(n.children) {
let c = n.children[i]
ly_fg_a = n.rfg_a
ly_measure(c, inner, n.rsize, n.rfg)
ui_st.ly_fg_a = n.rfg_a
ly_measure(ui_st, c, inner, n.rsize, n.rfg)
if c.pos >= 2 { continue }
let cw = ly_main(c, UI_ROW)
if count > 0 and line_w + n.gap + cw > inner {

View file

@ -1,8 +1,6 @@
# layout_run.ludic - a box's children given their places: along its direction by size, grow and
# justify, across it by align (or each child's own `self`), a wrapping row one line at a time
var ly_lead: float = 0.0
var ly_extra: float = 0.0
function ly_place(n: UiNode, x: float, y: float, w: float, h: float) -> void {
function ly_place(ui_st: mut UiState, n: UiNode, x: float, y: float, w: float, h: float) -> void {
n.x = x
n.y = y
n.cw = w
@ -12,9 +10,9 @@ function ly_place(n: UiNode, x: float, y: float, w: float, h: float) -> void {
var inner_h = h - ly_vin(n)
if n.kind == UI_SCROLL { inner_h = Math.max(n.content_h, h) - ly_vin(n) }
let top = n.y + n.pt + n.bt
if n.dir == UI_ROW and n.wrap { ly_place_wrap(n, top, inner_w) } else if n.dir == UI_ROW { ly_place_line(n, 0, len(n.children), top, inner_w, inner_h) } else { ly_place_line(n, 0, len(n.children), top, inner_h, inner_w) }
if n.dir == UI_ROW and n.wrap { ly_place_wrap(ui_st, n, top, inner_w) } else if n.dir == UI_ROW { ly_place_line(ui_st, n, 0, len(n.children), top, inner_w, inner_h) } else { ly_place_line(ui_st, n, 0, len(n.children), top, inner_h, inner_w) }
for i in 0 .. len(n.children) {
if n.children[i].pos >= 2 { ly_place_abs(n, n.children[i]) }
if n.children[i].pos >= 2 { ly_place_abs(ui_st, n, n.children[i]) }
}
}
function ly_fills(c: UiNode, dir: int) -> bool {
@ -39,7 +37,7 @@ function ly_stretches(n: UiNode, c: UiNode) -> bool {
return a == UI_STRETCH
}
# children from..to along one line starting at `top`: sizes, the spare room, then each place
function ly_place_line(n: UiNode, from: int, to: int, top: float, main_room: float, cross_room: float) -> void {
function ly_place_line(ui_st: mut UiState, n: UiNode, from: int, to: int, top: float, main_room: float, cross_room: float) -> void {
var used = 0.0
var grows = 0.0
var count = 0
@ -55,21 +53,21 @@ function ly_place_line(n: UiNode, from: int, to: int, top: float, main_room: flo
used = used + n.gap * float(Math.max(count - 1, 0))
let free = main_room - used
if free < 0.0 { ly_shrink_line(n, from, sizes, 0.0 - free) }
ly_justify(n, free, count, grows)
let extra = ly_extra
var at = ly_lead
ly_justify(ui_st, n, free, count, grows)
let extra = ui_st.ly_extra
var at = ui_st.ly_lead
for i in from .. to {
let c = n.children[i]
if c.pos >= 2 { continue }
var m = sizes[i - from]
if grows > 0.0 and free > 0.0 { m = m + free * ly_grow(c, n.dir) / grows }
ly_place_child(n, c, at, top, m, cross_room)
ly_place_child(ui_st, n, c, at, top, m, cross_room)
if c.pos == 1 { ly_nudge(n, c) }
at = at + m + n.gap + extra
}
}
# one child: `m` along the run and its own size (or the whole room, stretched) across, less margins
function ly_place_child(n: UiNode, c: UiNode, at: float, top: float, m: float, cross_room: float) -> void {
function ly_place_child(ui_st: mut UiState, n: UiNode, c: UiNode, at: float, top: float, m: float, cross_room: float) -> void {
var cr = ly_cross(c, n.dir)
if n.dir == UI_ROW and c.h_pct > 0.0 { cr = ly_ph(c, cross_room) + c.mt + c.mb }
if n.dir == UI_COL and c.w_pct > 0.0 { cr = ly_pw(c, cross_room) + c.ml + c.mr }
@ -82,9 +80,9 @@ function ly_place_child(n: UiNode, c: UiNode, at: float, top: float, m: float, c
let left = n.x + n.pl + n.bl
if n.dir == UI_ROW {
let cw = ly_clamp(m - c.ml - c.mr, c.min_w, c.max_w)
ly_place(c, left + at + c.ml, top + off + c.mt, cw, ly_clamp(cr - c.mt - c.mb, c.min_h, c.max_h))
ly_place(ui_st, c, left + at + c.ml, top + off + c.mt, cw, ly_clamp(cr - c.mt - c.mb, c.min_h, c.max_h))
} else {
let chh = ly_clamp(m - c.mt - c.mb, c.min_h, c.max_h)
ly_place(c, left + off + c.ml, top + at + c.mt, ly_clamp(cr - c.ml - c.mr, c.min_w, c.max_w), chh)
ly_place(ui_st, c, left + off + c.ml, top + at + c.mt, ly_clamp(cr - c.ml - c.mr, c.min_w, c.max_w), chh)
}
}

View file

@ -1,9 +1,9 @@
# layout_text.ludic - a text's size: one line, or as many as it takes to fit its room (white-space
# normal), breaking between words; nowrap keeps one line, and text-overflow: ellipsis cuts it
function ly_measure_text(n: UiNode, avail: float) -> void {
function ly_measure_text(ui_st: UiState, n: UiNode, avail: float) -> void {
n.lines = null
let lh = ly_line_h(n)
var one = ly_text_w(n.text, n.rsize)
var one = ly_text_w(ui_st, n.text, n.rsize)
var room = avail - n.ml - n.mr - ly_hin(n)
if n.w >= 0.0 { room = n.w - ly_hin(n) }
let fits = n.ex != null and n.ex.fit_min > 0.0
@ -13,13 +13,13 @@ function ly_measure_text(n: UiNode, avail: float) -> void {
n.mh = n.mh + lh
return
}
n.lines = ly_wrap(n.text, n.rsize, room)
n.lines = ly_wrap(ui_st, n.text, n.rsize, room)
var widest = 0.0
for i in 0 .. len(n.lines) { widest = Math.max(widest, ly_text_w(n.lines[i], n.rsize)) }
for i in 0 .. len(n.lines) { widest = Math.max(widest, ly_text_w(ui_st, n.lines[i], n.rsize)) }
n.mw = n.mw + widest
n.mh = n.mh + lh * float(len(n.lines))
}
function ly_wrap(s: string, size: float, room: float) -> []string {
function ly_wrap(ui_st: UiState, s: string, size: float, room: float) -> []string {
let words = lss_split(s, 32)
let out = new []string
var line = ""
@ -27,7 +27,7 @@ function ly_wrap(s: string, size: float, room: float) -> []string {
if len(words[i]) == 0 { continue }
var next = words[i]
if len(line) > 0 { next = line + " " + words[i] }
if len(line) > 0 and ly_text_w(next, size) > room {
if len(line) > 0 and ly_text_w(ui_st, next, size) > room {
push(out, line)
line = words[i]
} else { line = next }
@ -36,14 +36,14 @@ function ly_wrap(s: string, size: float, room: float) -> []string {
return out
}
# a line cut to `room` with an ellipsis, when it does not fit
function ly_ellipsis(s: string, size: float, room: float) -> string {
if ly_text_w(s, size) <= room { return s }
function ly_ellipsis(ui_st: UiState, s: string, size: float, room: float) -> string {
if ly_text_w(ui_st, s, size) <= room { return s }
var n = len(s)
while n > 0 and ly_text_w(s[0..n] + "...", size) > room { n -= 1 }
while n > 0 and ly_text_w(ui_st, s[0..n] + "...", size) > room { n -= 1 }
return s[0..n] + "..."
}
function ly_text_w(s: string, size: float) -> float {
if ui_be != null and ui_be.measure != null { return ui_be.measure(s, size) }
function ly_text_w(ui_st: UiState, s: string, size: float) -> float {
if ui_st.ui_be != null and ui_st.ui_be.measure != null { return ui_st.ui_be.measure(s, size) }
return float(len(s)) * size * 0.5
}
# a child's size along and across a direction, margins included

View file

@ -1,5 +1,5 @@
# layout_wrap.ludic - a wrapping row, line by line: each line is as tall as its tallest child
function ly_place_wrap(n: UiNode, top0: float, inner_w: float) -> void {
function ly_place_wrap(ui_st: mut UiState, n: UiNode, top0: float, inner_w: float) -> void {
var top = top0
var from = 0
while from < len(n.children) {
@ -16,23 +16,23 @@ function ly_place_wrap(n: UiNode, top0: float, inner_w: float) -> void {
line_h = Math.max(line_h, ly_cross(n.children[to], UI_ROW))
to += 1
}
ly_place_line(n, from, to, top, inner_w, line_h)
ly_place_line(ui_st, n, from, to, top, inner_w, line_h)
top = top + line_h + n.gap
from = to
}
}
# position: absolute (in the parent's padding box) or fixed (in the screen): the insets say where,
# and two opposite ones say how big; relative is placed as usual and moved by its insets
function ly_place_abs(n: UiNode, c: UiNode) -> void {
function ly_place_abs(ui_st: mut UiState, n: UiNode, c: UiNode) -> void {
var bx = n.x + n.bl
var by = n.y + n.bt
var bw = n.cw - n.bl - n.br
var bh = n.ch - n.bt - n.bb
if c.pos == 3 {
bx = ly_vp_x
by = ly_vp_y
bw = ly_vp_w
bh = ly_vp_h
bx = ui_st.ly_vp_x
by = ui_st.ly_vp_y
bw = ui_st.ly_vp_w
bh = ui_st.ly_vp_h
}
var w = c.mw
var h = c.mh
@ -48,7 +48,7 @@ function ly_place_abs(n: UiNode, c: UiNode) -> void {
if l != UI_AUTO { x = bx + l + c.ml } else if r != UI_AUTO { x = bx + bw - r - c.mr - w }
var y = by + n.pt + c.mt
if t != UI_AUTO { y = by + t + c.mt } else if b != UI_AUTO { y = by + bh - b - c.mb - h }
ly_place(c, x, y, w, h)
ly_place(ui_st, c, x, y, w, h)
}
function ly_or0(v: float) -> float {
if v == UI_AUTO { return 0.0 }
@ -60,19 +60,19 @@ function ly_offset(n: UiNode, dx: float, dy: float) -> void {
for i in 0 .. len(n.children) { ly_offset(n.children[i], dx, dy) }
}
# where the first child starts and what goes between, when nothing grows into the spare room
function ly_justify(n: UiNode, free: float, count: int, grows: float) -> void {
ly_lead = 0.0
ly_extra = 0.0
function ly_justify(ui_st: mut UiState, n: UiNode, free: float, count: int, grows: float) -> void {
ui_st.ly_lead = 0.0
ui_st.ly_extra = 0.0
if grows > 0.0 or free <= 0.0 or count == 0 { return }
if n.justify == UI_CENTER { ly_lead = free / 2.0 }
if n.justify == UI_END { ly_lead = free }
if n.justify == UI_BETWEEN and count > 1 { ly_extra = free / float(count - 1) }
if n.justify == UI_CENTER { ui_st.ly_lead = free / 2.0 }
if n.justify == UI_END { ui_st.ly_lead = free }
if n.justify == UI_BETWEEN and count > 1 { ui_st.ly_extra = free / float(count - 1) }
if n.justify == UI_AROUND {
ly_extra = free / float(count)
ly_lead = ly_extra / 2.0
ui_st.ly_extra = free / float(count)
ui_st.ly_lead = ui_st.ly_extra / 2.0
}
if n.justify == UI_EVENLY {
ly_extra = free / float(count + 1)
ly_lead = ly_extra
ui_st.ly_extra = free / float(count + 1)
ui_st.ly_lead = ui_st.ly_extra
}
}

View file

@ -14,51 +14,48 @@ export property UiFile {
src: string = "" # the text it was read from
disk: bool = false # read from a file (ui_reload looks again), or handed over as text
}
var lib_files: []UiFile = new []UiFile
var lib_screen_names: []string = new []string
var lib_screens: []UiTpl = new []UiTpl
# the file a path names, read once
export function ui_load(path: string) -> bool { return lib_file(path) != null }
function lib_file(path: string) -> UiFile {
for i in 0 .. len(lib_files) {
if lib_files[i].path == path { return lib_files[i] }
export function ui_load(ui_st: mut UiState, path: string) -> bool { return lib_file(ui_st, path) != null }
function lib_file(ui_st: mut UiState, path: string) -> UiFile {
for i in 0 .. len(ui_st.lib_files) {
if ui_st.lib_files[i].path == path { return ui_st.lib_files[i] }
}
let src = lib_text(path)
let src = lib_text(ui_st, path)
if len(src) == 0 {
ui_err(`{path}: cannot read it`)
ui_err(ui_st, `{path}: cannot read it`)
return null
}
var f: UiFile = null
if lss_is(path) { f = lss_file(src, path) } else { f = lib_read(src, path) }
if lss_is(path) { f = lss_file(ui_st, src, path) } else { f = lib_read(ui_st, src, path) }
f.disk = true
return f
}
# a file's text, as if read from `path` (its imports are found beside it)
export function ui_load_text(src: string, path: string) -> void {
if lss_is(path) { lss_file(src, path) } else { lib_read(src, path) }
export function ui_load_text(ui_st: mut UiState, src: string, path: string) -> void {
if lss_is(path) { lss_file(ui_st, src, path) } else { lib_read(ui_st, src, path) }
}
function lib_read(src: string, path: string) -> UiFile {
let f = lib_new(path)
function lib_read(ui_st: mut UiState, src: string, path: string) -> UiFile {
let f = lib_new(ui_st, path)
f.src = src
tp_lines(src)
let root = tpl_read(Xml.parse(src), path)
tp_lines(ui_st, src)
let root = tpl_read(ui_st, Xml.parse(src), path)
var tops = root.kids
if root.tag != "ui" { tops = [root] }
for i in 0 .. len(tops) { lib_top(f, tops[i]) }
for i in 0 .. len(tops) { lib_top(ui_st, f, tops[i]) }
f.sheet = sheet_of(f)
for i in 0 .. len(f.comps) { lib_link(f, f.comps[i]) }
for i in 0 .. len(f.comps) { lib_link(ui_st, f, f.comps[i]) }
for i in 0 .. len(tops) {
if tops[i].tag == "screen" { lib_link(f, tops[i]) }
if tops[i].tag == "screen" { lib_link(ui_st, f, tops[i]) }
}
return f
}
function lib_top(f: UiFile, t: UiTpl) -> void {
function lib_top(ui_st: mut UiState, f: UiFile, t: UiTpl) -> void {
if t.tag == "style" {
sheet_read(f, t)
sheet_read(ui_st, f, t)
return
}
if t.tag == "import" {
let other = lib_file(lib_beside(f.path, bd_name(t, "src", "")))
let other = lib_file(ui_st, lib_beside(f.path, bd_name(t, "src", "")))
if other != null {
push(f.imp, other)
push(f.imp_as, bd_name(t, "as", ""))
@ -67,14 +64,14 @@ function lib_top(f: UiFile, t: UiTpl) -> void {
}
let name = bd_name(t, "name", "")
if name == "" or (t.tag != "component" and t.tag != "screen") {
ui_err(`{f.path}: <{t.tag}> - a file holds <import>s, <style>s, <component name=...>s and <screen name=...>s`)
ui_err(ui_st, `{f.path}: <{t.tag}> - a file holds <import>s, <style>s, <component name=...>s and <screen name=...>s`)
return
}
if t.tag == "screen" {
let at = lib_find(lib_screen_names, name)
if at >= 0 { lib_screens[at] = t } else {
push(lib_screen_names, name)
push(lib_screens, t)
let at = lib_find(ui_st.lib_screen_names, name)
if at >= 0 { ui_st.lib_screens[at] = t } else {
push(ui_st.lib_screen_names, name)
push(ui_st.lib_screens, t)
}
return
}

View file

@ -6,9 +6,9 @@ function lib_find(names: []string, name: string) -> int {
}
return -1
}
function lib_new(path: string) -> UiFile {
function lib_new(ui_st: mut UiState, path: string) -> UiFile {
let f = lib_blank(path)
push(lib_files, f)
push(ui_st.lib_files, f)
return f
}
# a file that is not one of the loaded ones (a component's own)
@ -23,9 +23,9 @@ function lib_blank(path: string) -> UiFile {
f.own = new []UiRule
return f
}
function lib_text(path: string) -> string {
function lib_text(ui_st: UiState, path: string) -> string {
var src = ""
if ui_be != null and ui_be.read != null { src = ui_be.read(path) } else { src = Fs.read_text(path) }
if ui_st.ui_be != null and ui_st.ui_be.read != null { src = ui_st.ui_be.read(path) } else { src = Fs.read_text(path) }
if src == null { return "" }
return src
}

View file

@ -1,20 +1,13 @@
# life.ludic - on-mount and on-unmount: an element or component that appears in a frame it was not
# in last frame is mounted, and one that is gone is unmounted, each once, after the frame is drawn
var lc_keys: []string = new []string
var lc_mounts: []UiAct = new []UiAct
var lc_unmounts: []UiAct = new []UiAct
var lc_envs: []UiEnv = new []UiEnv
var lc_prev_keys: []string = new []string
var lc_prev_unmounts: []UiAct = new []UiAct
var lc_prev_envs: []UiEnv = new []UiEnv
# a build starts its own records: a screen built twice keeps only the last build's
function lc_begin() -> void {
lc_keys = new []string
lc_mounts = new []UiAct
lc_unmounts = new []UiAct
lc_envs = new []UiEnv
function lc_begin(ui_st: mut UiState) -> void {
ui_st.lc_keys = new []string
ui_st.lc_mounts = new []UiAct
ui_st.lc_unmounts = new []UiAct
ui_st.lc_envs = new []UiEnv
}
function lc_note(key: string, names: []string, acts: []UiAct, e: UiEnv) -> void {
function lc_note(ui_st: mut UiState, key: string, names: []string, acts: []UiAct, e: UiEnv) -> void {
var m: UiAct = null
var u: UiAct = null
for i in 0 .. len(names) {
@ -22,43 +15,43 @@ function lc_note(key: string, names: []string, acts: []UiAct, e: UiEnv) -> void
if names[i] == "unmount" { u = acts[i] }
}
if m == null and u == null { return }
push(lc_keys, key)
push(lc_mounts, m)
push(lc_unmounts, u)
push(lc_envs, e)
push(ui_st.lc_keys, key)
push(ui_st.lc_mounts, m)
push(ui_st.lc_unmounts, u)
push(ui_st.lc_envs, e)
}
function lc_mine(key: string, screen: string) -> bool {
let n = len(screen)
return key == screen or (len(key) > n and key[0..n] == screen and key[n] == 47)
}
# the screen's mounts and unmounts since its last frame; other screens' records are kept
function lc_run(screen: string) -> void {
for i in 0 .. len(lc_keys) {
if lc_mounts[i] != null and not sel_has(lc_prev_keys, lc_keys[i]) { act_run(lc_mounts[i], lc_envs[i]) }
function lc_run(ui_st: mut UiState, screen: string) -> void {
for i in 0 .. len(ui_st.lc_keys) {
if ui_st.lc_mounts[i] != null and not sel_has(ui_st.lc_prev_keys, ui_st.lc_keys[i]) { act_run(ui_st, ui_st.lc_mounts[i], ui_st.lc_envs[i]) }
}
let keys = new []string
let unmounts = new []UiAct
let envs = new []UiEnv
for i in 0 .. len(lc_prev_keys) {
let k = lc_prev_keys[i]
for i in 0 .. len(ui_st.lc_prev_keys) {
let k = ui_st.lc_prev_keys[i]
if lc_mine(k, screen) {
if lc_prev_unmounts[i] != null and not sel_has(lc_keys, k) { act_run(lc_prev_unmounts[i], lc_prev_envs[i]) }
if ui_st.lc_prev_unmounts[i] != null and not sel_has(ui_st.lc_keys, k) { act_run(ui_st, ui_st.lc_prev_unmounts[i], ui_st.lc_prev_envs[i]) }
} else {
push(keys, k)
push(unmounts, lc_prev_unmounts[i])
push(envs, lc_prev_envs[i])
push(unmounts, ui_st.lc_prev_unmounts[i])
push(envs, ui_st.lc_prev_envs[i])
}
}
for i in 0 .. len(lc_keys) {
push(keys, lc_keys[i])
push(unmounts, lc_unmounts[i])
push(envs, lc_envs[i])
for i in 0 .. len(ui_st.lc_keys) {
push(keys, ui_st.lc_keys[i])
push(unmounts, ui_st.lc_unmounts[i])
push(envs, ui_st.lc_envs[i])
}
lc_prev_keys = keys
lc_prev_unmounts = unmounts
lc_prev_envs = envs
lc_keys = new []string
lc_mounts = new []UiAct
lc_unmounts = new []UiAct
lc_envs = new []UiEnv
ui_st.lc_prev_keys = keys
ui_st.lc_prev_unmounts = unmounts
ui_st.lc_prev_envs = envs
ui_st.lc_keys = new []string
ui_st.lc_mounts = new []UiAct
ui_st.lc_unmounts = new []UiAct
ui_st.lc_envs = new []UiEnv
}

View file

@ -1,23 +1,23 @@
# link.ludic - each tag in a file that is not an element names a component: the file's own, an
# unnamed import's exported one, or `as:Name` from the import called `as`
const LK_OWN: string = " row button input select textarea progress meter spacer scroll img hr if else each slot state style component screen import let fragment template provide #text "
function lib_link(f: UiFile, t: UiTpl) -> void {
function lib_link(ui_st: mut UiState, f: UiFile, t: UiTpl) -> void {
t.sheet = f.sheet
if not lk_element(t.tag) {
if not lk_element(ui_st, t.tag) {
t.comp = lk_find(f, t.tag, false)
if t.comp == null { t.cls = uc_find(t.tag) }
if t.comp == null { t.cls = uc_find(ui_st, t.tag) }
if t.cls != null {
lk_kids(f, t)
lk_kids(ui_st, f, t)
return
}
if t.comp == null and lk_find(f, t.tag, true) != null { ui_err(`{t.where}: <{t.tag}>: that component is not exported - export="true" on it lets other files use it`) } else if t.comp == null { ui_err(`{t.where}: <{t.tag}>: there is no such element, and no component of that name here or exported by an import`) }
if t.comp == null and lk_find(f, t.tag, true) != null { ui_err(ui_st, `{t.where}: <{t.tag}>: that component is not exported - export="true" on it lets other files use it`) } else if t.comp == null { ui_err(ui_st, `{t.where}: <{t.tag}>: there is no such element, and no component of that name here or exported by an import`) }
}
lk_kids(f, t)
lk_kids(ui_st, f, t)
}
function lk_kids(f: UiFile, t: UiTpl) -> void {
for i in 0 .. len(t.kids) { lib_link(f, t.kids[i]) }
function lk_kids(ui_st: mut UiState, f: UiFile, t: UiTpl) -> void {
for i in 0 .. len(t.kids) { lib_link(ui_st, f, t.kids[i]) }
}
function lk_element(tag: string) -> bool { return lk_in(LK_OWN, tag) or lk_in(EL_BOXES, tag) or lk_in(EL_TEXTS, tag) or nt_find(tag) != null }
function lk_element(ui_st: UiState, tag: string) -> bool { return lk_in(LK_OWN, tag) or lk_in(EL_BOXES, tag) or lk_in(EL_TEXTS, tag) or nt_find(ui_st, tag) != null }
# a word in a list of words written " a b c "
function lk_in(words: string, w: string) -> bool {
let pat = ` {w} `

View file

@ -25,12 +25,12 @@ function md_parse(cond: string) -> UiMedia {
}
return m
}
function md_ok(r: UiRule) -> bool {
function md_ok(ui_st: UiState, r: UiRule) -> bool {
let m = r.md
if m == null { return true }
if m.min_w > 0.0 and ly_vp_w < m.min_w { return false }
if m.max_w > 0.0 and ly_vp_w > m.max_w { return false }
if m.min_h > 0.0 and ly_vp_h < m.min_h { return false }
if m.max_h > 0.0 and ly_vp_h > m.max_h { return false }
if m.min_w > 0.0 and ui_st.ly_vp_w < m.min_w { return false }
if m.max_w > 0.0 and ui_st.ly_vp_w > m.max_w { return false }
if m.min_h > 0.0 and ui_st.ly_vp_h < m.min_h { return false }
if m.max_h > 0.0 and ui_st.ly_vp_h > m.max_h { return false }
return true
}

View file

@ -8,59 +8,55 @@ export property UiNative {
draw: fn(UiNode) -> void = null
input: fn(UiNode, UiPointer) -> bool = null # the pointer on it (pointer_events.ludic)
}
var nt_list: []UiNative = new []UiNative
export function ui_native(tag: string, measure: fn(UiNode, float) -> void, draw: fn(UiNode) -> void) -> void {
export function ui_native(ui_st: mut UiState, tag: string, measure: fn(UiNode, float) -> void, draw: fn(UiNode) -> void) -> void {
let nv = new UiNative
nv.tag = tag
nv.measure = measure
nv.draw = draw
for i in 0 .. len(nt_list) {
if nt_list[i].tag == tag {
nv.input = nt_list[i].input
nt_list[i] = nv
for i in 0 .. len(ui_st.nt_list) {
if ui_st.nt_list[i].tag == tag {
nv.input = ui_st.nt_list[i].input
ui_st.nt_list[i] = nv
return
}
}
push(nt_list, nv)
push(ui_st.nt_list, nv)
}
# the pointer on a native: its presses, moves, drags, releases and the wheel, while it is over it or
# has it captured; answering false to a pointerdown leaves the pointer free
export function ui_native_input(tag: string, f: fn(UiNode, UiPointer) -> bool) -> void {
let nv = nt_find(tag)
export function ui_native_input(ui_st: mut UiState, tag: string, f: fn(UiNode, UiPointer) -> bool) -> void {
let nv = nt_find(ui_st, tag)
if nv == null {
ui_err(`ui_native_input: there is no native <{tag}> - ui_native it first`)
ui_err(ui_st, `ui_native_input: there is no native <{tag}> - ui_native it first`)
return
}
nv.input = f
}
function nt_find(tag: string) -> UiNative {
for i in 0 .. len(nt_list) {
if nt_list[i].tag == tag { return nt_list[i] }
function nt_find(ui_st: UiState, tag: string) -> UiNative {
for i in 0 .. len(ui_st.nt_list) {
if ui_st.nt_list[i].tag == tag { return ui_st.nt_list[i] }
}
return null
}
var nt_q_nodes: []UiNode = new []UiNode
var nt_q_names: []string = new []string
var nt_q_vals: []Val = new []Val
function nt_fired_clear() -> void {
nt_q_nodes = new []UiNode
nt_q_names = new []string
nt_q_vals = new []Val
function nt_fired_clear(ui_st: mut UiState) -> void {
ui_st.nt_q_nodes = new []UiNode
ui_st.nt_q_names = new []string
ui_st.nt_q_vals = new []Val
}
export function ui_fire(n: UiNode, name: string, value: Val) -> void {
push(nt_q_nodes, n)
push(nt_q_names, name)
push(nt_q_vals, value)
export function ui_fire(ui_st: mut UiState, n: UiNode, name: string, value: Val) -> void {
push(ui_st.nt_q_nodes, n)
push(ui_st.nt_q_names, name)
push(ui_st.nt_q_vals, value)
}
function nt_fired_run() -> void {
let nodes = nt_q_nodes
let names = nt_q_names
let vals = nt_q_vals
nt_fired_clear()
for i in 0 .. len(nodes) { nt_run(nodes[i], names[i], vals[i]) }
function nt_fired_run(ui_st: mut UiState) -> void {
let nodes = ui_st.nt_q_nodes
let names = ui_st.nt_q_names
let vals = ui_st.nt_q_vals
nt_fired_clear(ui_st)
for i in 0 .. len(nodes) { nt_run(ui_st, nodes[i], names[i], vals[i]) }
}
# the element's on-<name>, with `event` bound to what happened
function nt_run(n: UiNode, name: string, value: Val) -> void {
function nt_run(ui_st: mut UiState, n: UiNode, name: string, value: Val) -> void {
if n.on_names == null or not n.enabled { return }
for i in 0 .. len(n.on_names) {
if n.on_names[i] == name {
@ -70,7 +66,7 @@ function nt_run(n: UiNode, name: string, value: Val) -> void {
Value.put(ev, "value", value)
let e = env_new(n.env)
env_bind(e, "event", ev)
act_run(n.on_acts[i], e)
act_run(ui_st, n.on_acts[i], e)
}
}
}

View file

@ -1,47 +1,43 @@
# pointer.ludic - the pointer: what it is over is :hover (and all around it, as in CSS), a press on a
# control focuses it and makes it :active, letting go over it is a click. A dragged thumb or scrollbar
# keeps the pointer until let go; a press that takes a scrollbar or closes a popover presses nothing else.
var fr_hover: []string = new []string
var fr_press_key: string = ""
var fr_down_now: bool = false
var fr_key_press: string = "" # the control Enter or the pad's A is holding down
function fr_was_hovered(key: string) -> bool { return sel_has(fr_hover, key) }
function fr_pointer(root: UiNode) -> void {
let i = in_now
fr_down_now = i.down
function fr_was_hovered(ui_st: UiState, key: string) -> bool { return sel_has(ui_st.fr_hover, key) }
function fr_pointer(ui_st: mut UiState, root: UiNode) -> void {
let i = ui_st.in_now
ui_st.fr_down_now = i.down
let keys = new []string
let top = pp_top(root)
var scope = root
if top != null { scope = top }
fr_under(scope, i.x, i.y, keys)
fr_hover = keys
if top != null and pp_pointer(top) { return }
if sc_drag != "" or ct_key_mouse(root) { return }
ui_st.fr_hover = keys
if top != null and pp_pointer(ui_st, top) { return }
if ui_st.sc_drag != "" or ct_key_mouse(ui_st, root) { return }
let hit = fr_hit(scope, i.x, i.y)
pe_frame(root, scope)
if i.down and not in_was_down { fr_press(hit, i.x, i.y) }
if i.down and fr_press_key != "" { ct_drag(root, fr_press_key, i.x, i.y) }
if not i.down and in_was_down {
let was = fr_node(root, fr_press_key)
if was != null { ui_fire(was, "up", Value.null()) }
if hit != null and hit.key == fr_press_key and hit.enabled { ct_activate(hit) }
fr_press_key = ""
pe_frame(ui_st, root, scope)
if i.down and not ui_st.in_was_down { fr_press(ui_st, hit, i.x, i.y) }
if i.down and ui_st.fr_press_key != "" { ct_drag(ui_st, root, ui_st.fr_press_key, i.x, i.y) }
if not i.down and ui_st.in_was_down {
let was = fr_node(root, ui_st.fr_press_key)
if was != null { ui_fire(ui_st, was, "up", Value.null()) }
if hit != null and hit.key == ui_st.fr_press_key and hit.enabled { ct_activate(ui_st, hit) }
ui_st.fr_press_key = ""
}
}
function fr_press(hit: UiNode, x: float, y: float) -> void {
ct_key_pressed_away(hit)
fr_press_key = ""
function fr_press(ui_st: mut UiState, hit: UiNode, x: float, y: float) -> void {
ct_key_pressed_away(ui_st, hit)
ui_st.fr_press_key = ""
if hit == null { return }
fr_press_key = hit.key
if hit.focusable { ui_focus(hit.key) }
if hit.enabled { ui_fire(hit, "down", Value.null()) }
ct_press(hit, x, y)
ui_st.fr_press_key = hit.key
if hit.focusable { ui_focus(ui_st, hit.key) }
if hit.enabled { ui_fire(ui_st, hit, "down", Value.null()) }
ct_press(ui_st, hit, x, y)
}
# :active - the press began on it, or inside it, and is still held (by the pointer, Enter or A)
function fr_active(n: UiNode) -> bool {
if fr_down_now and fr_press_key != "" and lc_mine(fr_press_key, n.key) { return true }
if pe_key != "" and lc_mine(pe_key, n.key) { return true }
return fr_key_press != "" and lc_mine(fr_key_press, n.key)
function fr_active(ui_st: UiState, n: UiNode) -> bool {
if ui_st.fr_down_now and ui_st.fr_press_key != "" and lc_mine(ui_st.fr_press_key, n.key) { return true }
if ui_st.pe_key != "" and lc_mine(ui_st.pe_key, n.key) { return true }
return ui_st.fr_key_press != "" and lc_mine(ui_st.fr_key_press, n.key)
}
function fr_under(n: UiNode, px: float, py: float, keys: []string) -> void {
if px < n.x or py < n.y or px >= n.x + n.cw or py >= n.y + n.ch { return }

View file

@ -11,8 +11,6 @@ export property UiPointer {
wheel: float = 0.0
}
const PE_NAMES: string = " pointerdown pointermove pointerup wheel drag "
var pe_key: string = "" # the element the pointer is captured by
var pe_btn: int = 0
function pe_wants(n: UiNode, wheel: bool) -> bool {
if n.kind == UI_NATIVE and n.native != null and n.native.input != null { return true }
if n.on_names == null { return false }
@ -23,25 +21,25 @@ function pe_wants(n: UiNode, wheel: bool) -> bool {
return false
}
# the pointer's events this frame: to what captured it, else to what it is over
function pe_frame(root: UiNode, scope: UiNode) -> void {
let i = in_now
let p = in_last
function pe_frame(ui_st: mut UiState, root: UiNode, scope: UiNode) -> void {
let i = ui_st.in_now
let p = ui_st.in_last
let moved = p.x >= 0.0 and (i.x != p.x or i.y != p.y)
var over: UiNode = null
if fr_in(scope, i.x, i.y) { over = fr_pick(scope, i.x, i.y, PK_POINTER) }
if pe_key != "" {
let c = fr_node(root, pe_key)
if c != null and moved { pe_send(c, "pointermove", pe_btn) }
if c != null and moved { pe_send(c, "drag", pe_btn) }
if ui_st.pe_key != "" {
let c = fr_node(root, ui_st.pe_key)
if c != null and moved { pe_send(ui_st, c, "pointermove", ui_st.pe_btn) }
if c != null and moved { pe_send(ui_st, c, "drag", ui_st.pe_btn) }
if not (i.down or i.down_right or i.down_middle) {
if c != null { pe_send(c, "pointerup", pe_btn) }
pe_key = ""
if c != null { pe_send(ui_st, c, "pointerup", ui_st.pe_btn) }
ui_st.pe_key = ""
}
} else if over != null {
let b = pe_pressed(i, p)
if b >= 0 { pe_down(over, b) } else if moved { pe_send(over, "pointermove", 0) }
if b >= 0 { pe_down(ui_st, over, b) } else if moved { pe_send(ui_st, over, "pointermove", 0) }
}
if i.wheel != 0.0 and over != null { pe_send(over, "wheel", 0) }
if i.wheel != 0.0 and over != null { pe_send(ui_st, over, "wheel", 0) }
}
function pe_pressed(i: UiInput, p: UiInput) -> int {
if i.down and not p.down { return 0 }
@ -49,24 +47,24 @@ function pe_pressed(i: UiInput, p: UiInput) -> int {
if i.down_middle and not p.down_middle { return 2 }
return -1
}
function pe_down(n: UiNode, b: int) -> void {
pe_key = n.key
pe_btn = b
if not pe_send(n, "pointerdown", b) { pe_key = "" }
function pe_down(ui_st: mut UiState, n: UiNode, b: int) -> void {
ui_st.pe_key = n.key
ui_st.pe_btn = b
if not pe_send(ui_st, n, "pointerdown", b) { ui_st.pe_key = "" }
}
# one event: a native's input answers at once (false: not its to take); an element's on-<kind> runs
# once the frame is drawn, with the pointer's fields on `event`
function pe_send(n: UiNode, kind: string, b: int) -> bool {
function pe_send(ui_st: mut UiState, n: UiNode, kind: string, b: int) -> bool {
let e = new UiPointer
e.kind = kind
e.x = in_now.x - n.x
e.y = in_now.y - n.y
if in_last.x >= 0.0 {
e.dx = in_now.x - in_last.x
e.dy = in_now.y - in_last.y
e.x = ui_st.in_now.x - n.x
e.y = ui_st.in_now.y - n.y
if ui_st.in_last.x >= 0.0 {
e.dx = ui_st.in_now.x - ui_st.in_last.x
e.dy = ui_st.in_now.y - ui_st.in_last.y
}
e.button = b
e.wheel = in_now.wheel
e.wheel = ui_st.in_now.wheel
if n.kind == UI_NATIVE and n.native != null and n.native.input != null { return n.native.input(n, e) }
let v = Value.object()
Value.put(v, "x", Value.float(e.x))
@ -75,7 +73,7 @@ function pe_send(n: UiNode, kind: string, b: int) -> bool {
Value.put(v, "dy", Value.float(e.dy))
Value.put(v, "button", Value.int(b))
Value.put(v, "wheel", Value.float(e.wheel))
ui_fire(n, kind, v)
ui_fire(ui_st, n, kind, v)
return true
}
# a scroll box leaves the wheel to what is under the pointer inside it and takes it itself

View file

@ -2,7 +2,6 @@
# (absolutely, in its parent) and is drawn on the top layer, over everything. While one is up the
# pointer and the keyboard are its: a press outside it, or Esc, closes it (on-close), and Tab
# walks only its own controls.
var pop_list: []UiNode = new []UiNode
function pp_find(n: UiNode, out: []UiNode) -> void {
if n.popover {
push(out, n)
@ -18,16 +17,16 @@ function pp_top(root: UiNode) -> UiNode {
return all[len(all) - 1]
}
# the pointer against the popover: inside it is as usual; a press outside closes it and is spent
function pp_pointer(top: UiNode) -> bool {
let i = in_now
if not (i.down and not in_was_down) { return false }
function pp_pointer(ui_st: mut UiState, top: UiNode) -> bool {
let i = ui_st.in_now
if not (i.down and not ui_st.in_was_down) { return false }
if i.x >= top.x and i.y >= top.y and i.x < top.x + top.cw and i.y < top.y + top.ch { return false }
fr_press_key = ""
ui_fire(top, "close", Value.null())
ui_st.fr_press_key = ""
ui_fire(ui_st, top, "close", Value.null())
return true
}
function pp_draw_top() -> void {
let list = pop_list
pop_list = new []UiNode
for i in 0 .. len(list) { fr_draw(list[i]) }
function pp_draw_top(ui_st: mut UiState) -> void {
let list = ui_st.pop_list
ui_st.pop_list = new []UiNode
for i in 0 .. len(list) { fr_draw(ui_st, list[i]) }
}

View file

@ -1,16 +1,16 @@
# popover_anchor.ludic - `<div popover anchor="cell">` sits beside the element with that id (the
# nearest), a bare `anchor` beside the one before it: `placement` right, left, bottom or top (its margin
# is the gap), `align` start, center or end along that side, flipped and kept within its bounds.
function pp_anchor_all(n: UiNode) -> void {
function pp_anchor_all(ui_st: mut UiState, n: UiNode) -> void {
if n.children == null { return }
for i in 0 .. len(n.children) {
let c = n.children[i]
if c.popover and Value.kind(ui_attr(c, "anchor")) != 0 { pp_anchor(c, pp_anchor_of(n, i)) }
pp_anchor_all(c)
if c.popover and Value.kind(ui_attr(c, "anchor")) != 0 { pp_anchor(ui_st, c, pp_anchor_of(ui_st, n, i)) }
pp_anchor_all(ui_st, c)
}
}
# the element a popover (the i-th child of `parent`) is anchored to, or null
function pp_anchor_of(parent: UiNode, i: int) -> UiNode {
function pp_anchor_of(ui_st: mut UiState, parent: UiNode, i: int) -> UiNode {
let pop = parent.children[i]
let id = ui_attr_text(pop, "anchor")
if id == "" or id == "true" {
@ -25,7 +25,7 @@ function pp_anchor_of(parent: UiNode, i: int) -> UiNode {
if f != null { return f }
at = at.parent
}
ui_err(`anchor="{id}": no element has that id`)
ui_err(ui_st, `anchor="{id}": no element has that id`)
return null
}
function pp_by_id(n: UiNode, id: string, skip: UiNode) -> UiNode {
@ -38,11 +38,11 @@ function pp_by_id(n: UiNode, id: string, skip: UiNode) -> UiNode {
}
return null
}
function pp_anchor(p: UiNode, a: UiNode) -> void {
function pp_anchor(ui_st: mut UiState, p: UiNode, a: UiNode) -> void {
if a == null { return }
let side = ui_attr_text(p, "placement")
let align = ui_attr_text(p, "align")
pp_bounds(p)
pp_bounds(ui_st, p)
let w = p.cw
let h = p.ch
var x = a.x
@ -50,18 +50,18 @@ function pp_anchor(p: UiNode, a: UiNode) -> void {
if side == "top" or side == "bottom" {
let below = a.y + a.ch + p.mt
let above = a.y - h - p.mb
y = pp_side(side == "top", above, below, h, pp_by, pp_bh)
y = pp_side(side == "top", above, below, h, ui_st.pp_by, ui_st.pp_bh)
x = pp_along(align, a.x, a.cw, w) + p.ml
} else {
let right = a.x + a.cw + p.ml
let left = a.x - w - p.mr
x = pp_side(side == "left", left, right, w, pp_bx, pp_bw)
x = pp_side(side == "left", left, right, w, ui_st.pp_bx, ui_st.pp_bw)
y = pp_along(align, a.y, a.ch, h) + p.mt
}
x = Math.max(pp_bx, Math.min(x, pp_bx + pp_bw - w))
y = Math.max(pp_by, Math.min(y, pp_by + pp_bh - h))
ly_place(p, x, y, w, h)
sc_apply(p)
x = Math.max(ui_st.pp_bx, Math.min(x, ui_st.pp_bx + ui_st.pp_bw - w))
y = Math.max(ui_st.pp_by, Math.min(y, ui_st.pp_by + ui_st.pp_bh - h))
ly_place(ui_st, p, x, y, w, h)
sc_apply(ui_st, p)
}
# where along one axis: the side asked for (`low` first when `first_low`), else the other when the
# first leaves the screen and the other does not

View file

@ -1,14 +1,10 @@
# popover_within.ludic - what an anchored popover is kept inside: the element named by within="id",
# else the nearest scroll box (or overflow box) around it, else the screen; it flips against the same
var pp_bx: float = 0.0
var pp_by: float = 0.0
var pp_bw: float = 0.0
var pp_bh: float = 0.0
function pp_bounds(p: UiNode) -> void {
pp_bx = ly_vp_x
pp_by = ly_vp_y
pp_bw = ly_vp_w
pp_bh = ly_vp_h
function pp_bounds(ui_st: mut UiState, p: UiNode) -> void {
ui_st.pp_bx = ui_st.ly_vp_x
ui_st.pp_by = ui_st.ly_vp_y
ui_st.pp_bw = ui_st.ly_vp_w
ui_st.pp_bh = ui_st.ly_vp_h
var box: UiNode = null
let id = ui_attr_text(p, "within")
if id != "" {
@ -17,7 +13,7 @@ function pp_bounds(p: UiNode) -> void {
box = pp_by_id(at, id, p)
at = at.parent
}
if box == null { ui_err(`within="{id}": no element has that id`) }
if box == null { ui_err(ui_st, `within="{id}": no element has that id`) }
} else {
var at = p.parent
while at != null and box == null {
@ -26,10 +22,10 @@ function pp_bounds(p: UiNode) -> void {
}
}
if box == null { return }
pp_bx = box.x
pp_by = box.y
pp_bw = box.cw
pp_bh = box.ch
ui_st.pp_bx = box.x
ui_st.pp_by = box.y
ui_st.pp_bw = box.cw
ui_st.pp_bh = box.ch
}
# where a popover of `size` starts along its anchor's side: level with its start, centred, or its end
function pp_along(align: string, at: float, span: float, size: float) -> float {

View file

@ -1,33 +1,33 @@
# reload.ludic - a developer's templates and stylesheets read again when they change on disk: every
# file is re-read in the order it was first loaded (so an import is there before its importer), and
# the screens' state is kept, as React's fast refresh keeps it. True when anything changed.
export function ui_reload() -> bool {
var changed = uc_reload()
for i in 0 .. len(lib_files) {
let f = lib_files[i]
if f.disk and lib_text(f.path) != f.src { changed = true }
export function ui_reload(ui_st: mut UiState) -> bool {
var changed = uc_reload(ui_st)
for i in 0 .. len(ui_st.lib_files) {
let f = ui_st.lib_files[i]
if f.disk and lib_text(ui_st, f.path) != f.src { changed = true }
}
if not changed { return false }
ui_errs = new []string
if len(lib_files) == 0 {
ui_st.ui_errs = new []string
if len(ui_st.lib_files) == 0 {
print(`ui: templates reloaded`)
return true
}
let old = lib_files
lib_files = new []UiFile
lib_screen_names = new []string
lib_screens = new []UiTpl
ui_errs = new []string
let old = ui_st.lib_files
ui_st.lib_files = new []UiFile
ui_st.lib_screen_names = new []string
ui_st.lib_screens = new []UiTpl
ui_st.ui_errs = new []string
for i in 0 .. len(old) {
let f = old[i]
var have = false
for k in 0 .. len(lib_files) {
if lib_files[k].path == f.path { have = true }
for k in 0 .. len(ui_st.lib_files) {
if ui_st.lib_files[k].path == f.path { have = true }
}
if have { continue }
if f.disk { lib_file(f.path) } else { ui_load_text(f.src, f.path) }
if f.disk { lib_file(ui_st, f.path) } else { ui_load_text(ui_st, f.src, f.path) }
}
uc_invalidate()
uc_invalidate(ui_st)
print(`ui: templates reloaded`)
return true
}

View file

@ -7,19 +7,9 @@
module ludic_ui
import "render3d_image.ludic"
import "render3d_nine.ludic"
var r3u_user: float = 1.0
var r3u_tex_paths: []string = new []string
var r3u_tex_ids: []int = new []int
var r3u_tex_sizes: []int = new []int
var r3u_tex_seen: []float = new []float
var r3u_atlas: []string = new []string
var r3u_atlas_tex: []int = new []int
var r3u_atlas_cols: []int = new []int
var r3u_atlas_rows: []int = new []int
var r3u_atlas_names: [][]string = new [][]string
# importing this file is enough: it is the renderer from the program's first frame
def UiRenderers render3d { make: fn r3u_make }
export function ui_render3d() -> void { ui_backend(r3u_make()) }
export function ui_render3d(ui_st: mut UiState) -> void { ui_backend(ui_st, r3u_make()) }
function r3u_make() -> UiBackend {
let b = new UiBackend
b.rect = fn r3u_rect
@ -38,18 +28,18 @@ function r3u_make() -> UiBackend {
return b
}
# the player's interface size, 1.0 as designed
export function ui_render3d_scale(s: float) -> void { r3u_user = s }
export function ui_render3d_scale(ui_st: mut UiState, s: float) -> void { ui_st.r3u_user = s }
# a grid atlas: `names` are its cells, row by row, `cols` across and `rows` down; <img
# src="prefix:name"> draws one (by its name, or by its number: "item:12")
export function ui_atlas(prefix: string, tex: int, cols: int, rows: int, names: []string) -> void {
push(r3u_atlas, prefix)
push(r3u_atlas_tex, tex)
push(r3u_atlas_cols, cols)
push(r3u_atlas_rows, Math.max(rows, 1))
push(r3u_atlas_names, names)
export function ui_atlas(ui_st: mut UiState, prefix: string, tex: int, cols: int, rows: int, names: []string) -> void {
push(ui_st.r3u_atlas, prefix)
push(ui_st.r3u_atlas_tex, tex)
push(ui_st.r3u_atlas_cols, cols)
push(ui_st.r3u_atlas_rows, Math.max(rows, 1))
push(ui_st.r3u_atlas_names, names)
}
function r3u_now() -> float { return float(Time.now_us() / 1000) / 1000.0 }
function r3u_scale() -> float { return float(gl_h) / 1080.0 * r3u_user }
function r3u_scale(ui_st: UiState) -> float { return float(gl_h) / 1080.0 * ui_st.r3u_user }
function r3u_r(c: int) -> float { return float((c / 65536) % 256) / 255.0 }
function r3u_g(c: int) -> float { return float((c / 256) % 256) / 255.0 }
function r3u_b(c: int) -> float { return float(c % 256) / 255.0 }

View file

@ -1,28 +1,28 @@
# render3d_image.ludic - pictures for the render3d backend: a texture by path (its sRGB bytes as they
# are, clamped at its edges, loaded again when the file changes) and a cell of a named atlas
module ludic_ui
function r3u_tex(path: string) -> int {
for i in 0 .. len(r3u_tex_paths) {
if r3u_tex_paths[i] == path { return r3u_tex_fresh(i) }
function r3u_tex(ui_st: mut UiState, path: string) -> int {
for i in 0 .. len(ui_st.r3u_tex_paths) {
if ui_st.r3u_tex_paths[i] == path { return r3u_tex_fresh(ui_st, i) }
}
push(r3u_tex_paths, path)
push(r3u_tex_ids, r3u_tex_load(path))
push(r3u_tex_sizes, Fs.size(path))
push(r3u_tex_seen, r3u_now())
return r3u_tex_ids[len(r3u_tex_ids) - 1]
push(ui_st.r3u_tex_paths, path)
push(ui_st.r3u_tex_ids, r3u_tex_load(path))
push(ui_st.r3u_tex_sizes, Fs.size(path))
push(ui_st.r3u_tex_seen, r3u_now())
return ui_st.r3u_tex_ids[len(ui_st.r3u_tex_ids) - 1]
}
# a picture is looked at again every half second: a file that has changed (or that was not there,
# and is now) is loaded again, so an <img> of a photograph written over the old one shows the new one
function r3u_tex_fresh(i: int) -> int {
function r3u_tex_fresh(ui_st: mut UiState, i: int) -> int {
let now = r3u_now()
if now - r3u_tex_seen[i] < 0.5 and now >= r3u_tex_seen[i] { return r3u_tex_ids[i] }
r3u_tex_seen[i] = now
let size = Fs.size(r3u_tex_paths[i])
if size == r3u_tex_sizes[i] and (r3u_tex_ids[i] != 0 or size < 0) { return r3u_tex_ids[i] }
if r3u_tex_ids[i] != 0 { gpu_tex_free(r3u_tex_ids[i]) }
r3u_tex_ids[i] = r3u_tex_load(r3u_tex_paths[i])
r3u_tex_sizes[i] = size
return r3u_tex_ids[i]
if now - ui_st.r3u_tex_seen[i] < 0.5 and now >= ui_st.r3u_tex_seen[i] { return ui_st.r3u_tex_ids[i] }
ui_st.r3u_tex_seen[i] = now
let size = Fs.size(ui_st.r3u_tex_paths[i])
if size == ui_st.r3u_tex_sizes[i] and (ui_st.r3u_tex_ids[i] != 0 or size < 0) { return ui_st.r3u_tex_ids[i] }
if ui_st.r3u_tex_ids[i] != 0 { gpu_tex_free(ui_st.r3u_tex_ids[i]) }
ui_st.r3u_tex_ids[i] = r3u_tex_load(ui_st.r3u_tex_paths[i])
ui_st.r3u_tex_sizes[i] = size
return ui_st.r3u_tex_ids[i]
}
# the interface is drawn in sRGB, and a picture's bytes are sRGB, so they are taken as they are
# (not decoded to linear, which would darken it); clamped at its edges
@ -36,64 +36,64 @@ function r3u_tex_load(path: string) -> int {
return t
}
# a picture the program has written again, loaded again the next time it is drawn
export function ui_image_reload(path: string) -> void {
for i in 0 .. len(r3u_tex_paths) {
if r3u_tex_paths[i] == path {
r3u_tex_seen[i] = -1000000.0
r3u_tex_sizes[i] = -2
export function ui_image_reload(ui_st: mut UiState, path: string) -> void {
for i in 0 .. len(ui_st.r3u_tex_paths) {
if ui_st.r3u_tex_paths[i] == path {
ui_st.r3u_tex_seen[i] = -1000000.0
ui_st.r3u_tex_sizes[i] = -2
}
}
}
function r3u_image(src: string, x: float, y: float, w: float, h: float, c: int, a: float) -> void {
function r3u_image(ui_st: mut UiState, src: string, x: float, y: float, w: float, h: float, c: int, a: float) -> void {
var colon = -1
for i in 0 .. len(src) {
if colon < 0 and src[i] == 58 { colon = i }
}
if colon > 0 {
let prefix: string = src[0..colon]
for k in 0 .. len(r3u_atlas) {
if r3u_atlas[k] == prefix {
r3u_cell(k, src[colon + 1..len(src)], x, y, w, h, c, a)
for k in 0 .. len(ui_st.r3u_atlas) {
if ui_st.r3u_atlas[k] == prefix {
r3u_cell(ui_st, k, src[colon + 1..len(src)], x, y, w, h, c, a)
return
}
}
}
let t = r3u_tex(src)
let t = r3u_tex(ui_st, src)
if t != 0 { ov_sub(t, int(x), int(y), int(w), int(h), 0.0, 0.0, 1.0, 1.0, r3u_r(c), r3u_g(c), r3u_b(c), a) }
}
# a picture's own size, for object-fit; an atlas cell is drawn square
function r3u_image_w(src: string) -> float {
if r3u_is_cell(src) { return 1.0 }
return float(tex_width(r3u_tex(src)))
function r3u_image_w(ui_st: mut UiState, src: string) -> float {
if r3u_is_cell(ui_st, src) { return 1.0 }
return float(tex_width(r3u_tex(ui_st, src)))
}
function r3u_image_h(src: string) -> float {
if r3u_is_cell(src) { return 1.0 }
return float(tex_height(r3u_tex(src)))
function r3u_image_h(ui_st: mut UiState, src: string) -> float {
if r3u_is_cell(ui_st, src) { return 1.0 }
return float(tex_height(r3u_tex(ui_st, src)))
}
function r3u_is_cell(src: string) -> bool {
for k in 0 .. len(r3u_atlas) {
let p = r3u_atlas[k]
function r3u_is_cell(ui_st: UiState, src: string) -> bool {
for k in 0 .. len(ui_st.r3u_atlas) {
let p = ui_st.r3u_atlas[k]
if len(src) > len(p) and src[0..len(p)] == p and src[len(p)] == 58 { return true }
}
return false
}
function r3u_cell(k: int, name: string, x: float, y: float, w: float, h: float, c: int, a: float) -> void {
let names = r3u_atlas_names[k]
function r3u_cell(ui_st: mut UiState, k: int, name: string, x: float, y: float, w: float, h: float, c: int, a: float) -> void {
let names = ui_st.r3u_atlas_names[k]
var idx = -1
for i in 0 .. len(names) {
if names[i] == name { idx = i }
}
if idx < 0 and len(name) > 0 and name[0] >= 48 and name[0] <= 57 { idx = int(el_num(name)) }
if idx < 0 {
ui_err(`{r3u_atlas[k]}:{name}: the atlas has no such picture`)
ui_err(ui_st, `{ui_st.r3u_atlas[k]}:{name}: the atlas has no such picture`)
return
}
let cols = r3u_atlas_cols[k]
let rows = r3u_atlas_rows[k]
let cols = ui_st.r3u_atlas_cols[k]
let rows = ui_st.r3u_atlas_rows[k]
let cx = idx % cols
let cy = idx / cols
let u0 = float(cx) / float(cols)
let v0 = float(cy) / float(rows)
let s = Math.min(w, h)
ov_sub(r3u_atlas_tex[k], int(x + (w - s) / 2.0), int(y + (h - s) / 2.0), int(s), int(s), u0, v0, u0 + 1.0 / float(cols), v0 + 1.0 / float(rows), r3u_r(c), r3u_g(c), r3u_b(c), a)
ov_sub(ui_st.r3u_atlas_tex[k], int(x + (w - s) / 2.0), int(y + (h - s) / 2.0), int(s), int(s), u0, v0, u0 + 1.0 / float(cols), v0 + 1.0 / float(rows), r3u_r(c), r3u_g(c), r3u_b(c), a)
}

View file

@ -3,8 +3,8 @@ module ludic_ui
# border-image: the texture's `slice`-pixel corners kept, the rest stretched, drawn `dst` wide (at
# most half the box each way, ui_nine_cuts); the slice is a share of the texture's own width across
# and its own height down, so it need not be square
function r3u_nine(src: string, slice: float, x: float, y: float, w: float, h: float, dst: float, c: int, a: float) -> void {
let t = r3u_tex(src)
function r3u_nine(ui_st: mut UiState, src: string, slice: float, x: float, y: float, w: float, h: float, dst: float, c: int, a: float) -> void {
let t = r3u_tex(ui_st, src)
if t == 0 { return }
let su = slice / float(Math.max(tex_width(t), 1))
let sv = slice / float(Math.max(tex_height(t), 1))

View file

@ -1,10 +1,10 @@
# screen_class.ludic - a Ludic component shown as a whole screen
# a Ludic component shown as a screen: an instance at the top, keyed by its name
function fr_class_nodes(cls: UiClass) -> UiNode {
lc_begin()
in_build += 1
ui_view = null
uc_load(cls)
function fr_class_nodes(ui_st: mut UiState, cls: UiClass) -> UiNode {
lc_begin(ui_st)
ui_st.in_build += 1
ui_st.ui_view = null
uc_load(ui_st, cls)
let root = fr_root(cls.name, cls.sheet)
let use = new UiTpl
use.tag = cls.name
@ -17,9 +17,9 @@ function fr_class_nodes(cls: UiClass) -> UiNode {
let e = env_new(null)
e.model = Value.object()
root.env = e
bd_class(use, e, cls.name, root.children)
in_sweep(cls.name)
cs_tree(root)
bd_class(ui_st, use, e, cls.name, root.children)
in_sweep(ui_st, cls.name)
cs_tree(ui_st, root)
return root
}
function fr_root(key: string, sheet: UiSheet) -> UiNode {

View file

@ -1,60 +1,56 @@
# scroll.ludic - a box with overflow (or <scroll>) scrolls itself: the wheel over it, its scrollbar
# held and dragged, and the focused control pulled into view. Its content is shifted by how far it is
# scrolled and clipped to it, and the offset is kept by its key from frame to frame.
var sc_keys: []string = new []string
var sc_offs: []float = new []float
var sc_drag: string = ""
var sc_scope: UiNode = null # the popover that has the pointer this frame, if one is up
function sc_get(key: string) -> float {
for i in 0 .. len(sc_keys) {
if sc_keys[i] == key { return sc_offs[i] }
function sc_get(ui_st: UiState, key: string) -> float {
for i in 0 .. len(ui_st.sc_keys) {
if ui_st.sc_keys[i] == key { return ui_st.sc_offs[i] }
}
return 0.0
}
function sc_put(key: string, v: float) -> void {
for i in 0 .. len(sc_keys) {
if sc_keys[i] == key {
sc_offs[i] = v
function sc_put(ui_st: mut UiState, key: string, v: float) -> void {
for i in 0 .. len(ui_st.sc_keys) {
if ui_st.sc_keys[i] == key {
ui_st.sc_offs[i] = v
return
}
}
push(sc_keys, key)
push(sc_offs, v)
push(ui_st.sc_keys, key)
push(ui_st.sc_offs, v)
}
# every scroll box, outermost first: its offset (from input, when `live`), then its content moved
function sc_update(n: UiNode) -> void {
sc_scope = pp_top(n)
if not in_now.down { sc_drag = "" }
sc_walk(n, true)
function sc_update(ui_st: mut UiState, n: UiNode) -> void {
ui_st.sc_scope = pp_top(n)
if not ui_st.in_now.down { ui_st.sc_drag = "" }
sc_walk(ui_st, n, true)
}
function sc_apply(n: UiNode) -> void { sc_walk(n, false) }
function sc_walk(n: UiNode, live: bool) -> void {
function sc_apply(ui_st: mut UiState, n: UiNode) -> void { sc_walk(ui_st, n, false) }
function sc_walk(ui_st: mut UiState, n: UiNode, live: bool) -> void {
if n.kind == UI_SCROLL {
let room = n.ch - ly_vin(n)
let most = Math.max(n.content_h - ly_vin(n) - room, 0.0)
let held = sc_held(n, sc_get(n.key), live)
let held = sc_held(ui_st, n, sc_get(ui_st, n.key), live)
var off = held
if live { off = sc_input(n, off, room, most) }
if live { off = sc_input(ui_st, n, off, room, most) }
off = Math.clamp(off, 0.0, most)
if live and off != Math.clamp(held, 0.0, most) { ui_fire(n, "scroll", Value.float(off / ui_px(1.0))) }
sc_put(n.key, off)
if live and off != Math.clamp(held, 0.0, most) { ui_fire(ui_st, n, "scroll", Value.float(off / ui_px(ui_st, 1.0))) }
sc_put(ui_st, n.key, off)
n.scroll_y = off
if off != 0.0 {
for k in 0 .. len(n.children) { ly_offset(n.children[k], 0.0, 0.0 - off) }
}
}
for k in 0 .. len(n.children) { sc_walk(n.children[k], live) }
for k in 0 .. len(n.children) { sc_walk(ui_st, n.children[k], live) }
}
# the wheel over it, its bar held and dragged (scroll_bar.ludic), the keyboard's focus pulled into
# view; while a popover is up, only a box inside it answers the pointer
function sc_input(n: UiNode, off0: float, room: float, most: float) -> float {
function sc_input(ui_st: mut UiState, n: UiNode, off0: float, room: float, most: float) -> float {
var off = off0
if sc_scope != null and not sc_within(n, sc_scope) { return sc_into_view(n, off, room) }
let i = in_now
if ui_st.sc_scope != null and not sc_within(n, ui_st.sc_scope) { return sc_into_view(ui_st, n, off, room) }
let i = ui_st.in_now
let over = i.x >= n.x and i.y >= n.y and i.x < n.x + n.cw and i.y < n.y + n.ch
if over and i.wheel != 0.0 and not pe_takes_wheel(n, i.x, i.y + off0) { off = off - i.wheel * ui_px(40.0) }
off = sb_input(n, off, room, most)
return sc_into_view(n, off, room)
if over and i.wheel != 0.0 and not pe_takes_wheel(n, i.x, i.y + off0) { off = off - i.wheel * ui_px(ui_st, 40.0) }
off = sb_input(ui_st, n, off, room, most)
return sc_into_view(ui_st, n, off, room)
}
function sc_within(n: UiNode, top: UiNode) -> bool {
var at = n
@ -65,13 +61,13 @@ function sc_within(n: UiNode, top: UiNode) -> bool {
return false
}
# the focused control, moved by the keyboard, is brought into the box's view
function sc_into_view(n: UiNode, off: float, room: float) -> float {
if not fc_visible or fc_key == "" or not lc_mine(fc_key, n.key) { return off }
let f = fr_node(n, fc_key)
function sc_into_view(ui_st: UiState, n: UiNode, off: float, room: float) -> float {
if not ui_st.fc_visible or ui_st.fc_key == "" or not lc_mine(ui_st.fc_key, n.key) { return off }
let f = fr_node(n, ui_st.fc_key)
if f == null { return off }
let top = f.y - n.y - n.pt
if top < off { return top - ui_px(8.0) }
if top + f.ch > off + room { return top + f.ch - room + ui_px(8.0) }
if top < off { return top - ui_px(ui_st, 8.0) }
if top + f.ch > off + room { return top + f.ch - room + ui_px(ui_st, 8.0) }
return off
}
# ui_place moved every box by its stored offset; the frame takes that back before moving it again

View file

@ -1,63 +1,57 @@
# scroll_bar.ludic - a scroll box's bar: .ui-scrollbar, the track down its right edge, and its
# .ui-thumb, styled by the default sheet or a theme as the box's own parts. A press on the thumb holds
# it where it was taken; a press on the track jumps the thumb's middle to the pointer and holds it there.
var sb_x: float = 0.0
var sb_w: float = 0.0
var sb_ty: float = 0.0
var sb_th: float = 0.0
var sb_bar: UiNode = null
var sc_grab: float = 0.0 # how far below the thumb's top the pointer holds it
# the bar's parts, styled as descendants of the box, and where they are at this offset
function sb_geo(n: UiNode, off: float, room: float, most: float) -> void {
function sb_geo(ui_st: mut UiState, n: UiNode, off: float, room: float, most: float) -> void {
let bar = ct_part(n, "ui-scrollbar", "")
bar.tag = "div"
bar.parent = n
push(bar.children, ct_part(n, "ui-thumb", ""))
cs_tree(bar)
sb_bar = bar
sb_w = ui_px(6.0)
if bar.w >= 0.0 { sb_w = bar.w }
sb_x = n.x + n.cw - sb_w - bar.mr
var least = ui_px(24.0)
cs_tree(ui_st, bar)
ui_st.sb_bar = bar
ui_st.sb_w = ui_px(ui_st, 6.0)
if bar.w >= 0.0 { ui_st.sb_w = bar.w }
ui_st.sb_x = n.x + n.cw - ui_st.sb_w - bar.mr
var least = ui_px(ui_st, 24.0)
if len(bar.children) > 0 and bar.children[0].min_h > 0.0 { least = bar.children[0].min_h }
sb_th = Math.min(Math.max(n.ch * room / (room + most), least), n.ch)
sb_ty = n.y
if most > 0.0 { sb_ty = n.y + (n.ch - sb_th) * Math.clamp(off, 0.0, most) / most }
ui_st.sb_th = Math.min(Math.max(n.ch * room / (room + most), least), n.ch)
ui_st.sb_ty = n.y
if most > 0.0 { ui_st.sb_ty = n.y + (n.ch - ui_st.sb_th) * Math.clamp(off, 0.0, most) / most }
}
# the pointer on the bar: taken on a press over it, moved while held, let go on release
function sb_input(n: UiNode, off: float, room: float, most: float) -> float {
let i = in_now
function sb_input(ui_st: mut UiState, n: UiNode, off: float, room: float, most: float) -> float {
let i = ui_st.in_now
if most <= 0.0 or room <= 0.0 { return off }
if i.down and not in_was_down and sc_drag == "" {
sb_geo(n, off, room, most)
let reach = Math.min(sb_x, n.x + n.cw - ui_px(14.0))
if i.down and not ui_st.in_was_down and ui_st.sc_drag == "" {
sb_geo(ui_st, n, off, room, most)
let reach = Math.min(ui_st.sb_x, n.x + n.cw - ui_px(ui_st, 14.0))
let over = i.x >= reach and i.y >= n.y and i.x < n.x + n.cw and i.y < n.y + n.ch
if over and not sb_bar.hidden {
sc_drag = n.key
sc_grab = sb_th / 2.0
if i.y >= sb_ty and i.y < sb_ty + sb_th { sc_grab = i.y - sb_ty }
if over and not ui_st.sb_bar.hidden {
ui_st.sc_drag = n.key
ui_st.sc_grab = ui_st.sb_th / 2.0
if i.y >= ui_st.sb_ty and i.y < ui_st.sb_ty + ui_st.sb_th { ui_st.sc_grab = i.y - ui_st.sb_ty }
}
}
if sc_drag != n.key { return off }
sb_geo(n, off, room, most)
let span = n.ch - sb_th
if ui_st.sc_drag != n.key { return off }
sb_geo(ui_st, n, off, room, most)
let span = n.ch - ui_st.sb_th
if span <= 0.0 { return off }
return (i.y - sc_grab - n.y) / span * most
return (i.y - ui_st.sc_grab - n.y) / span * most
}
# the track, then the thumb over it; scrollbar-color on the box wins over the parts' backgrounds
function sc_bar(n: UiNode) -> void {
function sc_bar(ui_st: mut UiState, n: UiNode) -> void {
let room = n.ch - ly_vin(n)
let most = n.content_h - ly_vin(n) - room
if most <= 0.0 or ui_be.rect == null { return }
sb_geo(n, n.scroll_y, room, most)
let bar = sb_bar
if most <= 0.0 or ui_st.ui_be.rect == null { return }
sb_geo(ui_st, n, n.scroll_y, room, most)
let bar = ui_st.sb_bar
if bar.hidden { return }
var track = bar.bg
if n.bar_track != -2 { track = n.bar_track }
if track >= 0 { fr_box(sb_x, n.y, sb_w, n.ch, bar.radius, track, bar.bg_a * fr_ga) }
if track >= 0 { fr_box(ui_st, ui_st.sb_x, n.y, ui_st.sb_w, n.ch, bar.radius, track, bar.bg_a * ui_st.fr_ga) }
if len(bar.children) == 0 { return }
let t = bar.children[0]
var thumb = t.bg
if n.bar_c >= 0 { thumb = n.bar_c }
if thumb >= 0 { fr_box(sb_x, sb_ty, sb_w, sb_th, t.radius, thumb, t.bg_a * fr_ga) }
if thumb >= 0 { fr_box(ui_st, ui_st.sb_x, ui_st.sb_ty, ui_st.sb_w, ui_st.sb_th, t.radius, thumb, t.bg_a * ui_st.fr_ga) }
}

View file

@ -1,29 +1,27 @@
# scroll_held.ludic - a scroll box held at an offset: scroll-top="{px}" puts it there every frame
# (the wheel, the bar and the focus still move it, and say so with on-scroll and event.value, for
# the program to keep or not), and ui_scroll_set(id or key, px) moves it once
var sc_want_keys: []string = new []string
var sc_want_px: []float = new []float
export function ui_scroll_set(key: string, px: float) -> void {
push(sc_want_keys, key)
push(sc_want_px, px)
export function ui_scroll_set(ui_st: mut UiState, key: string, px: float) -> void {
push(ui_st.sc_want_keys, key)
push(ui_st.sc_want_px, px)
}
# how far a box is scrolled before this frame's input: what it was, what ui_scroll_set asked for,
# or what its scroll-top says
function sc_held(n: UiNode, off: float, live: bool) -> float {
function sc_held(ui_st: mut UiState, n: UiNode, off: float, live: bool) -> float {
var at = off
if live {
let keys = new []string
let pxs = new []float
for i in 0 .. len(sc_want_keys) {
if sc_want_keys[i] == n.key or (n.id != "" and sc_want_keys[i] == n.id) { at = ui_px(sc_want_px[i]) } else {
push(keys, sc_want_keys[i])
push(pxs, sc_want_px[i])
for i in 0 .. len(ui_st.sc_want_keys) {
if ui_st.sc_want_keys[i] == n.key or (n.id != "" and ui_st.sc_want_keys[i] == n.id) { at = ui_px(ui_st, ui_st.sc_want_px[i]) } else {
push(keys, ui_st.sc_want_keys[i])
push(pxs, ui_st.sc_want_px[i])
}
}
sc_want_keys = keys
sc_want_px = pxs
ui_st.sc_want_keys = keys
ui_st.sc_want_px = pxs
}
let v = ui_attr(n, "scroll-top")
if Value.kind(v) != 0 { at = ui_px(el_num(vl_text(v))) }
if Value.kind(v) != 0 { at = ui_px(ui_st, el_num(vl_text(v))) }
return at
}

View file

@ -11,26 +11,26 @@ function sel_spec(s: UiSel) -> int {
}
return n
}
function sel_match(s: UiSel, n: UiNode) -> bool {
if not sel_one(s, n) { return false }
function sel_match(ui_st: mut UiState, s: UiSel, n: UiNode) -> bool {
if not sel_one(ui_st, s, n) { return false }
if s.prev == null { return true }
if s.comb == 1 { return n.parent != null and sel_match(s.prev, n.parent) }
if s.comb == 2 { return n.parent != null and n.index > 0 and sel_match(s.prev, n.parent.children[n.index - 1]) }
if s.comb == 1 { return n.parent != null and sel_match(ui_st, s.prev, n.parent) }
if s.comb == 2 { return n.parent != null and n.index > 0 and sel_match(ui_st, s.prev, n.parent.children[n.index - 1]) }
if s.comb == 3 {
if n.parent == null { return false }
for i in 0 .. n.index {
if sel_match(s.prev, n.parent.children[i]) { return true }
if sel_match(ui_st, s.prev, n.parent.children[i]) { return true }
}
return false
}
var a = n.parent
while a != null {
if sel_match(s.prev, a) { return true }
if sel_match(ui_st, s.prev, a) { return true }
a = a.parent
}
return false
}
function sel_one(s: UiSel, n: UiNode) -> bool {
function sel_one(ui_st: mut UiState, s: UiSel, n: UiNode) -> bool {
if s.tag != "" and s.tag != n.tag { return false }
if s.id != "" and s.id != n.id { return false }
for i in 0 .. len(s.classes) {
@ -41,10 +41,10 @@ function sel_one(s: UiSel, n: UiNode) -> bool {
if v == null or (s.aops[i] == 1 and v != s.avals[i]) { return false }
}
for i in 0 .. len(s.pseudos) {
if not sel_pseudo_ok(s.pseudos[i], s.pargs[i], n) { return false }
if not sel_pseudo_ok(ui_st, s.pseudos[i], s.pargs[i], n) { return false }
}
for i in 0 .. len(s.nots) {
if sel_one(s.nots[i], n) { return false }
if sel_one(ui_st, s.nots[i], n) { return false }
}
return true
}
@ -62,11 +62,11 @@ function sel_attr_of(n: UiNode, key: string) -> string {
}
return null
}
function sel_pseudo_ok(p: string, arg: string, n: UiNode) -> bool {
function sel_pseudo_ok(ui_st: mut UiState, p: string, arg: string, n: UiNode) -> bool {
if p == "hover" { return n.hovered }
if p == "focus" { return fc_key != "" and n.key == fc_key }
if p == "focus-visible" { return fc_visible and fc_key != "" and n.key == fc_key }
if p == "focus-within" { return fc_key != "" and lc_mine(fc_key, n.key) }
if p == "focus" { return ui_st.fc_key != "" and n.key == ui_st.fc_key }
if p == "focus-visible" { return ui_st.fc_visible and ui_st.fc_key != "" and n.key == ui_st.fc_key }
if p == "focus-within" { return ui_st.fc_key != "" and lc_mine(ui_st.fc_key, n.key) }
if p == "disabled" { return not n.enabled }
if p == "enabled" { return n.enabled }
if p == "first-child" { return n.index == 0 }
@ -75,9 +75,9 @@ function sel_pseudo_ok(p: string, arg: string, n: UiNode) -> bool {
if p == "empty" { return len(n.children) == 0 and n.text == "" }
if p == "root" { return n.parent == null }
if p == "checked" { return ui_attr_on(n, "checked") }
if p == "active" { return fr_active(n) }
if p == "capturing" { return ct_capture != "" and n.key == ct_capture }
if p == "active" { return fr_active(ui_st, n) }
if p == "capturing" { return ui_st.ct_capture != "" and n.key == ui_st.ct_capture }
if p == "nth-child" { return sel_nth(arg, n.index + 1) }
ui_err(`:{p}: there is no such pseudo-class here`)
ui_err(ui_st, `:{p}: there is no such pseudo-class here`)
return false
}

View file

@ -1,34 +1,32 @@
# state.ludic - a component's or a screen's own values, <state name="{first}"/>, kept from frame
# to frame by where the instance is in the tree, and set by `set name = value`
var st_keys: []string = new []string
var st_vals: []Val = new []Val
function st_for(key: string, t: UiTpl, e: UiEnv) -> Val {
function st_for(ui_st: mut UiState, key: string, t: UiTpl, e: UiEnv) -> Val {
var decl: UiTpl = null
for i in 0 .. len(t.kids) {
if t.kids[i].tag == "state" { decl = t.kids[i] }
}
if decl == null { return null }
for i in 0 .. len(st_keys) {
if st_keys[i] == key { return st_vals[i] }
for i in 0 .. len(ui_st.st_keys) {
if ui_st.st_keys[i] == key { return ui_st.st_vals[i] }
}
let o = Value.object()
for i in 0 .. len(decl.keys) { Value.put(o, decl.keys[i], ui_eval(decl.vals[i], e)) }
push(st_keys, key)
push(st_vals, o)
for i in 0 .. len(decl.keys) { Value.put(o, decl.keys[i], ui_eval(ui_st, decl.vals[i], e)) }
push(ui_st.st_keys, key)
push(ui_st.st_vals, o)
return o
}
# a screen's state starts again the next time it opens
export function ui_forget(screen: string) -> void {
export function ui_forget(ui_st: mut UiState, screen: string) -> void {
let keys = new []string
let vals = new []Val
for i in 0 .. len(st_keys) {
let k = st_keys[i]
for i in 0 .. len(ui_st.st_keys) {
let k = ui_st.st_keys[i]
let mine = len(k) >= len(screen) and k[0..len(screen)] == screen and (len(k) == len(screen) or k[len(screen)] == 47)
if not mine {
push(keys, k)
push(vals, st_vals[i])
push(vals, ui_st.st_vals[i])
}
}
st_keys = keys
st_vals = vals
ui_st.st_keys = keys
ui_st.st_vals = vals
}

View file

@ -12,45 +12,45 @@ export property UiRule {
export property UiSheet {
rules: []UiRule = null
}
function sheet_read(f: UiFile, t: UiTpl) -> void {
function sheet_read(ui_st: mut UiState, f: UiFile, t: UiTpl) -> void {
var src = t.raw
let from = bd_name(t, "src", "")
if from != "" { src = lib_text(lib_beside(f.path, from)) }
lss_text(f, src)
if from != "" { src = lib_text(ui_st, lib_beside(f.path, from)) }
lss_text(ui_st, f, src)
}
function lss_is(path: string) -> bool { return len(path) > 4 and path[len(path) - 4..len(path)] == ".lss" }
# a stylesheet file: a file with rules and imports and nothing else
function lss_file(src: string, path: string) -> UiFile {
let f = lib_new(path)
function lss_file(ui_st: mut UiState, src: string, path: string) -> UiFile {
let f = lib_new(ui_st, path)
f.src = src
lss_text(f, src)
lss_text(ui_st, f, src)
f.sheet = sheet_of(f)
return f
}
function lss_text(f: UiFile, src: string) -> void {
function lss_text(ui_st: mut UiState, f: UiFile, src: string) -> void {
let err = new []string
lss_block(f, lss_strip(src), null, err)
for i in 0 .. len(err) { ui_err(`{f.path}: <style> {err[i]}`) }
lss_block(ui_st, f, lss_strip(src), null, err)
for i in 0 .. len(err) { ui_err(ui_st, `{f.path}: <style> {err[i]}`) }
}
# rules, @imports and @media blocks; the rules in a @media block carry its condition
function lss_block(f: UiFile, src: string, md: UiMedia, err: []string) -> void {
function lss_block(ui_st: mut UiState, f: UiFile, src: string, md: UiMedia, err: []string) -> void {
let r = new UiRd
r.s = src
while r.err == "" {
rd_ws(r)
if r.i >= len(r.s) { break }
if rd_op(r, "@import") {
lss_import(f, lss_upto(r, 59))
lss_import(ui_st, f, lss_upto(r, 59))
continue
}
if rd_op(r, "@keyframes") {
let name = lss_upto(r, 123)
an_read(name, lss_upto(r, 125), err)
an_read(ui_st, name, lss_upto(r, 125), err)
continue
}
if rd_op(r, "@media") {
let cond = md_parse(lss_upto(r, 123))
lss_block(f, lss_upto(r, 125), cond, err)
lss_block(ui_st, f, lss_upto(r, 125), cond, err)
continue
}
let sels = lss_upto(r, 123)
@ -62,11 +62,11 @@ function lss_block(f: UiFile, src: string, md: UiMedia, err: []string) -> void {
if r.err != "" { push(err, r.err) }
}
# @import "base.lss"; - its rules come before this file's own
function lss_import(f: UiFile, what: string) -> void {
function lss_import(ui_st: mut UiState, f: UiFile, what: string) -> void {
var p = tpl_words(what)
if len(p) > 5 and p[0..4] == "url(" { p = p[4..len(p) - 1] }
if len(p) >= 2 and (p[0] == 34 or p[0] == 39) { p = p[1..len(p) - 1] }
let other = lib_file(lib_beside(f.path, p))
let other = lib_file(ui_st, lib_beside(f.path, p))
if other != null {
push(f.imp, other)
push(f.imp_as, "")

View file

@ -1,13 +1,12 @@
# text_fit.ludic - text-fit: shrink 12px draws a line that does not fit its box smaller, down to that
# size, and cuts what still does not fit with an ellipsis; line-height (a multiple of the font size,
# px or em) spaces a text's lines. Both are inherited, as font properties are.
var tf_size: float = 0.0 # the size tf_fit drew at
function el_text_fit(n: UiNode, s: string) -> void {
function el_text_fit(ui_st: UiState, n: UiNode, s: string) -> void {
let parts = lss_split(tpl_words(s), 32)
nx(n).fit_min = 0.0
if len(parts) >= 2 and parts[0] == "shrink" { nx(n).fit_min = Math.max(el_unit(parts[1]), 1.0) }
if len(parts) >= 2 and parts[0] == "shrink" { nx(n).fit_min = Math.max(el_unit(ui_st, parts[1]), 1.0) }
}
function el_line_h(n: UiNode, s0: string) -> void {
function el_line_h(ui_st: UiState, n: UiNode, s0: string) -> void {
let s = tpl_words(s0)
let x = nx(n)
x.lh_kind = 0
@ -19,7 +18,7 @@ function el_line_h(n: UiNode, s0: string) -> void {
return
}
x.lh_kind = 2
x.lh_v = el_unit(s)
x.lh_v = el_unit(ui_st, s)
}
# the nearest element, out from this one, that says `what` (0 text-fit, 1 line-height), or null
function tf_up(n: UiNode, what: int) -> UiNodeX {
@ -39,14 +38,14 @@ function ly_line_h(n: UiNode) -> float {
}
# one line to fit its box: at its own size if it does, else smaller down to text-fit's least, then
# cut; tf_size says the size it is drawn at
function tf_fit(n: UiNode, s: string) -> string {
tf_size = n.rsize
function tf_fit(ui_st: mut UiState, n: UiNode, s: string) -> string {
ui_st.tf_size = n.rsize
let x = tf_up(n, 0)
let room = n.cw - ly_hin(n)
if x == null or room <= 0.0 { return s }
let w = ly_text_w(s, tf_size)
let w = ly_text_w(ui_st, s, ui_st.tf_size)
if w <= room { return s }
tf_size = Math.max(x.fit_min, n.rsize * room / w)
while tf_size > x.fit_min and ly_text_w(s, tf_size) > room { tf_size = Math.max(x.fit_min, tf_size - 0.5) }
return ly_ellipsis(s, tf_size, room)
ui_st.tf_size = Math.max(x.fit_min, n.rsize * room / w)
while ui_st.tf_size > x.fit_min and ly_text_w(ui_st, s, ui_st.tf_size) > room { ui_st.tf_size = Math.max(x.fit_min, ui_st.tf_size - 0.5) }
return ly_ellipsis(ui_st, s, ui_st.tf_size, room)
}

View file

@ -1,34 +1,29 @@
# tooltip.ludic - an element's title="..." shown beside the pointer once it has rested on it for half
# a second (or under it, when the keyboard's focus has), in a box styled by .ui-tooltip; a title of
# several lines (a newline, or \n) is one .ui-tooltip-text a line
var tt_key: string = ""
var tt_since: float = 0.0
var tt_text: string = ""
var tt_x: float = 0.0
var tt_y: float = 0.0
function tt_track(root: UiNode, x: float, y: float) -> void {
function tt_track(ui_st: mut UiState, root: UiNode, x: float, y: float) -> void {
var scope = pp_top(root)
if scope == null { scope = root }
var n = tt_under(scope, x, y)
var px = x
var py = y
let k = tt_focused(scope, x, y)
let k = tt_focused(ui_st, scope, x, y)
if k != null {
n = k
px = k.x
py = k.y + k.ch - ui_px(14.0)
py = k.y + k.ch - ui_px(ui_st, 14.0)
}
if n == null {
tt_key = ""
ui_st.tt_key = ""
return
}
if n.key != tt_key {
tt_key = n.key
tt_since = an_now()
if n.key != ui_st.tt_key {
ui_st.tt_key = n.key
ui_st.tt_since = an_now(ui_st)
}
tt_text = tt_translate(n, ui_attr_text(n, "title"))
tt_x = px
tt_y = py
ui_st.tt_text = tt_translate(ui_st, n, ui_attr_text(n, "title"))
ui_st.tt_x = px
ui_st.tt_y = py
}
# the deepest element under the point that has a title
function tt_under(n: UiNode, x: float, y: float) -> UiNode {
@ -42,20 +37,20 @@ function tt_under(n: UiNode, x: float, y: float) -> UiNode {
if n.akeys != null and ui_attr_text(n, "title") != "" { return n }
return null
}
function tt_draw(root: UiNode) -> void {
if tt_key == "" or tt_text == "" or an_now() - tt_since < 0.5 { return }
function tt_draw(ui_st: mut UiState, root: UiNode) -> void {
if ui_st.tt_key == "" or ui_st.tt_text == "" or an_now(ui_st) - ui_st.tt_since < 0.5 { return }
let n = ct_part(root, "ui-tooltip", "")
n.tag = "div"
n.parent = root
n.children = new []UiNode
let lines = tt_lines(tt_text)
let lines = tt_lines(ui_st.tt_text)
for i in 0 .. len(lines) { push(n.children, ct_part(root, "ui-tooltip-text", lines[i])) }
cs_tree(n)
ly_measure(n, ly_vp_w * 0.4, ui_px(16.0), root.rfg)
let x = Math.min(tt_x + ui_px(16.0), ly_vp_x + ly_vp_w - n.mw)
let y = Math.min(tt_y + ui_px(20.0), ly_vp_y + ly_vp_h - n.mh)
ly_place(n, x, y, n.mw, n.mh)
fr_draw(n)
cs_tree(ui_st, n)
ly_measure(ui_st, n, ui_st.ly_vp_w * 0.4, ui_px(ui_st, 16.0), root.rfg)
let x = Math.min(ui_st.tt_x + ui_px(ui_st, 16.0), ui_st.ly_vp_x + ui_st.ly_vp_w - n.mw)
let y = Math.min(ui_st.tt_y + ui_px(ui_st, 20.0), ui_st.ly_vp_y + ui_st.ly_vp_h - n.mh)
ly_place(ui_st, n, x, y, n.mw, n.mh)
fr_draw(ui_st, n)
}
# a title's lines: apart at a newline, or at a written \n (which an attribute cannot hold otherwise)
function tt_lines(s: string) -> []string {

View file

@ -1,18 +1,16 @@
# tooltip_text.ludic - a title in the program's language (whole when the translator knows it whole,
# else line by line, and not inside translate="no"), and a title for the keyboard's focus
var tt_by_keys: bool = false # the focus moved by the keyboard since the pointer last moved
var tt_focus_seen: string = ""
# the focused element, when the keyboard put the focus there and it has a title; else null
function tt_focused(scope: UiNode, x: float, y: float) -> UiNode {
if in_last.x != x or in_last.y != y { tt_by_keys = false }
if fc_visible and fc_key != "" and fc_key != tt_focus_seen { tt_by_keys = true }
tt_focus_seen = fc_key
if not tt_by_keys or not fc_visible { return null }
let n = fr_node(scope, fc_key)
function tt_focused(ui_st: mut UiState, scope: UiNode, x: float, y: float) -> UiNode {
if ui_st.in_last.x != x or ui_st.in_last.y != y { ui_st.tt_by_keys = false }
if ui_st.fc_visible and ui_st.fc_key != "" and ui_st.fc_key != ui_st.tt_focus_seen { ui_st.tt_by_keys = true }
ui_st.tt_focus_seen = ui_st.fc_key
if not ui_st.tt_by_keys or not ui_st.fc_visible { return null }
let n = fr_node(scope, ui_st.fc_key)
if n == null or n.akeys == null or ui_attr_text(n, "title") == "" { return null }
return n
}
function tt_translate(n: UiNode, s: string) -> string {
function tt_translate(ui_st: UiState, n: UiNode, s: string) -> string {
var at = n
while at != null {
let say = ui_attr_text(at, "translate")
@ -20,11 +18,11 @@ function tt_translate(n: UiNode, s: string) -> string {
if say == "yes" { break }
at = at.parent
}
let whole = ui_tr(s)
let whole = ui_tr(ui_st, s)
if whole != s { return whole }
let lines = tt_lines(s)
if len(lines) < 2 { return whole }
var out = ui_tr(lines[0])
for i in 1 .. len(lines) { out = out + "\n" + ui_tr(lines[i]) }
var out = ui_tr(ui_st, lines[0])
for i in 1 .. len(lines) { out = out + "\n" + ui_tr(ui_st, lines[i]) }
return out
}

View file

@ -16,7 +16,7 @@ export property UiTpl {
raw: string = "" # a <style>'s text, which is not a template's
where: string = "" # file:line, for an error
}
function tpl_read(x: Xml, file: string) -> UiTpl {
function tpl_read(ui_st: mut UiState, x: Xml, file: string) -> UiTpl {
let t = new UiTpl
t.tag = Xml.tag(x)
t.keys = new []string
@ -24,7 +24,7 @@ function tpl_read(x: Xml, file: string) -> UiTpl {
t.on_keys = new []string
t.on_acts = new []UiAct
t.kids = new []UiTpl
t.where = `{file}:{tp_line(x.at)}`
t.where = `{file}:{tp_line(ui_st, x.at)}`
let err = new []string
for i in 0 .. len(x.akeys) {
let k: string = x.akeys[i]
@ -40,8 +40,8 @@ function tpl_read(x: Xml, file: string) -> UiTpl {
push(t.vals, tpl_value(v, err))
}
}
if tp_mixed(x) { tp_read_mixed(t, x, file, err) } else {
for i in 0 .. Xml.child_count(x) { push(t.kids, tpl_read(Xml.child(x, i), file)) }
if tp_mixed(x) { tp_read_mixed(ui_st, t, x, file, err) } else {
for i in 0 .. Xml.child_count(x) { push(t.kids, tpl_read(ui_st, Xml.child(x, i), file)) }
}
if t.tag == "style" {
t.raw = Xml.text(x)
@ -49,7 +49,7 @@ function tpl_read(x: Xml, file: string) -> UiTpl {
}
let words = tpl_words(Xml.text(x))
if len(words) > 0 and Xml.child_count(x) == 0 { t.text = ex_text(words, err) }
for i in 0 .. len(err) { ui_err(`{t.where}: <{t.tag}> {err[i]}`) }
for i in 0 .. len(err) { ui_err(ui_st, `{t.where}: <{t.tag}> {err[i]}`) }
return t
}
# on-click is on-press, as a browser would have it

View file

@ -1,18 +1,17 @@
# tpl_lines.ludic - which line of its file an element is on
# the text being read, and where its lines start, so an element can say which line it is on
var tp_nl: []int = new []int
function tp_lines(src: string) -> void {
tp_nl = new []int
function tp_lines(ui_st: mut UiState, src: string) -> void {
ui_st.tp_nl = new []int
for i in 0 .. len(src) {
if src[i] == 10 { push(tp_nl, i) }
if src[i] == 10 { push(ui_st.tp_nl, i) }
}
}
function tp_line(at: int) -> int {
function tp_line(ui_st: UiState, at: int) -> int {
var lo = 0
var hi = len(tp_nl)
var hi = len(ui_st.tp_nl)
while lo < hi {
let mid = (lo + hi) / 2
if tp_nl[mid] < at { lo = mid + 1 } else { hi = mid }
if ui_st.tp_nl[mid] < at { lo = mid + 1 } else { hi = mid }
}
return lo + 1
}
@ -25,7 +24,7 @@ function tp_mixed(x: Xml) -> bool {
}
return false
}
function tp_read_mixed(t: UiTpl, x: Xml, file: string, err: []string) -> void {
function tp_read_mixed(ui_st: mut UiState, t: UiTpl, x: Xml, file: string, err: []string) -> void {
for i in 0 .. len(x.mixed) {
let m = x.mixed[i]
if m.tag == "#text" {
@ -39,6 +38,6 @@ function tp_read_mixed(t: UiTpl, x: Xml, file: string, err: []string) -> void {
r.kids = new []UiTpl
r.text = ex_text(tpl_words(m.text), err)
push(t.kids, r)
} else { push(t.kids, tpl_read(m, file)) }
} else { push(t.kids, tpl_read(ui_st, m, file)) }
}
}

View file

@ -1,32 +1,28 @@
# transition.ludic - `transition: opacity 0.2s`: when an element's opacity changes from one frame to
# the next it eases to the new value over that time instead of jumping (opacity is the property a
# game fades most; the others arrive as they are)
var tr_keys: []string = new []string
var tr_from: []float = new []float
var tr_to: []float = new []float
var tr_at: []float = new []float
function tr_apply(n: UiNode) -> void {
function tr_apply(ui_st: mut UiState, n: UiNode) -> void {
if n.tr_dur <= 0.0 { return }
let now = an_now()
let now = an_now(ui_st)
let want = n.alpha
for i in 0 .. len(tr_keys) {
if tr_keys[i] == n.key {
if tr_to[i] != want {
tr_from[i] = tr_value(i, now, n.tr_dur)
tr_to[i] = want
tr_at[i] = now
for i in 0 .. len(ui_st.tr_keys) {
if ui_st.tr_keys[i] == n.key {
if ui_st.tr_to[i] != want {
ui_st.tr_from[i] = tr_value(ui_st, i, now, n.tr_dur)
ui_st.tr_to[i] = want
ui_st.tr_at[i] = now
}
n.alpha = tr_value(i, now, n.tr_dur)
n.alpha = tr_value(ui_st, i, now, n.tr_dur)
return
}
}
push(tr_keys, n.key)
push(tr_from, want)
push(tr_to, want)
push(tr_at, now)
push(ui_st.tr_keys, n.key)
push(ui_st.tr_from, want)
push(ui_st.tr_to, want)
push(ui_st.tr_at, now)
}
function tr_value(i: int, now: float, dur: float) -> float {
let t = Math.clamp((now - tr_at[i]) / dur, 0.0, 1.0)
if t >= 1.0 { return tr_to[i] }
return tr_from[i] + (tr_to[i] - tr_from[i]) * t
function tr_value(ui_st: UiState, i: int, now: float, dur: float) -> float {
let t = Math.clamp((now - ui_st.tr_at[i]) / dur, 0.0, 1.0)
if t >= 1.0 { return ui_st.tr_to[i] }
return ui_st.tr_from[i] + (ui_st.tr_to[i] - ui_st.tr_from[i]) * t
}

View file

@ -1,30 +1,24 @@
# units.ludic - lengths in px (the renderer's scale), em (the font size here), rem (the root's),
# vw and vh (the screen's), and custom properties: --name set on an element, read by var(--name)
# in it and everything inside it
var el_cur: UiNode = null
var el_key: string = ""
var ly_vp_x: float = 0.0
var ly_vp_y: float = 0.0
var ly_vp_w: float = 0.0
var ly_vp_h: float = 0.0
function el_unit(s: string) -> float {
function el_unit(ui_st: UiState, s: string) -> float {
let f = el_num(s)
let n = len(s)
if n > 3 and s[n - 3..n] == "rem" { return f * el_px(16.0) }
if n > 3 and s[n - 3..n] == "rem" { return f * el_px(ui_st, 16.0) }
if n > 2 and s[n - 2..n] == "em" {
if el_cur == null { return f * el_px(16.0) }
if el_key == "size" and el_cur.parent != null { return f * cs_fs(el_cur.parent) }
return f * cs_fs(el_cur)
if ui_st.el_cur == null { return f * el_px(ui_st, 16.0) }
if ui_st.el_key == "size" and ui_st.el_cur.parent != null { return f * cs_fs(ui_st, ui_st.el_cur.parent) }
return f * cs_fs(ui_st, ui_st.el_cur)
}
if n > 2 and s[n - 2..n] == "vw" { return f * ly_vp_w / 100.0 }
if n > 2 and s[n - 2..n] == "vh" { return f * ly_vp_h / 100.0 }
return el_px(f)
if n > 2 and s[n - 2..n] == "vw" { return f * ui_st.ly_vp_w / 100.0 }
if n > 2 and s[n - 2..n] == "vh" { return f * ui_st.ly_vp_h / 100.0 }
return el_px(ui_st, f)
}
# the font size an element has so far in its cascade (its own, else its parent's)
function cs_fs(n: UiNode) -> float {
function cs_fs(ui_st: UiState, n: UiNode) -> float {
if n.size > 0.0 { return n.size }
if n.parent != null { return cs_fs(n.parent) * zm_own(n) }
return ui_px(16.0) * zm_own(n)
if n.parent != null { return cs_fs(ui_st, n.parent) * zm_own(n) }
return ui_px(ui_st, 16.0) * zm_own(n)
}
function cs_var_set(n: UiNode, name: string, value: string) -> void {
if n.var_names == null {
@ -53,7 +47,7 @@ function cs_var(n: UiNode, name: string) -> string {
return null
}
# a value with var(--name) or var(--name, fallback) in it, with each replaced by what it names
function cs_vars_in(n: UiNode, v: Val) -> Val {
function cs_vars_in(ui_st: mut UiState, n: UiNode, v: Val) -> Val {
if Value.kind(v) != 4 { return v }
let s = Value.as_str(v)
var at = 0
@ -70,7 +64,7 @@ function cs_vars_in(n: UiNode, v: Val) -> Val {
if s[j] == 41 { depth -= 1 }
j += 1
}
out = out + s[at..i] + cs_var_ref(n, s[i + 4..j - 1])
out = out + s[at..i] + cs_var_ref(ui_st, n, s[i + 4..j - 1])
at = j
i = j
} else { i += 1 }
@ -78,11 +72,11 @@ function cs_vars_in(n: UiNode, v: Val) -> Val {
if not found { return v }
return Value.str(tpl_words(out + s[at..len(s)]))
}
function cs_var_ref(n: UiNode, inner: string) -> string {
function cs_var_ref(ui_st: mut UiState, n: UiNode, inner: string) -> string {
let parts = lss_split(inner, 44)
let v = cs_var(n, tpl_words(parts[0]))
if v != null { return v }
if len(parts) > 1 { return tpl_words(inner[len(parts[0]) + 1..len(inner)]) }
ui_err(`var({tpl_words(parts[0])}): no element here or above sets it`)
ui_err(ui_st, `var({tpl_words(parts[0])}): no element here or above sets it`)
return ""
}

View file

@ -1,13 +1,13 @@
# zoom.ludic - zoom: 0.8 (or 80%, or calc(), min() and the rest: min(1, calc(100vw / 1800px))) makes
# an element and everything inside it that much larger or smaller, its lengths, its text and its
# layout; zooms multiply down the tree. A length in px is zoomed as it is read (el_px).
function el_zoom(n: UiNode, s0: string) -> void {
function el_zoom(ui_st: mut UiState, n: UiNode, s0: string) -> void {
let s = tpl_words(s0)
var z = 1.0
if ca_is(s) {
if ca_eval(s) {
z = ca_num
if not ca_bare { ui_err(`zoom: {s} - a zoom is a plain number (divide a length by a length)`) }
if ca_eval(ui_st, s) {
z = ui_st.ca_num
if not ui_st.ca_bare { ui_err(ui_st, `zoom: {s} - a zoom is a plain number (divide a length by a length)`) }
}
} else if len(s) > 1 and s[len(s) - 1] == 37 { z = el_num(s) / 100.0 } else if s != "normal" { z = el_num(s) }
if z <= 0.0 { z = 1.0 }
@ -29,9 +29,9 @@ function zm_all(n: UiNode) -> float {
}
# a length of `v` design pixels on the element being styled: the renderer's scale and every zoom
# around it (its own too, except while its zoom itself is read)
function el_px(v: float) -> float {
if el_cur == null { return ui_px(v) }
if el_key == "zoom" and el_cur.parent != null { return ui_px(v) * zm_all(el_cur.parent) }
if el_key == "zoom" { return ui_px(v) }
return ui_px(v) * zm_all(el_cur)
function el_px(ui_st: UiState, v: float) -> float {
if ui_st.el_cur == null { return ui_px(ui_st, v) }
if ui_st.el_key == "zoom" and ui_st.el_cur.parent != null { return ui_px(ui_st, v) * zm_all(ui_st.el_cur.parent) }
if ui_st.el_key == "zoom" { return ui_px(ui_st, v) }
return ui_px(ui_st, v) * zm_all(ui_st.el_cur)
}