ludic.ui: an override's path is compared normalised, and an absolute path matches the relative one it ends with - a lab build registers its components as lab/../src/ui/..., so the studio's src/ui/... (or absolute) path matched nothing and the override changed nothing; nothing is normalised while no override stands; examples/library/ui_override_up registers under a .. path as the lab does
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b3076597e5
commit
82c652ca31
6 changed files with 93 additions and 18 deletions
|
|
@ -6,3 +6,7 @@ and an importer reads the new text. `ui_override_clear(path)` (or `""` for every
|
||||||
and `ui_overridden(path)` says whether one stands. A developer build's disk polling leaves an overridden
|
and `ui_overridden(path)` says whether one stands. A developer build's disk polling leaves an overridden
|
||||||
component alone. For a studio editing a running game's interface (devlink's `ui_override`); a game that
|
component alone. For a studio editing a running game's interface (devlink's `ui_override`); a game that
|
||||||
overrides nothing pays a length check per file read.
|
overrides nothing pays a length check per file read.
|
||||||
|
|
||||||
|
The override's path is compared normalised, and an absolute path matches the relative one it ends with: a
|
||||||
|
game's lab build registers its components as `lab/../src/ui/...`, and a studio's `src/ui/...` (or its
|
||||||
|
absolute path) named nothing, so an override answered ok and changed nothing.
|
||||||
|
|
|
||||||
26
examples/library/ui_override_up.ludic
Normal file
26
examples/library/ui_override_up.ludic
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# ui_override_up.ludic - the override through a component registered under a path with `..` in it, as a
|
||||||
|
# game's lab build registers every component (lab/../src/ui/...): the studio names the file as the game's
|
||||||
|
# root has it, and it still matches. Prints `counters|[a=0]|+|[b=0]|+|outside| 1 1` - the override shown,
|
||||||
|
# and the same file found by its plain path and by a ./ path.
|
||||||
|
import "ludic.ui"
|
||||||
|
import "../library/ui_component_parts/Counter.ludic"
|
||||||
|
import "../library/ui_component_parts/App.ludic"
|
||||||
|
program UiOverrideUp {
|
||||||
|
numbers float
|
||||||
|
function texts(n: UiNode, out: []string) -> void {
|
||||||
|
if n.text != "" { push(out, n.text) }
|
||||||
|
for i in 0 .. len(n.children) { texts(n.children[i], out) }
|
||||||
|
}
|
||||||
|
entry (ui_st: mut UiState) {
|
||||||
|
ui_override(ui_st, "examples/library/ui_component_parts/Counter.xml", "<div class=\"counter\"><p>[{label}={count}]</p><button on-click=\"bump()\">+</button></div>")
|
||||||
|
let root: UiNode = ui_nodes(ui_st, "App", null)
|
||||||
|
ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0)
|
||||||
|
let out = new []string
|
||||||
|
texts(root, out)
|
||||||
|
var joined = ""
|
||||||
|
for i in 0 .. len(out) { joined = joined + out[i] + "|" }
|
||||||
|
let plain = ui_overridden(ui_st, "examples/library/ui_component_parts/Counter.xml")
|
||||||
|
let dotted = ui_overridden(ui_st, "./examples/library/../library/ui_component_parts/Counter.xml")
|
||||||
|
print(`{joined} {plain} {dotted}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -111,4 +111,5 @@ import "dump.ludic"
|
||||||
import "inspect.ludic"
|
import "inspect.ludic"
|
||||||
import "held.ludic"
|
import "held.ludic"
|
||||||
import "override.ludic"
|
import "override.ludic"
|
||||||
|
import "override_paths.ludic"
|
||||||
import "mounted.ludic"
|
import "mounted.ludic"
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,14 @@
|
||||||
# studio editing a running game's interface (devlink's ui_override). Every read of that path answers
|
# studio editing a running game's interface (devlink's ui_override). Every read of that path answers
|
||||||
# the given text, a component whose file it is swaps its own and is shown again with its state kept,
|
# the given text, a component whose file it is swaps its own and is shown again with its state kept,
|
||||||
# and letting go puts the file back. A game overrides nothing; a read asks an empty list first.
|
# and letting go puts the file back. A game overrides nothing; a read asks an empty list first.
|
||||||
|
# Paths are compared normalised (a lab build names its components lab/../src/ui/...), and an absolute
|
||||||
|
# path matches the relative one it ends with.
|
||||||
|
|
||||||
# from now on `path` reads as `text`; its component (or every importer) is reloaded, state kept
|
# from now on `path` reads as `text`; its component (or every importer) is reloaded, state kept
|
||||||
@alloc_ok("a studio's edit: the text kept until it is let go, never a frame")
|
@alloc_ok("a studio's edit: the text kept until it is let go, never a frame")
|
||||||
export function ui_override(ui_st: mut UiState, path: string, text: string) -> void {
|
export function ui_override(ui_st: mut UiState, path0: string, text: string) -> void {
|
||||||
let t = keep(text)
|
let t = keep(text)
|
||||||
|
let path = intern(Path.normalize(path0))
|
||||||
var found = false
|
var found = false
|
||||||
for i in 0 .. len(ui_st.ov_path) {
|
for i in 0 .. len(ui_st.ov_path) {
|
||||||
if ui_st.ov_path[i] == path {
|
if ui_st.ov_path[i] == path {
|
||||||
|
|
@ -15,23 +18,25 @@ export function ui_override(ui_st: mut UiState, path: string, text: string) -> v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if not found {
|
if not found {
|
||||||
push(ui_st.ov_path, intern(path))
|
push(ui_st.ov_path, path)
|
||||||
push(ui_st.ov_text, t)
|
push(ui_st.ov_text, t)
|
||||||
}
|
}
|
||||||
ov_classes(ui_st, path, t)
|
ov_classes(ui_st, path, t)
|
||||||
ui_file_forget(ui_st, path) # a file an import read is read again; every class re-shown
|
ov_forget(ui_st, path) # a file an import read is read again; every class re-shown
|
||||||
}
|
}
|
||||||
# let go of `path`'s override ("" every override): the file reads from disk again, a component's
|
# let go of `path`'s override ("" every override): the file reads from disk again, a component's
|
||||||
# own text comes back
|
# own text comes back
|
||||||
@alloc_ok("a studio's edit, never a frame")
|
@alloc_ok("a studio's edit, never a frame")
|
||||||
export function ui_override_clear(ui_st: mut UiState, path: string) -> void {
|
export function ui_override_clear(ui_st: mut UiState, path0: string) -> void {
|
||||||
|
var path = ""
|
||||||
|
if path0 != "" { path = Path.normalize(path0) }
|
||||||
var keep_p = new []string
|
var keep_p = new []string
|
||||||
var keep_t = new []string
|
var keep_t = new []string
|
||||||
for i in 0 .. len(ui_st.ov_path) {
|
for i in 0 .. len(ui_st.ov_path) {
|
||||||
let p = ui_st.ov_path[i]
|
let p = ui_st.ov_path[i]
|
||||||
if path == "" or p == path {
|
if path == "" or ov_same(p, path) {
|
||||||
ov_restore(ui_st, p)
|
ov_restore(ui_st, p)
|
||||||
ui_file_forget(ui_st, p)
|
ov_forget(ui_st, p)
|
||||||
} else {
|
} else {
|
||||||
push(keep_p, p)
|
push(keep_p, p)
|
||||||
push(keep_t, ui_st.ov_text[i])
|
push(keep_t, ui_st.ov_text[i])
|
||||||
|
|
@ -44,35 +49,35 @@ export function ui_override_clear(ui_st: mut UiState, path: string) -> void {
|
||||||
push(ui_st.ov_text, keep_t[i])
|
push(ui_st.ov_text, keep_t[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# is `path` overridden, and its text (null when it is not)
|
# is `path` overridden
|
||||||
export function ui_overridden(ui_st: UiState, path: string) -> bool { return ov_find(ui_st, path) >= 0 }
|
export function ui_overridden(ui_st: UiState, path: string) -> bool { return ov_find(ui_st, path) >= 0 }
|
||||||
function ov_find(ui_st: UiState, path: string) -> int {
|
|
||||||
for i in 0 .. len(ui_st.ov_path) {
|
|
||||||
if ui_st.ov_path[i] == path { return i }
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
# a component whose template or stylesheet is `path` takes `text`, its own kept to come back to
|
# a component whose template or stylesheet is `path` takes `text`, its own kept to come back to
|
||||||
|
@alloc_ok("a studio's edit, never a frame")
|
||||||
function ov_classes(ui_st: mut UiState, path: string, text: string) -> void {
|
function ov_classes(ui_st: mut UiState, path: string, text: string) -> void {
|
||||||
if ui_st.uc_list == null { return }
|
if ui_st.uc_list == null { return }
|
||||||
for i in 0 .. len(ui_st.uc_list) {
|
for i in 0 .. len(ui_st.uc_list) {
|
||||||
let k = ui_st.uc_list[i]
|
let k = ui_st.uc_list[i]
|
||||||
if k.xml_path == path or k.lss_path == path {
|
let on_xml = k.xml_path != "" and ov_same(path, Path.normalize(k.xml_path))
|
||||||
|
let on_lss = k.lss_path != "" and ov_same(path, Path.normalize(k.lss_path))
|
||||||
|
if on_xml or on_lss {
|
||||||
if k.ov_xml == null {
|
if k.ov_xml == null {
|
||||||
k.ov_xml = k.xml
|
k.ov_xml = k.xml
|
||||||
k.ov_lss = k.lss
|
k.ov_lss = k.lss
|
||||||
}
|
}
|
||||||
if k.xml_path == path { k.xml = text } else { k.lss = text }
|
if on_xml { k.xml = text } else { k.lss = text }
|
||||||
k.root = null
|
k.root = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@alloc_ok("a studio's edit, never a frame")
|
||||||
function ov_restore(ui_st: mut UiState, path: string) -> void {
|
function ov_restore(ui_st: mut UiState, path: string) -> void {
|
||||||
if ui_st.uc_list == null { return }
|
if ui_st.uc_list == null { return }
|
||||||
for i in 0 .. len(ui_st.uc_list) {
|
for i in 0 .. len(ui_st.uc_list) {
|
||||||
let k = ui_st.uc_list[i]
|
let k = ui_st.uc_list[i]
|
||||||
if k.ov_xml != null and (k.xml_path == path or k.lss_path == path) {
|
let on_xml = k.xml_path != "" and ov_same(path, Path.normalize(k.xml_path))
|
||||||
if k.xml_path == path { k.xml = k.ov_xml } else { k.lss = k.ov_lss }
|
let on_lss = k.lss_path != "" and ov_same(path, Path.normalize(k.lss_path))
|
||||||
|
if k.ov_xml != null and (on_xml or on_lss) {
|
||||||
|
if on_xml { k.xml = k.ov_xml } else { k.lss = k.ov_lss }
|
||||||
if not ov_class_held(ui_st, k, path) {
|
if not ov_class_held(ui_st, k, path) {
|
||||||
k.ov_xml = null
|
k.ov_xml = null
|
||||||
k.ov_lss = null
|
k.ov_lss = null
|
||||||
|
|
@ -82,10 +87,11 @@ function ov_restore(ui_st: mut UiState, path: string) -> void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# does the class's other file still have an override (so its own texts are still needed later)
|
# does the class's other file still have an override (so its own texts are still needed later)
|
||||||
|
@alloc_ok("a studio's edit, never a frame")
|
||||||
function ov_class_held(ui_st: UiState, k: UiClass, going: string) -> bool {
|
function ov_class_held(ui_st: UiState, k: UiClass, going: string) -> bool {
|
||||||
for i in 0 .. len(ui_st.ov_path) {
|
for i in 0 .. len(ui_st.ov_path) {
|
||||||
let p = ui_st.ov_path[i]
|
let p = ui_st.ov_path[i]
|
||||||
if p != going and (p == k.xml_path or p == k.lss_path) { return true }
|
if p != going and (ov_same(p, Path.normalize(k.xml_path)) or ov_same(p, Path.normalize(k.lss_path))) { return true }
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
packages/ludic.ui/override_paths.ludic
Normal file
37
packages/ludic.ui/override_paths.ludic
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# override_paths.ludic - which override a path is: compared normalised (a lab build names its components
|
||||||
|
# lab/../src/ui/..., a studio names them src/ui/... or by an absolute path), an absolute path matching
|
||||||
|
# the relative one it ends with. Nothing is normalised while no override stands.
|
||||||
|
@alloc_ok("only while a studio's override stands: the path asked, normalised")
|
||||||
|
function ov_find(ui_st: UiState, path: string) -> int {
|
||||||
|
if len(ui_st.ov_path) == 0 or path == "" { return -1 } # a game: nothing overridden, nothing made
|
||||||
|
let p = Path.normalize(path)
|
||||||
|
for i in 0 .. len(ui_st.ov_path) {
|
||||||
|
if ov_same(ui_st.ov_path[i], p) { return i }
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
# two normalised paths name one file: equal, or an absolute one ends with /<the relative one>
|
||||||
|
function ov_same(a: string, b: string) -> bool {
|
||||||
|
if a == b { return true }
|
||||||
|
if len(a) > 0 and a[0] == 47 and len(b) > 0 and b[0] != 47 { return ov_ends(a, b) }
|
||||||
|
if len(b) > 0 and b[0] == 47 and len(a) > 0 and a[0] != 47 { return ov_ends(b, a) }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
function ov_ends(abs: string, rel: string) -> bool {
|
||||||
|
let n = len(rel)
|
||||||
|
let m = len(abs)
|
||||||
|
if n + 1 > m or abs[m - n - 1] != 47 { return false }
|
||||||
|
for i in 0 .. n {
|
||||||
|
if abs[m - n + i] != rel[i] { return false }
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
# every loaded file an override names read again, by the path it was loaded under
|
||||||
|
@alloc_ok("a studio's edit, never a frame")
|
||||||
|
function ov_forget(ui_st: mut UiState, key: string) -> void {
|
||||||
|
for i in 0 .. len(ui_st.lib_files) {
|
||||||
|
let f = ui_st.lib_files[i]
|
||||||
|
if ov_same(key, Path.normalize(f.path)) { ui_file_forget(ui_st, f.path) }
|
||||||
|
}
|
||||||
|
uc_invalidate(ui_st)
|
||||||
|
}
|
||||||
|
|
@ -1296,6 +1296,7 @@ function cmd_dev_test() -> int {
|
||||||
controller_case("library/ui_reload", "", "ui: templates reloaded note 1 | reloaded 0 then 1 | note again 1", "ui_reload.ludic (L11: a template changed on disk is read again, and its state kept)")
|
controller_case("library/ui_reload", "", "ui: templates reloaded note 1 | reloaded 0 then 1 | note again 1", "ui_reload.ludic (L11: a template changed on disk is read again, and its state kept)")
|
||||||
controller_case("library/ui_component", "", "counters|a: 1 (2)|+|reset|b: 10 (20) big|+|reset|outside| b margin 9; b text 16711680; outside -1; button 30 b: 0 (0)", "ui_component.ludic (L11: components as NAME.ludic + .xml + .lss - props, state, fields, events, scoped styles, @import, class passed down)")
|
controller_case("library/ui_component", "", "counters|a: 1 (2)|+|reset|b: 10 (20) big|+|reset|outside| b margin 9; b text 16711680; outside -1; button 30 b: 0 (0)", "ui_component.ludic (L11: components as NAME.ludic + .xml + .lss - props, state, fields, events, scoped styles, @import, class passed down)")
|
||||||
controller_case("library/ui_override", "", "counters|a: 1 (2)|+|reset|b: 0 (0)|+|reset|outside| counters|[a=1]|+|[b=0]|+|outside| 1 counters|a: 1 (2)|+|reset|b: 0 (0)|+|reset|outside| 0", "ui_override.ludic (a template given from outside: shown with the state kept, and the file back when let go)")
|
controller_case("library/ui_override", "", "counters|a: 1 (2)|+|reset|b: 0 (0)|+|reset|outside| counters|[a=1]|+|[b=0]|+|outside| 1 counters|a: 1 (2)|+|reset|b: 0 (0)|+|reset|outside| 0", "ui_override.ludic (a template given from outside: shown with the state kept, and the file back when let go)")
|
||||||
|
controller_case("library/ui_override_up", "", "counters|[a=0]|+|[b=0]|+|outside| 1 1", "ui_override_up.ludic (a component registered under a path with .. in it - a game's lab build - still matched by the studio's plain path)")
|
||||||
controller_case("library/ui_mounted", "", "App|Counter| 1 1", "ui_mounted.ludic (the components the last frame showed, in tree order and each once, and one's model)")
|
controller_case("library/ui_mounted", "", "App|Counter| 1 1", "ui_mounted.ludic (the components the last frame showed, in tree order and each once, and one's model)")
|
||||||
reject_case("rejected/component_no_template", "there is no Lonely.xml beside it", "a component needs its template beside it")
|
reject_case("rejected/component_no_template", "there is no Lonely.xml beside it", "a component needs its template beside it")
|
||||||
controller_case("library/ui_remount", "", "c: 2 (4) | c: 0 (0) | heap 0", "ui_remount.ludic (a component unmounted and mounted again starts as new, and two thousand comings and goings hold the heap flat)")
|
controller_case("library/ui_remount", "", "c: 2 (4) | c: 0 (0) | heap 0", "ui_remount.ludic (a component unmounted and mounted again starts as new, and two thousand comings and goings hold the heap flat)")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue