use crate::color::Color;
use crate::window::WindowEnv;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Env {
pub refresh_hz: Option<f32>,
pub focused: bool,
pub system: SystemEnv,
pub window: WindowEnv,
pub audio: AudioEnv,
}
impl Default for Env {
fn default() -> Self {
Self {
refresh_hz: None,
focused: true,
system: SystemEnv::default(),
window: WindowEnv::default(),
audio: AudioEnv::default(),
}
}
}
impl Env {
pub const DEFAULT_HZ: f32 = 120.0;
pub fn frame_budget_ms(&self) -> f32 {
let hz = self
.refresh_hz
.filter(|hz| *hz > 0.0)
.unwrap_or(Self::DEFAULT_HZ);
1000.0 / hz
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct SystemEnv {
pub appearance: Appearance,
pub accent: Option<Color>,
pub motion: MotionPref,
pub locale: Option<Locale>,
pub assistive: Assistive,
}
impl SystemEnv {
pub fn over(self, base: SystemEnv) -> SystemEnv {
SystemEnv {
appearance: match self.appearance {
Appearance::Unknown => base.appearance,
pinned => pinned,
},
accent: self.accent.or(base.accent),
motion: match self.motion {
MotionPref::Unknown => base.motion,
pinned => pinned,
},
locale: self.locale.or(base.locale),
assistive: match self.assistive {
Assistive::Unknown => base.assistive,
pinned => pinned,
},
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Appearance {
#[default]
Unknown,
Light,
Dark,
}
impl Appearance {
pub const ALL: &'static [Appearance] =
&[Appearance::Unknown, Appearance::Light, Appearance::Dark];
pub fn name(self) -> &'static str {
match self {
Appearance::Unknown => "unknown",
Appearance::Light => "light",
Appearance::Dark => "dark",
}
}
pub fn parse(name: &str) -> Option<Self> {
Self::ALL.iter().copied().find(|a| a.name() == name)
}
pub fn code(self) -> u32 {
Self::ALL.iter().position(|a| *a == self).unwrap() as u32
}
pub fn from_code(code: u32) -> Option<Self> {
Self::ALL.get(code as usize).copied()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum MotionPref {
#[default]
Unknown,
Full,
Reduced,
}
impl MotionPref {
pub const ALL: &'static [MotionPref] =
&[MotionPref::Unknown, MotionPref::Full, MotionPref::Reduced];
pub fn name(self) -> &'static str {
match self {
MotionPref::Unknown => "unknown",
MotionPref::Full => "full",
MotionPref::Reduced => "reduced",
}
}
pub fn parse(name: &str) -> Option<Self> {
Self::ALL.iter().copied().find(|m| m.name() == name)
}
pub fn code(self) -> u32 {
Self::ALL.iter().position(|m| *m == self).unwrap() as u32
}
pub fn from_code(code: u32) -> Option<Self> {
Self::ALL.get(code as usize).copied()
}
pub fn is_reduced(self) -> bool {
self == MotionPref::Reduced
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Assistive {
#[default]
Unknown,
None,
Listening,
}
impl Assistive {
pub const ALL: &'static [Assistive] =
&[Assistive::Unknown, Assistive::None, Assistive::Listening];
pub fn name(self) -> &'static str {
match self {
Assistive::Unknown => "unknown",
Assistive::None => "none",
Assistive::Listening => "listening",
}
}
pub fn parse(name: &str) -> Option<Self> {
Self::ALL.iter().copied().find(|a| a.name() == name)
}
pub fn code(self) -> u32 {
Self::ALL.iter().position(|a| *a == self).unwrap() as u32
}
pub fn from_code(code: u32) -> Option<Self> {
Self::ALL.get(code as usize).copied()
}
pub fn is_listening(self) -> bool {
self == Assistive::Listening
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct AudioEnv {
pub device: AudioDevice,
pub live: u32,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum AudioDevice {
#[default]
Closed,
Opening,
Open,
Failed,
}
impl AudioDevice {
pub const ALL: &'static [AudioDevice] = &[
AudioDevice::Closed,
AudioDevice::Opening,
AudioDevice::Open,
AudioDevice::Failed,
];
pub fn name(self) -> &'static str {
match self {
AudioDevice::Closed => "closed",
AudioDevice::Opening => "opening",
AudioDevice::Open => "open",
AudioDevice::Failed => "failed",
}
}
pub fn parse(name: &str) -> Option<Self> {
Self::ALL.iter().copied().find(|d| d.name() == name)
}
pub fn code(self) -> u32 {
Self::ALL.iter().position(|d| *d == self).unwrap() as u32
}
pub fn from_code(code: u32) -> Option<Self> {
Self::ALL.get(code as usize).copied()
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Locale {
bytes: [u8; Locale::CAP],
len: u8,
}
impl Locale {
pub const CAP: usize = 31;
pub fn new(tag: &str) -> Option<Self> {
if tag.is_empty() || tag.len() > Self::CAP || !tag.is_ascii() {
return None;
}
let mut bytes = [0u8; Self::CAP];
bytes[..tag.len()].copy_from_slice(tag.as_bytes());
Some(Self {
bytes,
len: tag.len() as u8,
})
}
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.bytes[..self.len as usize]).unwrap_or("")
}
}
impl std::fmt::Display for Locale {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::fmt::Debug for Locale {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Locale({:?})", self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn budget_follows_reported_rate() {
let env = Env {
refresh_hz: Some(60.0),
..Default::default()
};
assert!((env.frame_budget_ms() - 16.666).abs() < 1e-2);
}
#[test]
fn budget_falls_back_when_unreported_or_bogus() {
assert!((Env::default().frame_budget_ms() - 8.333).abs() < 1e-2);
let bogus = Env {
refresh_hz: Some(0.0),
..Default::default()
};
assert!((bogus.frame_budget_ms() - 8.333).abs() < 1e-2);
}
#[test]
fn the_system_facts_default_to_unknown() {
let s = Env::default().system;
assert_eq!(s.appearance, Appearance::Unknown);
assert_eq!(s.accent, None);
assert_eq!(s.motion, MotionPref::Unknown);
assert_eq!(s.locale, None);
assert_eq!(s.assistive, Assistive::Unknown);
assert!(!s.motion.is_reduced(), "unknown is not a request to reduce");
assert!(!s.assistive.is_listening(), "unknown is not a listener");
assert_eq!(Env::default().audio, AudioEnv::default());
assert_eq!(AudioEnv::default().device, AudioDevice::Closed);
}
#[test]
fn schema_names_are_all_in_order() {
let appearances: Vec<&str> = Appearance::ALL.iter().map(|a| a.name()).collect();
assert_eq!(appearances, crate::schema::APPEARANCES);
let motions: Vec<&str> = MotionPref::ALL.iter().map(|m| m.name()).collect();
assert_eq!(motions, crate::schema::MOTIONS);
let devices: Vec<&str> = AudioDevice::ALL.iter().map(|d| d.name()).collect();
assert_eq!(devices, crate::schema::AUDIO_DEVICES);
let assistive: Vec<&str> = Assistive::ALL.iter().map(|a| a.name()).collect();
assert_eq!(assistive, crate::schema::ASSISTIVE);
assert_eq!(Appearance::default().code(), 0);
assert_eq!(MotionPref::default().code(), 0);
assert_eq!(AudioDevice::default().code(), 0);
assert_eq!(Assistive::default().code(), 0);
}
#[test]
fn codes_and_names_round_trip_and_reject_the_rest() {
for a in Appearance::ALL {
assert_eq!(Appearance::from_code(a.code()), Some(*a));
assert_eq!(Appearance::parse(a.name()), Some(*a));
}
for m in MotionPref::ALL {
assert_eq!(MotionPref::from_code(m.code()), Some(*m));
assert_eq!(MotionPref::parse(m.name()), Some(*m));
}
for d in AudioDevice::ALL {
assert_eq!(AudioDevice::from_code(d.code()), Some(*d));
assert_eq!(AudioDevice::parse(d.name()), Some(*d));
}
for a in Assistive::ALL {
assert_eq!(Assistive::from_code(a.code()), Some(*a));
assert_eq!(Assistive::parse(a.name()), Some(*a));
}
assert_eq!(Appearance::from_code(3), None);
assert_eq!(MotionPref::from_code(3), None);
assert_eq!(AudioDevice::from_code(4), None);
assert_eq!(Assistive::from_code(3), None);
assert_eq!(Appearance::parse("Dark"), None, "spelling is exact");
}
#[test]
fn a_locale_is_the_tag_it_was_given() {
let tag = Locale::new("en-US").unwrap();
assert_eq!(tag.as_str(), "en-US");
assert_eq!(tag.to_string(), "en-US");
assert_eq!(Locale::new("zh-Hant-HK").unwrap().as_str(), "zh-Hant-HK");
assert_eq!(Locale::new("en"), Locale::new("en"));
assert_ne!(Locale::new("en"), Locale::new("en-US"));
assert_eq!(Locale::new(""), None);
assert_eq!(Locale::new(&"x".repeat(Locale::CAP + 1)), None);
assert_eq!(Locale::new("ру-RU"), None, "a tag is ASCII");
assert!(Locale::new(&"x".repeat(Locale::CAP)).is_some(), "CAP fits");
}
#[test]
fn env_is_copy() {
fn takes_copy<T: Copy>(_: T) {}
takes_copy(Env::default());
}
}