Skip to main content

a7n_core/
focus.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3//! Focus ownership and routing (PROTOCOL.md 8). The caller supplies time,
4//! authenticated peer identities and drive policy, and executes returned actions.
5//! Poll due deadlines before processing incoming control messages.
6
7use std::collections::BTreeMap;
8use std::time::Instant;
9
10use a7n_proto::v1::{Focus as WireFocus, InputEvent, Landing, envelope::Body};
11use a7n_proto::{MachineId, Route};
12
13pub mod edge;
14pub mod held;
15mod owner;
16mod responder;
17
18use held::HeldInputs;
19
20/// A direct take, with the owner's current reachable peers.
21#[derive(Debug, Clone, PartialEq)]
22pub struct Take {
23    pub target: MachineId,
24    pub landing: Option<Landing>,
25    pub resistance_px: u32,
26    pub reach: Vec<MachineId>,
27}
28
29/// Where this machine's physical input is sent.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Cursor {
32    Home,
33    Driving(MachineId),
34}
35
36/// Effects to execute in order. Releases precede accepting a replacement owner.
37#[derive(Debug, Clone, PartialEq)]
38pub enum Action {
39    Send {
40        peer: MachineId,
41        body: Body,
42    },
43    Release(Vec<InputEvent>),
44    EndedDriven,
45    Cursor(Cursor),
46    /// A new remote owner; `landing` is absent for a stay re-arm.
47    Driven {
48        owner: MachineId,
49        landing: Option<Landing>,
50        /// The owner's raw value; pass it to `edge::EdgeResistance::new`.
51        resistance_px: u32,
52    },
53}
54
55#[derive(Debug, Clone)]
56struct Session {
57    take: Take,
58    route: Route,
59}
60
61#[derive(Debug)]
62enum Owner {
63    Home,
64    Driving(Session),
65    Taking {
66        session: Session,
67        previous: Option<Session>,
68        deadline: Instant,
69    },
70}
71
72#[derive(Debug)]
73struct Driven {
74    owner: MachineId,
75    route: Route,
76    last_input: Instant,
77    reach: Vec<MachineId>,
78}
79
80/// Both roles live together so local activity and remote ownership agree.
81#[derive(Debug)]
82pub struct Focus {
83    own_id: MachineId,
84    last_route: Route,
85    owner: Owner,
86    driven: Option<Driven>,
87    accepted: BTreeMap<MachineId, Route>,
88    last_local_input: Option<Instant>,
89    held: HeldInputs,
90    locked: bool,
91}
92
93impl Focus {
94    #[must_use]
95    pub fn new(own_id: MachineId) -> Self {
96        Self {
97            own_id,
98            last_route: Route(0),
99            owner: Owner::Home,
100            driven: None,
101            accepted: BTreeMap::new(),
102            last_local_input: None,
103            held: HeldInputs::default(),
104            locked: false,
105        }
106    }
107
108    /// Keep edge crossings on the current machine; explicit takes still work.
109    pub fn set_locked(&mut self, locked: bool) {
110        self.locked = locked;
111    }
112
113    pub fn toggle_lock(&mut self) {
114        self.locked = !self.locked;
115    }
116
117    #[must_use]
118    pub fn is_locked(&self) -> bool {
119        self.locked
120    }
121
122    #[must_use]
123    pub fn cursor(&self) -> Cursor {
124        match &self.owner {
125            Owner::Home => Cursor::Home,
126            Owner::Driving(session) => Cursor::Driving(session.take.target),
127            Owner::Taking { previous, .. } => previous
128                .as_ref()
129                .map_or(Cursor::Home, |s| Cursor::Driving(s.take.target)),
130        }
131    }
132
133    /// The active owner's route, including the previous route during a take.
134    #[must_use]
135    pub fn driving_route(&self) -> Option<Route> {
136        match &self.owner {
137            Owner::Home => None,
138            Owner::Driving(session) => Some(session.route),
139            Owner::Taking { previous, .. } => previous.as_ref().map(|s| s.route),
140        }
141    }
142
143    /// Whether physical input is currently suppressed by a remote owner.
144    #[must_use]
145    pub fn is_driven(&self) -> bool {
146        self.driven.is_some()
147    }
148
149    /// The peer currently driving this machine.
150    #[must_use]
151    pub fn driven_by(&self) -> Option<MachineId> {
152        self.driven.as_ref().map(|driven| driven.owner)
153    }
154
155    /// None when there is no take awaiting an answer.
156    #[must_use]
157    pub fn next_deadline(&self) -> Option<Instant> {
158        match self.owner {
159            Owner::Taking { deadline, .. } => Some(deadline),
160            _ => None,
161        }
162    }
163
164    fn next_route(&mut self) -> Option<Route> {
165        // Routes must never wrap into an older route.
166        self.last_route = Route(self.last_route.0.checked_add(1)?);
167        Some(self.last_route)
168    }
169
170    fn send(peer: MachineId, body: Body) -> Action {
171        Action::Send { peer, body }
172    }
173
174    fn focus_message(session: &Session, target: MachineId, stay: bool) -> WireFocus {
175        WireFocus {
176            route: session.route.0,
177            target: target.as_bytes().to_vec(),
178            landing: if stay {
179                None
180            } else {
181                session.take.landing.clone()
182            },
183            resistance_px: session.take.resistance_px,
184            reach: session
185                .take
186                .reach
187                .iter()
188                .map(|id| id.as_bytes().to_vec())
189                .collect(),
190            stay,
191        }
192    }
193}
194
195#[cfg(test)]
196mod tests;