medius 3.4.0

Host control library for the medius transparent mouse passthrough box
Documentation
//! `LOCK` control vocabulary (§3.8): what a lock addresses, its edge, blanket groups, and decoded locks.

use crate::protocol::opcode::{LOCK_CLS_AXIS, LOCK_SCALE_BLOCK, LOCK_SCALE_PASS};
use crate::types::{Axis, Class, Direction, Usage};

/// A whole-group blanket: the cursor aim (X+Y), the wheel, every mouse button, every key, or every media usage.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Blanket {
    /// The X and Y cursor axes.
    Aim,
    /// The wheel.
    Wheel,
    /// Every mouse button.
    Buttons,
    /// Every keyboard key and modifier.
    Keys,
    /// Every media (Consumer) usage.
    Media,
}

impl Blanket {
    /// Every input group, for a clip auto-lock over all physical input.
    pub const ALL: &'static [Blanket] = &[
        Blanket::Aim,
        Blanket::Wheel,
        Blanket::Buttons,
        Blanket::Keys,
        Blanket::Media,
    ];

    /// This group's clip auto-lock scope bit (`CLIP_LOCK_*`).
    pub(crate) fn clip_lock_bit(self) -> u8 {
        use crate::protocol::opcode::*;
        match self {
            Blanket::Aim => CLIP_LOCK_AIM,
            Blanket::Wheel => CLIP_LOCK_WHEEL,
            Blanket::Buttons => CLIP_LOCK_BUTTONS,
            Blanket::Keys => CLIP_LOCK_KEYS,
            Blanket::Media => CLIP_LOCK_MEDIA,
        }
    }
}

/// The autolock scope byte for a set of [`Blanket`] groups (an empty set = no autolock).
pub(crate) fn blanket_scope(scope: &[Blanket]) -> u8 {
    scope.iter().fold(0, |m, b| m | b.clip_lock_bit())
}

/// The [`Blanket`] groups a `CLIP_LOCK_*` scope byte names (the inverse of [`blanket_scope`]).
pub(crate) fn blanket_from_scope(scope: u8) -> Vec<Blanket> {
    Blanket::ALL
        .iter()
        .copied()
        .filter(|b| scope & b.clip_lock_bit() != 0)
        .collect()
}

/// One addressable input field: a relative axis or a momentary usage (button/key/media).
///
/// The box addresses a field the same way everywhere, so this is what a [`lock`](crate::Device::lock)
/// weighs and what a [`Transform`](crate::Transform) reads and writes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LockTarget {
    /// A relative axis (X/Y/wheel/pan), locked by sign.
    Axis(Axis),
    /// A momentary usage (button/key/media), locked by press/release edge.
    Usage(Usage),
}

impl LockTarget {
    /// The wire `(class, id)` this field encodes to.
    pub fn class_id(self) -> (u8, u16) {
        match self {
            LockTarget::Axis(a) => (LOCK_CLS_AXIS, a.as_u16()),
            LockTarget::Usage(u) => u.class_id(),
        }
    }

    /// Map a wire `(class, id)` back to a [`LockTarget`], or `None` for a class no field names or an
    /// axis id past the declared axes.
    pub fn from_class_id(class: u8, id: u16) -> Option<LockTarget> {
        if class == LOCK_CLS_AXIS {
            Some(LockTarget::Axis(Axis::from_u16(id)?))
        } else {
            Some(LockTarget::Usage(Usage::new(Class::from_u8(class)?, id)))
        }
    }

    /// The axis this field names, or `None` if it is a momentary usage.
    pub fn as_axis(self) -> Option<Axis> {
        match self {
            LockTarget::Axis(a) => Some(a),
            LockTarget::Usage(_) => None,
        }
    }
}

impl From<Axis> for LockTarget {
    fn from(a: Axis) -> LockTarget {
        LockTarget::Axis(a)
    }
}
impl<T: Into<Usage>> From<T> for LockTarget {
    fn from(u: T) -> LockTarget {
        LockTarget::Usage(u.into())
    }
}

/// What a `RESP(LOCKS)` entry addresses: a specific [`LockTarget`], or a whole-class blanket over a [`Class`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LockScope {
    /// A specific axis or usage.
    Target(LockTarget),
    /// A whole-class blanket (every button, key, or media usage of the class).
    Blanket(Class),
}

/// One entry in a decoded `RESP(LOCKS)` (§4.8): what is weighed, in which direction, and by how much.
///
/// Entries mirror the `LOCK` frame field for field, so what comes back is what you would send to
/// reproduce it. Only directions off [`LOCK_SCALE_PASS`] are reported; a target absent from the reply
/// is passing untouched.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LockEntry {
    /// What this entry weighs.
    pub scope: LockScope,
    /// Which direction of it.
    pub direction: Direction,
    /// Percent of the physical value kept: 0 blocks, 100 passes, above 100 amplifies, and a negative
    /// one reverses what it keeps. A momentary usage carries one bit, so the box stores the block or
    /// pass it amounts to and one never reports a value in between.
    ///
    /// This is the figure the box applies, not the number it was sent. In
    /// [`BearingMode::Vector`](crate::BearingMode) one relative scale governs both axes, the lower
    /// of X's and Y's, and both relative entries carry that number.
    pub scale: i16,
}

