diff --git a/Makefile b/Makefile
index fb64096b..81a78460 100644
--- a/Makefile
+++ b/Makefile
@@ -338,6 +338,9 @@ test-tab-interaction: app copyshare
test-node: app copyshare
$(TARGET) -test=tests/test_node.lua
+test-lisp: app copyshare
+ $(TARGET) -test=tests/test_lisp.lua
+
test: test-headless test-properties test-styles test-filesystem test-editor test-text-layout test-stack-layout test-grid-layout test-interaction test-tab-interaction test-node
$(TARGET) -test=tests/test.xml
@@ -367,5 +370,6 @@ test-headless: unite test-properties test-styles test-filesystem test-message-re
$(TARGET) -test=tests/test_parsers.lua
$(TARGET) -test=tests/test_object_hierarchy.lua
$(TARGET) -test=tests/test_tabbar.lua
+ $(TARGET) -test=tests/test_lisp.lua
include Makefile.webgl
diff --git a/samples/Example/Prefabs/FeatureCard.lisp b/samples/Example/Prefabs/FeatureCard.lisp
new file mode 100644
index 00000000..0bd35e5b
--- /dev/null
+++ b/samples/Example/Prefabs/FeatureCard.lisp
@@ -0,0 +1,7 @@
+(StackView Name="FeatureCard" Direction="Vertical" Spacing=10 BackgroundColor="$card-background" Padding=28
+ (TextBlock Name="FeatureCardIcon" FontSize=20 Width=44 Height=40
+ TextHorizontalAlignment="Center" TextVerticalAlignment="Center"
+ Text="{../Card.Icon}" ForegroundColor="{../Card.PrimaryColor}" BackgroundColor="{../Card.IconBackground}")
+ (Node2D Name="FeatureCardSpacer" Height=8)
+ (TextBlock Name="FeatureCardTitle" FontSize=16 ForegroundColor="$text-primary" Text="{../Card.Title}")
+ (TextBlock Name="FeatureCardBody" FontSize=13 ForegroundColor="$text-dim" WordWrap="true" Text="{../Card.Body}"))
diff --git a/samples/Example/Prefabs/FeatureCard.xml b/samples/Example/Prefabs/FeatureCard.xml
index a2370b9f..cf8db234 100644
--- a/samples/Example/Prefabs/FeatureCard.xml
+++ b/samples/Example/Prefabs/FeatureCard.xml
@@ -3,8 +3,8 @@
+ Text="{../Card.Icon}" ForegroundColor="{../Card.PrimaryColor}" BackgroundColor="{../Card.IconBackground}"/>
-
-
+
+
diff --git a/samples/Example/Prefabs/FeatureImageCard.lisp b/samples/Example/Prefabs/FeatureImageCard.lisp
new file mode 100644
index 00000000..1cf7a4b6
--- /dev/null
+++ b/samples/Example/Prefabs/FeatureImageCard.lisp
@@ -0,0 +1,4 @@
+(StackView Name="FeatureImageCard" Direction="Vertical" Spacing=12 BackgroundColor="$card-background" Padding=28
+ (ImageView Name="FeatureImageCardIcon" Width=40 Height=40 Source="{Card.Icon}" ForegroundColor="{Card.PrimaryColor}")
+ (TextBlock Name="FeatureImageCardTitle" Text="{Card.Title}" FontSize=16 ForegroundColor="$text-primary")
+ (TextBlock Name="FeatureImageCardBody" Text="{Card.Body}" FontSize=13 ForegroundColor="$text-dim" WordWrap="true"))
diff --git a/samples/Example/Prefabs/FeatureImageCard.xml b/samples/Example/Prefabs/FeatureImageCard.xml
index 27d7be45..9c5e65bf 100644
--- a/samples/Example/Prefabs/FeatureImageCard.xml
+++ b/samples/Example/Prefabs/FeatureImageCard.xml
@@ -1,7 +1,7 @@
-
-
-
+
+
+
diff --git a/samples/Example/Prefabs/GalleryCard.lisp b/samples/Example/Prefabs/GalleryCard.lisp
new file mode 100644
index 00000000..6c1def89
--- /dev/null
+++ b/samples/Example/Prefabs/GalleryCard.lisp
@@ -0,0 +1,5 @@
+(StackView Name="GalleryCard" Direction="Vertical" Spacing=0 BackgroundColor="$card-background"
+ (ImageView Name="GalleryCardImage" Height=180 Stretch="UniformToFill" Source="{../Card.Image}")
+ (StackView Name="GalleryCardCaption" Direction="Vertical" Spacing=4 Padding=16
+ (TextBlock Name="GalleryCardTitle" FontSize=15 ForegroundColor="$text-primary" Text="{../../Card.Title}")
+ (TextBlock Name="GalleryCardBody" FontSize=12 ForegroundColor="$text-dim" WordWrap="true" Text="{../../Card.Subtitle}")))
diff --git a/samples/Example/Prefabs/GalleryCard.xml b/samples/Example/Prefabs/GalleryCard.xml
index 8da77c21..9a46eb53 100644
--- a/samples/Example/Prefabs/GalleryCard.xml
+++ b/samples/Example/Prefabs/GalleryCard.xml
@@ -1,9 +1,9 @@
-
+
-
-
+
+
diff --git a/samples/Example/Prefabs/IconCard.lisp b/samples/Example/Prefabs/IconCard.lisp
new file mode 100644
index 00000000..5b4a89b7
--- /dev/null
+++ b/samples/Example/Prefabs/IconCard.lisp
@@ -0,0 +1,6 @@
+(StackView Name="IconCard" Direction="Vertical" Spacing=10 BackgroundColor="$card-background" Padding=28
+ (StackView Name="IconCardHeader" Direction="Horizontal" Spacing=12 AlignItems="Center"
+ (StackView Name="IconCardBadge" Width=48 Height=48 AlignItems="Center" JustifyContent="Center" BackgroundColor="{../../Card.IconBackground}"
+ (ImageView Name="IconCardImage" Width=24 Height=24 Stretch="Uniform" Source="{../../../Card.Icon}" ForegroundColor="{../../../Card.PrimaryColor}"))
+ (TextBlock Name="IconCardTitle" FontSize=16 ForegroundColor="$text-primary" Text="{../../Card.Title}"))
+ (TextBlock Name="IconCardBody" FontSize=13 ForegroundColor="$text-dim" WordWrap="true" Text="{../Card.Body}"))
diff --git a/samples/Example/Prefabs/IconCard.xml b/samples/Example/Prefabs/IconCard.xml
index 4d4c514f..aa45ab04 100644
--- a/samples/Example/Prefabs/IconCard.xml
+++ b/samples/Example/Prefabs/IconCard.xml
@@ -2,10 +2,10 @@
-
-
+
+
-
+
-
+
diff --git a/samples/Example/Prefabs/ImageCaptionCard.lisp b/samples/Example/Prefabs/ImageCaptionCard.lisp
new file mode 100644
index 00000000..f6c4bde9
--- /dev/null
+++ b/samples/Example/Prefabs/ImageCaptionCard.lisp
@@ -0,0 +1,3 @@
+(StackView Name="ImageCaptionCard" Direction="Vertical" Spacing=8 BackgroundColor="$panel-background" Padding=16
+ (ImageView Name="ImageCaptionCardImage" Height=160 Stretch="UniformToFill" Source="{Card.Image}")
+ (TextBlock Name="ImageCaptionCardTitle" Text="{Card.Title}" FontSize=13 ForegroundColor="$text-secondary"))
diff --git a/samples/Example/Prefabs/ImageCaptionCard.xml b/samples/Example/Prefabs/ImageCaptionCard.xml
index c293aa76..d6287456 100644
--- a/samples/Example/Prefabs/ImageCaptionCard.xml
+++ b/samples/Example/Prefabs/ImageCaptionCard.xml
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/samples/Example/Prefabs/Mertic.xml b/samples/Example/Prefabs/Mertic.xml
index 0730d83e..4198e504 100644
--- a/samples/Example/Prefabs/Mertic.xml
+++ b/samples/Example/Prefabs/Mertic.xml
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/samples/Example/Prefabs/Metric.lisp b/samples/Example/Prefabs/Metric.lisp
new file mode 100644
index 00000000..8aa2ee5e
--- /dev/null
+++ b/samples/Example/Prefabs/Metric.lisp
@@ -0,0 +1,3 @@
+(StackView Name="MetricUsers" Direction="Vertical" Spacing=4 BackgroundColor="$panel-background" Padding=20 Width=120
+ (TextBlock Name="MetricUsersValue" FontSize=28 ForegroundColor="{../Card.PrimaryColor}" Text="{../Card.Title}")
+ (TextBlock Name="MetricUsersLabel" FontSize=11 ForegroundColor="$text-dim" Text="{../Card.Subtitle}"))
diff --git a/samples/Example/Prefabs/Quote.lisp b/samples/Example/Prefabs/Quote.lisp
new file mode 100644
index 00000000..9e199bbe
--- /dev/null
+++ b/samples/Example/Prefabs/Quote.lisp
@@ -0,0 +1,6 @@
+(StackView Name="Quote" Direction="Vertical" Spacing=16 BackgroundColor="$card-background" Padding=28
+ (TextBlock Name="QuoteMark" Text="“" FontSize=48 ForegroundColor="{../Card.PrimaryColor}")
+ (TextBlock Name="QuoteText" FontSize=13 ForegroundColor="$text-secondary" WordWrap="true" Text="{../Card.Body}")
+ (StackView Name="QuoteAuthor" Direction="Horizontal" Spacing=12 AlignItems="Center"
+ (TextBlock Name="QuoteAuthorName" FontSize=13 ForegroundColor="$text-primary" Text="{../../Card.Title}")
+ (TextBlock Name="QuoteAuthorRole" FontSize=11 ForegroundColor="$text-dim" Text="{../../Card.Subtitle}")))
diff --git a/samples/Example/Prefabs/Quote.xml b/samples/Example/Prefabs/Quote.xml
index 9c4ee457..352802b9 100644
--- a/samples/Example/Prefabs/Quote.xml
+++ b/samples/Example/Prefabs/Quote.xml
@@ -1,10 +1,10 @@
-
-
+
+
-
-
+
+
diff --git a/samples/Example/Prefabs/SignalCard.lisp b/samples/Example/Prefabs/SignalCard.lisp
new file mode 100644
index 00000000..5ece0d97
--- /dev/null
+++ b/samples/Example/Prefabs/SignalCard.lisp
@@ -0,0 +1,5 @@
+(StackView Name="SignalCard" Direction="Horizontal" Spacing=12 BackgroundColor="$panel-background" Padding=16 AlignItems="Center"
+ (ImageView Name="SignalCardIcon" Source="{Card.Icon}" Width=28 Height=28 ForegroundColor="{Card.PrimaryColor}")
+ (StackView Name="SignalCardCopy" Direction="Vertical" Spacing=2
+ (TextBlock Name="SignalCardTitle" Text="{Card.Title}" FontSize=14 ForegroundColor="$text-primary")
+ (TextBlock Name="SignalCardBody" Text="{Card.Body}" FontSize=12 ForegroundColor="$text-muted")))
diff --git a/samples/Example/Prefabs/SignalCard.xml b/samples/Example/Prefabs/SignalCard.xml
index d58df8e0..1ecb11cd 100644
--- a/samples/Example/Prefabs/SignalCard.xml
+++ b/samples/Example/Prefabs/SignalCard.xml
@@ -1,9 +1,9 @@
-
+
-
-
+
+
diff --git a/samples/Example/Prefabs/TabPanelHeader.lisp b/samples/Example/Prefabs/TabPanelHeader.lisp
new file mode 100644
index 00000000..57f3ed15
--- /dev/null
+++ b/samples/Example/Prefabs/TabPanelHeader.lisp
@@ -0,0 +1,3 @@
+(StackView Name="TabPanelHeader" Direction="Horizontal" Spacing=12 AlignItems="Center"
+ (ImageView Name="TabPanelHeaderIcon" Width=26 Height=26 Source="{Card.Icon}" ForegroundColor="{Card.PrimaryColor}")
+ (TextBlock Name="TabPanelHeaderTitle" Text="{Card.Title}" FontSize=24 ForegroundColor="$text-primary"))
diff --git a/samples/Example/Prefabs/TabPanelHeader.xml b/samples/Example/Prefabs/TabPanelHeader.xml
index 8aa55d8e..4b485987 100644
--- a/samples/Example/Prefabs/TabPanelHeader.xml
+++ b/samples/Example/Prefabs/TabPanelHeader.xml
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/samples/Example/Prefabs/WorkflowStep.lisp b/samples/Example/Prefabs/WorkflowStep.lisp
new file mode 100644
index 00000000..e313fc13
--- /dev/null
+++ b/samples/Example/Prefabs/WorkflowStep.lisp
@@ -0,0 +1,4 @@
+(StackView Name="WorkflowStep" Direction="Vertical" Spacing=12 BackgroundColor="$card-background" Padding=24
+ (TextBlock Name="WorkflowStepNumber" FontSize=32 Text="{../Card.Title}" ForegroundColor="{../Card.PrimaryColor}")
+ (TextBlock Name="WorkflowStepTitle" FontSize=15 ForegroundColor="$text-primary" Text="{../Card.Subtitle}")
+ (TextBlock Name="WorkflowStepBody" FontSize=12 ForegroundColor="$text-dim" WordWrap="true" Text="{../Card.Body}"))
diff --git a/samples/Example/Prefabs/WorkflowStep.xml b/samples/Example/Prefabs/WorkflowStep.xml
index 900aa42f..4bb3a811 100644
--- a/samples/Example/Prefabs/WorkflowStep.xml
+++ b/samples/Example/Prefabs/WorkflowStep.xml
@@ -1,7 +1,7 @@
-
-
-
+
+
+
diff --git a/samples/Example/Prefabs/XmlModelNode.lisp b/samples/Example/Prefabs/XmlModelNode.lisp
new file mode 100644
index 00000000..0a1d4fd9
--- /dev/null
+++ b/samples/Example/Prefabs/XmlModelNode.lisp
@@ -0,0 +1,3 @@
+(StackView Name="XmlModelNode" Direction="Horizontal" Spacing=10 AlignItems="Center"
+ (ImageView Name="XmlModelNodeIcon" Width=20 Height=20 Source="{Card.Icon}" ForegroundColor="{Card.PrimaryColor}")
+ (TextBlock Name="XmlModelNodeText" Text="{Card.Title}" FontSize=14 ForegroundColor="{Card.PrimaryColor}"))
diff --git a/samples/Example/Prefabs/XmlModelNode.xml b/samples/Example/Prefabs/XmlModelNode.xml
index 1b529c7f..cc48ecbd 100644
--- a/samples/Example/Prefabs/XmlModelNode.xml
+++ b/samples/Example/Prefabs/XmlModelNode.xml
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/samples/Example/Screens/Application.lisp b/samples/Example/Screens/Application.lisp
new file mode 100644
index 00000000..10e5c1c4
--- /dev/null
+++ b/samples/Example/Screens/Application.lisp
@@ -0,0 +1,422 @@
+; Application.lisp — Lisp-format equivalent of Application.xml
+; Converted from XML to validate the Lisp loader at feature parity.
+;
+; Binding syntax: Key=(if (step 640 (bind "Node.ActualWidth")) "auto" "wide")
+; Directive syntax: (:bind Target="Prop" expr)
+
+(Screen Name="Application" Width=1280 Height=800 ClearColor="$surface"
+ (Grid Name="Root" Rows="64px auto" Spacing=0
+ (StackView Name="NavBar" BackgroundColor="$panel-background"
+ Direction="Horizontal" Spacing=32 AlignItems="Center"
+ HorizontalPadding=40
+ (StackView Name="BrandMark" Direction="Horizontal" Spacing=10 AlignItems="Center"
+ (ImageView Name="BrandIcon"
+ Source="Example/Icons/blocks.svg?width=22&type=mask"
+ Width=22 Height=22 ForegroundColor="$accent")
+ (TextBlock Name="Brand" FontSize=20 ForegroundColor="$accent"
+ "ORCA"))
+ (TextBlock Name="NavOverview" FontSize=14 ForegroundColor="$text-secondary"
+ "Overview")
+ (TextBlock Name="NavFeatures" FontSize=14 ForegroundColor="$text-secondary"
+ "Features")
+ (TextBlock Name="NavGallery" FontSize=14 ForegroundColor="$text-secondary"
+ "Gallery")
+ (TextBlock Name="NavDocs" FontSize=14 ForegroundColor="$text-secondary"
+ "Docs"))
+ (StackView Name="Body" Direction="Vertical" Spacing=40 OverflowY="Scroll"
+ ClipChildren="true" Padding=40 BackgroundColor="$surface-deep"
+ (:bind Target="Node.HorizontalPadding"
+ (if (step 640 (bind "Node.ActualWidth")) (vector2 40 40) (vector2 8 8)))
+ (Grid Name="Hero" Columns="auto auto" Spacing=24 BackgroundColor="$surface"
+ (:bind Target="Node.Visible"
+ (step 640 (bind "Node.ActualWidth")))
+ (:bind Target="Grid.Columns"
+ (if (step 640 (bind "Node.ActualWidth")) "auto auto" "auto"))
+ (StackView Name="HeroContent" Direction="Vertical" Spacing=0 Padding=16
+ (TextBlock Name="HeadlineLine1" FontSize=48 ForegroundColor="$text-primary"
+ "Build stunning")
+ (TextBlock Name="HeadlineLine2" FontSize=48 ForegroundColor="$accent"
+ "UI in real-time.")
+ (Node2D Name="HeadlineSpacer" Height=20)
+ (TextBlock Name="SubHeadline" FontSize=15 ForegroundColor="$text-muted"
+ WordWrap="true"
+ "A modern UI engine for games and native apps. Compose layouts from grids, stacks, text, and images — all rendered at 60 fps.")
+ (Node2D Name="MetricSpacer" Height=40)
+ (Grid Name="Metrics" Columns="auto auto auto" Spacing=16
+ (LayerPrefabPlaceholder Name="MetricUsers"
+ PlaceholderTemplate="Example/Prefabs/Metric.lisp"
+ Card.Title="2.4 M"
+ Card.Subtitle="Active users"
+ Card.PrimaryColor="$accent")
+ (LayerPrefabPlaceholder Name="MetricUptime"
+ PlaceholderTemplate="Example/Prefabs/Metric.lisp"
+ Card.Title="99.9 %"
+ Card.Subtitle="Uptime SLA"
+ Card.PrimaryColor="$accent-green")
+ (LayerPrefabPlaceholder Name="MetricFrame"
+ PlaceholderTemplate="Example/Prefabs/Metric.lisp"
+ Card.Title="1.2 ms"
+ Card.Subtitle="Frame time"
+ Card.PrimaryColor="$accent-blue")))
+ (ImageView Name="HeroImage" Source="Example/Images/people"
+ Stretch="UniformToFill"))
+ (StackView Name="TabbedTechSection" Direction="Vertical" Spacing=20
+ BackgroundColor="$surface" Padding=40
+ (:bind Target="Node.HorizontalPadding"
+ (if (step 640 (bind "Node.ActualWidth")) (vector2 40 40) (vector2 8 8)))
+ (TextBlock Name="TabbedTechHeading" Text="Build the UI, not the ceremony"
+ FontSize=28 ForegroundColor="$text-primary")
+ (TextBlock Name="TabbedTechSubheading"
+ Text="A clean XML surface, Lua where things move, MoonScript when the app grows, and one UI that can travel across targets."
+ FontSize=14 ForegroundColor="$text-muted")
+ (Grid Name="OrcaSignals" Columns="auto auto auto" Spacing=14
+ (:bind Target="Grid.Columns"
+ (if (step 640 (bind "Node.ActualWidth")) "auto auto auto" "auto auto"))
+ (LayerPrefabPlaceholder Name="SignalXml"
+ PlaceholderTemplate="Example/Prefabs/SignalCard.lisp"
+ Card.Icon="Example/Icons/code.svg?width=28&type=mask"
+ Card.Title="XML-first screens"
+ Card.Body="Layouts stay readable."
+ Card.PrimaryColor="$accent")
+ (LayerPrefabPlaceholder Name="SignalLua"
+ PlaceholderTemplate="Example/Prefabs/SignalCard.lisp"
+ Card.Icon="Example/Icons/file.svg?width=28&type=mask"
+ Card.Title="Lua beside the view"
+ Card.Body="Behavior stays close."
+ Card.PrimaryColor="$accent-blue")
+ (LayerPrefabPlaceholder Name="SignalMoon"
+ PlaceholderTemplate="Example/Prefabs/SignalCard.lisp"
+ Card.Icon="Example/Icons/moon.svg?width=28&type=mask"
+ Card.Title="MoonScript when it grows"
+ Card.Body="Less noise, same runtime."
+ Card.PrimaryColor="$text-secondary")
+ (LayerPrefabPlaceholder Name="SignalShip"
+ PlaceholderTemplate="Example/Prefabs/SignalCard.lisp"
+ Card.Icon="Example/Icons/globe.svg?width=28&type=mask"
+ Card.Title="Desktop, mobile, web"
+ Card.Body="One screen, many targets."
+ Card.PrimaryColor="$accent-green")
+ (LayerPrefabPlaceholder Name="SignalSpeed"
+ PlaceholderTemplate="Example/Prefabs/SignalCard.lisp"
+ Card.Icon="Example/Icons/zap.svg?width=28&type=mask"
+ Card.Title="Fast edit-run-refine"
+ Card.Body="See changes immediately."
+ Card.PrimaryColor="$accent-amber")
+ (LayerPrefabPlaceholder Name="SignalScale"
+ PlaceholderTemplate="Example/Prefabs/SignalCard.lisp"
+ Card.Icon="Example/Icons/cpu.svg?width=28&type=mask"
+ Card.Title="Built to scale"
+ Card.Body="Same UI, larger ambitions."
+ Card.PrimaryColor="$text-muted"))
+ (TabView Name="OrcaTabs" SelectedValue="xml"
+ (TabBar Name="OrcaTabBar" Height=44 BackgroundColor="$panel-background"
+ Padding="8 8 8 0" Spacing=12
+ (Tab Name="XmlTab" Value="xml" Width=140 "XML")
+ (Tab Name="LuaTab" Value="lua" Width=140 "Lua")
+ (Tab Name="MoonTab" Value="moon" Width=140 "MoonScript")
+ (Tab Name="DeployTab" Value="deploy" Width=140 "Deploy"))
+ (StackView Name="xml" Direction="Vertical" Spacing=24
+ BackgroundColor="$surface-deep" Padding=32
+ (LayerPrefabPlaceholder Name="XmlPanelHeader"
+ PlaceholderTemplate="Example/Prefabs/TabPanelHeader.lisp"
+ Card.Icon="Example/Icons/grid.svg?width=26&type=mask"
+ Card.Title="XML reads like a blueprint"
+ Card.PrimaryColor="$accent")
+ (Grid Name="XmlGrid" Columns="auto auto" Spacing=20
+ (StackView Name="XmlCopy" Direction="Vertical" Spacing=14
+ (TextBlock Name="XmlBody"
+ Text="The screen stays legible at a glance, which makes it easy to scan, compare, and reshape as the design evolves."
+ FontSize=14 ForegroundColor="$text-muted" WordWrap="true")
+ (StackView Name="XmlNotes" Direction="Vertical" Spacing=8
+ BackgroundColor="$panel-background" Padding=20
+ (TextBlock Name="XmlNote1"
+ Text="• Grids and stacks stay visible in the source."
+ FontSize=13 ForegroundColor="$text-secondary" WordWrap="true")
+ (TextBlock Name="XmlNote2"
+ Text="• Spacing, padding, and hierarchy remain explicit."
+ FontSize=13 ForegroundColor="$text-secondary" WordWrap="true")
+ (TextBlock Name="XmlNote3"
+ Text="• The shape of the screen is easy to read before it ever runs."
+ FontSize=13 ForegroundColor="$text-secondary" WordWrap="true")))
+ (StackView Name="XmlModel" Direction="Vertical" Spacing=10
+ BackgroundColor="$card-background" Padding=20
+ (TextBlock Name="XmlModelHeading" Text="The structure stays honest"
+ FontSize=16 ForegroundColor="$text-primary")
+ (LayerPrefabPlaceholder Name="XmlModelLine1"
+ PlaceholderTemplate="Example/Prefabs/XmlModelNode.lisp"
+ Card.Icon="Example/Icons/monitor.svg?width=20&type=mask"
+ Card.Title="Screen"
+ PaddingLeft=0
+ Card.PrimaryColor="$accent")
+ (LayerPrefabPlaceholder Name="XmlModelLine2"
+ PlaceholderTemplate="Example/Prefabs/XmlModelNode.lisp"
+ Card.Icon="Example/Icons/panel-top.svg?width=20&type=mask"
+ Card.Title="TabView"
+ PaddingLeft=16
+ Card.PrimaryColor="$text-secondary")
+ (LayerPrefabPlaceholder Name="XmlModelLine3"
+ PlaceholderTemplate="Example/Prefabs/XmlModelNode.lisp"
+ Card.Icon="Example/Icons/grid.svg?width=20&type=mask"
+ Card.Title="Grid"
+ PaddingLeft=32
+ Card.PrimaryColor="$text-secondary")
+ (LayerPrefabPlaceholder Name="XmlModelLine4"
+ PlaceholderTemplate="Example/Prefabs/XmlModelNode.lisp"
+ Card.Icon="Example/Icons/layers.svg?width=20&type=mask"
+ Card.Title="StackView"
+ PaddingLeft=32
+ Card.PrimaryColor="$text-secondary")
+ (LayerPrefabPlaceholder Name="XmlModelLine5"
+ PlaceholderTemplate="Example/Prefabs/XmlModelNode.lisp"
+ Card.Icon="Example/Icons/image.svg?width=20&type=mask"
+ Card.Title="ImageView"
+ PaddingLeft=48
+ Card.PrimaryColor="$text-secondary")
+ (LayerPrefabPlaceholder Name="XmlModelLine6"
+ PlaceholderTemplate="Example/Prefabs/XmlModelNode.lisp"
+ Card.Icon="Example/Icons/type.svg?width=20&type=mask"
+ Card.Title="TextBlock"
+ PaddingLeft=48
+ Card.PrimaryColor="$text-secondary"))))
+ (StackView Name="lua" Direction="Vertical" Spacing=24
+ BackgroundColor="$surface-deep" Padding=32
+ (LayerPrefabPlaceholder Name="LuaPanelHeader"
+ PlaceholderTemplate="Example/Prefabs/TabPanelHeader.lisp"
+ Card.Icon="Example/Icons/code.svg?width=26&type=mask"
+ Card.Title="Lua stays close to the surface"
+ Card.PrimaryColor="$accent-blue")
+ (TextBlock Name="LuaBody"
+ Text="The parts that move stay beside the screen that uses them, so interaction and state feel close and easy to change."
+ FontSize=14 ForegroundColor="$text-muted" WordWrap="true")
+ (Grid Name="LuaGrid" Columns="auto auto auto" Spacing=16
+ (LayerPrefabPlaceholder Name="LuaCard1"
+ PlaceholderTemplate="Example/Prefabs/IconCard.lisp"
+ Card.Icon="Example/Icons/mouse-pointer-click.svg?width=40&type=mask"
+ Card.Title="Input"
+ Card.Body="Tap, click, and selection handling can sit right beside the view that needs it."
+ Card.PrimaryColor="$accent"
+ Card.IconBackground="$icon-bg-purple")
+ (LayerPrefabPlaceholder Name="LuaCard2"
+ PlaceholderTemplate="Example/Prefabs/IconCard.lisp"
+ Card.Icon="Example/Icons/database.svg?width=40&type=mask"
+ Card.Title="State"
+ Card.Body="Small state objects stay local, so the screen remains easy to reason about."
+ Card.PrimaryColor="$accent-blue"
+ Card.IconBackground="$icon-bg-blue")
+ (LayerPrefabPlaceholder Name="LuaCard3"
+ PlaceholderTemplate="Example/Prefabs/IconCard.lisp"
+ Card.Icon="Example/Icons/link-2.svg?width=40&type=mask"
+ Card.Title="Binding"
+ Card.Body="Let Lua connect the moving pieces while XML keeps the hierarchy in view."
+ Card.PrimaryColor="$accent-green"
+ Card.IconBackground="$icon-bg-green")))
+ (StackView Name="moon" Direction="Vertical" Spacing=24
+ BackgroundColor="$surface-deep" Padding=32
+ (LayerPrefabPlaceholder Name="MoonPanelHeader"
+ PlaceholderTemplate="Example/Prefabs/TabPanelHeader.lisp"
+ Card.Icon="Example/Icons/moon.svg?width=26&type=mask"
+ Card.Title="MoonScript trims the noise"
+ Card.PrimaryColor="$text-secondary")
+ (Grid Name="MoonGrid" Columns="auto auto" Spacing=20
+ (StackView Name="MoonCopy" Direction="Vertical" Spacing=14
+ (TextBlock Name="MoonBody"
+ Text="MoonScript keeps larger app code compact and expressive, while still compiling to the same Lua runtime."
+ FontSize=14 ForegroundColor="$text-muted" WordWrap="true")
+ (StackView Name="MoonNotes" Direction="Vertical" Spacing=8
+ BackgroundColor="$panel-background" Padding=20
+ (TextBlock Name="MoonNote1"
+ Text="• Helpful when app modules get a little larger."
+ FontSize=13 ForegroundColor="$text-secondary" WordWrap="true")
+ (TextBlock Name="MoonNote2"
+ Text="• The syntax stays concise, but the runtime stays Lua."
+ FontSize=13 ForegroundColor="$text-secondary" WordWrap="true")
+ (TextBlock Name="MoonNote3"
+ Text="• It pairs naturally with XML screens and Lua helpers."
+ FontSize=13 ForegroundColor="$text-secondary" WordWrap="true")))
+ (StackView Name="MoonModel" Direction="Vertical" Spacing=10
+ BackgroundColor="$card-background" Padding=20
+ (TextBlock Name="MoonModelHeading" Text="Same runtime, less ceremony"
+ FontSize=16 ForegroundColor="$text-primary")
+ (TextBlock Name="MoonModelLine1" Text="Cleaner class bodies"
+ FontSize=14 ForegroundColor="$text-secondary")
+ (TextBlock Name="MoonModelLine2" Text="Readable app controllers"
+ FontSize=14 ForegroundColor="$text-secondary")
+ (TextBlock Name="MoonModelLine3" Text="Plain Lua at runtime"
+ FontSize=14 ForegroundColor="$text-secondary")
+ (TextBlock Name="MoonModelLine4" Text="Comfortable beside XML layouts"
+ FontSize=14 ForegroundColor="$text-secondary"))))
+ (StackView Name="deploy" Direction="Vertical" Spacing=24
+ BackgroundColor="$surface-deep" Padding=32
+ (LayerPrefabPlaceholder Name="DeployPanelHeader"
+ PlaceholderTemplate="Example/Prefabs/TabPanelHeader.lisp"
+ Card.Icon="Example/Icons/globe.svg?width=26&type=mask"
+ Card.Title="One UI, many targets"
+ Card.PrimaryColor="$accent-green")
+ (TextBlock Name="DeployBody"
+ Text="Build once, tune the spacing, and carry the same design language across desktop, mobile, and web-oriented targets."
+ FontSize=14 ForegroundColor="$text-muted" WordWrap="true")
+ (Grid Name="DeployGrid" Columns="auto auto auto" Spacing=16
+ (LayerPrefabPlaceholder Name="DeployCard1"
+ PlaceholderTemplate="Example/Prefabs/IconCard.lisp"
+ Card.Icon="Example/Icons/monitor.svg?width=40&type=mask"
+ Card.Title="Desktop"
+ Card.Body="Best when you want denser information, more breathing room, and precision input."
+ Card.PrimaryColor="$accent"
+ Card.IconBackground="$icon-bg-purple")
+ (LayerPrefabPlaceholder Name="DeployCard2"
+ PlaceholderTemplate="Example/Prefabs/IconCard.lisp"
+ Card.Icon="Example/Icons/smartphone.svg?width=40&type=mask"
+ Card.Title="Mobile"
+ Card.Body="Responsive spacing keeps the same interface calm and touch-friendly on smaller screens."
+ Card.PrimaryColor="$accent-green"
+ Card.IconBackground="$icon-bg-green")
+ (LayerPrefabPlaceholder Name="DeployCard3"
+ PlaceholderTemplate="Example/Prefabs/IconCard.lisp"
+ Card.Icon="Example/Icons/globe.svg?width=40&type=mask"
+ Card.Title="Web"
+ Card.Body="The same screen model can carry into browser workflows without being rewritten."
+ Card.PrimaryColor="$accent-blue"
+ Card.IconBackground="$icon-bg-blue"))
+ (Grid Name="DeployImages" Columns="auto auto" Spacing=12
+ (LayerPrefabPlaceholder Name="DeployImage1"
+ PlaceholderTemplate="Example/Prefabs/ImageCaptionCard.lisp"
+ Card.Image="Example/Images/orca-tab-city"
+ Card.Title="City glow")
+ (LayerPrefabPlaceholder Name="DeployImage2"
+ PlaceholderTemplate="Example/Prefabs/ImageCaptionCard.lisp"
+ Card.Image="Example/Images/orca-tab-lights"
+ Card.Title="Night light"))
+ (StackView Name="DeployCommandLink" Direction="Horizontal" Spacing=12
+ AlignItems="Center" BackgroundColor="$panel-background" Padding=16
+ (ImageView Name="DeployCommandIcon"
+ Source="Example/Icons/monitor.svg?width=20&type=mask"
+ Width=20 Height=20 ForegroundColor="$accent-green")
+ (StackView Name="DeployCommandCopy" Direction="Vertical" Spacing=2
+ (TextBlock Name="DeployCommandLabel" Text="Desktop build"
+ FontSize=13 ForegroundColor="$text-secondary")
+ (TextBlock Name="DeployCommandValue"
+ Text="make clean && make -j8 && build/bin/orca samples/Example"
+ FontSize=14 ForegroundColor="$text-primary" WordWrap="true"))))))
+ (Grid Name="FeatureSection" Columns="auto auto auto auto" Spacing=20
+ (LayerPrefabPlaceholder Name="FeatureXmlCard"
+ PlaceholderTemplate="Example/Prefabs/FeatureImageCard.lisp"
+ Card.Icon="Example/Icons/layers.svg?width=40&type=mask"
+ Card.Title="XML stays readable"
+ Card.Body="The layout tree is right there in the page, so structure and spacing stay easy to tune."
+ Card.PrimaryColor="$accent")
+ (LayerPrefabPlaceholder Name="FeatureLuaCard"
+ PlaceholderTemplate="Example/Prefabs/FeatureImageCard.lisp"
+ Card.Icon="Example/Icons/monitor.svg?width=40&type=mask"
+ Card.Title="Lua stays close"
+ Card.Body="Interactions, bindings, and state can live next to the screen that owns them."
+ Card.PrimaryColor="$accent-blue")
+ (LayerPrefabPlaceholder Name="FeatureShipCard"
+ PlaceholderTemplate="Example/Prefabs/FeatureImageCard.lisp"
+ Card.Icon="Example/Icons/globe.svg?width=40&type=mask"
+ Card.Title="Ships everywhere"
+ Card.Body="The same UI can move across desktop, mobile, and web-friendly targets."
+ Card.PrimaryColor="$accent-green")
+ (LayerPrefabPlaceholder Name="FeatureSpeedCard"
+ PlaceholderTemplate="Example/Prefabs/FeatureImageCard.lisp"
+ Card.Icon="Example/Icons/zap.svg?width=40&type=mask"
+ Card.Title="Fast iteration"
+ Card.Body="Edit, reload, and refine quickly without losing the overall shape of the interface."
+ Card.PrimaryColor="$accent-amber"))
+ (StackView Name="GallerySection" Direction="Vertical" Spacing=20
+ BackgroundColor="$surface" Padding=40
+ (:bind Target="Node.HorizontalPadding"
+ (if (step 640 (bind "Node.ActualWidth")) (vector2 40 40) (vector2 8 8)))
+ (TextBlock Name="GalleryHeading" Text="Gallery"
+ FontSize=28 ForegroundColor="$text-primary")
+ (TextBlock Name="GallerySubheading"
+ Text="Screenshots and scenes rendered entirely by the ORCA engine."
+ FontSize=14 ForegroundColor="$text-muted")
+ (Grid Name="GalleryGrid" Columns="auto auto" Spacing=16
+ (LayerPrefabPlaceholder Name="GalleryCard1"
+ PlaceholderTemplate="Example/Prefabs/GalleryCard.lisp"
+ Card.Image="Example/Images/lake"
+ Card.Title="Lake Scene"
+ Card.Subtitle="Real-time reflections and water surface rendered at 60 fps.")
+ (LayerPrefabPlaceholder Name="GalleryCard2"
+ PlaceholderTemplate="Example/Prefabs/GalleryCard.lisp"
+ Card.Image="Example/Images/peacock"
+ Card.Title="Peacock"
+ Card.Subtitle="High-fidelity texture sampling with anisotropic filtering.")))
+ (StackView Name="WorkflowSection" Direction="Vertical" Spacing=24
+ BackgroundColor="$surface-deep" Padding=40
+ (:bind Target="Node.HorizontalPadding"
+ (if (step 640 (bind "Node.ActualWidth")) (vector2 40 40) (vector2 8 8)))
+ (TextBlock Name="WorkflowHeading" Text="How It Works"
+ FontSize=28 ForegroundColor="$text-primary")
+ (TextBlock Name="WorkflowSubheading"
+ Text="From XML layout to a live rendered scene in four simple steps."
+ FontSize=14 ForegroundColor="$text-muted")
+ (Grid Name="WorkflowSteps" Columns="auto auto auto auto" Spacing=16
+ (:bind Target="Grid.Columns"
+ (if (step 640 (bind "Node.ActualWidth"))
+ "auto auto auto auto" "auto auto"))
+ (LayerPrefabPlaceholder Name="Step1"
+ PlaceholderTemplate="Example/Prefabs/WorkflowStep.lisp"
+ Card.Title="01" Card.Subtitle="Define Layout"
+ Card.PrimaryColor="$accent"
+ Card.Body="Write your screen as an XML file with familiar Grid and StackView containers.")
+ (LayerPrefabPlaceholder Name="Step2"
+ PlaceholderTemplate="Example/Prefabs/WorkflowStep.lisp"
+ Card.Title="02" Card.Subtitle="Load Assets"
+ Card.PrimaryColor="$accent-blue"
+ Card.Body="Images, fonts, and data files are streamed from packages at runtime.")
+ (LayerPrefabPlaceholder Name="Step3"
+ PlaceholderTemplate="Example/Prefabs/WorkflowStep.lisp"
+ Card.Title="03" Card.Subtitle="Script Logic"
+ Card.PrimaryColor="$accent-green"
+ Card.Body="Attach Lua scripts to any component for interactivity and data binding.")
+ (LayerPrefabPlaceholder Name="Step4"
+ PlaceholderTemplate="Example/Prefabs/WorkflowStep.lisp"
+ Card.Title="04" Card.Subtitle="Ship"
+ Card.PrimaryColor="$accent-amber"
+ Card.Body="Deploy to desktop, mobile, or WebGL with a single build command.")))
+ (StackView Name="TestimonialsSection" Direction="Vertical" Spacing=24
+ BackgroundColor="$surface" Padding=40
+ (:bind Target="Node.HorizontalPadding"
+ (if (step 640 (bind "Node.ActualWidth")) (vector2 40 40) (vector2 8 8)))
+ (TextBlock Name="TestimonialsHeading" Text="What Developers Say"
+ FontSize=28 ForegroundColor="$text-primary")
+ (Grid Name="TestimonialsGrid" Columns="auto auto auto" Spacing=20
+ (LayerPrefabPlaceholder Name="Quote1"
+ PlaceholderTemplate="Example/Prefabs/Quote.lisp"
+ Card.PrimaryColor="$accent"
+ Card.Title="— Alex R." Card.Subtitle="Lead Engineer"
+ Card.Body="ORCA lets us ship native-quality UI on every platform from a single XML source. It changed how we build products.")
+ (LayerPrefabPlaceholder Name="Quote2"
+ PlaceholderTemplate="Example/Prefabs/Quote.lisp"
+ Card.PrimaryColor="$accent-green"
+ Card.Title="— Sam T." Card.Subtitle="UI Architect"
+ Card.Body="The declarative layout system is a joy to work with. Our designers write XML and developers hook up the logic — perfect split.")
+ (LayerPrefabPlaceholder Name="Quote3"
+ PlaceholderTemplate="Example/Prefabs/Quote.lisp"
+ Card.PrimaryColor="$accent-blue"
+ Card.Title="— Jordan M." Card.Subtitle="Game Developer"
+ Card.Body="Sub-millisecond frame times even on mid-range hardware. We ditched our old HTML renderer the day we tried ORCA.")))
+ (StackView Name="CtaSection" Direction="Vertical" Spacing=24
+ BackgroundColor="$panel-background" Padding=60 AlignItems="Center"
+ (:bind Target="Node.HorizontalPadding"
+ (if (step 640 (bind "Node.ActualWidth")) (vector2 60 60) (vector2 16 16)))
+ (TextBlock Name="CtaHeading" Text="Ready to build something great?"
+ FontSize=36 ForegroundColor="$text-primary"
+ TextHorizontalAlignment="Center")
+ (TextBlock Name="CtaBody"
+ Text="Download ORCA, open a sample project, and have your first screen running in minutes."
+ FontSize=15 ForegroundColor="$text-muted" WordWrap="true"
+ TextHorizontalAlignment="Center")
+ (StackView Name="CtaButtons" Direction="Horizontal" Spacing=16
+ (TextBlock Name="CtaButtonPrimary" Text="Get Started"
+ FontSize=14 ForegroundColor="$white"
+ BackgroundColor="$accent" Padding=16 Width=140 Height=44
+ TextHorizontalAlignment="Center" TextVerticalAlignment="Center"
+ LeftButtonUp="{ShowModalAction Example/Screens/GetStartedPopup.lisp}")
+ (TextBlock Name="CtaButtonSecondary" Text="Learn more"
+ FontSize=14 ForegroundColor="$text-secondary"
+ BackgroundColor="$card-background" Padding=16
+ Width=140 Height=44
+ TextHorizontalAlignment="Center"
+ TextVerticalAlignment="Center"))))))
diff --git a/samples/Example/Screens/GetStartedPopup.lisp b/samples/Example/Screens/GetStartedPopup.lisp
new file mode 100644
index 00000000..9a857ac4
--- /dev/null
+++ b/samples/Example/Screens/GetStartedPopup.lisp
@@ -0,0 +1,32 @@
+(Popup Name="GetStartedPopup" BackgroundColor="#00000088"
+ (StackView Name="GetStartedPopupOverlay"
+ Direction="Vertical"
+ AlignItems="Center"
+ JustifyContent="Center"
+ Padding=40
+ (StackView Name="GetStartedPopupCard"
+ Direction="Vertical"
+ Spacing=14
+ Width=420
+ BackgroundColor="$surface-deep"
+ Padding=24
+ BorderWidth=2
+ BorderColor="$accent"
+ (TextBlock Name="GetStartedPopupTitle"
+ Text="What ORCA gives you"
+ FontSize=20
+ ForegroundColor="$text-primary")
+ (TextBlock Name="GetStartedPopupBody"
+ Text="XML keeps layout honest, Lua keeps behavior close, and MoonScript gives you a tighter way to grow the surface. The same codebase can ship to desktop, mobile, and the web."
+ FontSize=14
+ ForegroundColor="$text-muted"
+ WordWrap="true")
+ (TextBlock Name="GetStartedPopupClose"
+ Text="Close"
+ FontSize=14
+ Height=34
+ BackgroundColor="$accent"
+ Padding=8
+ TextHorizontalAlignment="Center"
+ TextVerticalAlignment="Center"
+ LeftButtonUp="{SendMessageAction Popup.ClosePopup ../../..}"))))
diff --git a/samples/Example/package.lua b/samples/Example/package.lua
index 0b849427..68185ee0 100644
--- a/samples/Example/package.lua
+++ b/samples/Example/package.lua
@@ -1,5 +1,5 @@
Name = "Example"
-StartupScreen = "Example/Screens/Application"
+StartupScreen = "Example/Screens/Application.lisp"
WindowWidth = 1024
WindowHeight = 768
ScreenLibrary = { IsExternal = true }
diff --git a/source/core/components/Trigger.c b/source/core/components/Trigger.c
index 5951b8f2..91325118 100644
--- a/source/core/components/Trigger.c
+++ b/source/core/components/Trigger.c
@@ -64,12 +64,10 @@ _BindingGetIntProperty(struct Object *obj, lpcString_t name, int fallback)
return *(int*)PROP_GetValue(prop);
}
-
static LRESULT
_BindingCompileToProperty(struct Object *hObject,
struct Binding *binding,
- struct Property *property,
- bool_t normalize_markup)
+ struct Property *property)
{
if (!property) {
Con_Error("Binding.Compile requires a target property in lParam");
@@ -90,23 +88,8 @@ _BindingCompileToProperty(struct Object *hObject,
_BindingGetIntProperty(hObject, "Mode", kBindingModeExpression);
bool_t enabled = _BindingGetIntProperty(hObject, "Enabled", TRUE) ? TRUE : FALSE;
- lpcString_t final_expr = expr;
- fixedString_t normalized_expr = {0};
- if (normalize_markup) {
- size_t len = strlen(expr);
- if (len > 9 && !strncmp(expr, "{Binding ", 9) && expr[len - 1] == '}') {
- /* {Binding X} → {X}; runtime default resolves X relative to template root */
- snprintf(normalized_expr, sizeof(normalized_expr), "{%.*s}", (int)(len - 10), expr + 9);
- final_expr = normalized_expr;
- } else if (*expr != '{') {
- /* Bare path X → {X}; runtime default resolves relative to template root */
- snprintf(normalized_expr, sizeof(normalized_expr), "{%s}", expr);
- final_expr = normalized_expr;
- }
- }
-
binding->property = property;
- if (!PROP_SetBinding(property, final_expr, mode, enabled)) {
+ if (!PROP_SetBinding(property, expr, mode, enabled)) {
Con_Error("Binding failed for property '%s' on '%s'",
PROP_GetName(property),
OBJ_GetClassName(PROP_GetObject(property)));
@@ -156,16 +139,22 @@ HANDLER(Binding, Binding, Compile)
{
return _BindingCompileToProperty(hObject,
pBinding,
- (struct Property *)pCompile,
- TRUE);
+ (struct Property *)pCompile);
}
HANDLER(BindingExpression, Binding, Compile)
{
return _BindingCompileToProperty(hObject,
(struct Binding *)pBindingExpression,
- (struct Property *)pCompile,
- FALSE);
+ (struct Property *)pCompile);
+}
+
+HANDLER(Binding, Binding, Evaluate)
+{
+ struct Property *property = pEvaluate && pEvaluate->Property
+ ? pEvaluate->Property
+ : (pBinding ? pBinding->property : NULL);
+ return PROP_EvaluateBinding(property, pBinding);
}
HANDLER(Trigger, Object, PropertyChanged)
diff --git a/source/core/core.cgen b/source/core/core.cgen
index 7ecb03ae..fa975665 100644
--- a/source/core/core.cgen
+++ b/source/core/core.cgen
@@ -800,6 +800,7 @@ This is the idiomatic way to build object hierarchies in Lua scripts and tests.<
http://schemas.corepunch.com/orca/2006/xml/presentation
+
Binding expression text. If omitted, object text content is used.
@@ -808,6 +809,7 @@ This is the idiomatic way to build object hierarchies in Lua scripts and tests.<
Resolved target property slot.
+ Binding holder object used for message-driven evaluation (optional in raw fallback mode).
Compiled VM token tree for this binding expression.
Last frame this binding executed.
@@ -815,6 +817,12 @@ This is the idiomatic way to build object hierarchies in Lua scripts and tests.<
Compiles and attaches this binding to the target property passed in lParam.
+
+ Evaluates the binding token program and imports the result into the target property.
+
+ Target property to import evaluation result into.
+
+
diff --git a/source/core/core.h b/source/core/core.h
index 55d33a64..d0dec40c 100644
--- a/source/core/core.h
+++ b/source/core/core.h
@@ -40,6 +40,7 @@ typedef struct StyleController_AddClassEventArgs StyleController_AddClassMsg_t,*
typedef struct StyleController_AddClassesEventArgs StyleController_AddClassesMsg_t,* StyleController_AddClassesMsgPtr;
typedef struct StateManagerController_ControllerChangedEventArgs StateManagerController_ControllerChangedMsg_t,* StateManagerController_ControllerChangedMsgPtr;
typedef struct Binding_CompileEventArgs Binding_CompileMsg_t,* Binding_CompileMsgPtr;
+typedef struct Binding_EvaluateEventArgs Binding_EvaluateMsg_t,* Binding_EvaluateMsgPtr;
typedef struct Trigger_TriggeredEventArgs Trigger_TriggeredMsg_t,* Trigger_TriggeredMsgPtr;
typedef struct Node_UpdateMatrixEventArgs Node_UpdateMatrixMsg_t,* Node_UpdateMatrixMsgPtr;
typedef struct Node_LoadViewEventArgs Node_LoadViewMsg_t,* Node_LoadViewMsgPtr;
@@ -766,6 +767,12 @@ struct Binding_CompileEventArgs {
};
ORCA_API void luaX_pushBinding_CompileEventArgs(lua_State *L, struct Binding_CompileEventArgs const* data);
ORCA_API struct Binding_CompileEventArgs* luaX_checkBinding_CompileEventArgs(lua_State *L, int idx);
+/** Binding_EvaluateEventArgs struct */
+struct Binding_EvaluateEventArgs {
+ struct Property* Property; ///< Target property to import evaluation result into.
+};
+ORCA_API void luaX_pushBinding_EvaluateEventArgs(lua_State *L, struct Binding_EvaluateEventArgs const* data);
+ORCA_API struct Binding_EvaluateEventArgs* luaX_checkBinding_EvaluateEventArgs(lua_State *L, int idx);
/** Trigger_TriggeredEventArgs struct */
struct Trigger_TriggeredEventArgs {
struct Trigger* Trigger;
@@ -1002,9 +1009,11 @@ struct Binding {
enum BindingMode Mode; ///< Binding mode (Expression, OneWay, TwoWay, OneWayToSource).
bool_t Enabled; ///< When false, stores the expression but does not compile/execute it.
struct Property* property; ///< Resolved target property slot.
+ struct Object* owner; ///< Binding holder object used for message-driven evaluation (optional in raw fallback mode).
struct token* token; ///< Compiled VM token tree for this binding expression.
uint32_t updateFrame; ///< Last frame this binding executed.
event_t Compile;
+ event_t Evaluate;
};
ORCA_API void luaX_pushBinding(lua_State *L, struct Binding const* Binding);
ORCA_API struct Binding* luaX_checkBinding(lua_State *L, int idx);
diff --git a/source/core/core_export.c b/source/core/core_export.c
index 7ee05b2f..1ea18d08 100644
--- a/source/core/core_export.c
+++ b/source/core/core_export.c
@@ -709,6 +709,11 @@ static luaL_Reg _Binding_CompileEventArgs_Methods[] = { { NULL, NULL } };
struct PropertyType _Binding_CompileEventArgs_Properties[] = {
};
#define _Binding_CompileEventArgs _Binding_CompileEventArgs_Properties
+static luaL_Reg _Binding_EvaluateEventArgs_Methods[] = { { NULL, NULL } };
+struct PropertyType _Binding_EvaluateEventArgs_Properties[] = {
+ DECL(0x5221f9e8, Binding_EvaluateEventArgs, Property, Property, kDataTypeStruct, .TypeString = "Property"), // Binding_EvaluateEventArgs.Property
+};
+#define _Binding_EvaluateEventArgs _Binding_EvaluateEventArgs_Properties
static luaL_Reg _Trigger_TriggeredEventArgs_Methods[] = { { NULL, NULL } };
struct PropertyType _Trigger_TriggeredEventArgs_Properties[] = {
DECL(0xa5ea0da3, Trigger_TriggeredEventArgs, Trigger, Trigger, kDataTypeObject, .TypeString = "Trigger"), // Trigger_TriggeredEventArgs.Trigger
@@ -985,6 +990,7 @@ STRUCT(StyleController_AddClassEventArgs, StyleController_AddClassEventArgs);
STRUCT(StyleController_AddClassesEventArgs, StyleController_AddClassesEventArgs);
STRUCT(StateManagerController_ControllerChangedEventArgs, StateManagerController_ControllerChangedEventArgs);
STRUCT(Binding_CompileEventArgs, Binding_CompileEventArgs);
+STRUCT(Binding_EvaluateEventArgs, Binding_EvaluateEventArgs);
STRUCT(Trigger_TriggeredEventArgs, Trigger_TriggeredEventArgs);
STRUCT(Node_UpdateMatrixEventArgs, Node_UpdateMatrixEventArgs);
STRUCT(Node_LoadViewEventArgs, Node_LoadViewEventArgs);
@@ -1327,11 +1333,13 @@ struct DataObject* luaX_checkDataObject(lua_State *L, int idx) {
}
REGISTER_CLASS(DataObject, 0);
HANDLER(Binding, Binding, Compile);
+HANDLER(Binding, Binding, Evaluate);
static struct PropertyType const BindingProperties[kBindingNumProperties] = {
DECL(0xeb81bd0b, Binding, Expression, Expression, kDataTypeString), // Binding.Expression
DECL(0x534e7732, Binding, Mode, Mode, kDataTypeEnum, .EnumValues = _BindingMode), // Binding.Mode
DECL(0x9c86e43e, Binding, Enabled, Enabled, kDataTypeBool), // Binding.Enabled
DECL(0x52fafc3e, Binding, Compile, Compile, kDataTypeEvent, .TypeString = "Binding_CompileEventArgs"), // Binding.Compile
+ DECL(0xa35bdf2a, Binding, Evaluate, Evaluate, kDataTypeEvent, .TypeString = "Binding_EvaluateEventArgs"), // Binding.Evaluate
};
static struct Binding BindingDefaults = {
@@ -1340,6 +1348,7 @@ static struct Binding BindingDefaults = {
LRESULT BindingProc(struct Object* object, void* cmp, uint32_t message, wParam_t wparm, lParam_t lparm) {
switch (message) {
case ID_Binding_Compile: return Binding_Compile(object, cmp, wparm, lparm); // Binding.Compile
+ case ID_Binding_Evaluate: return Binding_Evaluate(object, cmp, wparm, lparm); // Binding.Evaluate
}
return FALSE;
}
@@ -1730,6 +1739,7 @@ ORCA_API int luaopen_orca_core(lua_State *L) {
REGISTER_MESSAGE_TYPE(ID_StyleController_AddClasses, "StyleController.AddClasses", StyleController_AddClassesEventArgs);
REGISTER_MESSAGE_TYPE(ID_StateManagerController_ControllerChanged, "StateManagerController.ControllerChanged", StateManagerController_ControllerChangedEventArgs);
REGISTER_MESSAGE_TYPE(ID_Binding_Compile, "Binding.Compile", Binding_CompileEventArgs);
+ REGISTER_MESSAGE_TYPE(ID_Binding_Evaluate, "Binding.Evaluate", Binding_EvaluateEventArgs);
REGISTER_MESSAGE_TYPE(ID_Trigger_Triggered, "Trigger.Triggered", Trigger_TriggeredEventArgs);
REGISTER_MESSAGE_TYPE(ID_Node_UpdateMatrix, "Node.UpdateMatrix", Node_UpdateMatrixEventArgs);
REGISTER_MESSAGE_TYPE(ID_Node_LoadView, "Node.LoadView", Node_LoadViewEventArgs);
@@ -1792,6 +1802,7 @@ ORCA_API int luaopen_orca_core(lua_State *L) {
lua_setfield(L, ((void)luaopen_orca_StyleController_AddClassesEventArgs(L), -2), "StyleController_AddClassesEventArgs");
lua_setfield(L, ((void)luaopen_orca_StateManagerController_ControllerChangedEventArgs(L), -2), "StateManagerController_ControllerChangedEventArgs");
lua_setfield(L, ((void)luaopen_orca_Binding_CompileEventArgs(L), -2), "Binding_CompileEventArgs");
+ lua_setfield(L, ((void)luaopen_orca_Binding_EvaluateEventArgs(L), -2), "Binding_EvaluateEventArgs");
lua_setfield(L, ((void)luaopen_orca_Trigger_TriggeredEventArgs(L), -2), "Trigger_TriggeredEventArgs");
lua_setfield(L, ((void)luaopen_orca_Node_UpdateMatrixEventArgs(L), -2), "Node_UpdateMatrixEventArgs");
lua_setfield(L, ((void)luaopen_orca_Node_LoadViewEventArgs(L), -2), "Node_LoadViewEventArgs");
diff --git a/source/core/core_properties.h b/source/core/core_properties.h
index aa7865db..fd339044 100644
--- a/source/core/core_properties.h
+++ b/source/core/core_properties.h
@@ -197,12 +197,14 @@ enum LocaleProperties {
#define ID_Binding_Mode 0xe206dce7 // Binding.Mode
#define ID_Binding_Enabled 0x223361a5 // Binding.Enabled
#define ID_Binding_Compile 0xf02fb971 // Binding.Compile
-#define kBindingNumProperties 4
+#define ID_Binding_Evaluate 0x9d397507 // Binding.Evaluate
+#define kBindingNumProperties 5
enum BindingProperties {
kBindingExpression,
kBindingMode,
kBindingEnabled,
kBindingCompile,
+ kBindingEvaluate,
};
// BindingExpression
#define ID_BindingExpression 0xddf6d2b0
@@ -651,6 +653,9 @@ enum NodeProperties {
#define ID_StateManagerController_ControllerChangedEventArgs_Property 0x1ed8c3b6 // StateManagerController_ControllerChangedEventArgs.Property
#define ID_Binding_CompileEventArgs 0x068eacf9
+#define ID_Binding_EvaluateEventArgs 0x712631ad
+
+#define ID_Binding_EvaluateEventArgs_Property 0xf93f76b4 // Binding_EvaluateEventArgs.Property
#define ID_Trigger_TriggeredEventArgs 0x86976046
#define ID_Trigger_TriggeredEventArgs_Trigger 0xa8841074 // Trigger_TriggeredEventArgs.Trigger
diff --git a/source/core/property/property_core.c b/source/core/property/property_core.c
index b05ec8ab..4153a432 100644
--- a/source/core/property/property_core.c
+++ b/source/core/property/property_core.c
@@ -58,13 +58,29 @@ _ReleaseBindingNode(struct Binding *binding)
return;
}
SafeDelete(binding->token, Token_Release);
- free(binding);
+ struct Object *binding_object = binding->owner;
+ if (binding_object) {
+ OBJ_ReleaseRef(binding_object);
+ } else {
+ free(binding);
+ }
}
static bool_t
_RunBinding(struct Property *property, struct Binding *binding)
{
if (!binding->token) return TRUE;
+ struct Object *binding_object = binding->owner;
+ if (binding_object) {
+ return _SendMessage(binding_object, Binding, Evaluate, .Property = property);
+ }
+ return PROP_EvaluateBinding(property, binding);
+}
+
+bool_t
+PROP_EvaluateBinding(struct Property *property, struct Binding *binding)
+{
+ if (!property || !binding || !binding->token) return TRUE;
if (binding->updateFrame == core.frame) return TRUE;
binding->updateFrame = core.frame;
diff --git a/source/core/property/property_events.c b/source/core/property/property_events.c
index d5d799ac..5d324fd9 100644
--- a/source/core/property/property_events.c
+++ b/source/core/property/property_events.c
@@ -66,8 +66,28 @@ PROP_AttachProgram(struct Property *p,
SafeSet(p->binding->token, program, Token_Release);
return;
}
- p->binding = ZeroAlloc(sizeof(struct Binding));
+ struct Object *binding_object = NULL;
+ if (OBJ_FindClassW(ID_Binding)) {
+ binding_object = OBJ_Create(ID_Binding);
+ if (binding_object) {
+ OBJ_AddRef(binding_object);
+ p->binding = GetBinding(binding_object);
+ }
+ }
+ if (!p->binding) {
+ if (binding_object) {
+ OBJ_ReleaseRef(binding_object);
+ binding_object = NULL;
+ }
+ p->binding = ZeroAlloc(sizeof(struct Binding));
+ if (!p->binding) {
+ SafeDelete(program, Token_Release);
+ return;
+ }
+ }
+
p->binding->property = p;
+ p->binding->owner = binding_object;
p->binding->token = program;
if (!p->inBindingIndex) {
diff --git a/source/core/property/property_internal.h b/source/core/property/property_internal.h
index 82951a9d..c926bbd4 100644
--- a/source/core/property/property_internal.h
+++ b/source/core/property/property_internal.h
@@ -42,5 +42,6 @@ PROP_HasHandler(struct Property const *property)
struct Object *PROP_GetObjectValue(struct Property const *property);
bool_t _AssignCallback(lua_State* L, struct Property *property);
void PROP_FireNotification(lua_State* L, struct Property *property, struct Object *object);
+bool_t PROP_EvaluateBinding(struct Property *property, struct Binding *binding);
#endif /* __PROPERTY_INTERNAL_H__ */
diff --git a/source/core/property/property_program.c b/source/core/property/property_program.c
index 6dbc4b8c..ea565566 100644
--- a/source/core/property/property_program.c
+++ b/source/core/property/property_program.c
@@ -358,9 +358,304 @@ _compile(lpcString_t code, lpcString_t filename)
return token;
}
+struct _lisp_buf {
+ char *s;
+ size_t n;
+ size_t cap;
+};
+
+static bool_t
+_lisp_buf_putn(struct _lisp_buf *b, lpcString_t s, size_t len)
+{
+ if (!len) return TRUE;
+ if (b->n + len + 1 > b->cap) {
+ size_t need = b->n + len + 1;
+ size_t cap = b->cap ? b->cap * 2 : 128;
+ if (cap < need) cap = need;
+ char *next = realloc(b->s, cap);
+ if (!next) return FALSE;
+ b->s = next;
+ b->cap = cap;
+ }
+ memcpy(b->s + b->n, s, len);
+ b->n += len;
+ b->s[b->n] = '\0';
+ return TRUE;
+}
+
+static bool_t
+_lisp_buf_put(struct _lisp_buf *b, lpcString_t s)
+{
+ return _lisp_buf_putn(b, s, strlen(s));
+}
+
+static bool_t
+_lisp_buf_putc(struct _lisp_buf *b, char c)
+{
+ return _lisp_buf_putn(b, &c, 1);
+}
+
+static lpcString_t
+_lisp_skip(lpcString_t p)
+{
+ while (*p && isspace((unsigned char)*p)) p++;
+ return p;
+}
+
+static char *
+_lisp_read_atom(lpcString_t *pp)
+{
+ lpcString_t p = _lisp_skip(*pp);
+ lpcString_t start = p;
+ while (*p && !isspace((unsigned char)*p) && *p != '(' && *p != ')') p++;
+ size_t len = (size_t)(p - start);
+ char *out = malloc(len + 1);
+ if (!out) return NULL;
+ memcpy(out, start, len);
+ out[len] = '\0';
+ *pp = p;
+ return out;
+}
+
+static char *
+_lisp_parse_expr(lpcString_t *pp, bool_t *ok);
+
+static char *
+_lisp_parse_string(lpcString_t *pp, bool_t *ok)
+{
+ lpcString_t p = _lisp_skip(*pp);
+ struct _lisp_buf out = {0};
+ if (*p != '"') {
+ *ok = FALSE;
+ return NULL;
+ }
+ p++;
+ if (!_lisp_buf_putc(&out, '"')) {
+ *ok = FALSE;
+ free(out.s);
+ return NULL;
+ }
+ while (*p && *p != '"') {
+ char c = *p++;
+ if (c == '\\' && *p) {
+ char esc = *p++;
+ if (esc == '"' || esc == '\\') {
+ if (!_lisp_buf_putc(&out, '\\') || !_lisp_buf_putc(&out, esc)) {
+ *ok = FALSE;
+ free(out.s);
+ return NULL;
+ }
+ } else if (esc == 'n') {
+ if (!_lisp_buf_put(&out, "\\n")) {
+ *ok = FALSE;
+ free(out.s);
+ return NULL;
+ }
+ } else if (esc == 't') {
+ if (!_lisp_buf_put(&out, "\\t")) {
+ *ok = FALSE;
+ free(out.s);
+ return NULL;
+ }
+ } else {
+ if (!_lisp_buf_putc(&out, '\\') || !_lisp_buf_putc(&out, esc)) {
+ *ok = FALSE;
+ free(out.s);
+ return NULL;
+ }
+ }
+ } else {
+ if (c == '"' && !_lisp_buf_putc(&out, '\\')) {
+ *ok = FALSE;
+ free(out.s);
+ return NULL;
+ }
+ if (!_lisp_buf_putc(&out, c)) {
+ *ok = FALSE;
+ free(out.s);
+ return NULL;
+ }
+ }
+ }
+ if (*p != '"') {
+ *ok = FALSE;
+ free(out.s);
+ return NULL;
+ }
+ p++;
+ if (!_lisp_buf_putc(&out, '"')) {
+ *ok = FALSE;
+ free(out.s);
+ return NULL;
+ }
+ *pp = p;
+ return out.s ? out.s : strdup("\"\"");
+}
+
+static char *
+_lisp_parse_list(lpcString_t *pp, bool_t *ok)
+{
+ lpcString_t p = _lisp_skip(*pp);
+ struct _lisp_buf out = {0};
+ if (*p != '(') {
+ *ok = FALSE;
+ return NULL;
+ }
+ p++;
+
+ char *fn = _lisp_read_atom(&p);
+ if (!fn || !*fn) {
+ free(fn);
+ *ok = FALSE;
+ return NULL;
+ }
+
+ if (!strcasecmp(fn, "bind")) {
+ free(fn);
+ char *arg = _lisp_parse_expr(&p, ok);
+ if (!*ok || !arg) {
+ free(arg);
+ return NULL;
+ }
+ size_t len = strlen(arg);
+ bool_t quoted = len >= 2 && arg[0] == '"' && arg[len - 1] == '"';
+ if (!quoted) {
+ free(arg);
+ *ok = FALSE;
+ return NULL;
+ }
+ if (!_lisp_buf_putc(&out, '{') ||
+ !_lisp_buf_putn(&out, arg + 1, len - 2) ||
+ !_lisp_buf_putc(&out, '}')) {
+ free(arg);
+ free(out.s);
+ *ok = FALSE;
+ return NULL;
+ }
+ free(arg);
+ p = _lisp_skip(p);
+ if (*p != ')') {
+ *ok = FALSE;
+ free(out.s);
+ return NULL;
+ }
+ p++;
+ *pp = p;
+ return out.s;
+ }
+
+ for (char *c = fn; *c; c++) {
+ *c = (char)toupper((unsigned char)*c);
+ }
+ if (!_lisp_buf_put(&out, fn) || !_lisp_buf_putc(&out, '(')) {
+ free(fn);
+ free(out.s);
+ *ok = FALSE;
+ return NULL;
+ }
+ free(fn);
+
+ bool_t first = TRUE;
+ for (;;) {
+ p = _lisp_skip(p);
+ if (*p == ')') {
+ p++;
+ break;
+ }
+ if (!*p) {
+ free(out.s);
+ *ok = FALSE;
+ return NULL;
+ }
+ char *arg = _lisp_parse_expr(&p, ok);
+ if (!*ok || !arg) {
+ free(arg);
+ free(out.s);
+ return NULL;
+ }
+ if (!first && !_lisp_buf_put(&out, ", ")) {
+ free(arg);
+ free(out.s);
+ *ok = FALSE;
+ return NULL;
+ }
+ if (!_lisp_buf_put(&out, arg)) {
+ free(arg);
+ free(out.s);
+ *ok = FALSE;
+ return NULL;
+ }
+ free(arg);
+ first = FALSE;
+ }
+
+ if (!_lisp_buf_putc(&out, ')')) {
+ free(out.s);
+ *ok = FALSE;
+ return NULL;
+ }
+ *pp = p;
+ return out.s;
+}
+
+static char *
+_lisp_parse_expr(lpcString_t *pp, bool_t *ok)
+{
+ lpcString_t p = _lisp_skip(*pp);
+ if (!*p) {
+ *ok = FALSE;
+ return NULL;
+ }
+ if (*p == '(') {
+ char *list = _lisp_parse_list(&p, ok);
+ if (!*ok) return NULL;
+ *pp = p;
+ return list;
+ }
+ if (*p == '"') {
+ char *s = _lisp_parse_string(&p, ok);
+ if (!*ok) return NULL;
+ *pp = p;
+ return s;
+ }
+ char *atom = _lisp_read_atom(&p);
+ if (!atom) {
+ *ok = FALSE;
+ return NULL;
+ }
+ *pp = p;
+ return atom;
+}
+
+ORCA_API char *
+PROP_TranslateLispBinding(lpcString_t code)
+{
+ lpcString_t p = _lisp_skip(code);
+ bool_t ok = TRUE;
+ if (*p != '(') {
+ return NULL;
+ }
+ char *translated = _lisp_parse_expr(&p, &ok);
+ p = _lisp_skip(p);
+ if (!ok || !translated || *p) {
+ free(translated);
+ return NULL;
+ }
+ return translated;
+}
+
ORCA_API struct token*
Token_Create(lpcString_t code)
{
struct token* _compile(lpcString_t code, lpcString_t filename);
+ lpcString_t p = _lisp_skip(code);
+ if (*p == '(') {
+ char *lisp = PROP_TranslateLispBinding(code);
+ if (lisp) {
+ struct token *result = _compile(lisp, "binding");
+ free(lisp);
+ return result;
+ }
+ }
return _compile(code, "binding");
}
diff --git a/source/filesystem/fs_lisp.c b/source/filesystem/fs_lisp.c
new file mode 100644
index 00000000..5a373f9b
--- /dev/null
+++ b/source/filesystem/fs_lisp.c
@@ -0,0 +1,793 @@
+/*
+ * fs_lisp.c — Lisp-format UI asset loader.
+ *
+ * Syntax overview:
+ * (TypeName Key="val" Key=(binding-expr)
+ * (:bind Target="Prop" expr)
+ * (ChildType ...)
+ * "text content")
+ *
+ * Binding expressions are translated from Lisp to the ORCA expression string
+ * recognised by Token_Create / OBJ_AttachPropertyProgram:
+ * (bind "Node.ActualWidth") → {Node.ActualWidth}
+ * (Binding "Node.ActualWidth") → {Node.ActualWidth} (alias)
+ * (if a b c) → IF(a, b, c)
+ * (step n x) → STEP(n, x)
+ * (vector2 x y) → Vector2(x, y)
+ * Any other (fname ...) → FNAME(...) (uppercased)
+ */
+
+#include
+#include
+#include
+#include "fs_xml_inline.h"
+
+#include
+#include
+#include
+#include
+
+/* Forward declaration of the property parser from parsers module */
+extern int parse_property(const char *str,
+ struct PropertyType const *prop,
+ void *valueptr);
+
+/* =========================================================================
+ * String buffer
+ * ====================================================================== */
+
+struct sbuf { char *s; size_t n, cap; };
+
+static void
+sbuf_put(struct sbuf *b, const char *s)
+{
+ if (!s || !*s) return;
+ size_t len = strlen(s);
+ if (b->n + len + 1 > b->cap) {
+ size_t need = b->n + len + 1;
+ b->cap = b->cap ? b->cap * 2 : 256;
+ if (b->cap < need) b->cap = need;
+ b->s = realloc(b->s, b->cap);
+ }
+ if (!b->s) return;
+ memcpy(b->s + b->n, s, len);
+ b->n += len;
+ b->s[b->n] = '\0';
+}
+
+static void
+sbuf_putc(struct sbuf *b, char c)
+{
+ char tmp[2] = {c, '\0'};
+ sbuf_put(b, tmp);
+}
+
+/* =========================================================================
+ * Lexer
+ * ====================================================================== */
+
+enum lsp_tok {
+ TOK_EOF = 0,
+ TOK_LPAREN, /* ( */
+ TOK_RPAREN, /* ) */
+ TOK_STRING, /* "..." */
+ TOK_ATOM, /* anything else */
+};
+
+struct lsp_lex {
+ const char *p;
+ char *str; /* heap-allocated text for current STRING / ATOM token */
+ int tok;
+ int line;
+};
+
+static void
+lsp_skip(struct lsp_lex *l)
+{
+ for (;;) {
+ while (*l->p && isspace((unsigned char)*l->p)) {
+ if (*l->p == '\n') l->line++;
+ l->p++;
+ }
+ if (*l->p == ';') {
+ while (*l->p && *l->p != '\n') l->p++;
+ } else {
+ break;
+ }
+ }
+}
+
+static void
+lsp_next(struct lsp_lex *l)
+{
+ free(l->str);
+ l->str = NULL;
+ lsp_skip(l);
+
+ if (!*l->p) { l->tok = TOK_EOF; return; }
+ if (*l->p == '(') { l->tok = TOK_LPAREN; l->p++; return; }
+ if (*l->p == ')') { l->tok = TOK_RPAREN; l->p++; return; }
+
+ if (*l->p == '"') {
+ l->p++;
+ struct sbuf b = {0};
+ while (*l->p && *l->p != '"') {
+ if (*l->p == '\\' && l->p[1]) {
+ l->p++;
+ switch (*l->p) {
+ case 'n': sbuf_putc(&b, '\n'); break;
+ case 't': sbuf_putc(&b, '\t'); break;
+ case '"': sbuf_putc(&b, '"'); break;
+ case '\\': sbuf_putc(&b, '\\'); break;
+ default: sbuf_putc(&b, '\\'); sbuf_putc(&b, *l->p); break;
+ }
+ } else {
+ sbuf_putc(&b, *l->p);
+ }
+ l->p++;
+ }
+ if (*l->p == '"') l->p++;
+ l->str = b.s ? b.s : strdup("");
+ if (!l->str) { l->tok = TOK_EOF; return; }
+ l->tok = TOK_STRING;
+ return;
+ }
+
+ /* Atom: up to whitespace, parens, quotes, semicolons */
+ const char *start = l->p;
+ while (*l->p && !isspace((unsigned char)*l->p) &&
+ *l->p != '(' && *l->p != ')' && *l->p != '"' && *l->p != ';') {
+ l->p++;
+ }
+ size_t n = (size_t)(l->p - start);
+ l->str = malloc(n + 1);
+ if (!l->str) { l->tok = TOK_EOF; return; }
+ memcpy(l->str, start, n);
+ l->str[n] = '\0';
+ l->tok = TOK_ATOM;
+}
+
+/* =========================================================================
+ * Binding expression translation: Lisp form → ORCA expression string
+ *
+ * translate_expr(l, out):
+ * Pre: l->tok is the first token of an expression.
+ * Post: l->tok is the token AFTER the expression.
+ *
+ * translate_list_body(l, out):
+ * Pre: '(' was just consumed and lsp_next was called → l->tok is the
+ * function-name atom.
+ * Post: l->tok is the token AFTER the closing ')'.
+ * ====================================================================== */
+
+static void translate_expr(struct lsp_lex *l, struct sbuf *out);
+
+static void
+translate_list_body(struct lsp_lex *l, struct sbuf *out)
+{
+ if (l->tok != TOK_ATOM) {
+ Con_Error("Lisp binding: expected function name in list (line %d)", l->line);
+ return;
+ }
+
+ char fname[64];
+ snprintf(fname, sizeof(fname), "%s", l->str);
+ lsp_next(l);
+
+ /* (bind "PropPath") or (Binding "PropPath") → {PropPath} */
+ if (!strcasecmp(fname, "bind") || !strcasecmp(fname, "Binding")) {
+ if (l->tok == TOK_STRING) {
+ sbuf_putc(out, '{');
+ sbuf_put(out, l->str);
+ sbuf_putc(out, '}');
+ lsp_next(l);
+ } else {
+ Con_Error("Lisp (bind) expects a string path (line %d)", l->line);
+ }
+ if (l->tok == TOK_RPAREN) lsp_next(l);
+ return;
+ }
+
+ /* General function: UPPERCASE(arg, arg, ...) */
+ char orca_name[64];
+ size_t flen = strlen(fname);
+ size_t cap = sizeof(orca_name) - 1;
+ for (size_t i = 0; i < flen && i < cap; i++)
+ orca_name[i] = (char)toupper((unsigned char)fname[i]);
+ orca_name[flen < cap ? flen : cap] = '\0';
+
+ sbuf_put(out, orca_name);
+ sbuf_putc(out, '(');
+
+ bool_t first = TRUE;
+ while (l->tok != TOK_RPAREN && l->tok != TOK_EOF) {
+ if (!first) sbuf_put(out, ", ");
+ first = FALSE;
+ translate_expr(l, out);
+ }
+
+ sbuf_putc(out, ')');
+ if (l->tok == TOK_RPAREN) lsp_next(l);
+}
+
+static void
+translate_expr(struct lsp_lex *l, struct sbuf *out)
+{
+ if (l->tok == TOK_LPAREN) {
+ lsp_next(l); /* advance to function name */
+ translate_list_body(l, out);
+ return;
+ }
+ if (l->tok == TOK_STRING) {
+ sbuf_putc(out, '"');
+ for (const char *p = l->str; *p; p++) {
+ if (*p == '"') sbuf_put(out, "\\\"");
+ else sbuf_putc(out, *p);
+ }
+ sbuf_putc(out, '"');
+ lsp_next(l);
+ return;
+ }
+ if (l->tok == TOK_ATOM) {
+ sbuf_put(out, l->str);
+ lsp_next(l);
+ return;
+ }
+ Con_Error("Lisp: unexpected token %d in binding expression (line %d)", l->tok, l->line);
+}
+
+/* Translate the binding expression starting at l->tok, return heap-alloc string
+ * in ORCA expression format. Caller must free(). */
+static char *
+lsp_translate_binding(struct lsp_lex *l)
+{
+ struct sbuf out = {0};
+ translate_expr(l, &out);
+ return out.s ? out.s : strdup("");
+}
+
+/* =========================================================================
+ * AST: parsed node tree
+ * ====================================================================== */
+
+struct lisp_attr {
+ char *key; /* property name */
+ char *val; /* string value, or NULL for binding attrs */
+ char *binding; /* ORCA expression string, or NULL for string attrs */
+ struct lisp_attr *next;
+};
+
+struct lisp_node {
+ char *type; /* element class name, e.g. "Grid" */
+ struct lisp_attr *attrs; /* singly-linked list of attribute k/v pairs */
+ struct lisp_node *children; /* singly-linked list of child elements */
+ struct lisp_node *next; /* next sibling */
+ char *text; /* text content, if any */
+};
+
+static void
+lisp_attr_free(struct lisp_attr *a)
+{
+ while (a) {
+ struct lisp_attr *nx = a->next;
+ free(a->key);
+ free(a->val);
+ free(a->binding);
+ free(a);
+ a = nx;
+ }
+}
+
+static void
+lisp_node_free(struct lisp_node *n)
+{
+ while (n) {
+ struct lisp_node *nx = n->next;
+ free(n->type);
+ free(n->text);
+ lisp_attr_free(n->attrs);
+ lisp_node_free(n->children);
+ free(n);
+ n = nx;
+ }
+}
+
+/* =========================================================================
+ * Parser: Lisp text → lisp_node AST
+ * ====================================================================== */
+
+static struct lisp_node *parse_node(struct lsp_lex *l);
+
+/* parse_element_body: l->tok is positioned after reading the element type.
+ * Reads attributes, :bind directives, children, and optional text until ')'.
+ * Consumes the closing ')' and advances l to the next token. */
+static void
+parse_element_body(struct lsp_lex *l, struct lisp_node *node)
+{
+ struct lisp_attr **attr_tail = &node->attrs;
+ struct lisp_node **child_tail = &node->children;
+
+ while (l->tok != TOK_RPAREN && l->tok != TOK_EOF) {
+
+ /* --- Parenthesised form: child element or :bind directive --- */
+ if (l->tok == TOK_LPAREN) {
+ lsp_next(l); /* read head of the list */
+
+ if (l->tok == TOK_ATOM && l->str[0] == ':') {
+ /* Directive form: (:bind Target="Prop" expr) */
+ if (!strcmp(l->str, ":bind")) {
+ lsp_next(l); /* advance to the Target= atom or bare "prop.name" */
+ char *target = NULL;
+
+ if (l->tok == TOK_ATOM) {
+ size_t klen = strlen(l->str);
+ if (klen > 1 && l->str[klen - 1] == '=') {
+ /* Key="Value" form: consume the Key= atom, then read the value */
+ lsp_next(l); /* advance past "Key=" to the string/atom value */
+ if (l->tok == TOK_STRING || l->tok == TOK_ATOM) {
+ target = strdup(l->str); /* the property path */
+ lsp_next(l); /* advance past value to the expression */
+ }
+ } else {
+ /* Bare atom (no trailing '=') is the target path directly */
+ target = strdup(l->str);
+ lsp_next(l);
+ }
+ } else if (l->tok == TOK_STRING) {
+ /* Bare quoted string as target path */
+ target = strdup(l->str);
+ lsp_next(l);
+ }
+
+ char *expr = lsp_translate_binding(l);
+ if (target && expr) {
+ struct lisp_attr *a = calloc(1, sizeof(*a));
+ a->key = target;
+ a->binding = expr;
+ *attr_tail = a;
+ attr_tail = &a->next;
+ } else {
+ free(target);
+ free(expr);
+ }
+
+ /* Drain any remaining tokens to the closing ')' of :bind */
+ while (l->tok != TOK_RPAREN && l->tok != TOK_EOF) lsp_next(l);
+ if (l->tok == TOK_RPAREN) lsp_next(l);
+
+ } else {
+ /* Unknown directive — skip */
+ Con_Error("Lisp: unknown directive '%s' (line %d)", l->str, l->line);
+ int depth = 1;
+ while (depth > 0 && l->tok != TOK_EOF) {
+ lsp_next(l);
+ if (l->tok == TOK_LPAREN) depth++;
+ else if (l->tok == TOK_RPAREN) depth--;
+ }
+ if (l->tok == TOK_RPAREN) lsp_next(l);
+ }
+
+ } else {
+ /* Child element — l->tok is the type name atom */
+ struct lisp_node *child = parse_node(l);
+ if (child) { *child_tail = child; child_tail = &child->next; }
+ }
+ continue;
+ }
+
+ /* --- Bare string: text content --- */
+ if (l->tok == TOK_STRING) {
+ free(node->text);
+ node->text = strdup(l->str);
+ lsp_next(l);
+ continue;
+ }
+
+ /* --- Atom: key="val", key=(binding), or bare key (unquoted value) --- */
+ if (l->tok == TOK_ATOM) {
+ size_t alen = strlen(l->str);
+ const char *eq = strchr(l->str, '=');
+ if (eq && eq > l->str && eq[1] != '\0') {
+ /* Inline atom assignment, e.g. Width=1280 */
+ char *key = strndup(l->str, (size_t)(eq - l->str));
+ if (!key) {
+ Con_Error("Lisp: out of memory parsing attribute key (line %d)", l->line);
+ lsp_next(l);
+ continue;
+ }
+ struct lisp_attr *a = calloc(1, sizeof(*a));
+ if (!a) {
+ Con_Error("Lisp: out of memory creating attribute node (line %d)", l->line);
+ free(key);
+ lsp_next(l);
+ continue;
+ }
+ a->key = key;
+ a->val = strdup(eq + 1);
+ if (!a->val) {
+ Con_Error("Lisp: out of memory parsing attribute value (line %d)", l->line);
+ free(a->key);
+ free(a);
+ lsp_next(l);
+ continue;
+ }
+ *attr_tail = a;
+ attr_tail = &a->next;
+ lsp_next(l);
+
+ } else if (alen > 1 && l->str[alen - 1] == '=') {
+ char *key = strndup(l->str, alen - 1);
+ lsp_next(l);
+
+ if (l->tok == TOK_STRING) {
+ struct lisp_attr *a = calloc(1, sizeof(*a));
+ a->key = key;
+ a->val = strdup(l->str);
+ *attr_tail = a;
+ attr_tail = &a->next;
+ lsp_next(l);
+
+ } else if (l->tok == TOK_LPAREN) {
+ /* Inline binding expression: Key=(lisp-expr) */
+ char *expr = lsp_translate_binding(l);
+ struct lisp_attr *a = calloc(1, sizeof(*a));
+ a->key = key;
+ a->binding = expr;
+ *attr_tail = a;
+ attr_tail = &a->next;
+
+ } else if (l->tok == TOK_ATOM) {
+ /* Bare-word value, e.g. a number or enum without quotes */
+ struct lisp_attr *a = calloc(1, sizeof(*a));
+ a->key = key;
+ a->val = strdup(l->str);
+ *attr_tail = a;
+ attr_tail = &a->next;
+ lsp_next(l);
+
+ } else {
+ Con_Error("Lisp: expected value after '%s=' (line %d)", key, l->line);
+ free(key);
+ }
+ } else {
+ Con_Error("Lisp: unexpected atom '%s' in element body (line %d)", l->str, l->line);
+ lsp_next(l);
+ }
+ continue;
+ }
+
+ /* Unexpected token — skip */
+ Con_Error("Lisp: unexpected token %d in element body (line %d)", l->tok, l->line);
+ lsp_next(l);
+ }
+
+ /* Consume closing ')' */
+ if (l->tok == TOK_RPAREN) lsp_next(l);
+}
+
+/* parse_node: l->tok is the element type name (ATOM), after '(' was consumed.
+ * Returns the new node; l->tok points past the closing ')' on return. */
+static struct lisp_node *
+parse_node(struct lsp_lex *l)
+{
+ if (l->tok != TOK_ATOM) {
+ Con_Error("Lisp: expected element type name (line %d), got tok=%d", l->line, l->tok);
+ /* Drain the malformed list */
+ int depth = 1;
+ while (depth > 0 && l->tok != TOK_EOF) {
+ lsp_next(l);
+ if (l->tok == TOK_LPAREN) depth++;
+ else if (l->tok == TOK_RPAREN) depth--;
+ }
+ if (l->tok == TOK_RPAREN) lsp_next(l);
+ return NULL;
+ }
+
+ struct lisp_node *node = calloc(1, sizeof(*node));
+ node->type = strdup(l->str);
+ lsp_next(l);
+ parse_element_body(l, node);
+ return node;
+}
+
+/* parse_file: parse a complete Lisp UI source string and return the root node. */
+static struct lisp_node *
+parse_file(const char *src)
+{
+ struct lsp_lex l = { .p = src, .line = 1 };
+ lsp_next(&l);
+ if (l.tok != TOK_LPAREN) {
+ Con_Error("Lisp: expected '(' at start of file");
+ free(l.str);
+ return NULL;
+ }
+ lsp_next(&l); /* advance to element type name */
+ struct lisp_node *root = parse_node(&l);
+ free(l.str);
+ return root;
+}
+
+/* =========================================================================
+ * Object factory: lisp_node → ORCA Object
+ * ====================================================================== */
+
+static struct Object *apply_node(struct lisp_node *node);
+
+/* Look up a property descriptor by "implicit" (short name) or explicit (Ns.Name). */
+static struct PropertyType const *
+lisp_propdesc(struct Object *o, const char *name)
+{
+ struct PropertyType const *pd = OBJ_FindImplicitProperty(o, name);
+ return pd ? pd : OBJ_FindExplicitProperty(o, name);
+}
+
+/* Handle special property names that need engine-level calls. */
+static bool_t
+apply_special_attr(struct Object *o, const char *key, const char *val)
+{
+ if (!strcmp(key, "ClassName") || !strcmp(key, "PlaceholderTemplate")) return TRUE;
+ if (!strcmp(key, "Name") || !strcmp(key, "id")) {
+ OBJ_SetName(o, val);
+ return TRUE;
+ }
+ if (!strcmp(key, "class")) {
+ _SendMessage(o, StyleController, AddClasses, .ClassNames = val);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/* Append a trigger object to the object's Triggers array. */
+static void
+lisp_append_trigger(struct Property *triggers_prop, struct Object *trigger)
+{
+ struct PropertyType const *pd = PROP_GetDesc(triggers_prop);
+ void *old_items = NULL;
+ int old_count = 0;
+
+ if (!pd || !pd->IsArray || pd->DataType != kDataTypeObject) {
+ OBJ_ReleaseRef(trigger);
+ return;
+ }
+ if (PROP_GetValue(triggers_prop)) {
+ memcpy(&old_items, PROP_GetValue(triggers_prop), sizeof(old_items));
+ memcpy(&old_count,
+ (char *)PROP_GetValue(triggers_prop) + sizeof(old_items),
+ sizeof(old_count));
+ }
+ int count = old_count + 1;
+ struct Object **items = calloc((size_t)count, sizeof(struct Object *));
+ if (!items) { OBJ_ReleaseRef(trigger); return; }
+ if (old_items && old_count > 0)
+ memcpy(items, old_items, (size_t)old_count * sizeof(struct Object *));
+ items[old_count] = trigger;
+
+ struct { void *items; int count; } value = { items, count };
+ PROP_SetValue(triggers_prop, &value);
+}
+
+/* Create an inline EventTrigger from a {ShorthandObject ...} string value. */
+static void
+lisp_inline_event(struct Object *o,
+ struct PropertyType const *pd,
+ const char *value)
+{
+ struct Property *triggers = NULL;
+ struct PropertyType const *td = OBJ_FindImplicitProperty(o, "Triggers");
+ struct Object *action = _LoadObjectFromXmlFragment(value, 0);
+ struct Object *trigger = action ? OBJ_Create(ID_EventTrigger) : NULL;
+ char routed[MAX_PROPERTY_STRING] = {0};
+
+ if (!td || FAILED(OBJ_FindLongProperty(o, td->FullIdentifier, &triggers)) || !trigger) {
+ if (action) OBJ_ReleaseRef(action);
+ Con_Error("Lisp: property '%s' does not support inline trigger shorthand", pd->Name);
+ return;
+ }
+
+ snprintf(routed, sizeof(routed),
+ (pd->Category && *pd->Category) ? "%s.%s" : "%s",
+ (pd->Category && *pd->Category) ? pd->Category : pd->Name,
+ pd->Name);
+ const char *rv = routed;
+ if (FAILED(OBJ_SetPropertyValue(trigger, "RoutedEvent", &rv))) {
+ OBJ_ReleaseRef(trigger);
+ OBJ_ReleaseRef(action);
+ return;
+ }
+
+ OBJ_AddChild(trigger, action, FALSE);
+ lisp_append_trigger(triggers, trigger);
+}
+
+/* Apply a string-valued attribute: theme lookup, object refs, inline objects,
+ * event triggers, binding shorthands, and typed property values. */
+static void
+apply_str_attr(struct Object *o, const char *key, const char *val)
+{
+ struct PropertyType const *pd = lisp_propdesc(o, key);
+ if (!pd) {
+ Con_Error("Lisp: unknown property '%s' on '%s'", key, OBJ_GetClassName(o));
+ return;
+ }
+
+ /* {Binding path} shorthand → expression program */
+ size_t vlen = strlen(val);
+ if (vlen > 10 && !strncmp(val, "{Binding ", 9) && val[vlen - 1] == '}') {
+ char expr[MAX_PROPERTY_STRING];
+ snprintf(expr, sizeof(expr), "{%.*s}", (int)(vlen - 10), val + 9);
+ OBJ_AttachPropertyProgram(o, key, expr, kBindingModeExpression, TRUE);
+ return;
+ }
+
+ /* Theme lookup: $key → resolved string */
+ const char *resolved = val;
+ char theme_buf[MAX_PROPERTY_STRING] = {0};
+ if (val[0] == '$') {
+ const char *tv = FS_GetThemeValue(val);
+ if (!tv) { Con_Error("Lisp: missing theme key '%s'", val); return; }
+ strncpy(theme_buf, tv, sizeof(theme_buf) - 1);
+ resolved = theme_buf;
+ }
+
+ struct Property *p = NULL;
+ if (FAILED(OBJ_FindLongProperty(o, pd->FullIdentifier, &p))) {
+ Con_Error("Lisp: could not get property slot '%s'", key);
+ return;
+ }
+
+ /* @path → object reference */
+ if (resolved[0] == '@') {
+ struct Object *ref = OBJ_FindByPath(o, resolved + 1);
+ if (ref && pd->DataType == kDataTypeObject) PROP_SetValue(p, &ref);
+ else Con_Error("Lisp: could not resolve object reference '%s'", resolved + 1);
+ return;
+ }
+
+ /* { or < → inline object / event trigger */
+ if (resolved[0] == '{' || resolved[0] == '<') {
+ if (pd->DataType == kDataTypeEvent) {
+ lisp_inline_event(o, pd, resolved);
+ } else if (pd->DataType == kDataTypeStruct) {
+ _LoadStructFromXmlFragment(p, pd, resolved);
+ } else {
+ struct Object *child = _LoadObjectFromXmlFragment(resolved, 0);
+ if (child) {
+ if (GetBinding(child) || GetBindingExpression(child)) {
+ OBJ_SendMessageW(child, ID_Binding_Compile, 0, p);
+ OBJ_ReleaseRef(child);
+ } else if (pd->DataType == kDataTypeObject) {
+ PROP_SetValue(p, &child);
+ } else {
+ OBJ_ReleaseRef(child);
+ Con_Error("Lisp: unsupported inline object for property '%s'", key);
+ }
+ }
+ }
+ return;
+ }
+
+ /* Default: parse as typed property value */
+ char tmp[MAX_PROPERTY_STRING] = {0};
+ if (!parse_property(resolved, pd, tmp)) return;
+ PROP_SetValue(p, tmp);
+ if (pd->DataType == kDataTypeString) free(*(char **)tmp);
+}
+
+static const char * const PREFAB_TYPES[] = {
+ "LayerPrefabPlaceholder",
+ "ObjectPrefabPlaceholder",
+ "LibraryPlaceholder",
+ NULL
+};
+
+static bool_t
+is_prefab_type(const char *type)
+{
+ for (int i = 0; PREFAB_TYPES[i]; i++)
+ if (!strcmp(type, PREFAB_TYPES[i])) return TRUE;
+ return FALSE;
+}
+
+/* Convert a parsed AST node to an ORCA Object. */
+static struct Object *
+apply_node(struct lisp_node *node)
+{
+ if (!node) return NULL;
+
+ bool_t is_prefab = is_prefab_type(node->type);
+ struct Object *o = NULL;
+
+ if (is_prefab) {
+ const char *tmpl = NULL;
+ for (struct lisp_attr *a = node->attrs; a; a = a->next) {
+ if (!strcmp(a->key, "PlaceholderTemplate") && a->val) { tmpl = a->val; break; }
+ }
+ if (tmpl) {
+ o = FS_LoadObject(tmpl);
+ if (o) OBJ_SetFlags(o, OBJ_GetFlags(o) | OF_TEMPLATE);
+ }
+ if (!o) {
+ Con_Error("Lisp: prefab placeholder missing PlaceholderTemplate");
+ return NULL;
+ }
+ } else {
+ struct ClassDesc const *cls = OBJ_FindClass(node->type);
+ if (!cls) {
+ Con_Error("Lisp: unknown element type '%s'", node->type);
+ return NULL;
+ }
+ o = OBJ_Create(cls->ClassID);
+ }
+
+ if (!o) return NULL;
+
+ /* Apply attributes */
+ for (struct lisp_attr *a = node->attrs; a; a = a->next) {
+ if (!strcmp(a->key, "PlaceholderTemplate")) continue;
+
+ if (a->binding) {
+ OBJ_AttachPropertyProgram(o, a->key, a->binding, kBindingModeExpression, TRUE);
+ } else if (a->val) {
+ if (!apply_special_attr(o, a->key, a->val))
+ apply_str_attr(o, a->key, a->val);
+ }
+ }
+
+ /* Text content: replicate XML loader behaviour (short-circuit, no children) */
+ if (node->text) {
+ OBJ_SetTextContent(o, node->text);
+ if (!is_prefab) OBJ_SendMessageW(o, ID_Object_Start, 0, NULL);
+ return o;
+ }
+
+ /* Child elements */
+ for (struct lisp_node *child = node->children; child; child = child->next) {
+ struct Object *child_obj = apply_node(child);
+ if (child_obj) OBJ_AddChild(o, child_obj, FALSE);
+ }
+
+ if (!is_prefab) OBJ_SendMessageW(o, ID_Object_Start, 0, NULL);
+ return o;
+}
+
+/* =========================================================================
+ * Public API
+ * ====================================================================== */
+
+ORCA_API struct Object *
+FS_LoadObjectFromLispString(const char *lisp_string)
+{
+ if (!lisp_string || !*lisp_string) {
+ Con_Error("FS_LoadObjectFromLispString: empty input");
+ return NULL;
+ }
+ struct lisp_node *root = parse_file(lisp_string);
+ if (!root) {
+ Con_Error("FS_LoadObjectFromLispString: parse failed");
+ return NULL;
+ }
+ struct Object *o = apply_node(root);
+ lisp_node_free(root);
+ if (!o) Con_Error("FS_LoadObjectFromLispString: object creation failed");
+ return o;
+}
+
+ORCA_API struct Object *
+FS_LoadObjectFromLisp(const char *path)
+{
+ struct file *fp = FS_LoadFile(path);
+ if (!fp) {
+ Con_Error("Lisp: failed to load file '%s'", path);
+ return NULL;
+ }
+ char *src = malloc(fp->size + 1);
+ if (!src) { FS_FreeFile(fp); return NULL; }
+ memcpy(src, fp->data, fp->size);
+ src[fp->size] = '\0';
+ FS_FreeFile(fp);
+
+ struct Object *o = FS_LoadObjectFromLispString(src);
+ free(src);
+ if (o) OBJ_SetSourceFile(o, path);
+ return o;
+}
diff --git a/source/filesystem/fs_local.h b/source/filesystem/fs_local.h
index 7c0de564..a472e114 100644
--- a/source/filesystem/fs_local.h
+++ b/source/filesystem/fs_local.h
@@ -34,4 +34,10 @@ struct file *_ReadOnDisk(FILE *fp);
struct Object *
FS_LoadObjectFromXml(lpcString_t path);
+ORCA_API struct Object *
+FS_LoadObjectFromLisp(const char *path);
+
+ORCA_API struct Object *
+FS_LoadObjectFromLispString(const char *lisp_string);
+
#endif
diff --git a/source/filesystem/w_filesystem.c b/source/filesystem/w_filesystem.c
index b59d0e55..b413fddc 100644
--- a/source/filesystem/w_filesystem.c
+++ b/source/filesystem/w_filesystem.c
@@ -378,10 +378,21 @@ static struct Object* _css_file_loader(int argc, const char* argv[]) {
return (argc > 0) ? FS_LoadObjectFromCss(argv[0]) : NULL;
}
+static struct Object* _lisp_file_loader(int argc, const char* argv[]) {
+ return (argc > 0) ? FS_LoadObjectFromLisp(argv[0]) : NULL;
+}
+
+static int f_loadObjectFromLispString(lua_State* L) {
+ const char* lisp_string = luaL_checkstring(L, 1);
+ struct Object* result = FS_LoadObjectFromLispString(lisp_string);
+ luaX_pushObject(L, result);
+ return 1;
+}
+
void on_filesystem_module_registered(lua_State* L)
{
- OBJ_RegisterFileLoader(".xml", _xml_file_loader);
OBJ_RegisterFileLoader(".css", _css_file_loader);
+ OBJ_RegisterFileLoader(".lisp", _lisp_file_loader);
lua_register(L, "fs_findmodule", f_find_module);
luaL_dostring(L, "table.insert(package.searchers, fs_findmodule)");
@@ -404,6 +415,9 @@ void on_filesystem_module_registered(lua_State* L)
lua_pushcfunction(L, f_init);
lua_setfield(L, -2, "init");
+
+ lua_pushcfunction(L, f_loadObjectFromLispString);
+ lua_setfield(L, -2, "loadObjectFromLispString");
luaopen_orca_pipe(L);
lua_setfield(L, -2, "pipe");
diff --git a/tests/test_layout.lua b/tests/test_layout.lua
index bafffc62..0bb62dda 100644
--- a/tests/test_layout.lua
+++ b/tests/test_layout.lua
@@ -827,12 +827,12 @@ local function test_example_application_xml()
local icon_card_uses_image = icon_card:find("{../Card.Icon}", 1, true)
local icon_card_header = icon_card:find('{../Card.IconBackground}', 1, true)
local icon_property = package_lua:find('Name="Icon", DataType="Object", TypeString="Texture"', 1, true)
local icon_bg_blue = package_lua:find('Key = "icon-bg-blue", Value = "#10203A"', 1, true)
diff --git a/tests/test_lisp.lua b/tests/test_lisp.lua
new file mode 100644
index 00000000..cc66bc31
--- /dev/null
+++ b/tests/test_lisp.lua
@@ -0,0 +1,255 @@
+local test = require "orca.test"
+-- Tests for the Lisp UI asset loader.
+--
+-- Covers:
+-- * S-expression parser: nested elements, text content, bare string children.
+-- * Attribute binding (Key=(expr)) and :bind directives are attached as programs.
+-- * Theme-value attributes ($key) resolve without error.
+-- * Prefab placeholders (LayerPrefabPlaceholder) load templates correctly.
+-- * Full parity check: Application.lisp produces the same named objects as
+-- Application.xml when parsed.
+--
+-- All tests are headless (no display required).
+-- Run with: $(TARGET) -test=tests/test_lisp.lua
+
+local core = require "orca.core"
+local ui = require "orca.UIKit"
+local filesystem = require "orca.filesystem"
+
+local filesystem_initialized = false
+local function ensure_fs()
+ if filesystem_initialized then return end
+ filesystem.init("samples/Example")
+ filesystem_initialized = true
+end
+
+-- ---------------------------------------------------------------------------
+-- Helpers
+-- ---------------------------------------------------------------------------
+
+local function find_object(root, name)
+ if root:getName() == name then return root end
+ local child = root:getFirstChild()
+ while child do
+ local found = find_object(child, name)
+ if found then return found end
+ child = child:getNext()
+ end
+ return nil
+end
+
+-- ---------------------------------------------------------------------------
+-- test_basic_elements: minimal Lisp → object tree
+-- ---------------------------------------------------------------------------
+local function test_basic_elements()
+ ensure_fs()
+ local obj = filesystem.loadObjectFromLispString([[
+ (Screen Name="TestScreen" Width=100 Height=100
+ (StackView Name="Inner" Direction="Vertical"))
+ ]])
+ test.expect(obj ~= nil, "basic: root object should be created")
+ if not obj then return end
+ test.expect(obj:getName() == "TestScreen", "basic: root name should be TestScreen, got " .. tostring(obj:getName()))
+ local inner = find_object(obj, "Inner")
+ test.expect(inner ~= nil, "basic: child StackView should exist")
+ print("PASS: test_basic_elements")
+end
+
+-- ---------------------------------------------------------------------------
+-- test_text_content: bare string → OBJ_SetTextContent
+-- ---------------------------------------------------------------------------
+local function test_text_content()
+ ensure_fs()
+ local obj = filesystem.loadObjectFromLispString([[
+ (StackView Name="Row"
+ (TextBlock Name="Label" FontSize=14 ForegroundColor="$text-primary"
+ "Hello, world"))
+ ]])
+ test.expect(obj ~= nil, "text: root should exist")
+ if not obj then return end
+ local label = find_object(obj, "Label")
+ test.expect(label ~= nil, "text: TextBlock should exist")
+ if label then
+ local text = label:getTextContent()
+ test.expect(text == "Hello, world",
+ "text: content should be 'Hello, world', got '" .. tostring(text) .. "'")
+ end
+ print("PASS: test_text_content")
+end
+
+-- ---------------------------------------------------------------------------
+-- test_binding_attr: inline binding on a property attribute
+-- ---------------------------------------------------------------------------
+local function test_binding_attr()
+ ensure_fs()
+ local ok, err = pcall(function()
+ local obj = filesystem.loadObjectFromLispString([[
+ (Grid Name="TestGrid" Spacing=0
+ Columns=(if (step 640 (bind "Node.ActualWidth")) "auto auto" "auto"))
+ ]])
+ test.expect(obj ~= nil, "binding_attr: object should be created")
+ end)
+ test.expect(ok, "binding_attr: inline binding should not raise an error: " .. tostring(err))
+ print("PASS: test_binding_attr")
+end
+
+-- ---------------------------------------------------------------------------
+-- test_bind_directive: (:bind Target="Prop" expr) directive
+-- ---------------------------------------------------------------------------
+local function test_bind_directive()
+ ensure_fs()
+ local ok, err = pcall(function()
+ local obj = filesystem.loadObjectFromLispString([[
+ (StackView Name="Adaptive" Direction="Vertical"
+ (:bind Target="Node.HorizontalPadding"
+ (if (step 640 (bind "Node.ActualWidth")) (vector2 40 40) (vector2 8 8))))
+ ]])
+ test.expect(obj ~= nil, "bind_directive: object should be created")
+ end)
+ test.expect(ok, "bind_directive: :bind directive should not raise an error: " .. tostring(err))
+ print("PASS: test_bind_directive")
+end
+
+-- ---------------------------------------------------------------------------
+-- test_theme_values: $key attributes resolve to theme colors
+-- ---------------------------------------------------------------------------
+local function test_theme_values()
+ ensure_fs()
+ local obj = filesystem.loadObjectFromLispString([[
+ (StackView Name="Themed" BackgroundColor="$surface"
+ (TextBlock Name="Lbl" ForegroundColor="$text-primary" "Text"))
+ ]])
+ test.expect(obj ~= nil, "theme: root should be created")
+ print("PASS: test_theme_values")
+end
+
+-- ---------------------------------------------------------------------------
+-- test_comments: Lisp semicolon comments are ignored
+-- ---------------------------------------------------------------------------
+local function test_comments()
+ ensure_fs()
+ local obj = filesystem.loadObjectFromLispString([[
+ ; Root node
+ (StackView Name="WithComment" ; inline comment
+ (TextBlock Name="T" "text")) ; trailing comment
+ ]])
+ test.expect(obj ~= nil, "comments: object should be created despite comments")
+ if not obj then return end
+ test.expect(obj:getName() == "WithComment", "comments: name should be WithComment")
+ print("PASS: test_comments")
+end
+
+-- ---------------------------------------------------------------------------
+-- test_application_lisp_readable: Application.lisp file can be read
+-- ---------------------------------------------------------------------------
+local function test_application_lisp_readable()
+ ensure_fs()
+ local src = filesystem.readTextFile("samples/Example/Screens/Application.lisp")
+ test.expect(src ~= nil and src ~= "", "application_lisp: file should be readable")
+ local has_screen = src:find('(Screen ', 1, true) ~= nil
+ local has_tabview = src:find('(TabView ', 1, true) ~= nil
+ local has_brandmark = src:find('Name="BrandMark"', 1, true) ~= nil
+ local has_bind = src:find('(:bind ', 1, true) ~= nil
+ local has_prefab = src:find('LayerPrefabPlaceholder', 1, true) ~= nil
+ local has_numeric_atoms = src:find('Width=1280', 1, true) ~= nil
+ local no_numeric_strings = src:find('Width="1280"', 1, true) == nil
+ local no_xml_entities = src:find('&', 1, true) == nil
+ test.expect(has_screen, "application_lisp: should contain (Screen ...")
+ test.expect(has_tabview, "application_lisp: should contain (TabView ...")
+ test.expect(has_brandmark, "application_lisp: should contain BrandMark element")
+ test.expect(has_bind, "application_lisp: should contain :bind directives")
+ test.expect(has_prefab, "application_lisp: should contain LayerPrefabPlaceholder")
+ test.expect(has_numeric_atoms, "application_lisp: numeric properties should use bare atoms (e.g. Width=1280)")
+ test.expect(no_numeric_strings, "application_lisp: numeric properties should not use quoted numeric strings")
+ test.expect(no_xml_entities, "application_lisp: should NOT contain & XML entities (use & directly)")
+ print("PASS: test_application_lisp_readable")
+end
+
+-- ---------------------------------------------------------------------------
+-- test_application_lisp_parity: Application.lisp produces the same key
+-- named objects as Application.xml after parsing.
+-- ---------------------------------------------------------------------------
+local function test_application_lisp_parity()
+ ensure_fs()
+
+ -- Key object names that must be present in both XML and Lisp trees.
+ local key_names = {
+ "Application", "Root", "NavBar", "BrandMark", "BrandIcon", "Brand",
+ "Body", "Hero", "HeroContent", "HeroImage",
+ "TabbedTechSection", "OrcaTabs", "OrcaTabBar",
+ "XmlTab", "LuaTab", "MoonTab", "DeployTab",
+ "FeatureSection", "GallerySection", "GalleryGrid",
+ "WorkflowSection", "TestimonialsSection", "CtaSection", "CtaButtons",
+ "CtaButtonPrimary",
+ }
+
+ local src = filesystem.readTextFile("samples/Example/Screens/Application.lisp")
+ test.expect(src ~= nil and src ~= "", "parity: Application.lisp should be readable")
+ if not src or src == "" then return end
+
+ local ok, obj_or_err = pcall(filesystem.loadObjectFromLispString, src)
+ test.expect(ok, "parity: Application.lisp should parse without error: " .. tostring(obj_or_err))
+ if not ok then return end
+ local obj = obj_or_err
+
+ for _, name in ipairs(key_names) do
+ local found = find_object(obj, name)
+ test.expect(found ~= nil, "parity: object '" .. name .. "' should exist in Lisp tree")
+ end
+
+ -- CTA modal action: CtaButtonPrimary should exist and have a BackgroundColor set.
+ local cta = find_object(obj, "CtaButtonPrimary")
+ test.expect(cta ~= nil, "parity: CtaButtonPrimary should exist in Lisp tree")
+
+ print("PASS: test_application_lisp_parity")
+end
+
+-- ---------------------------------------------------------------------------
+-- test_binding_translation: specific expression shapes round-trip correctly
+-- ---------------------------------------------------------------------------
+local function test_binding_translation()
+ ensure_fs()
+
+ -- Each fixture is a minimal Lisp snippet whose binding must not crash.
+ local fixtures = {
+ -- STEP scalar binding
+ [[(StackView Name="T1"
+ (:bind Target="Node.Visible" (step 640 (bind "Node.ActualWidth"))))
+ ]],
+ -- IF / STEP / Vector2
+ [[(StackView Name="T2"
+ (:bind Target="Node.HorizontalPadding"
+ (if (step 640 (bind "Node.ActualWidth")) (vector2 40 40) (vector2 8 8))))
+ ]],
+ -- IF with string branches on column definition
+ [[(Grid Name="T3" Spacing=0
+ (:bind Target="Grid.Columns"
+ (if (step 640 (bind "Node.ActualWidth")) "auto auto" "auto")))
+ ]],
+ -- (Binding "path") alias
+ [[(TextBlock Name="T4" Text=(Binding "Card.Title"))
+ ]],
+ }
+
+ for i, src in ipairs(fixtures) do
+ local ok, err = pcall(function()
+ local obj = filesystem.loadObjectFromLispString(src)
+ test.expect(obj ~= nil, "binding_translation[" .. i .. "]: object should be created")
+ end)
+ test.expect(ok, "binding_translation[" .. i .. "]: should not raise: " .. tostring(err))
+ end
+ print("PASS: test_binding_translation")
+end
+
+-- ---------------------------------------------------------------------------
+-- Run all tests
+-- ---------------------------------------------------------------------------
+test_basic_elements()
+test_text_content()
+test_binding_attr()
+test_bind_directive()
+test_theme_values()
+test_comments()
+test_application_lisp_readable()
+test_binding_translation()
+test_application_lisp_parity()
diff --git a/tests/test_properties.c b/tests/test_properties.c
index 7d1eab7e..c12f09bb 100644
--- a/tests/test_properties.c
+++ b/tests/test_properties.c
@@ -686,6 +686,12 @@ static void test_runtime_token_create_arithmetic(void) {
}
}
+static void test_runtime_token_create_lisp_if(void) {
+ WITH(struct token, prog, Token_Create("(if 1 10 20)"), Token_Release) {
+ EXPECT(prog != NULL);
+ }
+}
+
static void test_runtime_run_int_constant(void) {
WITH(struct Object, obj, make_rt_object(), destroy_object) {
struct vm_register r = {0};
@@ -857,6 +863,14 @@ static void test_runtime_if_false_branch(void) {
}
}
+static void test_runtime_if_lisp_branch(void) {
+ WITH(struct Object, obj, make_rt_object(), destroy_object) {
+ struct vm_register r = {0};
+ RUN_PROG(obj, "(if 0 10 20)", &r);
+ EXPECT((int)r.value[0] == 20);
+ }
+}
+
static void test_runtime_if_string_branch(void) {
WITH(struct Object, obj, make_rt_object(), destroy_object) {
struct vm_register r = {0};
@@ -1354,6 +1368,7 @@ int main(void) {
DECL_TEST(test_runtime_token_create_float),
DECL_TEST(test_runtime_token_create_string),
DECL_TEST(test_runtime_token_create_arithmetic),
+ DECL_TEST(test_runtime_token_create_lisp_if),
DECL_TEST(test_runtime_run_int_constant),
DECL_TEST(test_runtime_run_float_constant),
DECL_TEST(test_runtime_run_string_constant),
@@ -1367,6 +1382,7 @@ int main(void) {
DECL_TEST(test_runtime_string_concat_program),
DECL_TEST(test_runtime_if_true_branch),
DECL_TEST(test_runtime_if_false_branch),
+ DECL_TEST(test_runtime_if_lisp_branch),
DECL_TEST(test_runtime_if_string_branch),
DECL_TEST(test_object_refcount_direct),
DECL_TEST(test_object_property_holds_reference),