use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Platform {
Mac,
Windows,
Neutral,
}
impl Platform {
pub const fn detect() -> Platform {
if cfg!(target_os = "macos") {
Platform::Mac
} else if cfg!(target_os = "windows") {
Platform::Windows
} else {
Platform::Neutral
}
}
pub fn label(self) -> &'static str {
match self {
Platform::Mac => "mac",
Platform::Windows => "windows",
Platform::Neutral => "neutral",
}
}
pub fn from_label(s: &str) -> Option<Platform> {
match s.trim().to_ascii_lowercase().as_str() {
"mac" | "macos" | "osx" | "darwin" | "apple" => Some(Platform::Mac),
"windows" | "win" | "win11" | "win32" => Some(Platform::Windows),
"neutral" | "linux" | "nix" | "default" | "other" => Some(Platform::Neutral),
_ => None,
}
}
pub fn cycle(self) -> Platform {
match self {
Platform::Mac => Platform::Windows,
Platform::Windows => Platform::Neutral,
Platform::Neutral => Platform::Mac,
}
}
pub fn native_feel(self) -> NativeFeel {
match self {
Platform::Mac => NativeFeel::macos(),
Platform::Windows => NativeFeel::windows(),
Platform::Neutral => NativeFeel::neutral(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum WindowControls {
TrafficLights,
Caption,
None,
}
impl WindowControls {
pub fn on_left(self) -> bool {
matches!(self, WindowControls::TrafficLights)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct FocusRing {
pub width: f32,
pub expansion: f32,
pub glow: bool,
pub accent_tinted: bool,
}
impl FocusRing {
pub fn macos() -> Self {
Self { width: 3.0, expansion: 2.5, glow: true, accent_tinted: true }
}
pub fn windows() -> Self {
Self { width: 2.0, expansion: 1.0, glow: false, accent_tinted: true }
}
pub fn neutral() -> Self {
Self { width: 2.0, expansion: 1.5, glow: false, accent_tinted: true }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct NativeFeel {
pub platform: Platform,
pub reveal_highlight: bool,
pub rubber_band: bool,
pub window_controls: WindowControls,
pub focus_ring: FocusRing,
pub accent_tint: f32,
pub elevation: f32,
}
impl Default for NativeFeel {
fn default() -> Self {
Platform::detect().native_feel()
}
}
impl NativeFeel {
pub fn macos() -> Self {
Self {
platform: Platform::Mac,
reveal_highlight: false,
rubber_band: true,
window_controls: WindowControls::TrafficLights,
focus_ring: FocusRing::macos(),
accent_tint: 0.85,
elevation: 0.30,
}
}
pub fn windows() -> Self {
Self {
platform: Platform::Windows,
reveal_highlight: true,
rubber_band: false,
window_controls: WindowControls::Caption,
focus_ring: FocusRing::windows(),
accent_tint: 0.65,
elevation: 0.70,
}
}
pub fn neutral() -> Self {
Self {
platform: Platform::Neutral,
reveal_highlight: false,
rubber_band: false,
window_controls: WindowControls::None,
focus_ring: FocusRing::neutral(),
accent_tint: 0.55,
elevation: 0.45,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_is_a_valid_platform() {
let p = Platform::detect();
assert!(matches!(p, Platform::Mac | Platform::Windows | Platform::Neutral));
#[cfg(target_os = "macos")]
assert_eq!(p, Platform::Mac);
#[cfg(target_os = "windows")]
assert_eq!(p, Platform::Windows);
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
assert_eq!(p, Platform::Neutral);
}
#[test]
fn label_round_trips_and_is_fuzzy() {
for p in [Platform::Mac, Platform::Windows, Platform::Neutral] {
assert_eq!(Platform::from_label(p.label()), Some(p));
}
assert_eq!(Platform::from_label("macOS"), Some(Platform::Mac));
assert_eq!(Platform::from_label("WIN"), Some(Platform::Windows));
assert_eq!(Platform::from_label("linux"), Some(Platform::Neutral));
assert_eq!(Platform::from_label("plan9"), None);
}
#[test]
fn cycle_visits_all_three() {
let mut seen = std::collections::HashSet::new();
let mut p = Platform::Mac;
for _ in 0..3 {
seen.insert(p.label());
p = p.cycle();
}
assert_eq!(seen.len(), 3, "cycle must visit Mac, Windows, Neutral");
assert_eq!(p, Platform::Mac, "cycle returns to start after 3 steps");
}
#[test]
fn native_feel_cues_differ_per_platform() {
let m = NativeFeel::macos();
let w = NativeFeel::windows();
assert!(w.reveal_highlight && !m.reveal_highlight, "reveal is a Windows cue");
assert!(m.rubber_band && !w.rubber_band, "rubber-band is a macOS cue");
assert_ne!(m.window_controls, w.window_controls, "controls live on opposite sides");
assert!(m.window_controls.on_left() && !w.window_controls.on_left());
assert!(m.focus_ring.glow && !w.focus_ring.glow, "mac ring glows; win is a crisp rect");
assert!(w.elevation > m.elevation, "Windows elevation shadow is heavier");
assert!(m.accent_tint > w.accent_tint, "macOS leans on accent more");
}
#[test]
fn native_feel_serde_round_trips() {
let f = NativeFeel::macos();
let json = serde_json::to_string(&f).unwrap();
let back: NativeFeel = serde_json::from_str(&json).unwrap();
assert_eq!(f, back);
}
}