oxiui_accessibility/focus.rs
1//! Focus indicator visual properties for OxiUI accessibility.
2//!
3//! Provides [`FocusRing`] (the visual spec for the focus outline) and
4//! [`FocusIndicator`] (tracks which node currently has focus and what ring
5//! spec to use when rendering it). Renderers consume these types to draw
6//! the platform-appropriate focus ring without knowing the full a11y tree.
7
8use accesskit::NodeId;
9
10// ── Focus ring spec ───────────────────────────────────────────────────────────
11
12/// Visual properties for a focus ring, consumed by renderers.
13///
14/// Describes the outline drawn around the currently-focused widget.
15/// All measurements are in logical pixels.
16#[derive(Debug, Clone, PartialEq)]
17pub struct FocusRing {
18 /// Colour of the ring in RGBA byte order `[r, g, b, a]`.
19 pub color: [u8; 4],
20 /// Stroke width in logical pixels.
21 pub width: f32,
22 /// Outset distance from the widget's bounding box in logical pixels.
23 pub offset: f32,
24 /// Corner radius of the ring in logical pixels (`0.0` = sharp corners).
25 pub radius: f32,
26}
27
28impl Default for FocusRing {
29 fn default() -> Self {
30 Self {
31 // Windows system-highlight blue (#0078D7), fully opaque.
32 color: [0, 120, 215, 255],
33 width: 2.0,
34 offset: 2.0,
35 radius: 3.0,
36 }
37 }
38}
39
40impl FocusRing {
41 /// Compute the bounding rectangle for the ring given the widget's bounding
42 /// box `(x, y, width, height)` in logical pixels.
43 ///
44 /// Returns `(rx, ry, rw, rh)` where the ring is outset by `self.offset` on
45 /// all sides and the stroke of `self.width` is applied further outward.
46 /// This is the rectangle that a renderer should stroke / outline.
47 ///
48 /// # Note for renderers
49 ///
50 /// The returned rectangle is the *outer* boundary of the ring stroke.
51 /// Renderers should stroke the rectangle inward by `self.width / 2.0` to
52 /// position the stroke centrally on the boundary.
53 ///
54 /// # Example
55 ///
56 /// ```rust
57 /// use oxiui_accessibility::FocusRing;
58 ///
59 /// let ring = FocusRing { width: 2.0, offset: 2.0, ..Default::default() };
60 /// let (rx, ry, rw, rh) = ring.ring_rect(10.0, 20.0, 100.0, 30.0);
61 /// // outset by offset (2) + half width (1) on each side
62 /// assert_eq!(rx, 10.0 - 2.0 - 1.0);
63 /// assert_eq!(ry, 20.0 - 2.0 - 1.0);
64 /// assert_eq!(rw, 100.0 + (2.0 + 1.0) * 2.0);
65 /// assert_eq!(rh, 30.0 + (2.0 + 1.0) * 2.0);
66 /// ```
67 pub fn ring_rect(&self, x: f32, y: f32, width: f32, height: f32) -> (f32, f32, f32, f32) {
68 let grow = self.offset + self.width / 2.0;
69 (x - grow, y - grow, width + grow * 2.0, height + grow * 2.0)
70 }
71
72 /// Returns `true` when the ring should be rendered (i.e. it has a non-zero
73 /// stroke width and a non-fully-transparent colour).
74 ///
75 /// Renderers may skip drawing the ring when this returns `false`.
76 ///
77 /// # Example
78 ///
79 /// ```rust
80 /// use oxiui_accessibility::FocusRing;
81 ///
82 /// let visible = FocusRing::default();
83 /// assert!(visible.is_visible());
84 ///
85 /// let invisible = FocusRing { color: [0, 0, 0, 0], ..Default::default() };
86 /// assert!(!invisible.is_visible());
87 /// ```
88 pub fn is_visible(&self) -> bool {
89 self.width > 0.0 && self.color[3] > 0
90 }
91}
92
93// ── Focus indicator ───────────────────────────────────────────────────────────
94
95/// Tracks which node currently holds focus and the visual ring spec to use.
96///
97/// Renderers query [`FocusIndicator::focused_node`] to know which widget is
98/// focused and [`FocusIndicator::ring`] to know how to draw its outline.
99///
100/// This is intentionally decoupled from [`crate::tree::A11yTree`]'s focus
101/// field (which drives the AccessKit `TreeUpdate::focus` field for screen
102/// readers). Both should be kept in sync, but keeping them separate allows
103/// the render layer to style the ring independently of the a11y adapter.
104pub struct FocusIndicator {
105 focused_node: Option<NodeId>,
106 ring: FocusRing,
107}
108
109impl Default for FocusIndicator {
110 fn default() -> Self {
111 Self::new()
112 }
113}
114
115impl FocusIndicator {
116 /// Create a new indicator with no focused node and the default ring spec.
117 pub fn new() -> Self {
118 Self {
119 focused_node: None,
120 ring: FocusRing::default(),
121 }
122 }
123
124 /// Set (or clear) the currently focused node.
125 ///
126 /// Pass `None` to clear the focus — no ring will be rendered.
127 pub fn set_focus(&mut self, id: Option<NodeId>) {
128 self.focused_node = id;
129 }
130
131 /// Return the [`NodeId`] of the currently focused node, if any.
132 pub fn focused_node(&self) -> Option<NodeId> {
133 self.focused_node
134 }
135
136 /// Return a shared reference to the current [`FocusRing`] spec.
137 pub fn ring(&self) -> &FocusRing {
138 &self.ring
139 }
140
141 /// Replace the ring spec with a custom one (builder-style).
142 pub fn with_ring(mut self, ring: FocusRing) -> Self {
143 self.ring = ring;
144 self
145 }
146}