1use crate::tree::NodeId;
26use crate::layout::ComputedLayout;
27use crate::platform::TextStyle;
28use crate::navigation::NavigationAction;
29use serde::{Serialize, Deserialize};
30use std::collections::HashMap;
31use std::ffi::{CStr, CString};
32use std::os::raw::c_char;
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(tag = "call")]
42pub enum SyncCall {
43 #[serde(rename = "measure")]
45 Measure { node_id: NodeId },
46
47 #[serde(rename = "measure_text")]
49 MeasureText {
50 text: String,
51 #[serde(default)]
52 style: TextStyleInput,
53 #[serde(default = "default_max_width")]
54 max_width: f32,
55 },
56
57 #[serde(rename = "is_focused")]
59 IsFocused { node_id: NodeId },
60
61 #[serde(rename = "get_focused_node")]
63 GetFocusedNode,
64
65 #[serde(rename = "get_scroll_offset")]
67 GetScrollOffset { node_id: NodeId },
68
69 #[serde(rename = "supports_capability")]
71 SupportsCapability { capability: String },
72
73 #[serde(rename = "get_screen_info")]
75 GetScreenInfo,
76
77 #[serde(rename = "is_processing")]
79 IsProcessing,
80
81 #[serde(rename = "get_accessibility_role")]
83 GetAccessibilityRole { node_id: NodeId },
84
85 #[serde(rename = "get_frame_stats")]
87 GetFrameStats,
88
89 #[serde(rename = "node_exists")]
91 NodeExists { node_id: NodeId },
92
93 #[serde(rename = "get_child_count")]
95 GetChildCount { node_id: NodeId },
96
97 #[serde(rename = "can_go_back")]
99 CanGoBack,
100
101 #[serde(rename = "get_active_route")]
103 GetActiveRoute,
104}
105
106fn default_max_width() -> f32 { f32::INFINITY }
107
108#[derive(Debug, Clone, Default, Serialize, Deserialize)]
110pub struct TextStyleInput {
111 #[serde(default)]
112 pub font_size: Option<f32>,
113 #[serde(default)]
114 pub font_family: Option<String>,
115 #[serde(default)]
116 pub font_weight: Option<String>,
117}
118
119impl TextStyleInput {
120 pub fn to_platform_style(&self) -> TextStyle {
121 use crate::platform::FontWeight;
122 TextStyle {
123 font_size: self.font_size,
124 font_family: self.font_family.clone(),
125 font_weight: self.font_weight.as_deref().and_then(|w| match w {
126 "thin" => Some(FontWeight::Thin),
127 "light" => Some(FontWeight::Light),
128 "regular" => Some(FontWeight::Regular),
129 "medium" => Some(FontWeight::Medium),
130 "semibold" => Some(FontWeight::SemiBold),
131 "bold" => Some(FontWeight::Bold),
132 "heavy" => Some(FontWeight::Heavy),
133 _ => None,
134 }),
135 ..Default::default()
136 }
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142#[serde(tag = "result")]
143pub enum SyncResult {
144 #[serde(rename = "layout")]
146 Layout {
147 x: f32,
148 y: f32,
149 width: f32,
150 height: f32,
151 },
152
153 #[serde(rename = "text_metrics")]
155 TextMetrics {
156 width: f32,
157 height: f32,
158 baseline: f32,
159 line_count: u32,
160 },
161
162 #[serde(rename = "bool")]
164 Bool { value: bool },
165
166 #[serde(rename = "node_id")]
168 NodeIdResult { node_id: Option<u64> },
169
170 #[serde(rename = "scroll_offset")]
172 ScrollOffset { x: f32, y: f32 },
173
174 #[serde(rename = "screen_info")]
176 ScreenInfo {
177 width: f32,
178 height: f32,
179 scale: f32,
180 },
181
182 #[serde(rename = "role")]
184 Role { role: String },
185
186 #[serde(rename = "frame_stats")]
188 FrameStats {
189 frame_count: u64,
190 frames_dropped: u32,
191 last_frame_ms: f64,
192 last_layout_ms: f64,
193 last_mount_ms: f64,
194 },
195
196 #[serde(rename = "int")]
198 Int { value: u64 },
199
200 #[serde(rename = "active_route")]
202 ActiveRoute {
203 route_name: Option<String>,
204 params: HashMap<String, String>,
205 },
206
207 #[serde(rename = "not_found")]
209 NotFound,
210
211 #[serde(rename = "error")]
213 Error { message: String },
214}
215
216impl SyncResult {
217 pub fn from_layout(layout: &ComputedLayout) -> Self {
219 Self::Layout {
220 x: layout.x,
221 y: layout.y,
222 width: layout.width,
223 height: layout.height,
224 }
225 }
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
237#[serde(tag = "call")]
238pub enum AsyncCall {
239 #[serde(rename = "navigate")]
241 Navigate {
242 action: String,
243 #[serde(default)]
244 route: Option<String>,
245 #[serde(default)]
246 params: HashMap<String, String>,
247 #[serde(default)]
248 url: Option<String>,
249 #[serde(default)]
250 index: Option<usize>,
251 },
252
253 #[serde(rename = "set_focus")]
255 SetFocus { node_id: NodeId },
256
257 #[serde(rename = "move_focus")]
259 MoveFocus { direction: String },
260
261 #[serde(rename = "announce")]
263 Announce { message: String },
264}
265
266impl AsyncCall {
267 pub fn to_navigation_action(&self) -> Option<NavigationAction> {
269 match self {
270 AsyncCall::Navigate { action, route, params, url, index } => {
271 match action.as_str() {
272 "push" => route.as_ref().map(|r| NavigationAction::Push {
273 route: r.clone(),
274 params: params.clone(),
275 }),
276 "pop" => Some(NavigationAction::Pop),
277 "popToRoot" => Some(NavigationAction::PopToRoot),
278 "replace" => route.as_ref().map(|r| NavigationAction::Replace {
279 route: r.clone(),
280 params: params.clone(),
281 }),
282 "presentModal" => route.as_ref().map(|r| NavigationAction::PresentModal {
283 route: r.clone(),
284 params: params.clone(),
285 }),
286 "dismissModal" => Some(NavigationAction::DismissModal),
287 "switchTab" => index.map(|i| NavigationAction::SwitchTab { index: i }),
288 "deepLink" => url.as_ref().map(|u| NavigationAction::DeepLink {
289 url: u.clone(),
290 }),
291 "goBack" => Some(NavigationAction::GoBack),
292 _ => None,
293 }
294 }
295 _ => None,
296 }
297 }
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
307#[serde(tag = "event")]
308pub enum NativeCallback {
309 #[serde(rename = "pointer")]
311 Pointer {
312 node_id: NodeId,
313 event_type: String, x: f32,
315 y: f32,
316 },
317
318 #[serde(rename = "keyboard")]
320 Keyboard {
321 node_id: NodeId,
322 event_type: String, key: String,
324 code: String,
325 },
326
327 #[serde(rename = "focus")]
329 FocusChange {
330 previous: Option<NodeId>,
331 current: NodeId,
332 },
333
334 #[serde(rename = "navigation")]
336 Navigation {
337 action: String,
338 url: Option<String>,
339 },
340
341 #[serde(rename = "scroll")]
343 Scroll {
344 node_id: NodeId,
345 offset_x: f32,
346 offset_y: f32,
347 },
348
349 #[serde(rename = "text_change")]
351 TextChange {
352 node_id: NodeId,
353 text: String,
354 },
355}
356
357pub trait SyncSafe {}
366
367impl SyncSafe for SyncCall {}
368
369static mut ENGINE_PTR: *mut super::Engine = std::ptr::null_mut();
376
377pub unsafe fn set_engine_ptr(engine: *mut super::Engine) {
382 ENGINE_PTR = engine;
383}
384
385#[no_mangle]
392pub unsafe extern "C" fn appscale_sync_call(json_input: *const c_char) -> *mut c_char {
393 let c_str = unsafe { CStr::from_ptr(json_input) };
394 let input = match c_str.to_str() {
395 Ok(s) => s,
396 Err(_) => return to_c_string_or_null(r#"{"result":"error","message":"invalid UTF-8"}"#),
397 };
398
399 let call: SyncCall = match serde_json::from_str(input) {
400 Ok(c) => c,
401 Err(e) => {
402 let err = SyncResult::Error { message: format!("parse error: {}", e) };
403 return to_c_string_or_null(&serde_json::to_string(&err).unwrap_or_default());
404 }
405 };
406
407 let engine = unsafe { &*ENGINE_PTR };
408 let result = engine.handle_sync(&call);
409 to_c_string_or_null(&serde_json::to_string(&result).unwrap_or_default())
410}
411
412#[no_mangle]
418pub unsafe extern "C" fn appscale_async_call(json_input: *const c_char) {
419 let c_str = unsafe { CStr::from_ptr(json_input) };
420 let input = match c_str.to_str() {
421 Ok(s) => s,
422 Err(_) => return,
423 };
424
425 let call: AsyncCall = match serde_json::from_str(input) {
426 Ok(c) => c,
427 Err(_) => return,
428 };
429
430 let engine = unsafe { &mut *ENGINE_PTR };
431 engine.handle_async(call);
432}
433
434#[no_mangle]
439pub unsafe extern "C" fn appscale_free_string(ptr: *mut c_char) {
440 if !ptr.is_null() {
441 drop(unsafe { CString::from_raw(ptr) });
442 }
443}
444
445fn to_c_string_or_null(s: &str) -> *mut c_char {
447 CString::new(s).map(|c| c.into_raw()).unwrap_or(std::ptr::null_mut())
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453
454 #[test]
455 fn test_sync_call_roundtrip() {
456 let call = SyncCall::Measure { node_id: NodeId(42) };
457 let json = serde_json::to_string(&call).unwrap();
458 let decoded: SyncCall = serde_json::from_str(&json).unwrap();
459 match decoded {
460 SyncCall::Measure { node_id } => assert_eq!(node_id, NodeId(42)),
461 _ => panic!("wrong variant"),
462 }
463 }
464
465 #[test]
466 fn test_sync_result_roundtrip() {
467 let result = SyncResult::Layout { x: 10.0, y: 20.0, width: 100.0, height: 50.0 };
468 let json = serde_json::to_string(&result).unwrap();
469 let decoded: SyncResult = serde_json::from_str(&json).unwrap();
470 match decoded {
471 SyncResult::Layout { x, y, width, height } => {
472 assert_eq!(x, 10.0);
473 assert_eq!(y, 20.0);
474 assert_eq!(width, 100.0);
475 assert_eq!(height, 50.0);
476 },
477 _ => panic!("wrong variant"),
478 }
479 }
480
481 #[test]
482 fn test_native_callback_roundtrip() {
483 let cb = NativeCallback::Pointer {
484 node_id: NodeId(5),
485 event_type: "down".into(),
486 x: 150.0,
487 y: 300.0,
488 };
489 let json = serde_json::to_string(&cb).unwrap();
490 assert!(json.contains("\"event\":\"pointer\""));
491 let decoded: NativeCallback = serde_json::from_str(&json).unwrap();
492 match decoded {
493 NativeCallback::Pointer { node_id, .. } => assert_eq!(node_id, NodeId(5)),
494 _ => panic!("wrong variant"),
495 }
496 }
497
498 #[test]
499 fn test_async_call_roundtrip() {
500 let call = AsyncCall::Navigate {
502 action: "push".into(),
503 route: Some("Settings".into()),
504 params: HashMap::from([("id".into(), "42".into())]),
505 url: None,
506 index: None,
507 };
508 let json = serde_json::to_string(&call).unwrap();
509 assert!(json.contains("\"call\":\"navigate\""));
510 let decoded: AsyncCall = serde_json::from_str(&json).unwrap();
511 match &decoded {
512 AsyncCall::Navigate { action, route, params, .. } => {
513 assert_eq!(action, "push");
514 assert_eq!(route.as_deref(), Some("Settings"));
515 assert_eq!(params.get("id").map(|s| s.as_str()), Some("42"));
516 }
517 _ => panic!("wrong variant"),
518 }
519
520 let call = AsyncCall::SetFocus { node_id: NodeId(99) };
522 let json = serde_json::to_string(&call).unwrap();
523 let decoded: AsyncCall = serde_json::from_str(&json).unwrap();
524 match decoded {
525 AsyncCall::SetFocus { node_id } => assert_eq!(node_id, NodeId(99)),
526 _ => panic!("wrong variant"),
527 }
528
529 let call = AsyncCall::Announce { message: "Item added".into() };
531 let json = serde_json::to_string(&call).unwrap();
532 let decoded: AsyncCall = serde_json::from_str(&json).unwrap();
533 match decoded {
534 AsyncCall::Announce { message } => assert_eq!(message, "Item added"),
535 _ => panic!("wrong variant"),
536 }
537 }
538
539 #[test]
540 fn test_new_sync_calls_roundtrip() {
541 let call = SyncCall::MeasureText {
543 text: "Hello".into(),
544 style: TextStyleInput {
545 font_size: Some(16.0),
546 font_family: Some("Inter".into()),
547 font_weight: None,
548 },
549 max_width: 200.0,
550 };
551 let json = serde_json::to_string(&call).unwrap();
552 assert!(json.contains("\"call\":\"measure_text\""));
553 let decoded: SyncCall = serde_json::from_str(&json).unwrap();
554 match decoded {
555 SyncCall::MeasureText { text, style, max_width } => {
556 assert_eq!(text, "Hello");
557 assert_eq!(style.font_size, Some(16.0));
558 assert_eq!(max_width, 200.0);
559 }
560 _ => panic!("wrong variant"),
561 }
562
563 let json = r#"{"call":"can_go_back"}"#;
565 let decoded: SyncCall = serde_json::from_str(json).unwrap();
566 assert!(matches!(decoded, SyncCall::CanGoBack));
567
568 let json = r#"{"call":"get_active_route"}"#;
570 let decoded: SyncCall = serde_json::from_str(json).unwrap();
571 assert!(matches!(decoded, SyncCall::GetActiveRoute));
572
573 let json = r#"{"call":"get_focused_node"}"#;
575 let decoded: SyncCall = serde_json::from_str(json).unwrap();
576 assert!(matches!(decoded, SyncCall::GetFocusedNode));
577 }
578
579 #[test]
580 fn test_new_sync_results_roundtrip() {
581 let result = SyncResult::TextMetrics {
583 width: 80.0,
584 height: 20.0,
585 baseline: 16.0,
586 line_count: 1,
587 };
588 let json = serde_json::to_string(&result).unwrap();
589 let decoded: SyncResult = serde_json::from_str(&json).unwrap();
590 match decoded {
591 SyncResult::TextMetrics { width, height, baseline, line_count } => {
592 assert_eq!(width, 80.0);
593 assert_eq!(height, 20.0);
594 assert_eq!(baseline, 16.0);
595 assert_eq!(line_count, 1);
596 }
597 _ => panic!("wrong variant"),
598 }
599
600 let result = SyncResult::ActiveRoute {
602 route_name: Some("Home".into()),
603 params: HashMap::from([("tab".into(), "feed".into())]),
604 };
605 let json = serde_json::to_string(&result).unwrap();
606 let decoded: SyncResult = serde_json::from_str(&json).unwrap();
607 match decoded {
608 SyncResult::ActiveRoute { route_name, params } => {
609 assert_eq!(route_name.as_deref(), Some("Home"));
610 assert_eq!(params.get("tab").map(|s| s.as_str()), Some("feed"));
611 }
612 _ => panic!("wrong variant"),
613 }
614
615 let result = SyncResult::NodeIdResult { node_id: Some(42) };
617 let json = serde_json::to_string(&result).unwrap();
618 let decoded: SyncResult = serde_json::from_str(&json).unwrap();
619 match decoded {
620 SyncResult::NodeIdResult { node_id } => assert_eq!(node_id, Some(42)),
621 _ => panic!("wrong variant"),
622 }
623 }
624
625 #[test]
626 fn test_navigate_to_action_conversion() {
627 let call = AsyncCall::Navigate {
628 action: "push".into(),
629 route: Some("Profile".into()),
630 params: HashMap::new(),
631 url: None,
632 index: None,
633 };
634 let action = call.to_navigation_action().unwrap();
635 match action {
636 crate::navigation::NavigationAction::Push { route, .. } => {
637 assert_eq!(route, "Profile");
638 }
639 _ => panic!("wrong action"),
640 }
641
642 let call = AsyncCall::Navigate {
643 action: "deepLink".into(),
644 route: None,
645 params: HashMap::new(),
646 url: Some("myapp://profile/42".into()),
647 index: None,
648 };
649 let action = call.to_navigation_action().unwrap();
650 match action {
651 crate::navigation::NavigationAction::DeepLink { url } => {
652 assert_eq!(url, "myapp://profile/42");
653 }
654 _ => panic!("wrong action"),
655 }
656
657 let call = AsyncCall::SetFocus { node_id: NodeId(1) };
658 assert!(call.to_navigation_action().is_none());
659 }
660}