Skip to main content

sim_lib_view/
profiles.rs

1//! Device-class projection profiles.
2//!
3//! VIEW_4 promotes the web from "the view surface" to one surface among many.
4//! The capability presets (`watch`/`glasses`/`phone`/`desktop`, plus
5//! `cli`/`tui`/`webui`) live in [`crate::surface`]; the projection that fits a
6//! Scene to a surface lives in [`crate::codec::reduce_for_caps`]. This module
7//! ties them together and proves, via fixtures, that one semantic Scene projects
8//! DIFFERENTLY and DETERMINISTICALLY for each device class -- a glance watch sees
9//! a one-line summary where a dense desktop sees the whole tree, from the same
10//! input.
11//!
12//! # Example
13//!
14//! ```
15//! use sim_lib_view::profiles::project_for_preset;
16//!
17//! let scene = sim_lib_scene::build::stack(
18//!     "column",
19//!     vec![
20//!         sim_lib_scene::build::text_node("a"),
21//!         sim_lib_scene::build::text_node("b"),
22//!         sim_lib_scene::build::text_node("c"),
23//!     ],
24//! );
25//! // The same Scene reduces hard for a glance watch, not at all for a desktop.
26//! let watch = project_for_preset(&scene, "watch").unwrap();
27//! let desktop = project_for_preset(&scene, "desktop").unwrap();
28//! assert!(sim_lib_scene::validate_scene(&watch).is_ok());
29//! assert_ne!(watch, desktop);
30//! ```
31
32use sim_kernel::Expr;
33
34use crate::codec::reduce_for_caps;
35use crate::surface;
36
37/// The device-class presets this profile set covers, beyond `cli`/`tui`/`webui`.
38pub const DEVICE_PRESETS: &[&str] = &["watch", "glasses", "phone", "desktop"];
39
40/// Projects `scene` toward the named surface preset's capabilities.
41///
42/// Looks up the preset in [`crate::surface::preset`] and reduces the Scene to its
43/// display density via [`crate::codec::reduce_for_caps`]. Returns `None` for an
44/// unknown preset. Deterministic for a given `(scene, preset_name)`.
45pub fn project_for_preset(scene: &Expr, preset_name: &str) -> Option<Expr> {
46    let caps = surface::preset(preset_name)?;
47    Some(reduce_for_caps(scene, &caps))
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    fn rich_scene() -> Expr {
55        sim_lib_scene::build::stack(
56            "column",
57            vec![
58                sim_lib_scene::build::text_node("one"),
59                sim_lib_scene::build::text_node("two"),
60                sim_lib_scene::build::text_node("three"),
61                sim_lib_scene::build::text_node("four"),
62                sim_lib_scene::build::text_node("five"),
63            ],
64        )
65    }
66
67    fn child_count(scene: &Expr) -> usize {
68        let Expr::Map(entries) = scene else {
69            return 0;
70        };
71        for (key, value) in entries {
72            match (key, value) {
73                (Expr::Symbol(symbol), Expr::List(items)) if &*symbol.name == "children" => {
74                    return items.len();
75                }
76                _ => {}
77            }
78        }
79        0
80    }
81
82    #[test]
83    fn each_device_class_projects_deterministically_and_validly() {
84        let scene = rich_scene();
85        for preset in DEVICE_PRESETS {
86            let first = project_for_preset(&scene, preset).unwrap();
87            let second = project_for_preset(&scene, preset).unwrap();
88            assert_eq!(first, second, "{preset} projection must be deterministic");
89            assert!(sim_lib_scene::validate_scene(&first).is_ok());
90        }
91    }
92
93    #[test]
94    fn glance_classes_reduce_harder_than_dense() {
95        let scene = rich_scene();
96        // watch + glasses are glance density -> keep 1; phone is compact -> keep 3;
97        // desktop is dense -> keep all 5.
98        assert_eq!(
99            child_count(&project_for_preset(&scene, "watch").unwrap()),
100            1
101        );
102        assert_eq!(
103            child_count(&project_for_preset(&scene, "glasses").unwrap()),
104            1
105        );
106        assert_eq!(
107            child_count(&project_for_preset(&scene, "phone").unwrap()),
108            3
109        );
110        assert_eq!(
111            child_count(&project_for_preset(&scene, "desktop").unwrap()),
112            5
113        );
114    }
115
116    #[test]
117    fn unknown_preset_projects_to_none() {
118        assert!(project_for_preset(&rich_scene(), "hologram").is_none());
119    }
120
121    #[test]
122    fn device_presets_carry_distinguishing_capabilities() {
123        let watch = surface::preset("watch").unwrap();
124        assert!(watch.input_flag("haptic-ack"));
125        assert_eq!(watch.display_density().unwrap().name.as_ref(), "glance");
126
127        let glasses = surface::preset("glasses").unwrap();
128        assert!(glasses.input_flag("voice"));
129        assert_eq!(glasses.display_density().unwrap().name.as_ref(), "glance");
130
131        let phone = surface::preset("phone").unwrap();
132        assert!(phone.input_flag("camera"));
133
134        let desktop = surface::preset("desktop").unwrap();
135        assert!(desktop.input_flag("file-drop"));
136        assert_eq!(desktop.display_density().unwrap().name.as_ref(), "dense");
137    }
138}