Skip to main content

appscale_core/
components.rs

1//! Component Library — Built-in UI Primitives
2//!
3//! Defines the core set of React-like components that AppScale supports
4//! out of the box. Each component descriptor maps a component name to:
5//! - a `ViewType` (what native view to create)
6//! - default props
7//! - default layout style
8//! - supported children mode (leaf vs container)
9//!
10//! The host-config (TypeScript side) uses component names like "View", "Text",
11//! "Image", etc. The engine resolves them here to determine how to create and
12//! configure the native view.
13
14use crate::platform::{ViewType, PropValue, PropsDiff};
15use crate::layout::LayoutStyle;
16use std::collections::HashMap;
17
18// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
19// Component Descriptor
20// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
21
22/// How a component handles children.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ChildrenMode {
25    /// Accepts child components (e.g., View, ScrollView).
26    Container,
27    /// Leaf node — no children allowed (e.g., Image, ActivityIndicator).
28    Leaf,
29    /// Accepts only text children (e.g., Text, TextInput).
30    TextOnly,
31}
32
33/// Describes a built-in component.
34#[derive(Debug, Clone)]
35pub struct ComponentDescriptor {
36    pub name: &'static str,
37    pub view_type: ViewType,
38    pub children_mode: ChildrenMode,
39    pub default_props: PropsDiff,
40    pub default_style: LayoutStyle,
41}
42
43// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
44// Component Registry
45// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
46
47/// Registry of all built-in components.
48pub struct ComponentRegistry {
49    components: HashMap<&'static str, ComponentDescriptor>,
50}
51
52impl ComponentRegistry {
53    /// Create a registry populated with all built-in components.
54    pub fn new() -> Self {
55        let mut registry = Self {
56            components: HashMap::new(),
57        };
58        registry.register_builtins();
59        registry
60    }
61
62    /// Look up a component by name.
63    pub fn get(&self, name: &str) -> Option<&ComponentDescriptor> {
64        self.components.get(name)
65    }
66
67    /// Resolve a component name to a ViewType.
68    pub fn resolve_view_type(&self, name: &str) -> Option<ViewType> {
69        self.components.get(name).map(|d| d.view_type.clone())
70    }
71
72    /// List all registered component names.
73    pub fn component_names(&self) -> Vec<&'static str> {
74        let mut names: Vec<_> = self.components.keys().copied().collect();
75        names.sort();
76        names
77    }
78
79    /// Number of registered components.
80    pub fn count(&self) -> usize {
81        self.components.len()
82    }
83
84    fn register(&mut self, descriptor: ComponentDescriptor) {
85        self.components.insert(descriptor.name, descriptor);
86    }
87
88    fn register_builtins(&mut self) {
89        // ──── Core Primitives ────
90        self.register(view_component());
91        self.register(text_component());
92        self.register(image_component());
93        self.register(scroll_view_component());
94        self.register(text_input_component());
95
96        // ──── Lists ────
97        self.register(flat_list_component());
98        self.register(section_list_component());
99
100        // ──── Navigation ────
101        self.register(stack_navigator_component());
102        self.register(tab_navigator_component());
103        self.register(drawer_navigator_component());
104
105        // ──── Form Controls ────
106        self.register(switch_component());
107        self.register(slider_component());
108        self.register(picker_component());
109        self.register(date_picker_component());
110
111        // ──── Feedback ────
112        self.register(button_component());
113        self.register(pressable_component());
114        self.register(touchable_opacity_component());
115        self.register(activity_indicator_component());
116
117        // ──── Layout ────
118        self.register(safe_area_view_component());
119        self.register(keyboard_avoiding_view_component());
120        self.register(modal_component());
121
122        // ──── Media ────
123        self.register(video_component());
124        self.register(camera_component());
125        self.register(web_view_component());
126    }
127}
128
129impl Default for ComponentRegistry {
130    fn default() -> Self { Self::new() }
131}
132
133// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
134// Core Primitives
135// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
136
137fn view_component() -> ComponentDescriptor {
138    ComponentDescriptor {
139        name: "View",
140        view_type: ViewType::Container,
141        children_mode: ChildrenMode::Container,
142        default_props: PropsDiff::new(),
143        default_style: LayoutStyle::default(),
144    }
145}
146
147fn text_component() -> ComponentDescriptor {
148    let mut props = PropsDiff::new();
149    props.set("numberOfLines", PropValue::I32(0)); // 0 = unlimited
150    props.set("selectable", PropValue::Bool(false));
151
152    ComponentDescriptor {
153        name: "Text",
154        view_type: ViewType::Text,
155        children_mode: ChildrenMode::TextOnly,
156        default_props: props,
157        default_style: LayoutStyle::default(),
158    }
159}
160
161fn image_component() -> ComponentDescriptor {
162    let mut props = PropsDiff::new();
163    props.set("resizeMode", PropValue::String("cover".into()));
164
165    ComponentDescriptor {
166        name: "Image",
167        view_type: ViewType::Image,
168        children_mode: ChildrenMode::Leaf,
169        default_props: props,
170        default_style: LayoutStyle::default(),
171    }
172}
173
174fn scroll_view_component() -> ComponentDescriptor {
175    let mut props = PropsDiff::new();
176    props.set("horizontal", PropValue::Bool(false));
177    props.set("showsScrollIndicator", PropValue::Bool(true));
178    props.set("bounces", PropValue::Bool(true));
179    props.set("pagingEnabled", PropValue::Bool(false));
180
181    ComponentDescriptor {
182        name: "ScrollView",
183        view_type: ViewType::ScrollView,
184        children_mode: ChildrenMode::Container,
185        default_props: props,
186        default_style: LayoutStyle::default(),
187    }
188}
189
190fn text_input_component() -> ComponentDescriptor {
191    let mut props = PropsDiff::new();
192    props.set("editable", PropValue::Bool(true));
193    props.set("multiline", PropValue::Bool(false));
194    props.set("secureTextEntry", PropValue::Bool(false));
195    props.set("autoCapitalize", PropValue::String("sentences".into()));
196    props.set("autoCorrect", PropValue::Bool(true));
197    props.set("placeholder", PropValue::String(String::new()));
198
199    ComponentDescriptor {
200        name: "TextInput",
201        view_type: ViewType::TextInput,
202        children_mode: ChildrenMode::Leaf,
203        default_props: props,
204        default_style: LayoutStyle::default(),
205    }
206}
207
208// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
209// Lists
210// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
211
212fn flat_list_component() -> ComponentDescriptor {
213    let mut props = PropsDiff::new();
214    props.set("horizontal", PropValue::Bool(false));
215    props.set("initialNumToRender", PropValue::I32(10));
216    props.set("windowSize", PropValue::I32(21)); // items above/below viewport
217    props.set("removeClippedSubviews", PropValue::Bool(true));
218
219    ComponentDescriptor {
220        name: "FlatList",
221        view_type: ViewType::ScrollView, // Backed by ScrollView with recycling
222        children_mode: ChildrenMode::Container,
223        default_props: props,
224        default_style: LayoutStyle::default(),
225    }
226}
227
228fn section_list_component() -> ComponentDescriptor {
229    let mut props = PropsDiff::new();
230    props.set("stickySectionHeaders", PropValue::Bool(true));
231    props.set("initialNumToRender", PropValue::I32(10));
232
233    ComponentDescriptor {
234        name: "SectionList",
235        view_type: ViewType::ScrollView,
236        children_mode: ChildrenMode::Container,
237        default_props: props,
238        default_style: LayoutStyle::default(),
239    }
240}
241
242// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
243// Navigation Components
244// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
245
246fn stack_navigator_component() -> ComponentDescriptor {
247    let mut props = PropsDiff::new();
248    props.set("headerShown", PropValue::Bool(true));
249    props.set("gestureEnabled", PropValue::Bool(true));
250    props.set("animationEnabled", PropValue::Bool(true));
251
252    ComponentDescriptor {
253        name: "StackNavigator",
254        view_type: ViewType::Container,
255        children_mode: ChildrenMode::Container,
256        default_props: props,
257        default_style: LayoutStyle::default(),
258    }
259}
260
261fn tab_navigator_component() -> ComponentDescriptor {
262    let mut props = PropsDiff::new();
263    props.set("tabBarPosition", PropValue::String("bottom".into()));
264    props.set("lazy", PropValue::Bool(true));
265
266    ComponentDescriptor {
267        name: "TabNavigator",
268        view_type: ViewType::Container,
269        children_mode: ChildrenMode::Container,
270        default_props: props,
271        default_style: LayoutStyle::default(),
272    }
273}
274
275fn drawer_navigator_component() -> ComponentDescriptor {
276    let mut props = PropsDiff::new();
277    props.set("drawerPosition", PropValue::String("left".into()));
278    props.set("drawerType", PropValue::String("front".into()));
279    props.set("swipeEnabled", PropValue::Bool(true));
280
281    ComponentDescriptor {
282        name: "DrawerNavigator",
283        view_type: ViewType::Container,
284        children_mode: ChildrenMode::Container,
285        default_props: props,
286        default_style: LayoutStyle::default(),
287    }
288}
289
290// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
291// Form Controls
292// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
293
294fn switch_component() -> ComponentDescriptor {
295    let mut props = PropsDiff::new();
296    props.set("value", PropValue::Bool(false));
297    props.set("disabled", PropValue::Bool(false));
298
299    ComponentDescriptor {
300        name: "Switch",
301        view_type: ViewType::Switch,
302        children_mode: ChildrenMode::Leaf,
303        default_props: props,
304        default_style: LayoutStyle::default(),
305    }
306}
307
308fn slider_component() -> ComponentDescriptor {
309    let mut props = PropsDiff::new();
310    props.set("minimumValue", PropValue::F32(0.0));
311    props.set("maximumValue", PropValue::F32(1.0));
312    props.set("step", PropValue::F32(0.0)); // continuous
313    props.set("value", PropValue::F32(0.0));
314    props.set("disabled", PropValue::Bool(false));
315
316    ComponentDescriptor {
317        name: "Slider",
318        view_type: ViewType::Slider,
319        children_mode: ChildrenMode::Leaf,
320        default_props: props,
321        default_style: LayoutStyle::default(),
322    }
323}
324
325fn picker_component() -> ComponentDescriptor {
326    let mut props = PropsDiff::new();
327    props.set("enabled", PropValue::Bool(true));
328
329    ComponentDescriptor {
330        name: "Picker",
331        view_type: ViewType::Custom("Picker".into()),
332        children_mode: ChildrenMode::Leaf,
333        default_props: props,
334        default_style: LayoutStyle::default(),
335    }
336}
337
338fn date_picker_component() -> ComponentDescriptor {
339    let mut props = PropsDiff::new();
340    props.set("mode", PropValue::String("date".into())); // date, time, datetime
341
342    ComponentDescriptor {
343        name: "DatePicker",
344        view_type: ViewType::DatePicker,
345        children_mode: ChildrenMode::Leaf,
346        default_props: props,
347        default_style: LayoutStyle::default(),
348    }
349}
350
351// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
352// Feedback Components
353// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
354
355fn button_component() -> ComponentDescriptor {
356    let mut props = PropsDiff::new();
357    props.set("disabled", PropValue::Bool(false));
358
359    ComponentDescriptor {
360        name: "Button",
361        view_type: ViewType::Button,
362        children_mode: ChildrenMode::TextOnly,
363        default_props: props,
364        default_style: LayoutStyle::default(),
365    }
366}
367
368fn pressable_component() -> ComponentDescriptor {
369    let mut props = PropsDiff::new();
370    props.set("disabled", PropValue::Bool(false));
371
372    ComponentDescriptor {
373        name: "Pressable",
374        view_type: ViewType::Container,
375        children_mode: ChildrenMode::Container,
376        default_props: props,
377        default_style: LayoutStyle::default(),
378    }
379}
380
381fn touchable_opacity_component() -> ComponentDescriptor {
382    let mut props = PropsDiff::new();
383    props.set("activeOpacity", PropValue::F32(0.2));
384    props.set("disabled", PropValue::Bool(false));
385
386    ComponentDescriptor {
387        name: "TouchableOpacity",
388        view_type: ViewType::Container,
389        children_mode: ChildrenMode::Container,
390        default_props: props,
391        default_style: LayoutStyle::default(),
392    }
393}
394
395fn activity_indicator_component() -> ComponentDescriptor {
396    let mut props = PropsDiff::new();
397    props.set("animating", PropValue::Bool(true));
398    props.set("size", PropValue::String("small".into()));
399
400    ComponentDescriptor {
401        name: "ActivityIndicator",
402        view_type: ViewType::ActivityIndicator,
403        children_mode: ChildrenMode::Leaf,
404        default_props: props,
405        default_style: LayoutStyle::default(),
406    }
407}
408
409// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
410// Layout Components
411// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
412
413fn safe_area_view_component() -> ComponentDescriptor {
414    ComponentDescriptor {
415        name: "SafeAreaView",
416        view_type: ViewType::Container,
417        children_mode: ChildrenMode::Container,
418        default_props: PropsDiff::new(),
419        default_style: LayoutStyle::default(),
420    }
421}
422
423fn keyboard_avoiding_view_component() -> ComponentDescriptor {
424    let mut props = PropsDiff::new();
425    props.set("behavior", PropValue::String("padding".into())); // padding, height, position
426    props.set("enabled", PropValue::Bool(true));
427
428    ComponentDescriptor {
429        name: "KeyboardAvoidingView",
430        view_type: ViewType::Container,
431        children_mode: ChildrenMode::Container,
432        default_props: props,
433        default_style: LayoutStyle::default(),
434    }
435}
436
437fn modal_component() -> ComponentDescriptor {
438    let mut props = PropsDiff::new();
439    props.set("visible", PropValue::Bool(false));
440    props.set("animationType", PropValue::String("none".into())); // none, slide, fade
441    props.set("transparent", PropValue::Bool(false));
442
443    ComponentDescriptor {
444        name: "Modal",
445        view_type: ViewType::Modal,
446        children_mode: ChildrenMode::Container,
447        default_props: props,
448        default_style: LayoutStyle::default(),
449    }
450}
451
452// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
453// Media Components
454// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
455
456fn video_component() -> ComponentDescriptor {
457    let mut props = PropsDiff::new();
458    props.set("paused", PropValue::Bool(true));
459    props.set("muted", PropValue::Bool(false));
460    props.set("repeat", PropValue::Bool(false));
461    props.set("resizeMode", PropValue::String("contain".into()));
462    props.set("controls", PropValue::Bool(true));
463
464    ComponentDescriptor {
465        name: "Video",
466        view_type: ViewType::Custom("Video".into()),
467        children_mode: ChildrenMode::Leaf,
468        default_props: props,
469        default_style: LayoutStyle::default(),
470    }
471}
472
473fn camera_component() -> ComponentDescriptor {
474    let mut props = PropsDiff::new();
475    props.set("facing", PropValue::String("back".into())); // front, back
476    props.set("flashMode", PropValue::String("off".into())); // on, off, auto
477
478    ComponentDescriptor {
479        name: "Camera",
480        view_type: ViewType::Custom("Camera".into()),
481        children_mode: ChildrenMode::Leaf,
482        default_props: props,
483        default_style: LayoutStyle::default(),
484    }
485}
486
487fn web_view_component() -> ComponentDescriptor {
488    let mut props = PropsDiff::new();
489    props.set("javaScriptEnabled", PropValue::Bool(true));
490    props.set("domStorageEnabled", PropValue::Bool(true));
491    props.set("scalesPageToFit", PropValue::Bool(true));
492
493    ComponentDescriptor {
494        name: "WebView",
495        view_type: ViewType::Custom("WebView".into()),
496        children_mode: ChildrenMode::Leaf,
497        default_props: props,
498        default_style: LayoutStyle::default(),
499    }
500}
501
502// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
503// Tests
504// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509
510    #[test]
511    fn test_registry_has_all_components() {
512        let registry = ComponentRegistry::new();
513        // 5 core + 2 lists + 3 nav + 4 form + 4 feedback + 3 layout + 3 media = 24
514        assert_eq!(registry.count(), 24);
515    }
516
517    #[test]
518    fn test_core_primitives_registered() {
519        let registry = ComponentRegistry::new();
520        assert!(registry.get("View").is_some());
521        assert!(registry.get("Text").is_some());
522        assert!(registry.get("Image").is_some());
523        assert!(registry.get("ScrollView").is_some());
524        assert!(registry.get("TextInput").is_some());
525    }
526
527    #[test]
528    fn test_view_type_mapping() {
529        let registry = ComponentRegistry::new();
530        assert_eq!(registry.resolve_view_type("View"), Some(ViewType::Container));
531        assert_eq!(registry.resolve_view_type("Text"), Some(ViewType::Text));
532        assert_eq!(registry.resolve_view_type("Image"), Some(ViewType::Image));
533        assert_eq!(registry.resolve_view_type("Button"), Some(ViewType::Button));
534        assert_eq!(registry.resolve_view_type("Switch"), Some(ViewType::Switch));
535        assert_eq!(registry.resolve_view_type("Modal"), Some(ViewType::Modal));
536    }
537
538    #[test]
539    fn test_children_mode() {
540        let registry = ComponentRegistry::new();
541        assert_eq!(registry.get("View").unwrap().children_mode, ChildrenMode::Container);
542        assert_eq!(registry.get("Text").unwrap().children_mode, ChildrenMode::TextOnly);
543        assert_eq!(registry.get("Image").unwrap().children_mode, ChildrenMode::Leaf);
544        assert_eq!(registry.get("ActivityIndicator").unwrap().children_mode, ChildrenMode::Leaf);
545        assert_eq!(registry.get("ScrollView").unwrap().children_mode, ChildrenMode::Container);
546    }
547
548    #[test]
549    fn test_default_props() {
550        let registry = ComponentRegistry::new();
551
552        let text_input = registry.get("TextInput").unwrap();
553        assert!(!text_input.default_props.is_empty());
554
555        let view = registry.get("View").unwrap();
556        assert!(view.default_props.is_empty());
557    }
558
559    #[test]
560    fn test_component_names_sorted() {
561        let registry = ComponentRegistry::new();
562        let names = registry.component_names();
563        assert!(names.len() == 24);
564        // Verify sorted
565        let mut sorted = names.clone();
566        sorted.sort();
567        assert_eq!(names, sorted);
568    }
569
570    #[test]
571    fn test_unknown_component() {
572        let registry = ComponentRegistry::new();
573        assert!(registry.get("NonExistent").is_none());
574        assert_eq!(registry.resolve_view_type("NonExistent"), None);
575    }
576
577    #[test]
578    fn test_list_components() {
579        let registry = ComponentRegistry::new();
580        let flat_list = registry.get("FlatList").unwrap();
581        assert_eq!(flat_list.view_type, ViewType::ScrollView);
582        assert_eq!(flat_list.children_mode, ChildrenMode::Container);
583
584        let section_list = registry.get("SectionList").unwrap();
585        assert_eq!(section_list.view_type, ViewType::ScrollView);
586    }
587
588    #[test]
589    fn test_navigation_components() {
590        let registry = ComponentRegistry::new();
591        assert!(registry.get("StackNavigator").is_some());
592        assert!(registry.get("TabNavigator").is_some());
593        assert!(registry.get("DrawerNavigator").is_some());
594    }
595
596    #[test]
597    fn test_media_components() {
598        let registry = ComponentRegistry::new();
599        let video = registry.get("Video").unwrap();
600        assert_eq!(video.view_type, ViewType::Custom("Video".into()));
601        assert_eq!(video.children_mode, ChildrenMode::Leaf);
602
603        let camera = registry.get("Camera").unwrap();
604        assert_eq!(camera.view_type, ViewType::Custom("Camera".into()));
605
606        let webview = registry.get("WebView").unwrap();
607        assert_eq!(webview.view_type, ViewType::Custom("WebView".into()));
608    }
609}