1 | // Copyright 2022 The AccessKit Authors. All rights reserved. |
2 | // Licensed under the Apache License, Version 2.0 (found in |
3 | // the LICENSE-APACHE file) or the MIT license (found in |
4 | // the LICENSE-MIT file), at your option. |
5 | |
6 | // Derived from Chromium's accessibility abstraction. |
7 | // Copyright 2017 The Chromium Authors. All rights reserved. |
8 | // Use of this source code is governed by a BSD-style license that can be |
9 | // found in the LICENSE.chromium file. |
10 | |
11 | use accesskit::{ |
12 | Action, ActionData, ActionRequest, Affine, Live, NodeId, Orientation, Point, Rect, Role, |
13 | Toggled, |
14 | }; |
15 | use accesskit_consumer::{FilterResult, Node, TreeState}; |
16 | use atspi_common::{ |
17 | CoordType, Granularity, Interface, InterfaceSet, Layer, Live as AtspiLive, Role as AtspiRole, |
18 | ScrollType, State, StateSet, |
19 | }; |
20 | use std::{ |
21 | collections::HashMap, |
22 | hash::{Hash, Hasher}, |
23 | iter::FusedIterator, |
24 | sync::{Arc, RwLock, RwLockReadGuard, Weak}, |
25 | }; |
26 | |
27 | use crate::{ |
28 | adapter::Adapter, |
29 | context::{AppContext, Context}, |
30 | filters::filter, |
31 | util::*, |
32 | Action as AtspiAction, Error, ObjectEvent, Property, Rect as AtspiRect, Result, |
33 | }; |
34 | |
35 | pub(crate) struct NodeWrapper<'a>(pub(crate) &'a Node<'a>); |
36 | |
37 | impl<'a> NodeWrapper<'a> { |
38 | pub(crate) fn name(&self) -> Option<String> { |
39 | if self.0.label_comes_from_value() { |
40 | self.0.value() |
41 | } else { |
42 | self.0.label() |
43 | } |
44 | } |
45 | |
46 | pub(crate) fn description(&self) -> Option<String> { |
47 | self.0.description() |
48 | } |
49 | |
50 | pub(crate) fn parent_id(&self) -> Option<NodeId> { |
51 | self.0.parent_id() |
52 | } |
53 | |
54 | pub(crate) fn id(&self) -> NodeId { |
55 | self.0.id() |
56 | } |
57 | |
58 | fn filtered_child_ids( |
59 | &self, |
60 | ) -> impl DoubleEndedIterator<Item = NodeId> + FusedIterator<Item = NodeId> + '_ { |
61 | self.0.filtered_children(&filter).map(|child| child.id()) |
62 | } |
63 | |
64 | pub(crate) fn role(&self) -> AtspiRole { |
65 | if self.0.has_role_description() { |
66 | return AtspiRole::Extended; |
67 | } |
68 | |
69 | match self.0.role() { |
70 | Role::Alert => AtspiRole::Notification, |
71 | Role::AlertDialog => AtspiRole::Alert, |
72 | Role::Comment | Role::Suggestion => AtspiRole::Section, |
73 | // TODO: See how to represent ARIA role="application" |
74 | Role::Application => AtspiRole::Embedded, |
75 | Role::Article => AtspiRole::Article, |
76 | Role::Audio => AtspiRole::Audio, |
77 | Role::Banner | Role::Header => AtspiRole::Landmark, |
78 | Role::Blockquote => AtspiRole::BlockQuote, |
79 | Role::Caret => AtspiRole::Unknown, |
80 | Role::Button => { |
81 | if self.0.toggled().is_some() { |
82 | AtspiRole::ToggleButton |
83 | } else { |
84 | AtspiRole::PushButton |
85 | } |
86 | } |
87 | Role::DefaultButton => AtspiRole::PushButton, |
88 | Role::Canvas => AtspiRole::Canvas, |
89 | Role::Caption => AtspiRole::Caption, |
90 | Role::Cell => AtspiRole::TableCell, |
91 | Role::CheckBox => AtspiRole::CheckBox, |
92 | Role::Switch => AtspiRole::ToggleButton, |
93 | Role::ColorWell => AtspiRole::PushButton, |
94 | Role::ColumnHeader => AtspiRole::ColumnHeader, |
95 | Role::ComboBox | Role::EditableComboBox => AtspiRole::ComboBox, |
96 | Role::Complementary => AtspiRole::Landmark, |
97 | Role::ContentDeletion => AtspiRole::ContentDeletion, |
98 | Role::ContentInsertion => AtspiRole::ContentInsertion, |
99 | Role::ContentInfo | Role::Footer => AtspiRole::Landmark, |
100 | Role::Definition | Role::DescriptionListDetail => AtspiRole::DescriptionValue, |
101 | Role::DescriptionList => AtspiRole::DescriptionList, |
102 | Role::DescriptionListTerm => AtspiRole::DescriptionTerm, |
103 | Role::Details => AtspiRole::Panel, |
104 | Role::Dialog => AtspiRole::Dialog, |
105 | Role::Directory => AtspiRole::List, |
106 | Role::DisclosureTriangle => AtspiRole::ToggleButton, |
107 | Role::DocCover => AtspiRole::Image, |
108 | Role::DocBackLink | Role::DocBiblioRef | Role::DocGlossRef | Role::DocNoteRef => { |
109 | AtspiRole::Link |
110 | } |
111 | Role::DocBiblioEntry | Role::DocEndnote => AtspiRole::ListItem, |
112 | Role::DocNotice | Role::DocTip => AtspiRole::Comment, |
113 | Role::DocFootnote => AtspiRole::Footnote, |
114 | Role::DocPageBreak => AtspiRole::Separator, |
115 | Role::DocPageFooter => AtspiRole::Footer, |
116 | Role::DocPageHeader => AtspiRole::Header, |
117 | Role::DocAcknowledgements |
118 | | Role::DocAfterword |
119 | | Role::DocAppendix |
120 | | Role::DocBibliography |
121 | | Role::DocChapter |
122 | | Role::DocConclusion |
123 | | Role::DocCredits |
124 | | Role::DocEndnotes |
125 | | Role::DocEpilogue |
126 | | Role::DocErrata |
127 | | Role::DocForeword |
128 | | Role::DocGlossary |
129 | | Role::DocIndex |
130 | | Role::DocIntroduction |
131 | | Role::DocPageList |
132 | | Role::DocPart |
133 | | Role::DocPreface |
134 | | Role::DocPrologue |
135 | | Role::DocToc => AtspiRole::Landmark, |
136 | Role::DocAbstract |
137 | | Role::DocColophon |
138 | | Role::DocCredit |
139 | | Role::DocDedication |
140 | | Role::DocEpigraph |
141 | | Role::DocExample |
142 | | Role::DocPullquote |
143 | | Role::DocQna => AtspiRole::Section, |
144 | Role::DocSubtitle => AtspiRole::Heading, |
145 | Role::Document => AtspiRole::DocumentFrame, |
146 | Role::EmbeddedObject => AtspiRole::Embedded, |
147 | // TODO: Forms which lack an accessible name are no longer |
148 | // exposed as forms. Forms which have accessible |
149 | // names should be exposed as `AtspiRole::Landmark` according to Core AAM. |
150 | Role::Form => AtspiRole::Form, |
151 | Role::Figure | Role::Feed => AtspiRole::Panel, |
152 | Role::GenericContainer |
153 | | Role::FooterAsNonLandmark |
154 | | Role::HeaderAsNonLandmark |
155 | | Role::Ruby => AtspiRole::Section, |
156 | Role::GraphicsDocument => AtspiRole::DocumentFrame, |
157 | Role::GraphicsObject => AtspiRole::Panel, |
158 | Role::GraphicsSymbol => AtspiRole::Image, |
159 | Role::Grid => AtspiRole::Table, |
160 | Role::Group => AtspiRole::Panel, |
161 | Role::Heading => AtspiRole::Heading, |
162 | Role::Iframe | Role::IframePresentational => AtspiRole::InternalFrame, |
163 | // TODO: If there are unignored children, then it should be AtspiRole::ImageMap. |
164 | Role::Image => AtspiRole::Image, |
165 | Role::TextRun => AtspiRole::Static, |
166 | Role::Legend => AtspiRole::Label, |
167 | // Layout table objects are treated the same as `Role::GenericContainer`. |
168 | Role::LayoutTable => AtspiRole::Section, |
169 | Role::LayoutTableCell => AtspiRole::Section, |
170 | Role::LayoutTableRow => AtspiRole::Section, |
171 | // TODO: Having a separate accessible object for line breaks |
172 | // is inconsistent with other implementations. |
173 | Role::LineBreak => AtspiRole::Static, |
174 | Role::Link => AtspiRole::Link, |
175 | Role::List => AtspiRole::List, |
176 | Role::ListBox => AtspiRole::ListBox, |
177 | // TODO: Use `AtspiRole::MenuItem' inside a combo box. |
178 | Role::ListBoxOption => AtspiRole::ListItem, |
179 | Role::ListGrid => AtspiRole::Table, |
180 | Role::ListItem => AtspiRole::ListItem, |
181 | // Regular list markers only expose their alternative text, but do not |
182 | // expose their descendants, and the descendants should be ignored. This |
183 | // is because the alternative text depends on the counter style and can |
184 | // be different from the actual (visual) marker text, and hence, |
185 | // inconsistent with the descendants. We treat a list marker as non-text |
186 | // only if it still has non-ignored descendants, which happens only when => |
187 | // - The list marker itself is ignored but the descendants are not |
188 | // - Or the list marker contains images |
189 | Role::ListMarker => AtspiRole::Static, |
190 | Role::Log => AtspiRole::Log, |
191 | Role::Main => AtspiRole::Landmark, |
192 | Role::Mark => AtspiRole::Static, |
193 | Role::Math => AtspiRole::Math, |
194 | Role::Marquee => AtspiRole::Marquee, |
195 | Role::Menu | Role::MenuListPopup => AtspiRole::Menu, |
196 | Role::MenuBar => AtspiRole::MenuBar, |
197 | Role::MenuItem | Role::MenuListOption => AtspiRole::MenuItem, |
198 | Role::MenuItemCheckBox => AtspiRole::CheckMenuItem, |
199 | Role::MenuItemRadio => AtspiRole::RadioMenuItem, |
200 | Role::Meter => AtspiRole::LevelBar, |
201 | Role::Navigation => AtspiRole::Landmark, |
202 | Role::Note => AtspiRole::Comment, |
203 | Role::Pane | Role::ScrollView => AtspiRole::Panel, |
204 | Role::Paragraph => AtspiRole::Paragraph, |
205 | Role::PdfActionableHighlight => AtspiRole::PushButton, |
206 | Role::PdfRoot => AtspiRole::DocumentFrame, |
207 | Role::PluginObject => AtspiRole::Embedded, |
208 | Role::Portal => AtspiRole::PushButton, |
209 | Role::Pre => AtspiRole::Section, |
210 | Role::ProgressIndicator => AtspiRole::ProgressBar, |
211 | Role::RadioButton => AtspiRole::RadioButton, |
212 | Role::RadioGroup => AtspiRole::Panel, |
213 | Role::Region => AtspiRole::Landmark, |
214 | Role::RootWebArea => AtspiRole::DocumentWeb, |
215 | Role::Row => AtspiRole::TableRow, |
216 | Role::RowGroup => AtspiRole::Panel, |
217 | Role::RowHeader => AtspiRole::RowHeader, |
218 | // TODO: Generally exposed as description on <ruby> (`Role::Ruby`) element, not |
219 | // as its own object in the tree. |
220 | // However, it's possible to make a `Role::RubyAnnotation` element show up in the |
221 | // tree, for example by adding tabindex="0" to the source <rp> or <rt> |
222 | // element or making the source element the target of an aria-owns. |
223 | // Therefore, we need to gracefully handle it if it actually |
224 | // shows up in the tree. |
225 | Role::RubyAnnotation => AtspiRole::Static, |
226 | Role::Section => AtspiRole::Section, |
227 | Role::ScrollBar => AtspiRole::ScrollBar, |
228 | Role::Search => AtspiRole::Landmark, |
229 | Role::Slider => AtspiRole::Slider, |
230 | Role::SpinButton => AtspiRole::SpinButton, |
231 | Role::Splitter => AtspiRole::Separator, |
232 | Role::Label => AtspiRole::Label, |
233 | Role::Status => AtspiRole::StatusBar, |
234 | Role::SvgRoot => AtspiRole::DocumentFrame, |
235 | Role::Tab => AtspiRole::PageTab, |
236 | Role::Table => AtspiRole::Table, |
237 | Role::TabList => AtspiRole::PageTabList, |
238 | Role::TabPanel => AtspiRole::ScrollPane, |
239 | // TODO: This mapping should also be applied to the dfn |
240 | // element. |
241 | Role::Term => AtspiRole::DescriptionTerm, |
242 | Role::TitleBar => AtspiRole::TitleBar, |
243 | Role::TextInput |
244 | | Role::MultilineTextInput |
245 | | Role::SearchInput |
246 | | Role::EmailInput |
247 | | Role::NumberInput |
248 | | Role::PhoneNumberInput |
249 | | Role::UrlInput => AtspiRole::Entry, |
250 | Role::DateInput |
251 | | Role::DateTimeInput |
252 | | Role::WeekInput |
253 | | Role::MonthInput |
254 | | Role::TimeInput => AtspiRole::DateEditor, |
255 | Role::PasswordInput => AtspiRole::PasswordText, |
256 | Role::Abbr | Role::Code | Role::Emphasis | Role::Strong | Role::Time => { |
257 | AtspiRole::Static |
258 | } |
259 | Role::Timer => AtspiRole::Timer, |
260 | Role::Toolbar => AtspiRole::ToolBar, |
261 | Role::Tooltip => AtspiRole::ToolTip, |
262 | Role::Tree => AtspiRole::Tree, |
263 | Role::TreeItem => AtspiRole::TreeItem, |
264 | Role::TreeGrid => AtspiRole::TreeTable, |
265 | Role::Video => AtspiRole::Video, |
266 | // In AT-SPI, elements with `AtspiRole::Frame` are windows with titles and |
267 | // buttons, while those with `AtspiRole::Window` are windows without those |
268 | // elements. |
269 | Role::Window => AtspiRole::Frame, |
270 | Role::WebView => AtspiRole::Panel, |
271 | Role::FigureCaption => AtspiRole::Caption, |
272 | // TODO: Are there special cases to consider? |
273 | Role::Unknown => AtspiRole::Unknown, |
274 | Role::ImeCandidate | Role::Keyboard => AtspiRole::RedundantObject, |
275 | Role::Terminal => AtspiRole::Terminal, |
276 | } |
277 | } |
278 | |
279 | fn is_focused(&self) -> bool { |
280 | self.0.is_focused() |
281 | } |
282 | |
283 | pub(crate) fn state(&self, is_window_focused: bool) -> StateSet { |
284 | let state = self.0; |
285 | let atspi_role = self.role(); |
286 | let mut atspi_state = StateSet::empty(); |
287 | if state.parent_id().is_none() && state.role() == Role::Window && is_window_focused { |
288 | atspi_state.insert(State::Active); |
289 | } |
290 | if state.is_text_input() && !state.is_read_only() { |
291 | atspi_state.insert(State::Editable); |
292 | } |
293 | // TODO: Focus and selection. |
294 | if state.is_focusable() { |
295 | atspi_state.insert(State::Focusable); |
296 | } |
297 | if let Some(orientation) = state.orientation() { |
298 | atspi_state.insert(if orientation == Orientation::Horizontal { |
299 | State::Horizontal |
300 | } else { |
301 | State::Vertical |
302 | }); |
303 | } |
304 | let filter_result = filter(self.0); |
305 | if filter_result == FilterResult::Include { |
306 | atspi_state.insert(State::Visible | State::Showing); |
307 | } |
308 | if atspi_role != AtspiRole::ToggleButton && state.toggled().is_some() { |
309 | atspi_state.insert(State::Checkable); |
310 | } |
311 | if let Some(selected) = state.is_selected() { |
312 | if !state.is_disabled() { |
313 | atspi_state.insert(State::Selectable); |
314 | } |
315 | if selected { |
316 | atspi_state.insert(State::Selected); |
317 | } |
318 | } |
319 | if state.is_text_input() { |
320 | atspi_state.insert(State::SelectableText); |
321 | atspi_state.insert(match state.is_multiline() { |
322 | true => State::MultiLine, |
323 | false => State::SingleLine, |
324 | }); |
325 | } |
326 | |
327 | // Special case for indeterminate progressbar. |
328 | if state.role() == Role::ProgressIndicator && state.numeric_value().is_none() { |
329 | atspi_state.insert(State::Indeterminate); |
330 | } |
331 | |
332 | // Toggled state |
333 | match state.toggled() { |
334 | Some(Toggled::Mixed) => atspi_state.insert(State::Indeterminate), |
335 | Some(Toggled::True) if atspi_role == AtspiRole::ToggleButton => { |
336 | atspi_state.insert(State::Pressed) |
337 | } |
338 | Some(Toggled::True) => atspi_state.insert(State::Checked), |
339 | _ => {} |
340 | } |
341 | |
342 | if state.is_read_only_supported() && state.is_read_only_or_disabled() { |
343 | atspi_state.insert(State::ReadOnly); |
344 | } else { |
345 | atspi_state.insert(State::Enabled | State::Sensitive); |
346 | } |
347 | |
348 | if self.is_focused() { |
349 | atspi_state.insert(State::Focused); |
350 | } |
351 | |
352 | atspi_state |
353 | } |
354 | |
355 | fn attributes(&self) -> HashMap<&'static str, String> { |
356 | let mut attributes = HashMap::new(); |
357 | if let Some(placeholder) = self.0.placeholder() { |
358 | attributes.insert("placeholder-text" , placeholder); |
359 | } |
360 | attributes |
361 | } |
362 | |
363 | fn is_root(&self) -> bool { |
364 | self.0.is_root() |
365 | } |
366 | |
367 | fn supports_action(&self) -> bool { |
368 | self.0.is_clickable() |
369 | } |
370 | |
371 | fn supports_component(&self) -> bool { |
372 | self.0.raw_bounds().is_some() || self.is_root() |
373 | } |
374 | |
375 | fn supports_text(&self) -> bool { |
376 | self.0.supports_text_ranges() |
377 | } |
378 | |
379 | fn supports_value(&self) -> bool { |
380 | self.current_value().is_some() |
381 | } |
382 | |
383 | pub(crate) fn interfaces(&self) -> InterfaceSet { |
384 | let mut interfaces = InterfaceSet::new(Interface::Accessible); |
385 | if self.supports_action() { |
386 | interfaces.insert(Interface::Action); |
387 | } |
388 | if self.supports_component() { |
389 | interfaces.insert(Interface::Component); |
390 | } |
391 | if self.supports_text() { |
392 | interfaces.insert(Interface::Text); |
393 | } |
394 | if self.supports_value() { |
395 | interfaces.insert(Interface::Value); |
396 | } |
397 | interfaces |
398 | } |
399 | |
400 | pub(crate) fn live(&self) -> AtspiLive { |
401 | let live = self.0.live(); |
402 | match live { |
403 | Live::Off => AtspiLive::None, |
404 | Live::Polite => AtspiLive::Polite, |
405 | Live::Assertive => AtspiLive::Assertive, |
406 | } |
407 | } |
408 | |
409 | fn n_actions(&self) -> i32 { |
410 | if self.0.is_clickable() { |
411 | 1 |
412 | } else { |
413 | 0 |
414 | } |
415 | } |
416 | |
417 | fn get_action_name(&self, index: i32) -> String { |
418 | if index != 0 { |
419 | return String::new(); |
420 | } |
421 | String::from(if self.0.is_clickable() { "click" } else { "" }) |
422 | } |
423 | |
424 | fn raw_bounds_and_transform(&self) -> (Option<Rect>, Affine) { |
425 | let state = self.0; |
426 | (state.raw_bounds(), state.direct_transform()) |
427 | } |
428 | |
429 | fn extents(&self, window_bounds: &WindowBounds, coord_type: CoordType) -> Option<Rect> { |
430 | let mut bounds = self.0.bounding_box(); |
431 | if self.is_root() { |
432 | let window_bounds = window_bounds.inner.with_origin(Point::ZERO); |
433 | if !window_bounds.is_empty() { |
434 | if let Some(bounds) = &mut bounds { |
435 | bounds.x0 = bounds.x0.min(window_bounds.x1); |
436 | bounds.y0 = bounds.y0.min(window_bounds.y1); |
437 | bounds.x1 = bounds.x1.min(window_bounds.x1); |
438 | bounds.y1 = bounds.y1.min(window_bounds.y1); |
439 | } else { |
440 | bounds = Some(window_bounds); |
441 | } |
442 | } |
443 | } |
444 | bounds.map(|bounds| { |
445 | let new_origin = window_bounds.accesskit_point_to_atspi_point( |
446 | bounds.origin(), |
447 | self.0.filtered_parent(&filter), |
448 | coord_type, |
449 | ); |
450 | bounds.with_origin(new_origin) |
451 | }) |
452 | } |
453 | |
454 | fn current_value(&self) -> Option<f64> { |
455 | self.0.numeric_value() |
456 | } |
457 | |
458 | pub(crate) fn notify_changes( |
459 | &self, |
460 | window_bounds: &WindowBounds, |
461 | adapter: &Adapter, |
462 | old: &NodeWrapper<'_>, |
463 | ) { |
464 | self.notify_state_changes(adapter, old); |
465 | self.notify_property_changes(adapter, old); |
466 | self.notify_bounds_changes(window_bounds, adapter, old); |
467 | self.notify_children_changes(adapter, old); |
468 | } |
469 | |
470 | fn notify_state_changes(&self, adapter: &Adapter, old: &NodeWrapper<'_>) { |
471 | let old_state = old.state(true); |
472 | let new_state = self.state(true); |
473 | let changed_states = old_state ^ new_state; |
474 | for state in changed_states.iter() { |
475 | if state == State::Focused { |
476 | // This is handled specially in `focus_moved`. |
477 | continue; |
478 | } |
479 | adapter.emit_object_event( |
480 | self.id(), |
481 | ObjectEvent::StateChanged(state, new_state.contains(state)), |
482 | ); |
483 | } |
484 | } |
485 | |
486 | fn notify_property_changes(&self, adapter: &Adapter, old: &NodeWrapper<'_>) { |
487 | let name = self.name(); |
488 | if name != old.name() { |
489 | let name = name.unwrap_or_default(); |
490 | adapter.emit_object_event( |
491 | self.id(), |
492 | ObjectEvent::PropertyChanged(Property::Name(name.clone())), |
493 | ); |
494 | |
495 | let live = self.live(); |
496 | if live != AtspiLive::None { |
497 | adapter.emit_object_event(self.id(), ObjectEvent::Announcement(name, live)); |
498 | } |
499 | } |
500 | let description = self.description(); |
501 | if description != old.description() { |
502 | adapter.emit_object_event( |
503 | self.id(), |
504 | ObjectEvent::PropertyChanged(Property::Description( |
505 | description.unwrap_or_default(), |
506 | )), |
507 | ); |
508 | } |
509 | let parent_id = self.parent_id(); |
510 | if parent_id != old.parent_id() { |
511 | let parent = self |
512 | .0 |
513 | .filtered_parent(&filter) |
514 | .map_or(NodeIdOrRoot::Root, |node| NodeIdOrRoot::Node(node.id())); |
515 | adapter.emit_object_event( |
516 | self.id(), |
517 | ObjectEvent::PropertyChanged(Property::Parent(parent)), |
518 | ); |
519 | } |
520 | let role = self.role(); |
521 | if role != old.role() { |
522 | adapter.emit_object_event( |
523 | self.id(), |
524 | ObjectEvent::PropertyChanged(Property::Role(role)), |
525 | ); |
526 | } |
527 | if let Some(value) = self.current_value() { |
528 | if Some(value) != old.current_value() { |
529 | adapter.emit_object_event( |
530 | self.id(), |
531 | ObjectEvent::PropertyChanged(Property::Value(value)), |
532 | ); |
533 | } |
534 | } |
535 | } |
536 | |
537 | fn notify_bounds_changes( |
538 | &self, |
539 | window_bounds: &WindowBounds, |
540 | adapter: &Adapter, |
541 | old: &NodeWrapper<'_>, |
542 | ) { |
543 | if self.raw_bounds_and_transform() != old.raw_bounds_and_transform() { |
544 | if let Some(extents) = self.extents(window_bounds, CoordType::Window) { |
545 | adapter.emit_object_event(self.id(), ObjectEvent::BoundsChanged(extents.into())); |
546 | } |
547 | } |
548 | } |
549 | |
550 | fn notify_children_changes(&self, adapter: &Adapter, old: &NodeWrapper<'_>) { |
551 | let old_filtered_children = old.filtered_child_ids().collect::<Vec<NodeId>>(); |
552 | let new_filtered_children = self.filtered_child_ids().collect::<Vec<NodeId>>(); |
553 | for (index, child) in new_filtered_children.iter().enumerate() { |
554 | if !old_filtered_children.contains(child) { |
555 | adapter.emit_object_event(self.id(), ObjectEvent::ChildAdded(index, *child)); |
556 | } |
557 | } |
558 | for child in old_filtered_children.into_iter() { |
559 | if !new_filtered_children.contains(&child) { |
560 | adapter.emit_object_event(self.id(), ObjectEvent::ChildRemoved(child)); |
561 | } |
562 | } |
563 | } |
564 | } |
565 | |
566 | #[derive (Clone)] |
567 | pub struct PlatformNode { |
568 | context: Weak<Context>, |
569 | adapter_id: usize, |
570 | id: NodeId, |
571 | } |
572 | |
573 | impl PlatformNode { |
574 | pub(crate) fn new(context: &Arc<Context>, adapter_id: usize, id: NodeId) -> Self { |
575 | Self { |
576 | context: Arc::downgrade(context), |
577 | adapter_id, |
578 | id, |
579 | } |
580 | } |
581 | |
582 | fn from_adapter_root(adapter_id_and_context: &(usize, Arc<Context>)) -> Self { |
583 | let (adapter_id, context) = adapter_id_and_context; |
584 | Self::new(context, *adapter_id, context.read_tree().state().root_id()) |
585 | } |
586 | |
587 | fn upgrade_context(&self) -> Result<Arc<Context>> { |
588 | if let Some(context) = self.context.upgrade() { |
589 | Ok(context) |
590 | } else { |
591 | Err(Error::Defunct) |
592 | } |
593 | } |
594 | |
595 | fn with_tree_state<F, T>(&self, f: F) -> Result<T> |
596 | where |
597 | F: FnOnce(&TreeState) -> Result<T>, |
598 | { |
599 | let context = self.upgrade_context()?; |
600 | let tree = context.read_tree(); |
601 | f(tree.state()) |
602 | } |
603 | |
604 | fn with_tree_state_and_context<F, T>(&self, f: F) -> Result<T> |
605 | where |
606 | F: FnOnce(&TreeState, &Context) -> Result<T>, |
607 | { |
608 | let context = self.upgrade_context()?; |
609 | let tree = context.read_tree(); |
610 | f(tree.state(), &context) |
611 | } |
612 | |
613 | fn resolve_with_context<F, T>(&self, f: F) -> Result<T> |
614 | where |
615 | for<'a> F: FnOnce(Node<'a>, &Context) -> Result<T>, |
616 | { |
617 | self.with_tree_state_and_context(|state, context| { |
618 | if let Some(node) = state.node_by_id(self.id) { |
619 | f(node, context) |
620 | } else { |
621 | Err(Error::Defunct) |
622 | } |
623 | }) |
624 | } |
625 | |
626 | fn resolve_for_text_with_context<F, T>(&self, f: F) -> Result<T> |
627 | where |
628 | for<'a> F: FnOnce(Node<'a>, &Context) -> Result<T>, |
629 | { |
630 | self.resolve_with_context(|node, context| { |
631 | let wrapper = NodeWrapper(&node); |
632 | if wrapper.supports_text() { |
633 | f(node, context) |
634 | } else { |
635 | Err(Error::UnsupportedInterface) |
636 | } |
637 | }) |
638 | } |
639 | |
640 | fn resolve<F, T>(&self, f: F) -> Result<T> |
641 | where |
642 | for<'a> F: FnOnce(Node<'a>) -> Result<T>, |
643 | { |
644 | self.resolve_with_context(|node, _| f(node)) |
645 | } |
646 | |
647 | fn resolve_for_text<F, T>(&self, f: F) -> Result<T> |
648 | where |
649 | for<'a> F: FnOnce(Node<'a>) -> Result<T>, |
650 | { |
651 | self.resolve_for_text_with_context(|node, _| f(node)) |
652 | } |
653 | |
654 | fn do_action_internal<F>(&self, f: F) -> Result<()> |
655 | where |
656 | F: FnOnce(&TreeState, &Context) -> ActionRequest, |
657 | { |
658 | let context = self.upgrade_context()?; |
659 | let tree = context.read_tree(); |
660 | if tree.state().has_node(self.id) { |
661 | let request = f(tree.state(), &context); |
662 | drop(tree); |
663 | context.do_action(request); |
664 | Ok(()) |
665 | } else { |
666 | Err(Error::Defunct) |
667 | } |
668 | } |
669 | |
670 | pub fn name(&self) -> Result<String> { |
671 | self.resolve(|node| { |
672 | let wrapper = NodeWrapper(&node); |
673 | Ok(wrapper.name().unwrap_or_default()) |
674 | }) |
675 | } |
676 | |
677 | pub fn description(&self) -> Result<String> { |
678 | self.resolve(|node| { |
679 | let wrapper = NodeWrapper(&node); |
680 | Ok(wrapper.description().unwrap_or_default()) |
681 | }) |
682 | } |
683 | |
684 | pub fn relative(&self, id: NodeId) -> Self { |
685 | Self { |
686 | context: self.context.clone(), |
687 | adapter_id: self.adapter_id, |
688 | id, |
689 | } |
690 | } |
691 | |
692 | pub fn root(&self) -> Result<PlatformRoot> { |
693 | let context = self.upgrade_context()?; |
694 | Ok(PlatformRoot::new(&context.app_context)) |
695 | } |
696 | |
697 | pub fn toolkit_name(&self) -> Result<String> { |
698 | self.with_tree_state(|state| Ok(state.toolkit_name().unwrap_or_default())) |
699 | } |
700 | |
701 | pub fn toolkit_version(&self) -> Result<String> { |
702 | self.with_tree_state(|state| Ok(state.toolkit_version().unwrap_or_default())) |
703 | } |
704 | |
705 | pub fn parent(&self) -> Result<NodeIdOrRoot> { |
706 | self.resolve(|node| { |
707 | let parent = node |
708 | .filtered_parent(&filter) |
709 | .map_or(NodeIdOrRoot::Root, |node| NodeIdOrRoot::Node(node.id())); |
710 | Ok(parent) |
711 | }) |
712 | } |
713 | |
714 | pub fn child_count(&self) -> Result<i32> { |
715 | self.resolve(|node| { |
716 | i32::try_from(node.filtered_children(&filter).count()) |
717 | .map_err(|_| Error::TooManyChildren) |
718 | }) |
719 | } |
720 | |
721 | pub fn adapter_id(&self) -> usize { |
722 | self.adapter_id |
723 | } |
724 | |
725 | pub fn id(&self) -> NodeId { |
726 | self.id |
727 | } |
728 | |
729 | pub fn accessible_id(&self) -> Result<String> { |
730 | self.resolve(|node| { |
731 | if let Some(author_id) = node.author_id() { |
732 | Ok(author_id.to_string()) |
733 | } else { |
734 | Ok(String::new()) |
735 | } |
736 | }) |
737 | } |
738 | |
739 | pub fn child_at_index(&self, index: usize) -> Result<Option<NodeId>> { |
740 | self.resolve(|node| { |
741 | let child = node |
742 | .filtered_children(&filter) |
743 | .nth(index) |
744 | .map(|child| child.id()); |
745 | Ok(child) |
746 | }) |
747 | } |
748 | |
749 | pub fn map_children<T, I>(&self, f: impl Fn(NodeId) -> I) -> Result<T> |
750 | where |
751 | T: FromIterator<I>, |
752 | { |
753 | self.resolve(|node| { |
754 | let children = node |
755 | .filtered_children(&filter) |
756 | .map(|child| child.id()) |
757 | .map(f) |
758 | .collect(); |
759 | Ok(children) |
760 | }) |
761 | } |
762 | |
763 | pub fn index_in_parent(&self) -> Result<i32> { |
764 | self.resolve(|node| { |
765 | i32::try_from(node.preceding_filtered_siblings(&filter).count()) |
766 | .map_err(|_| Error::IndexOutOfRange) |
767 | }) |
768 | } |
769 | |
770 | pub fn role(&self) -> Result<AtspiRole> { |
771 | self.resolve(|node| { |
772 | let wrapper = NodeWrapper(&node); |
773 | Ok(wrapper.role()) |
774 | }) |
775 | } |
776 | |
777 | pub fn localized_role_name(&self) -> Result<String> { |
778 | self.resolve(|node| Ok(node.role_description().unwrap_or_default())) |
779 | } |
780 | |
781 | pub fn state(&self) -> StateSet { |
782 | self.resolve_with_context(|node, context| { |
783 | let wrapper = NodeWrapper(&node); |
784 | Ok(wrapper.state(context.read_tree().state().focus_id().is_some())) |
785 | }) |
786 | .unwrap_or(State::Defunct.into()) |
787 | } |
788 | |
789 | pub fn attributes(&self) -> Result<HashMap<&'static str, String>> { |
790 | self.resolve(|node| { |
791 | let wrapper = NodeWrapper(&node); |
792 | Ok(wrapper.attributes()) |
793 | }) |
794 | } |
795 | |
796 | pub fn supports_action(&self) -> Result<bool> { |
797 | self.resolve(|node| { |
798 | let wrapper = NodeWrapper(&node); |
799 | Ok(wrapper.supports_action()) |
800 | }) |
801 | } |
802 | |
803 | pub fn supports_component(&self) -> Result<bool> { |
804 | self.resolve(|node| { |
805 | let wrapper = NodeWrapper(&node); |
806 | Ok(wrapper.supports_component()) |
807 | }) |
808 | } |
809 | |
810 | pub fn supports_text(&self) -> Result<bool> { |
811 | self.resolve(|node| { |
812 | let wrapper = NodeWrapper(&node); |
813 | Ok(wrapper.supports_text()) |
814 | }) |
815 | } |
816 | |
817 | pub fn supports_value(&self) -> Result<bool> { |
818 | self.resolve(|node| { |
819 | let wrapper = NodeWrapper(&node); |
820 | Ok(wrapper.supports_value()) |
821 | }) |
822 | } |
823 | |
824 | pub fn interfaces(&self) -> Result<InterfaceSet> { |
825 | self.resolve(|node| { |
826 | let wrapper = NodeWrapper(&node); |
827 | Ok(wrapper.interfaces()) |
828 | }) |
829 | } |
830 | |
831 | pub fn n_actions(&self) -> Result<i32> { |
832 | self.resolve(|node| { |
833 | let wrapper = NodeWrapper(&node); |
834 | Ok(wrapper.n_actions()) |
835 | }) |
836 | } |
837 | |
838 | pub fn action_name(&self, index: i32) -> Result<String> { |
839 | self.resolve(|node| { |
840 | let wrapper = NodeWrapper(&node); |
841 | Ok(wrapper.get_action_name(index)) |
842 | }) |
843 | } |
844 | |
845 | pub fn actions(&self) -> Result<Vec<AtspiAction>> { |
846 | self.resolve(|node| { |
847 | let wrapper = NodeWrapper(&node); |
848 | let n_actions = wrapper.n_actions() as usize; |
849 | let mut actions = Vec::with_capacity(n_actions); |
850 | for i in 0..n_actions { |
851 | actions.push(AtspiAction { |
852 | localized_name: wrapper.get_action_name(i as i32), |
853 | description: "" .into(), |
854 | key_binding: "" .into(), |
855 | }); |
856 | } |
857 | Ok(actions) |
858 | }) |
859 | } |
860 | |
861 | pub fn do_action(&self, index: i32) -> Result<bool> { |
862 | if index != 0 { |
863 | return Ok(false); |
864 | } |
865 | self.do_action_internal(|_, _| ActionRequest { |
866 | action: Action::Click, |
867 | target: self.id, |
868 | data: None, |
869 | })?; |
870 | Ok(true) |
871 | } |
872 | |
873 | pub fn contains(&self, x: i32, y: i32, coord_type: CoordType) -> Result<bool> { |
874 | self.resolve_with_context(|node, context| { |
875 | let window_bounds = context.read_root_window_bounds(); |
876 | let wrapper = NodeWrapper(&node); |
877 | if let Some(extents) = wrapper.extents(&window_bounds, coord_type) { |
878 | Ok(extents.contains(Point::new(x.into(), y.into()))) |
879 | } else { |
880 | Ok(false) |
881 | } |
882 | }) |
883 | } |
884 | |
885 | pub fn accessible_at_point( |
886 | &self, |
887 | x: i32, |
888 | y: i32, |
889 | coord_type: CoordType, |
890 | ) -> Result<Option<NodeId>> { |
891 | self.resolve_with_context(|node, context| { |
892 | let window_bounds = context.read_root_window_bounds(); |
893 | let point = window_bounds.atspi_point_to_accesskit_point( |
894 | Point::new(x.into(), y.into()), |
895 | Some(node), |
896 | coord_type, |
897 | ); |
898 | let point = node.transform().inverse() * point; |
899 | Ok(node.node_at_point(point, &filter).map(|node| node.id())) |
900 | }) |
901 | } |
902 | |
903 | pub fn extents(&self, coord_type: CoordType) -> Result<AtspiRect> { |
904 | self.resolve_with_context(|node, context| { |
905 | let window_bounds = context.read_root_window_bounds(); |
906 | let wrapper = NodeWrapper(&node); |
907 | Ok(wrapper |
908 | .extents(&window_bounds, coord_type) |
909 | .map_or(AtspiRect::INVALID, AtspiRect::from)) |
910 | }) |
911 | } |
912 | |
913 | pub fn layer(&self) -> Result<Layer> { |
914 | self.resolve(|node| { |
915 | let wrapper = NodeWrapper(&node); |
916 | if wrapper.role() == AtspiRole::Window { |
917 | Ok(Layer::Window) |
918 | } else { |
919 | Ok(Layer::Widget) |
920 | } |
921 | }) |
922 | } |
923 | |
924 | pub fn grab_focus(&self) -> Result<bool> { |
925 | self.do_action_internal(|_, _| ActionRequest { |
926 | action: Action::Focus, |
927 | target: self.id, |
928 | data: None, |
929 | })?; |
930 | Ok(true) |
931 | } |
932 | |
933 | pub fn scroll_to_point(&self, coord_type: CoordType, x: i32, y: i32) -> Result<bool> { |
934 | self.resolve_with_context(|node, context| { |
935 | let window_bounds = context.read_root_window_bounds(); |
936 | let point = window_bounds.atspi_point_to_accesskit_point( |
937 | Point::new(x.into(), y.into()), |
938 | node.filtered_parent(&filter), |
939 | coord_type, |
940 | ); |
941 | context.do_action(ActionRequest { |
942 | action: Action::ScrollToPoint, |
943 | target: self.id, |
944 | data: Some(ActionData::ScrollToPoint(point)), |
945 | }); |
946 | Ok(()) |
947 | })?; |
948 | Ok(true) |
949 | } |
950 | |
951 | pub fn character_count(&self) -> Result<i32> { |
952 | self.resolve_for_text(|node| { |
953 | node.document_range() |
954 | .end() |
955 | .to_global_usv_index() |
956 | .try_into() |
957 | .map_err(|_| Error::TooManyCharacters) |
958 | }) |
959 | } |
960 | |
961 | pub fn caret_offset(&self) -> Result<i32> { |
962 | self.resolve_for_text(|node| { |
963 | node.text_selection_focus().map_or(Ok(-1), |focus| { |
964 | focus |
965 | .to_global_usv_index() |
966 | .try_into() |
967 | .map_err(|_| Error::TooManyCharacters) |
968 | }) |
969 | }) |
970 | } |
971 | |
972 | pub fn string_at_offset( |
973 | &self, |
974 | offset: i32, |
975 | granularity: Granularity, |
976 | ) -> Result<(String, i32, i32)> { |
977 | self.resolve_for_text(|node| { |
978 | let range = text_range_from_offset(&node, offset, granularity)?; |
979 | let text = range.text(); |
980 | let start = range |
981 | .start() |
982 | .to_global_usv_index() |
983 | .try_into() |
984 | .map_err(|_| Error::TooManyCharacters)?; |
985 | let end = range |
986 | .end() |
987 | .to_global_usv_index() |
988 | .try_into() |
989 | .map_err(|_| Error::TooManyCharacters)?; |
990 | |
991 | Ok((text, start, end)) |
992 | }) |
993 | } |
994 | |
995 | pub fn text(&self, start_offset: i32, end_offset: i32) -> Result<String> { |
996 | self.resolve_for_text(|node| { |
997 | let range = text_range_from_offsets(&node, start_offset, end_offset) |
998 | .ok_or(Error::IndexOutOfRange)?; |
999 | Ok(range.text()) |
1000 | }) |
1001 | } |
1002 | |
1003 | pub fn set_caret_offset(&self, offset: i32) -> Result<bool> { |
1004 | self.resolve_for_text_with_context(|node, context| { |
1005 | let offset = text_position_from_offset(&node, offset).ok_or(Error::IndexOutOfRange)?; |
1006 | context.do_action(ActionRequest { |
1007 | action: Action::SetTextSelection, |
1008 | target: node.id(), |
1009 | data: Some(ActionData::SetTextSelection( |
1010 | offset.to_degenerate_range().to_text_selection(), |
1011 | )), |
1012 | }); |
1013 | Ok(true) |
1014 | }) |
1015 | } |
1016 | |
1017 | pub fn text_attribute_value(&self, _offset: i32, _attribute_name: &str) -> Result<String> { |
1018 | // TODO: Implement rich text. |
1019 | Err(Error::UnsupportedInterface) |
1020 | } |
1021 | |
1022 | pub fn text_attributes(&self, _offset: i32) -> Result<(HashMap<String, String>, i32, i32)> { |
1023 | // TODO: Implement rich text. |
1024 | Err(Error::UnsupportedInterface) |
1025 | } |
1026 | |
1027 | pub fn default_text_attributes(&self) -> Result<HashMap<String, String>> { |
1028 | // TODO: Implement rich text. |
1029 | Err(Error::UnsupportedInterface) |
1030 | } |
1031 | |
1032 | pub fn character_extents(&self, offset: i32, coord_type: CoordType) -> Result<AtspiRect> { |
1033 | self.resolve_for_text_with_context(|node, context| { |
1034 | let range = text_range_from_offset(&node, offset, Granularity::Char)?; |
1035 | if let Some(bounds) = range.bounding_boxes().first() { |
1036 | let window_bounds = context.read_root_window_bounds(); |
1037 | let new_origin = window_bounds.accesskit_point_to_atspi_point( |
1038 | bounds.origin(), |
1039 | Some(node), |
1040 | coord_type, |
1041 | ); |
1042 | Ok(bounds.with_origin(new_origin).into()) |
1043 | } else { |
1044 | Ok(AtspiRect::INVALID) |
1045 | } |
1046 | }) |
1047 | } |
1048 | |
1049 | pub fn offset_at_point(&self, x: i32, y: i32, coord_type: CoordType) -> Result<i32> { |
1050 | self.resolve_for_text_with_context(|node, context| { |
1051 | let window_bounds = context.read_root_window_bounds(); |
1052 | let point = window_bounds.atspi_point_to_accesskit_point( |
1053 | Point::new(x.into(), y.into()), |
1054 | Some(node), |
1055 | coord_type, |
1056 | ); |
1057 | let point = node.transform().inverse() * point; |
1058 | node.text_position_at_point(point) |
1059 | .to_global_usv_index() |
1060 | .try_into() |
1061 | .map_err(|_| Error::TooManyCharacters) |
1062 | }) |
1063 | } |
1064 | |
1065 | pub fn n_selections(&self) -> Result<i32> { |
1066 | self.resolve_for_text(|node| { |
1067 | match node.text_selection().filter(|range| !range.is_degenerate()) { |
1068 | Some(_) => Ok(1), |
1069 | None => Ok(0), |
1070 | } |
1071 | }) |
1072 | } |
1073 | |
1074 | pub fn selection(&self, selection_num: i32) -> Result<(i32, i32)> { |
1075 | if selection_num != 0 { |
1076 | return Ok((-1, -1)); |
1077 | } |
1078 | |
1079 | self.resolve_for_text(|node| { |
1080 | node.text_selection() |
1081 | .filter(|range| !range.is_degenerate()) |
1082 | .map_or(Ok((-1, -1)), |range| { |
1083 | let start = range |
1084 | .start() |
1085 | .to_global_usv_index() |
1086 | .try_into() |
1087 | .map_err(|_| Error::TooManyCharacters)?; |
1088 | let end = range |
1089 | .end() |
1090 | .to_global_usv_index() |
1091 | .try_into() |
1092 | .map_err(|_| Error::TooManyCharacters)?; |
1093 | |
1094 | Ok((start, end)) |
1095 | }) |
1096 | }) |
1097 | } |
1098 | |
1099 | pub fn add_selection(&self, start_offset: i32, end_offset: i32) -> Result<bool> { |
1100 | // We only support one selection. |
1101 | self.set_selection(0, start_offset, end_offset) |
1102 | } |
1103 | |
1104 | pub fn remove_selection(&self, selection_num: i32) -> Result<bool> { |
1105 | if selection_num != 0 { |
1106 | return Ok(false); |
1107 | } |
1108 | |
1109 | self.resolve_for_text_with_context(|node, context| { |
1110 | // Simply collapse the selection to the position of the caret if a caret is |
1111 | // visible, otherwise set the selection to 0. |
1112 | let selection_end = node |
1113 | .text_selection_focus() |
1114 | .unwrap_or_else(|| node.document_range().start()); |
1115 | context.do_action(ActionRequest { |
1116 | action: Action::SetTextSelection, |
1117 | target: node.id(), |
1118 | data: Some(ActionData::SetTextSelection( |
1119 | selection_end.to_degenerate_range().to_text_selection(), |
1120 | )), |
1121 | }); |
1122 | Ok(true) |
1123 | }) |
1124 | } |
1125 | |
1126 | pub fn set_selection( |
1127 | &self, |
1128 | selection_num: i32, |
1129 | start_offset: i32, |
1130 | end_offset: i32, |
1131 | ) -> Result<bool> { |
1132 | if selection_num != 0 { |
1133 | return Ok(false); |
1134 | } |
1135 | |
1136 | self.resolve_for_text_with_context(|node, context| { |
1137 | let range = text_range_from_offsets(&node, start_offset, end_offset) |
1138 | .ok_or(Error::IndexOutOfRange)?; |
1139 | context.do_action(ActionRequest { |
1140 | action: Action::SetTextSelection, |
1141 | target: node.id(), |
1142 | data: Some(ActionData::SetTextSelection(range.to_text_selection())), |
1143 | }); |
1144 | Ok(true) |
1145 | }) |
1146 | } |
1147 | |
1148 | pub fn range_extents( |
1149 | &self, |
1150 | start_offset: i32, |
1151 | end_offset: i32, |
1152 | coord_type: CoordType, |
1153 | ) -> Result<AtspiRect> { |
1154 | self.resolve_for_text_with_context(|node, context| { |
1155 | if let Some(rect) = text_range_bounds_from_offsets(&node, start_offset, end_offset) { |
1156 | let window_bounds = context.read_root_window_bounds(); |
1157 | let new_origin = window_bounds.accesskit_point_to_atspi_point( |
1158 | rect.origin(), |
1159 | Some(node), |
1160 | coord_type, |
1161 | ); |
1162 | Ok(rect.with_origin(new_origin).into()) |
1163 | } else { |
1164 | Ok(AtspiRect::INVALID) |
1165 | } |
1166 | }) |
1167 | } |
1168 | |
1169 | pub fn text_attribute_run( |
1170 | &self, |
1171 | _offset: i32, |
1172 | _include_defaults: bool, |
1173 | ) -> Result<(HashMap<String, String>, i32, i32)> { |
1174 | // TODO: Implement rich text. |
1175 | // For now, just report a range spanning the entire text with no attributes, |
1176 | // this is required by Orca to announce selection content and caret movements. |
1177 | let character_count = self.character_count()?; |
1178 | Ok((HashMap::new(), 0, character_count)) |
1179 | } |
1180 | |
1181 | pub fn scroll_substring_to( |
1182 | &self, |
1183 | start_offset: i32, |
1184 | end_offset: i32, |
1185 | _: ScrollType, |
1186 | ) -> Result<bool> { |
1187 | self.resolve_for_text_with_context(|node, context| { |
1188 | if let Some(rect) = text_range_bounds_from_offsets(&node, start_offset, end_offset) { |
1189 | context.do_action(ActionRequest { |
1190 | action: Action::ScrollIntoView, |
1191 | target: node.id(), |
1192 | data: Some(ActionData::ScrollTargetRect(rect)), |
1193 | }); |
1194 | Ok(true) |
1195 | } else { |
1196 | Ok(false) |
1197 | } |
1198 | }) |
1199 | } |
1200 | |
1201 | pub fn scroll_substring_to_point( |
1202 | &self, |
1203 | start_offset: i32, |
1204 | end_offset: i32, |
1205 | coord_type: CoordType, |
1206 | x: i32, |
1207 | y: i32, |
1208 | ) -> Result<bool> { |
1209 | self.resolve_for_text_with_context(|node, context| { |
1210 | let window_bounds = context.read_root_window_bounds(); |
1211 | let target_point = window_bounds.atspi_point_to_accesskit_point( |
1212 | Point::new(x.into(), y.into()), |
1213 | Some(node), |
1214 | coord_type, |
1215 | ); |
1216 | |
1217 | if let Some(rect) = text_range_bounds_from_offsets(&node, start_offset, end_offset) { |
1218 | let point = Point::new(target_point.x - rect.x0, target_point.y - rect.y0); |
1219 | context.do_action(ActionRequest { |
1220 | action: Action::ScrollToPoint, |
1221 | target: node.id(), |
1222 | data: Some(ActionData::ScrollToPoint(point)), |
1223 | }); |
1224 | return Ok(true); |
1225 | } |
1226 | Ok(false) |
1227 | }) |
1228 | } |
1229 | |
1230 | pub fn minimum_value(&self) -> Result<f64> { |
1231 | self.resolve(|node| Ok(node.min_numeric_value().unwrap_or(f64::MIN))) |
1232 | } |
1233 | |
1234 | pub fn maximum_value(&self) -> Result<f64> { |
1235 | self.resolve(|node| Ok(node.max_numeric_value().unwrap_or(f64::MAX))) |
1236 | } |
1237 | |
1238 | pub fn minimum_increment(&self) -> Result<f64> { |
1239 | self.resolve(|node| Ok(node.numeric_value_step().unwrap_or(0.0))) |
1240 | } |
1241 | |
1242 | pub fn current_value(&self) -> Result<f64> { |
1243 | self.resolve(|node| { |
1244 | let wrapper = NodeWrapper(&node); |
1245 | Ok(wrapper.current_value().unwrap_or(0.0)) |
1246 | }) |
1247 | } |
1248 | |
1249 | pub fn set_current_value(&self, value: f64) -> Result<()> { |
1250 | self.do_action_internal(|_, _| ActionRequest { |
1251 | action: Action::SetValue, |
1252 | target: self.id, |
1253 | data: Some(ActionData::NumericValue(value)), |
1254 | }) |
1255 | } |
1256 | } |
1257 | |
1258 | impl PartialEq for PlatformNode { |
1259 | fn eq(&self, other: &Self) -> bool { |
1260 | self.adapter_id == other.adapter_id && self.id == other.id |
1261 | } |
1262 | } |
1263 | |
1264 | impl Eq for PlatformNode {} |
1265 | |
1266 | impl Hash for PlatformNode { |
1267 | fn hash<H: Hasher>(&self, state: &mut H) { |
1268 | self.adapter_id.hash(state); |
1269 | self.id.hash(state); |
1270 | } |
1271 | } |
1272 | |
1273 | #[derive (Clone)] |
1274 | pub struct PlatformRoot { |
1275 | app_context: Weak<RwLock<AppContext>>, |
1276 | } |
1277 | |
1278 | impl PlatformRoot { |
1279 | pub fn new(app_context: &Arc<RwLock<AppContext>>) -> Self { |
1280 | Self { |
1281 | app_context: Arc::downgrade(app_context), |
1282 | } |
1283 | } |
1284 | |
1285 | fn resolve_app_context<F, T>(&self, f: F) -> Result<T> |
1286 | where |
1287 | for<'a> F: FnOnce(RwLockReadGuard<'a, AppContext>) -> Result<T>, |
1288 | { |
1289 | let app_context = match self.app_context.upgrade() { |
1290 | Some(context) => context, |
1291 | None => return Err(Error::Defunct), |
1292 | }; |
1293 | let app_context = app_context.read().unwrap(); |
1294 | f(app_context) |
1295 | } |
1296 | |
1297 | pub fn name(&self) -> Result<String> { |
1298 | self.resolve_app_context(|context| Ok(context.name.clone().unwrap_or_default())) |
1299 | } |
1300 | |
1301 | pub fn child_count(&self) -> Result<i32> { |
1302 | self.resolve_app_context(|context| { |
1303 | i32::try_from(context.adapters.len()).map_err(|_| Error::TooManyChildren) |
1304 | }) |
1305 | } |
1306 | |
1307 | pub fn child_at_index(&self, index: usize) -> Result<Option<PlatformNode>> { |
1308 | self.resolve_app_context(|context| { |
1309 | let child = context |
1310 | .adapters |
1311 | .get(index) |
1312 | .map(PlatformNode::from_adapter_root); |
1313 | Ok(child) |
1314 | }) |
1315 | } |
1316 | |
1317 | pub fn child_id_at_index(&self, index: usize) -> Result<Option<(usize, NodeId)>> { |
1318 | self.resolve_app_context(|context| { |
1319 | let child = context |
1320 | .adapters |
1321 | .get(index) |
1322 | .map(|(adapter_id, context)| (*adapter_id, context.read_tree().state().root_id())); |
1323 | Ok(child) |
1324 | }) |
1325 | } |
1326 | |
1327 | pub fn map_children<T, I>(&self, f: impl Fn(PlatformNode) -> I) -> Result<T> |
1328 | where |
1329 | T: FromIterator<I>, |
1330 | { |
1331 | self.resolve_app_context(|context| { |
1332 | let children = context |
1333 | .adapters |
1334 | .iter() |
1335 | .map(PlatformNode::from_adapter_root) |
1336 | .map(f) |
1337 | .collect(); |
1338 | Ok(children) |
1339 | }) |
1340 | } |
1341 | |
1342 | pub fn map_child_ids<T, I>(&self, f: impl Fn((usize, NodeId)) -> I) -> Result<T> |
1343 | where |
1344 | T: FromIterator<I>, |
1345 | { |
1346 | self.resolve_app_context(|context| { |
1347 | let children = context |
1348 | .adapters |
1349 | .iter() |
1350 | .map(|(adapter_id, context)| (*adapter_id, context.read_tree().state().root_id())) |
1351 | .map(f) |
1352 | .collect(); |
1353 | Ok(children) |
1354 | }) |
1355 | } |
1356 | |
1357 | pub fn toolkit_name(&self) -> Result<String> { |
1358 | self.resolve_app_context(|context| Ok(context.toolkit_name.clone().unwrap_or_default())) |
1359 | } |
1360 | |
1361 | pub fn toolkit_version(&self) -> Result<String> { |
1362 | self.resolve_app_context(|context| Ok(context.toolkit_version.clone().unwrap_or_default())) |
1363 | } |
1364 | |
1365 | pub fn id(&self) -> Result<i32> { |
1366 | self.resolve_app_context(|context| Ok(context.id.unwrap_or(-1))) |
1367 | } |
1368 | |
1369 | pub fn set_id(&mut self, id: i32) -> Result<()> { |
1370 | let app_context = match self.app_context.upgrade() { |
1371 | Some(context) => context, |
1372 | None => return Err(Error::Defunct), |
1373 | }; |
1374 | let mut app_context = app_context.write().unwrap(); |
1375 | app_context.id = Some(id); |
1376 | Ok(()) |
1377 | } |
1378 | } |
1379 | |
1380 | impl PartialEq for PlatformRoot { |
1381 | fn eq(&self, other: &Self) -> bool { |
1382 | self.app_context.ptr_eq(&other.app_context) |
1383 | } |
1384 | } |
1385 | |
1386 | impl Hash for PlatformRoot { |
1387 | fn hash<H: Hasher>(&self, state: &mut H) { |
1388 | self.app_context.as_ptr().hash(state); |
1389 | } |
1390 | } |
1391 | |
1392 | #[derive (Clone, Copy, Debug, PartialEq, Eq)] |
1393 | pub enum NodeIdOrRoot { |
1394 | Node(NodeId), |
1395 | Root, |
1396 | } |
1397 | |