1use crate::{LiveCommand, LiveDiagnostic, LiveError, LiveRuntime, Snapshot, Value};
10use konjure_sdk::presentation::{
11 PresentationError, UiActionId, UiDocument, UiDocumentId, UiEvent, UiEventAction, UiNode,
12 UiNodeId, UiNodeKind, UiPlacement,
13};
14
15struct Binding {
16 node: UiNodeId,
17 entity: u64,
18 component: String,
19}
20
21fn project(
22 snapshot: &Snapshot,
23 revision: u64,
24) -> Result<(UiDocument, Vec<Binding>), PresentationError> {
25 let mut document = UiDocument {
26 id: UiDocumentId("scene-controls".into()),
27 revision,
28 placement: UiPlacement::Screen,
29 modal: false,
30 nodes: Vec::new(),
31 };
32 let mut bindings = Vec::new();
33 for entity in &snapshot.entities {
34 for (component, value) in &entity.components {
35 let Value::Record(record) = value else {
36 continue;
37 };
38 let text = |name: &str| match record.fields.get(name) {
39 Some(Value::Text(value)) => value.to_string(),
40 _ => String::new(),
41 };
42 let number = |name: &str| match record.fields.get(name) {
43 Some(Value::Number(value)) => *value as f32,
44 _ => f32::NAN,
45 };
46 let action = UiActionId(
47 u32::try_from(bindings.len() + 1).map_err(|_| PresentationError::TooManyNodes)?,
48 );
49 let kind = match component.as_str() {
50 "Button" => UiNodeKind::Button {
51 label: text("label"),
52 action,
53 },
54 "Slider" => UiNodeKind::Slider {
55 label: text("label"),
56 value: number("value"),
57 minimum: number("min"),
58 maximum: number("max"),
59 step: None,
60 action,
61 },
62 "Text" => UiNodeKind::Text {
63 text: text("value"),
64 },
65 _ => continue,
66 };
67 let node = UiNodeId(format!("entity_{}.{}", entity.id, component));
68 let interactive = matches!(kind, UiNodeKind::Button { .. } | UiNodeKind::Slider { .. });
69 if interactive {
70 bindings.push(Binding {
71 node: node.clone(),
72 entity: entity.id,
73 component: component.clone(),
74 });
75 }
76 document.nodes.push(UiNode {
77 id: node,
78 enabled: !snapshot.finished
79 && (!interactive
80 || matches!(record.fields.get("action"), Some(Value::Function(_)))),
81 kind,
82 });
83 }
84 }
85 document.validate()?;
86 Ok((document, bindings))
87}
88
89pub fn document(snapshot: &Snapshot, revision: u64) -> Result<UiDocument, PresentationError> {
94 project(snapshot, revision).map(|(document, _)| document)
95}
96
97fn command(
98 snapshot: &Snapshot,
99 revision: u64,
100 event: &UiEvent,
101) -> Result<LiveCommand, PresentationError> {
102 let (document, bindings) = project(snapshot, revision)?;
103 document.validate_event(event)?;
104 let binding = bindings
105 .iter()
106 .find(|binding| binding.node == event.node)
107 .ok_or(PresentationError::UnknownNode)?;
108 let arguments = match event.action {
109 UiEventAction::Activate { .. } => Vec::new(),
110 UiEventAction::SetSlider { value, .. } => vec![Value::Number(f64::from(value))],
111 _ => return Err(PresentationError::WrongEventForNode),
112 };
113 Ok(LiveCommand::InvokeComponent {
114 entity: binding.entity,
115 component: binding.component.clone(),
116 field: "action".into(),
117 arguments,
118 })
119}
120
121fn live_error(error: PresentationError) -> LiveError {
122 LiveError::Runtime {
123 diagnostics: vec![LiveDiagnostic {
124 code: "ui_event".into(),
125 message: error.to_string(),
126 }],
127 }
128}
129
130impl LiveRuntime {
131 pub fn ui_document(&self) -> Result<UiDocument, LiveError> {
133 document(
134 &self.snapshot().ok_or(LiveError::NotLoaded)?,
135 self.source_revision(),
136 )
137 .map_err(live_error)
138 }
139
140 pub fn ui_command(&self, event: &UiEvent) -> Result<LiveCommand, LiveError> {
143 command(
144 &self.snapshot().ok_or(LiveError::NotLoaded)?,
145 self.source_revision(),
146 event,
147 )
148 .map_err(live_error)
149 }
150}
151
152impl crate::spatial::SpatialRuntime {
153 pub fn ui_document(&self) -> Result<UiDocument, PresentationError> {
155 document(&self.snapshot(), 0)
156 }
157
158 pub fn invoke_ui(&mut self, event: &UiEvent) -> Result<Snapshot, crate::Diagnostic> {
160 let command = command(&self.snapshot(), 0, event).map_err(|error| {
161 crate::Diagnostic::new("ui_event", error.to_string(), crate::Span::default())
162 })?;
163 let LiveCommand::InvokeComponent {
164 entity,
165 component,
166 field,
167 arguments,
168 } = command
169 else {
170 unreachable!()
171 };
172 self.invoke_component(entity, &component, &field, &arguments)?;
173 Ok(self.snapshot())
174 }
175}