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),