impl LockEntry {
    /// Whether this entry blocks its direction outright, rather than merely weighing it.
    pub fn is_block(&self) -> bool {
        self.scale == LOCK_SCALE_BLOCK
    }
}

/// Decoded `RESP(LOCKS)` (§4.8): every active lock across every class.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Locks {
    entries: Vec<LockEntry>,
}

impl Locks {
    /// Decode a `RESP(LOCKS)` payload: `[what][n]` then `n × [class][id u16 LE][dir][scale i16 LE]`;
    /// unknown entries skip.
    pub(crate) fn from_payload(p: &[u8]) -> Option<Locks> {
        let n = *p.get(1)? as usize;
        let mut entries = Vec::with_capacity(n);
        for i in 0..n {
            let off = 2 + 6 * i;
            let cls = *p.get(off)?;
            let id = u16::from_le_bytes([*p.get(off + 1)?, *p.get(off + 2)?]);
            let dir = *p.get(off + 3)?;
            let scale = i16::from_le_bytes([*p.get(off + 4)?, *p.get(off + 5)?]);
            let (Some(scope), Some(direction)) = (decode_scope(cls, id), Direction::from_u8(dir))
            else {
                continue;
            };
            entries.push(LockEntry {
                scope,
                direction,
                scale,
            });
        }
        Some(Locks { entries })
    }

    /// Build a [`Locks`] from decoded entries; useful for tests and [`MockBox`](crate::MockBox).
    pub fn from_entries(entries: Vec<LockEntry>) -> Locks {
        Locks { entries }
    }

    /// Every active lock entry.
    pub fn entries(&self) -> &[LockEntry] {
        &self.entries
    }

    /// Whether the given target is blocked outright on the given direction, by a specific entry or a
    /// covering blanket. A direction merely weighed (any scale between block and pass) is not locked.
    ///
    /// [`Direction::Both`] asks about the two absolute signs, the pair it has always named. A relative
    /// direction is asked for by name, because a target can be blocked against the bearing while both
    /// of its fixed signs pass.
    pub fn is_locked(&self, target: impl Into<LockTarget>, dir: Direction) -> bool {
        let target = target.into();
        match dir {
            Direction::Both => {
                self.scale_of(target, Direction::Positive) == LOCK_SCALE_BLOCK
                    && self.scale_of(target, Direction::Negative) == LOCK_SCALE_BLOCK
            }
            d => self.scale_of(target, d) == LOCK_SCALE_BLOCK,
        }
    }

    /// The scale in effect on one target and direction: percent of the physical value kept, so
    /// [`LOCK_SCALE_PASS`] when nothing weighs it.
    ///
    /// [`Direction::Both`] reports the least that survives across any direction, ranked by magnitude
    /// so a block outranks a reversal of any size: `Positive` blocked with `Against` at `-50` reports
    /// `0`, not `-50`. That is not what a delta meets: a delta picks up one fixed-direction scale and
    /// one bearing-relative one, and the box multiplies them, so `Negative` 50 with `Against` 40 lands
    /// at 20% while this returns 40. Ask by direction and multiply if you need the figure a delta
    /// actually sees.
    ///
    /// A covering blanket counts, and where several entries cover the same direction the least that
    /// survives wins.
    pub fn scale_of(&self, target: impl Into<LockTarget>, dir: Direction) -> i16 {
        let target = target.into();
        let covers = |e: &LockEntry| match e.scope {
            LockScope::Target(t) => t == target,
            LockScope::Blanket(class) => matches!(target, LockTarget::Usage(u) if u.class == class),
        };
        self.entries
            .iter()
            .filter(|e| covers(e) && (dir == Direction::Both || e.direction.admits(dir)))
            .map(|e| e.scale)
            // By magnitude, not by value: a signed minimum would rank -50 below 0 and report a
            // reversal over a block, when the block is what the delta actually meets.
            .min_by_key(|s| (s.unsigned_abs(), *s))
            .unwrap_or(LOCK_SCALE_PASS)
    }
}

fn decode_scope(cls: u8, id: u16) -> Option<LockScope> {
    if cls == LOCK_CLS_AXIS {
        Some(LockScope::Target(LockTarget::Axis(Axis::from_u16(id)?)))
    } else if id == crate::protocol::opcode::LOCK_ID_ALL {
        Some(LockScope::Blanket(Class::from_u8(cls)?))
    } else {
        let class = Class::from_u8(cls)?;
        Some(LockScope::Target(LockTarget::Usage(Usage::new(class, id))))
    }
}