灵符lingfu

ling-input

↓ 0 downloads

Sensorium — Ling's unified input system: gamepads, joysticks, haptics, motion, XR controllers, and themeable on-screen touch controls (灵 organic ↔ 机 mechanical)

lingfu install ling-input
LicenseApache-2.0 OR MIT
AuthorsLing Lin, Sanny Lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (23)

src/theme.rs
//! Themer for on-screen controls — colors, shapes, opacity, presets.
//!
//! This crate doesn't draw; it produces *data* a renderer (ling-graphics /
//! ling-ui) consumes. A [`ControlTheme`] is fully serializable so players can
//! save/share skins. The default [`ControlTheme::ling`] uses the project
//! palette: navy / teal / rose-red / grey / vine-green.

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// 8-bit straight-alpha RGBA color.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Color {
    pub r: u8,
    pub g: u8,
    pub b: u8,
    pub a: u8,
}

impl Color {
    #[must_use]
    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
        Self { r, g, b, a }
    }

    #[must_use]
    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
        Self::rgba(r, g, b, 255)
    }

    /// From a packed `0xRRGGBB` literal.
    #[must_use]
    pub const fn hex(rgb: u32) -> Self {
        Self::rgb((rgb >> 16) as u8, (rgb >> 8) as u8, rgb as u8)
    }

    #[must_use]
    pub const fn with_alpha(self, a: u8) -> Self {
        Self { a, ..self }
    }

    /// Multiply alpha by `k` (`0..=1`), for fade-out of idle controls.
    #[must_use]
    pub fn faded(self, k: f32) -> Self {
        Self { a: (f32::from(self.a) * k.clamp(0.0, 1.0)) as u8, ..self }
    }

    /// Linear-ish blend toward `other` by `t` (`0..=1`).
    #[must_use]
    pub fn lerp(self, other: Self, t: f32) -> Self {
        let t = t.clamp(0.0, 1.0);
        let mix = |a: u8, b: u8| (f32::from(a) + (f32::from(b) - f32::from(a)) * t) as u8;
        Self {
            r: mix(self.r, other.r),
            g: mix(self.g, other.g),
            b: mix(self.b, other.b),
            a: mix(self.a, other.a),
        }
    }

    /// As normalized `[r, g, b, a]` floats for a renderer.
    #[must_use]
    pub fn to_f32(self) -> [f32; 4] {
        [
            f32::from(self.r) / 255.0,
            f32::from(self.g) / 255.0,
            f32::from(self.b) / 255.0,
            f32::from(self.a) / 255.0,
        ]
    }
}

/// The Ling default palette (matches diagnostics + editor theme).
pub mod palette {
    use super::Color;
    /// Navy blue — secondary / notes (`#3B6EA5`).
    pub const NAVY: Color = Color::hex(0x3B_6E_A5);
    /// Dark navy — backgrounds (`#14233D`).
    pub const NAVY_BG: Color = Color::hex(0x14_23_3D);
    /// Teal — structure / frames (`#2A9D8F`).
    pub const TEAL: Color = Color::hex(0x2A_9D_8F);
    /// Rose red — accents / active (`#E84A6F`).
    pub const ROSE: Color = Color::hex(0xE8_4A_6F);
    /// Grey — neutral / disabled (`#8D99AE`).
    pub const GREY: Color = Color::hex(0x8D_99_AE);
    /// Vine green — success / confirm (`#7FB069`).
    pub const VINE: Color = Color::hex(0x7F_B0_69);
}

/// Outline / body shape of a control.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ControlShape {
    #[default]
    Circle,
    RoundedRect,
    Pill,
    Hexagon,
}

/// A complete skin for on-screen controls. Renderer-agnostic data.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ControlTheme {
    pub name: String,
    /// Control body fill.
    pub base: Color,
    /// Stick knob / pressed-button fill.
    pub knob: Color,
    pub outline: Color,
    pub label: Color,
    /// Highlight tint while pressed/active.
    pub pressed: Color,
    /// Accent for rings, ticks, focus.
    pub accent: Color,
    /// Global opacity multiplier while touched (`0..=1`).
    pub opacity: f32,
    /// Opacity while idle/untouched — controls fade away on mobile.
    pub idle_opacity: f32,
    /// Seconds to fade between idle and active.
    pub fade_secs: f32,
    /// Corner radius (for `RoundedRect`).
    pub corner: f32,
    pub outline_width: f32,
    pub shape: ControlShape,
}

impl Default for ControlTheme {
    fn default() -> Self {
        Self::ling()
    }
}

impl ControlTheme {
    /// The default Ling skin (navy/teal/rose palette, semi-transparent).
    #[must_use]
    pub fn ling() -> Self {
        Self {
            name: "Ling".into(),
            base: palette::NAVY_BG.with_alpha(150),
            knob: palette::TEAL.with_alpha(220),
            outline: palette::NAVY.with_alpha(220),
            label: palette::GREY,
            pressed: palette::ROSE.with_alpha(230),
            accent: palette::VINE,
            opacity: 0.95,
            idle_opacity: 0.45,
            fade_secs: 0.20,
            corner: 18.0,
            outline_width: 2.5,
            shape: ControlShape::Circle,
        }
    }

    /// Minimal, mostly-transparent — only the knob is visible.
    #[must_use]
    pub fn minimal() -> Self {
        Self {
            name: "Minimal".into(),
            base: palette::GREY.with_alpha(40),
            knob: Color::rgb(255, 255, 255).with_alpha(180),
            outline: palette::GREY.with_alpha(80),
            opacity: 0.8,
            idle_opacity: 0.15,
            ..Self::ling()
        }
    }

    /// High-contrast accessibility skin (opaque, thick outlines).
    #[must_use]
    pub fn high_contrast() -> Self {
        Self {
            name: "High Contrast".into(),
            base: Color::rgb(0, 0, 0),
            knob: Color::rgb(255, 255, 0),
            outline: Color::rgb(255, 255, 255),
            label: Color::rgb(255, 255, 255),
            pressed: Color::rgb(255, 80, 80),
            accent: Color::rgb(0, 255, 255),
            opacity: 1.0,
            idle_opacity: 1.0,
            fade_secs: 0.0,
            outline_width: 5.0,
            ..Self::ling()
        }
    }

    /// Neon — rose knob on dark, vine accent. Pure vibes.
    #[must_use]
    pub fn neon() -> Self {
        Self {
            name: "Neon".into(),
            base: Color::hex(0x0A_0A_14).with_alpha(160),
            knob: palette::ROSE.with_alpha(235),
            outline: palette::TEAL,
            accent: palette::VINE,
            pressed: palette::VINE.with_alpha(235),
            shape: ControlShape::Hexagon,
            ..Self::ling()
        }
    }

    /// The opacity to draw at given how recently the control was touched
    /// (`active_blend` `0` idle .. `1` active), eased over `fade_secs`.
    #[must_use]
    pub fn current_opacity(&self, active_blend: f32) -> f32 {
        let t = active_blend.clamp(0.0, 1.0);
        (self.idle_opacity + (self.opacity - self.idle_opacity) * t).clamp(0.0, 1.0)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn hex_unpacks() {
        assert_eq!(Color::hex(0x3B_6E_A5), Color::rgb(0x3B, 0x6E, 0xA5));
    }

    #[test]
    fn opacity_interpolates() {
        let t = ControlTheme::ling();
        assert!((t.current_opacity(0.0) - t.idle_opacity).abs() < 1e-6);
        assert!((t.current_opacity(1.0) - t.opacity).abs() < 1e-6);
    }
}

← Back to index