1use std::collections::HashMap;
4use std::fmt::Debug;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::OnceLock;
7use std::sync::{Arc, Mutex};
8
9use crate::messages::EvidenceProviderRegistration;
10use crate::tree::{
11 EvidenceMethod, EvidenceProvenance, EvidenceSource, EvidenceStrength, PointerHitGrid,
12 PointerHitRegion, ProviderActionRecipes, ProviderFocusState, ProviderPaintedRegion,
13 ProviderPointerRegion, ProviderRevisionEvidence, ProviderScrollState,
14 ProviderTerminalInputModes, Rect,
15};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct Context {
20 pub session_id: String,
22 pub revision: i64,
24 pub columns: i64,
26 pub rows: i64,
28}
29
30pub struct PointerObservation {
32 pub pointer_regions: Vec<ProviderPointerRegion>,
34 pub hit_test: Option<HitTest>,
36}
37
38pub type HitTest = Arc<dyn Fn(i64, i64) -> Option<String> + Send + Sync>;
40
41pub trait PointerProvider: Debug + Send + Sync {
43 fn id(&self) -> &str;
45 fn version(&self) -> &str;
47 fn method(&self) -> EvidenceMethod;
49 fn capabilities(&self) -> Vec<String>;
53 fn observe(&self, context: &Context) -> Result<PointerObservation, String>;
55}
56
57pub trait ActionStrategyProvider: Debug + Send + Sync {
59 fn id(&self) -> &str;
61 fn version(&self) -> &str;
63 fn method(&self) -> EvidenceMethod;
65 fn observe(&self, context: &Context) -> Result<Vec<ProviderActionRecipes>, String>;
67}
68
69pub trait FocusProvider: Debug + Send + Sync {
71 fn id(&self) -> &str;
73 fn version(&self) -> &str;
75 fn method(&self) -> EvidenceMethod;
77 fn observe(&self, context: &Context) -> Result<Option<String>, String>;
79}
80
81pub trait ScrollProvider: Debug + Send + Sync {
83 fn id(&self) -> &str;
85 fn version(&self) -> &str;
87 fn method(&self) -> EvidenceMethod;
89 fn observe(&self, context: &Context) -> Result<Vec<ProviderScrollState>, String>;
91}
92
93pub trait PaintProvider: Debug + Send + Sync {
95 fn id(&self) -> &str;
97 fn version(&self) -> &str;
99 fn method(&self) -> EvidenceMethod;
101 fn observe(&self, context: &Context) -> Result<Vec<ProviderPaintedRegion>, String>;
103}
104
105pub trait InputModeProvider: Debug + Send + Sync {
107 fn id(&self) -> &str;
109 fn version(&self) -> &str;
111 fn method(&self) -> EvidenceMethod;
113 fn observe(&self, context: &Context) -> Result<ProviderTerminalInputModes, String>;
115}
116
117trait Provider: Debug + Send + Sync {
118 fn id(&self) -> &str;
119 fn version(&self) -> &str;
120 fn method(&self) -> EvidenceMethod;
121 fn capabilities(&self) -> Vec<String>;
122 fn observe(&self, context: &Context) -> Result<ProviderObservation, String>;
123}
124
125struct ProviderObservation {
126 pointer_regions: Vec<ProviderPointerRegion>,
127 hit_test: Option<HitTest>,
128 action_recipes: Option<Vec<ProviderActionRecipes>>,
129 focus_state: Option<ProviderFocusState>,
130 scroll_states: Option<Vec<ProviderScrollState>>,
131 painted_regions: Option<Vec<ProviderPaintedRegion>>,
132 input_modes: Option<ProviderTerminalInputModes>,
133}
134
135#[derive(Debug)]
136struct PointerProviderAdapter(Arc<dyn PointerProvider>);
137impl Provider for PointerProviderAdapter {
138 fn id(&self) -> &str {
139 self.0.id()
140 }
141 fn version(&self) -> &str {
142 self.0.version()
143 }
144 fn method(&self) -> EvidenceMethod {
145 self.0.method()
146 }
147 fn capabilities(&self) -> Vec<String> {
148 self.0.capabilities()
149 }
150 fn observe(&self, context: &Context) -> Result<ProviderObservation, String> {
151 let value = self.0.observe(context)?;
152 Ok(ProviderObservation {
153 pointer_regions: value.pointer_regions,
154 hit_test: value.hit_test,
155 action_recipes: None,
156 focus_state: None,
157 scroll_states: None,
158 painted_regions: None,
159 input_modes: None,
160 })
161 }
162}
163
164#[derive(Debug)]
165struct ActionStrategyProviderAdapter(Arc<dyn ActionStrategyProvider>);
166impl Provider for ActionStrategyProviderAdapter {
167 fn id(&self) -> &str {
168 self.0.id()
169 }
170 fn version(&self) -> &str {
171 self.0.version()
172 }
173 fn method(&self) -> EvidenceMethod {
174 self.0.method()
175 }
176 fn capabilities(&self) -> Vec<String> {
177 vec!["action-recipes".into()]
178 }
179 fn observe(&self, context: &Context) -> Result<ProviderObservation, String> {
180 Ok(ProviderObservation {
181 pointer_regions: Vec::new(),
182 hit_test: None,
183 action_recipes: Some(self.0.observe(context)?),
184 focus_state: None,
185 scroll_states: None,
186 painted_regions: None,
187 input_modes: None,
188 })
189 }
190}
191
192#[derive(Debug)]
193struct FocusProviderAdapter(Arc<dyn FocusProvider>);
194impl Provider for FocusProviderAdapter {
195 fn id(&self) -> &str {
196 self.0.id()
197 }
198 fn version(&self) -> &str {
199 self.0.version()
200 }
201 fn method(&self) -> EvidenceMethod {
202 self.0.method()
203 }
204 fn capabilities(&self) -> Vec<String> {
205 vec!["focus-state".into()]
206 }
207 fn observe(&self, context: &Context) -> Result<ProviderObservation, String> {
208 Ok(ProviderObservation {
209 pointer_regions: Vec::new(),
210 hit_test: None,
211 action_recipes: None,
212 focus_state: Some(match self.0.observe(context)? {
213 Some(recipient_id) => ProviderFocusState::Focused { recipient_id },
214 None => ProviderFocusState::None,
215 }),
216 scroll_states: None,
217 painted_regions: None,
218 input_modes: None,
219 })
220 }
221}
222
223#[derive(Debug)]
224struct ScrollProviderAdapter(Arc<dyn ScrollProvider>);
225impl Provider for ScrollProviderAdapter {
226 fn id(&self) -> &str {
227 self.0.id()
228 }
229 fn version(&self) -> &str {
230 self.0.version()
231 }
232 fn method(&self) -> EvidenceMethod {
233 self.0.method()
234 }
235 fn capabilities(&self) -> Vec<String> {
236 vec!["scroll-state".into()]
237 }
238 fn observe(&self, context: &Context) -> Result<ProviderObservation, String> {
239 Ok(ProviderObservation {
240 pointer_regions: Vec::new(),
241 hit_test: None,
242 action_recipes: None,
243 focus_state: None,
244 scroll_states: Some(self.0.observe(context)?),
245 painted_regions: None,
246 input_modes: None,
247 })
248 }
249}
250
251#[derive(Debug)]
252struct PaintProviderAdapter(Arc<dyn PaintProvider>);
253impl Provider for PaintProviderAdapter {
254 fn id(&self) -> &str {
255 self.0.id()
256 }
257 fn version(&self) -> &str {
258 self.0.version()
259 }
260 fn method(&self) -> EvidenceMethod {
261 self.0.method()
262 }
263 fn capabilities(&self) -> Vec<String> {
264 vec!["painted-regions".into()]
265 }
266 fn observe(&self, context: &Context) -> Result<ProviderObservation, String> {
267 Ok(ProviderObservation {
268 pointer_regions: Vec::new(),
269 hit_test: None,
270 action_recipes: None,
271 focus_state: None,
272 scroll_states: None,
273 painted_regions: Some(self.0.observe(context)?),
274 input_modes: None,
275 })
276 }
277}
278
279#[derive(Debug)]
280struct InputModeProviderAdapter(Arc<dyn InputModeProvider>);
281impl Provider for InputModeProviderAdapter {
282 fn id(&self) -> &str {
283 self.0.id()
284 }
285 fn version(&self) -> &str {
286 self.0.version()
287 }
288 fn method(&self) -> EvidenceMethod {
289 self.0.method()
290 }
291 fn capabilities(&self) -> Vec<String> {
292 vec!["terminal-input-modes".into()]
293 }
294 fn observe(&self, context: &Context) -> Result<ProviderObservation, String> {
295 Ok(ProviderObservation {
296 pointer_regions: Vec::new(),
297 hit_test: None,
298 action_recipes: None,
299 focus_state: None,
300 scroll_states: None,
301 painted_regions: None,
302 input_modes: Some(self.0.observe(context)?),
303 })
304 }
305}
306
307#[derive(Debug)]
308struct Entry {
309 provider: Arc<dyn Provider>,
310 capabilities: Vec<String>,
311 active: AtomicBool,
312}
313#[derive(Debug, Default)]
314struct State {
315 active_leases: usize,
316 entries: HashMap<String, Arc<Entry>>,
317}
318
319#[derive(Debug, Clone, Default)]
321pub struct Registry {
322 state: Arc<Mutex<State>>,
323}
324
325#[derive(Debug)]
327pub struct Registration {
328 state: Arc<Mutex<State>>,
329 id: String,
330 entry: Arc<Entry>,
331}
332impl Registration {
333 pub fn dispose(&self) {
335 self.entry.active.store(false, Ordering::SeqCst);
336 self.state
337 .lock()
338 .expect("evidence registry poisoned")
339 .entries
340 .remove(&self.id);
341 }
342}
343
344impl Registry {
345 pub fn new() -> Self {
347 Self::default()
348 }
349 pub fn register_pointer(
352 &self,
353 provider: Arc<dyn PointerProvider>,
354 ) -> Result<Registration, String> {
355 for capability in provider.capabilities() {
356 if capability != "pointer-regions" && capability != "hit-test" {
357 return Err(format!("pointer provider cannot declare {capability}"));
358 }
359 }
360 self.register(Arc::new(PointerProviderAdapter(provider)))
361 }
362
363 pub fn register_action_strategies(
365 &self,
366 provider: Arc<dyn ActionStrategyProvider>,
367 ) -> Result<Registration, String> {
368 self.register(Arc::new(ActionStrategyProviderAdapter(provider)))
369 }
370
371 pub fn register_focus(&self, provider: Arc<dyn FocusProvider>) -> Result<Registration, String> {
373 self.register(Arc::new(FocusProviderAdapter(provider)))
374 }
375
376 pub fn register_scroll(
378 &self,
379 provider: Arc<dyn ScrollProvider>,
380 ) -> Result<Registration, String> {
381 self.register(Arc::new(ScrollProviderAdapter(provider)))
382 }
383
384 pub fn register_paint(&self, provider: Arc<dyn PaintProvider>) -> Result<Registration, String> {
386 self.register(Arc::new(PaintProviderAdapter(provider)))
387 }
388
389 pub fn register_input_modes(
391 &self,
392 provider: Arc<dyn InputModeProvider>,
393 ) -> Result<Registration, String> {
394 self.register(Arc::new(InputModeProviderAdapter(provider)))
395 }
396
397 fn register(&self, provider: Arc<dyn Provider>) -> Result<Registration, String> {
398 let mut state = self
399 .state
400 .lock()
401 .map_err(|_| "evidence registry poisoned".to_owned())?;
402 if state.active_leases > 0 {
403 return Err(format!(
404 "provider {} registered after contract freeze",
405 provider.id()
406 ));
407 }
408 if provider.id().is_empty() || provider.version().is_empty() {
409 return Err("invalid provider identity".into());
410 }
411 if state.entries.contains_key(provider.id()) {
412 return Err(format!("duplicate provider {}", provider.id()));
413 }
414 let capabilities = provider.capabilities();
415 validate_capabilities(&capabilities)?;
416 let id = provider.id().to_owned();
417 let entry = Arc::new(Entry {
418 provider,
419 capabilities,
420 active: AtomicBool::new(true),
421 });
422 state.entries.insert(id.clone(), entry.clone());
423 Ok(Registration {
424 state: self.state.clone(),
425 id,
426 entry,
427 })
428 }
429 pub(crate) fn freeze(&self) -> Lease {
430 let mut state = self.state.lock().expect("evidence registry poisoned");
431 state.active_leases += 1;
432 Lease {
433 state: self.state.clone(),
434 entries: state.entries.values().cloned().collect(),
435 closed: false,
436 }
437 }
438}
439
440fn validate_capabilities(capabilities: &[String]) -> Result<(), String> {
441 if capabilities.is_empty() {
442 return Err("provider must declare at least one capability".into());
443 }
444 let mut seen = std::collections::HashSet::new();
445 for capability in capabilities {
446 if capability != "pointer-regions"
447 && capability != "hit-test"
448 && capability != "action-recipes"
449 && capability != "focus-state"
450 && capability != "scroll-state"
451 && capability != "painted-regions"
452 && capability != "terminal-input-modes"
453 {
454 return Err(format!("unknown provider capability {capability}"));
455 }
456 if !seen.insert(capability) {
457 return Err(format!("duplicate provider capability {capability}"));
458 }
459 }
460 Ok(())
461}
462
463pub fn global_registry() -> Registry {
466 static REGISTRY: OnceLock<Registry> = OnceLock::new();
467 REGISTRY.get_or_init(Registry::new).clone()
468}
469
470pub fn register_pointer_evidence_provider(
472 provider: Arc<dyn PointerProvider>,
473) -> Result<Registration, String> {
474 global_registry().register_pointer(provider)
475}
476
477pub fn register_action_strategy_provider(
479 provider: Arc<dyn ActionStrategyProvider>,
480) -> Result<Registration, String> {
481 global_registry().register_action_strategies(provider)
482}
483
484pub fn register_focus_evidence_provider(
486 provider: Arc<dyn FocusProvider>,
487) -> Result<Registration, String> {
488 global_registry().register_focus(provider)
489}
490
491pub fn register_scroll_evidence_provider(
493 provider: Arc<dyn ScrollProvider>,
494) -> Result<Registration, String> {
495 global_registry().register_scroll(provider)
496}
497
498pub fn register_paint_evidence_provider(
500 provider: Arc<dyn PaintProvider>,
501) -> Result<Registration, String> {
502 global_registry().register_paint(provider)
503}
504
505pub fn register_terminal_input_mode_evidence_provider(
507 provider: Arc<dyn InputModeProvider>,
508) -> Result<Registration, String> {
509 global_registry().register_input_modes(provider)
510}
511
512#[derive(Debug)]
513pub(crate) struct Lease {
514 state: Arc<Mutex<State>>,
515 entries: Vec<Arc<Entry>>,
516 closed: bool,
517}
518impl Lease {
519 pub(crate) fn registrations(&self) -> Vec<EvidenceProviderRegistration> {
520 self.entries
521 .iter()
522 .map(|e| EvidenceProviderRegistration {
523 id: e.provider.id().into(),
524 version: e.provider.version().into(),
525 method: method_name(e.provider.method()).into(),
526 capabilities: e.capabilities.clone(),
527 })
528 .collect()
529 }
530 pub(crate) fn collect(
531 &self,
532 session_id: &str,
533 revision: i64,
534 columns: i64,
535 rows: i64,
536 ) -> Vec<ProviderRevisionEvidence> {
537 self.entries
538 .iter()
539 .map(|e| collect_entry(e, session_id, revision, columns, rows))
540 .collect()
541 }
542 pub(crate) fn close(&mut self) {
543 if self.closed {
544 return;
545 }
546 self.closed = true;
547 self.state
548 .lock()
549 .expect("evidence registry poisoned")
550 .active_leases -= 1;
551 }
552}
553impl Drop for Lease {
554 fn drop(&mut self) {
555 self.close()
556 }
557}
558fn method_name(method: EvidenceMethod) -> &'static str {
559 match method {
560 EvidenceMethod::Native => "native",
561 EvidenceMethod::Declared => "declared",
562 _ => "instrumented",
563 }
564}
565
566fn collect_entry(
567 entry: &Entry,
568 session_id: &str,
569 revision: i64,
570 columns: i64,
571 rows: i64,
572) -> ProviderRevisionEvidence {
573 let base = |status: &str, reason: Option<String>| ProviderRevisionEvidence {
574 provider_id: entry.provider.id().into(),
575 session_id: session_id.into(),
576 revision,
577 status: status.into(),
578 evidence: None,
579 pointer_regions: None,
580 focus_state: None,
581 action_recipes: None,
582 scroll_states: None,
583 painted_regions: None,
584 input_modes: None,
585 hit_grid: None,
586 reason,
587 };
588 if !entry.active.load(Ordering::SeqCst) {
589 return base("lost", Some("provider disposed after negotiation".into()));
590 }
591 let observation = match entry.provider.observe(&Context {
592 session_id: session_id.into(),
593 revision,
594 columns,
595 rows,
596 }) {
597 Ok(v) => v,
598 Err(e) => return base("violation", Some(e)),
599 };
600 let mut result = base("available", None);
601 result.evidence = Some(EvidenceProvenance {
602 source: EvidenceSource::Application,
603 method: entry.provider.method(),
604 strength: EvidenceStrength::Authoritative,
605 provider_id: entry.provider.id().into(),
606 });
607 let capabilities = &entry.capabilities;
608 if !capabilities.iter().any(|v| v == "pointer-regions")
609 && !observation.pointer_regions.is_empty()
610 {
611 return base(
612 "violation",
613 Some("published pointer regions without negotiating pointer-regions".into()),
614 );
615 }
616 if !capabilities.iter().any(|v| v == "hit-test") && observation.hit_test.is_some() {
617 return base(
618 "violation",
619 Some("published a hit-test callback without negotiating hit-test".into()),
620 );
621 }
622 if capabilities.iter().any(|v| v == "action-recipes") && observation.action_recipes.is_none() {
623 return base(
624 "violation",
625 Some("negotiated action-recipes evidence is unavailable".into()),
626 );
627 }
628 if !capabilities.iter().any(|v| v == "action-recipes") && observation.action_recipes.is_some() {
629 return base(
630 "violation",
631 Some("published action recipes without negotiating action-recipes".into()),
632 );
633 }
634 if capabilities.iter().any(|v| v == "focus-state") && observation.focus_state.is_none() {
635 return base(
636 "violation",
637 Some("negotiated focus-state evidence is unavailable".into()),
638 );
639 }
640 if !capabilities.iter().any(|v| v == "focus-state") && observation.focus_state.is_some() {
641 return base(
642 "violation",
643 Some("published focus state without negotiating focus-state".into()),
644 );
645 }
646 if capabilities.iter().any(|v| v == "scroll-state") && observation.scroll_states.is_none() {
647 return base(
648 "violation",
649 Some("negotiated scroll-state evidence is unavailable".into()),
650 );
651 }
652 if !capabilities.iter().any(|v| v == "scroll-state") && observation.scroll_states.is_some() {
653 return base(
654 "violation",
655 Some("published scroll state without negotiating scroll-state".into()),
656 );
657 }
658 if let Some(states) = &observation.scroll_states {
659 if states.iter().any(|state| {
660 state.offset < 0
661 || state.viewport < 0
662 || state.extent < 0
663 || state.offset + state.viewport > state.extent
664 }) {
665 return base(
666 "violation",
667 Some("scroll state must fit inside its extent".into()),
668 );
669 }
670 }
671 if capabilities.iter().any(|v| v == "painted-regions") && observation.painted_regions.is_none()
672 {
673 return base(
674 "violation",
675 Some("negotiated painted-regions evidence is unavailable".into()),
676 );
677 }
678 if !capabilities.iter().any(|v| v == "painted-regions") && observation.painted_regions.is_some()
679 {
680 return base(
681 "violation",
682 Some("published painted regions without negotiating painted-regions".into()),
683 );
684 }
685 if capabilities.iter().any(|v| v == "terminal-input-modes") && observation.input_modes.is_none()
686 {
687 return base(
688 "violation",
689 Some("negotiated terminal-input-modes evidence is unavailable".into()),
690 );
691 }
692 if !capabilities.iter().any(|v| v == "terminal-input-modes")
693 && observation.input_modes.is_some()
694 {
695 return base(
696 "violation",
697 Some("published input modes without negotiating terminal-input-modes".into()),
698 );
699 }
700 if let Some(modes) = &observation.input_modes {
701 if !matches!(
702 modes.mouse_tracking.as_str(),
703 "none" | "x10" | "vt200" | "drag" | "any"
704 ) || !matches!(
705 modes.mouse_encoding.as_str(),
706 "default" | "sgr" | "urxvt" | "utf8"
707 ) || !matches!(modes.focus_reporting.as_str(), "on" | "off")
708 {
709 return base(
710 "violation",
711 Some("terminal input modes contain an invalid value".into()),
712 );
713 }
714 }
715 result.pointer_regions = Some(observation.pointer_regions);
716 result.action_recipes = observation.action_recipes;
717 result.focus_state = observation.focus_state;
718 result.scroll_states = observation.scroll_states;
719 result.painted_regions = observation.painted_regions;
720 result.input_modes = observation.input_modes;
721 if capabilities.iter().any(|v| v == "hit-test") {
722 match exact_grid(
723 result.pointer_regions.as_deref().unwrap_or_default(),
724 observation.hit_test,
725 columns,
726 rows,
727 capabilities.iter().any(|v| v == "pointer-regions"),
728 ) {
729 Ok(grid) => result.hit_grid = Some(grid),
730 Err(error) => return base("violation", Some(error)),
731 }
732 }
733 result
734}
735
736fn exact_grid(
737 regions: &[ProviderPointerRegion],
738 hit_test: Option<HitTest>,
739 columns: i64,
740 rows: i64,
741 verify_declared_regions: bool,
742) -> Result<PointerHitGrid, String> {
743 let hit_test = hit_test.ok_or_else(|| "negotiated hit-test callback unavailable".to_owned())?;
744 if columns.checked_mul(rows).unwrap_or(i64::MAX) > 1_000_000 {
745 return Err("hit-test viewport exceeds provider limit".into());
746 }
747 let mut declared = HashMap::new();
748 for region in regions {
749 for span in ®ion.spans {
750 for column in span.from..span.to {
751 let key = (span.row, column);
752 if let Some(old) = declared.insert(key, region.recipient_id.clone()) {
753 if old != region.recipient_id {
754 return Err("overlapping pointer regions".into());
755 }
756 }
757 }
758 }
759 }
760 let mut output = Vec::new();
761 for row in 0..rows {
762 let mut start: Option<i64> = None;
763 let mut owner: Option<String> = None;
764 for column in 0..=columns {
765 let actual = if column == columns {
766 None
767 } else {
768 hit_test(column, row)
769 };
770 let expected = if column == columns {
771 None
772 } else {
773 declared.get(&(row, column)).cloned()
774 };
775 if verify_declared_regions && actual != expected {
776 return Err(format!("production hit test disagrees at {column},{row}"));
777 }
778 if actual != owner {
779 if let (Some(from), Some(id)) = (start.take(), owner.take()) {
780 output.push(PointerHitRegion {
781 recipient_id: id,
782 rect: Rect {
783 row,
784 column: from,
785 width: column - from,
786 height: 1,
787 },
788 })
789 }
790 if actual.is_some() {
791 start = Some(column);
792 owner = actual
793 }
794 }
795 }
796 }
797 Ok(PointerHitGrid { regions: output })
798}
799
800#[cfg(test)]
801mod tests {
802 use super::*;
803 use crate::tree::{ProviderPointerSpan, Rect};
804
805 #[derive(Debug)]
806 struct Router;
807 impl PointerProvider for Router {
808 fn id(&self) -> &str {
809 "router"
810 }
811 fn version(&self) -> &str {
812 "1"
813 }
814 fn method(&self) -> EvidenceMethod {
815 EvidenceMethod::Native
816 }
817 fn capabilities(&self) -> Vec<String> {
818 vec!["pointer-regions".into(), "hit-test".into()]
819 }
820 fn observe(&self, _context: &Context) -> Result<PointerObservation, String> {
821 Ok(PointerObservation {
822 pointer_regions: vec![ProviderPointerRegion {
823 recipient_id: "reject".into(),
824 region_bounds: Rect {
825 row: 1,
826 column: 2,
827 width: 3,
828 height: 1,
829 },
830 spans: vec![ProviderPointerSpan {
831 row: 1,
832 from: 2,
833 to: 5,
834 }],
835 }],
836 hit_test: Some(Arc::new(|column, row| {
837 (row == 1 && (2..5).contains(&column)).then(|| "reject".into())
838 })),
839 })
840 }
841 }
842
843 #[derive(Debug)]
844 struct Regions;
845 impl PointerProvider for Regions {
846 fn id(&self) -> &str {
847 "regions"
848 }
849 fn version(&self) -> &str {
850 "1"
851 }
852 fn method(&self) -> EvidenceMethod {
853 EvidenceMethod::Declared
854 }
855 fn capabilities(&self) -> Vec<String> {
856 vec!["pointer-regions".into()]
857 }
858 fn observe(&self, _context: &Context) -> Result<PointerObservation, String> {
859 Ok(PointerObservation {
860 pointer_regions: Router.observe(_context)?.pointer_regions,
861 hit_test: None,
862 })
863 }
864 }
865
866 #[derive(Debug)]
867 struct HitTestOnly;
868 impl PointerProvider for HitTestOnly {
869 fn id(&self) -> &str {
870 "production-router"
871 }
872 fn version(&self) -> &str {
873 "2"
874 }
875 fn method(&self) -> EvidenceMethod {
876 EvidenceMethod::Native
877 }
878 fn capabilities(&self) -> Vec<String> {
879 vec!["hit-test".into()]
880 }
881 fn observe(&self, _context: &Context) -> Result<PointerObservation, String> {
882 Ok(PointerObservation {
883 pointer_regions: Vec::new(),
884 hit_test: Some(Arc::new(|column, row| {
885 (row == 1 && (2..5).contains(&column)).then(|| "reject".into())
886 })),
887 })
888 }
889 }
890
891 #[test]
892 fn freezes_per_session_and_fails_closed_after_loss() {
893 let registry = Registry::new();
894 let registration = registry
895 .register_pointer(Arc::new(Router))
896 .expect("register");
897 let lease = registry.freeze();
898 assert!(registry.register_pointer(Arc::new(Router)).is_err());
899 let evidence = lease.collect("s1", 1, 10, 4);
900 assert_eq!(evidence[0].status, "available");
901 assert_eq!(evidence[0].hit_grid.as_ref().unwrap().regions.len(), 1);
902 registration.dispose();
903 assert_eq!(lease.collect("s1", 2, 10, 4)[0].status, "lost");
904 }
905
906 #[test]
907 fn composes_independent_region_and_hit_test_providers() {
908 let registry = Registry::new();
909 registry
910 .register_pointer(Arc::new(Regions))
911 .expect("regions");
912 registry
913 .register_pointer(Arc::new(HitTestOnly))
914 .expect("hit test");
915 let evidence = registry.freeze().collect("s1", 4, 10, 4);
916 let regions = evidence
917 .iter()
918 .find(|entry| entry.provider_id == "regions")
919 .unwrap();
920 let hits = evidence
921 .iter()
922 .find(|entry| entry.provider_id == "production-router")
923 .unwrap();
924 assert_eq!(regions.status, "available");
925 assert_eq!(regions.pointer_regions.as_ref().unwrap().len(), 1);
926 assert!(regions.hit_grid.is_none());
927 assert_eq!(hits.status, "available");
928 assert!(hits.pointer_regions.as_ref().unwrap().is_empty());
929 assert_eq!(hits.hit_grid.as_ref().unwrap().regions.len(), 1);
930 }
931
932 #[derive(Debug)]
933 struct Keys;
934 impl ActionStrategyProvider for Keys {
935 fn id(&self) -> &str {
936 "app.keys"
937 }
938 fn version(&self) -> &str {
939 "1"
940 }
941 fn method(&self) -> EvidenceMethod {
942 EvidenceMethod::Native
943 }
944 fn observe(&self, _context: &Context) -> Result<Vec<ProviderActionRecipes>, String> {
945 Ok(vec![ProviderActionRecipes {
946 recipient_id: "editor".into(),
947 recipes: vec![crate::tree::PhysicalInputRecipe {
948 action: crate::tree::PhysicalInputRecipeAction::SetValue,
949 requires_focus: true,
950 steps: vec![
951 crate::tree::PhysicalInputRecipeStep::Press {
952 key: "Control+U".into(),
953 },
954 crate::tree::PhysicalInputRecipeStep::InsertActionValue,
955 ],
956 }],
957 }])
958 }
959 }
960
961 #[derive(Debug)]
962 struct Focus;
963 impl FocusProvider for Focus {
964 fn id(&self) -> &str {
965 "app.focus"
966 }
967 fn version(&self) -> &str {
968 "1"
969 }
970 fn method(&self) -> EvidenceMethod {
971 EvidenceMethod::Native
972 }
973 fn observe(&self, context: &Context) -> Result<Option<String>, String> {
974 Ok((context.revision == 1).then(|| "editor".into()))
975 }
976 }
977
978 #[derive(Debug)]
979 struct Scroll;
980 impl ScrollProvider for Scroll {
981 fn id(&self) -> &str {
982 "app.scroll"
983 }
984 fn version(&self) -> &str {
985 "1"
986 }
987 fn method(&self) -> EvidenceMethod {
988 EvidenceMethod::Native
989 }
990 fn observe(&self, _context: &Context) -> Result<Vec<ProviderScrollState>, String> {
991 Ok(vec![ProviderScrollState {
992 recipient_id: "results".into(),
993 axis: crate::tree::Orientation::Vertical,
994 offset: 3,
995 viewport: 4,
996 extent: 20,
997 }])
998 }
999 }
1000
1001 #[test]
1002 fn focus_provider_preserves_authoritative_none() {
1003 let registry = Registry::new();
1004 registry.register_focus(Arc::new(Focus)).expect("focus");
1005 let lease = registry.freeze();
1006 assert_eq!(lease.registrations()[0].capabilities, vec!["focus-state"]);
1007 assert!(matches!(
1008 lease.collect("s", 1, 80, 24)[0].focus_state,
1009 Some(ProviderFocusState::Focused { ref recipient_id }) if recipient_id == "editor"
1010 ));
1011 assert!(matches!(
1012 lease.collect("s", 2, 80, 24)[0].focus_state,
1013 Some(ProviderFocusState::None)
1014 ));
1015 }
1016
1017 #[test]
1018 fn scroll_provider_publishes_bounded_application_viewport_state() {
1019 let registry = Registry::new();
1020 registry.register_scroll(Arc::new(Scroll)).expect("scroll");
1021 let lease = registry.freeze();
1022 assert_eq!(lease.registrations()[0].capabilities, vec!["scroll-state"]);
1023 let frame = lease.collect("s", 1, 80, 24).remove(0);
1024 assert_eq!(frame.status, "available");
1025 assert_eq!(frame.scroll_states.as_ref().unwrap()[0].offset, 3);
1026 }
1027
1028 #[test]
1029 fn action_strategy_provider_is_a_separate_closed_family() {
1030 let registry = Registry::new();
1031 registry
1032 .register_action_strategies(Arc::new(Keys))
1033 .expect("keys");
1034 let frame = registry.freeze().collect("s", 3, 80, 24).remove(0);
1035 assert_eq!(frame.status, "available");
1036 assert_eq!(frame.action_recipes.as_ref().map(Vec::len), Some(1));
1037 assert!(frame.pointer_regions.as_ref().is_some_and(Vec::is_empty));
1038 assert!(frame.hit_grid.is_none());
1039 }
1040
1041 #[test]
1042 fn rejects_invalid_and_competing_capability_declarations() {
1043 #[derive(Debug)]
1044 struct Invalid;
1045 impl PointerProvider for Invalid {
1046 fn id(&self) -> &str {
1047 "invalid"
1048 }
1049 fn version(&self) -> &str {
1050 "1"
1051 }
1052 fn method(&self) -> EvidenceMethod {
1053 EvidenceMethod::Declared
1054 }
1055 fn capabilities(&self) -> Vec<String> {
1056 vec!["unknown".into()]
1057 }
1058 fn observe(&self, _: &Context) -> Result<PointerObservation, String> {
1059 unreachable!()
1060 }
1061 }
1062 let registry = Registry::new();
1063 assert!(registry.register_pointer(Arc::new(Invalid)).is_err());
1064 registry
1065 .register_pointer(Arc::new(Regions))
1066 .expect("regions");
1067 assert!(registry.register_pointer(Arc::new(Regions)).is_err());
1068 registry
1069 .register_pointer(Arc::new(HitTestOnly))
1070 .expect("hit test");
1071 }
1072}