#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Hmi {
pub engine: HmiEngine,
pub backend: HmiBackend,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub debug: Option<String>,
pub source: String,
pub build: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub app: Option<String>,
#[serde(default)]
pub logical: HmiLogical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HmiLogical {
pub width: u32,
pub height: u32,
}
impl Default for HmiLogical {
fn default() -> Self {
Self {
width: 480,
height: 272,
}
}
}
const POCKET_MAX_LOGICAL: u32 = 1023;
impl Hmi {
pub fn validate(&self) -> Result<(), String> {
if self.logical.width == 0 || self.logical.height == 0 {
return Err("[hmi.logical] width and height must be non-zero".to_string());
}
match self.engine {
HmiEngine::Cog => Ok(()),
HmiEngine::Pocket => {
if self.logical.width > POCKET_MAX_LOGICAL
|| self.logical.height > POCKET_MAX_LOGICAL
{
return Err(format!(
"[hmi.logical] {}x{} exceeds the {POCKET_MAX_LOGICAL}px per-axis limit \
of the PocketJS packed-contact format; pointer and touch beyond \
{POCKET_MAX_LOGICAL} cannot be addressed",
self.logical.width, self.logical.height,
));
}
if self.app.as_deref().unwrap_or_default().trim().is_empty() {
return Err(
"[hmi] engine = \"pocket\" requires `app` (the PocketJS entry name, \
e.g. \"hero-main\")"
.to_string(),
);
}
if self.backend == HmiBackend::Drm {
return Err(
"[hmi] engine = \"pocket\" does not yet support backend = \"drm\"; \
use \"wayland\" or \"headless\""
.to_string(),
);
}
Ok(())
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HmiEngine {
Cog,
Pocket,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HmiBackend {
Wayland,
Drm,
Headless,
}
#[cfg(test)]
mod tests {
use super::*;
fn pocket(app: Option<&str>, backend: HmiBackend) -> Hmi {
Hmi {
engine: HmiEngine::Pocket,
backend,
debug: None,
source: "submodules/pocketjs".to_string(),
build: "dist".to_string(),
app: app.map(str::to_string),
logical: HmiLogical::default(),
}
}
#[test]
fn pocket_requires_app_entry() {
assert!(
pocket(Some("hero-main"), HmiBackend::Wayland)
.validate()
.is_ok()
);
assert!(pocket(None, HmiBackend::Wayland).validate().is_err());
assert!(pocket(Some(" "), HmiBackend::Wayland).validate().is_err());
}
#[test]
fn pocket_rejects_drm_backend_for_now() {
assert!(
pocket(Some("hero-main"), HmiBackend::Drm)
.validate()
.is_err()
);
assert!(
pocket(Some("hero-main"), HmiBackend::Headless)
.validate()
.is_ok()
);
}
#[test]
fn rejects_a_viewport_pocketjs_cannot_address() {
let mut hmi = pocket(Some("hero-main"), HmiBackend::Headless);
hmi.logical.width = 1920;
hmi.logical.height = 1080;
let error = hmi.validate().unwrap_err();
assert!(error.contains("1920x1080"), "{error}");
assert!(error.contains("1023"), "{error}");
hmi.logical.width = POCKET_MAX_LOGICAL;
hmi.logical.height = POCKET_MAX_LOGICAL;
assert!(hmi.validate().is_ok());
}
#[test]
fn cog_accepts_a_viewport_larger_than_the_pocket_limit() {
let hmi = Hmi {
engine: HmiEngine::Cog,
backend: HmiBackend::Wayland,
debug: None,
source: "website".to_string(),
build: "website/dist".to_string(),
app: None,
logical: HmiLogical {
width: 1920,
height: 1080,
},
};
assert!(hmi.validate().is_ok());
}
#[test]
fn cog_ignores_pocket_only_fields() {
let hmi = Hmi {
engine: HmiEngine::Cog,
backend: HmiBackend::Wayland,
debug: None,
source: "website".to_string(),
build: "website/dist".to_string(),
app: None,
logical: HmiLogical::default(),
};
assert!(hmi.validate().is_ok());
}
#[test]
fn rejects_zero_logical_dimensions() {
let mut hmi = pocket(Some("hero-main"), HmiBackend::Headless);
hmi.logical.width = 0;
assert!(hmi.validate().is_err());
}
}