1use std::{
2 fs::File,
3 io::Write,
4 path::PathBuf,
5 sync::Arc,
6 time::Duration,
7};
8
9use accesskit::NodeId as AccessibilityId;
10use dioxus_core::{
11 Event,
12 VirtualDom,
13};
14use freya_core::{
15 accessibility::AccessibilityTree,
16 dom::SafeDOM,
17 event_loop_messages::EventLoopMessage,
18 events::{
19 process_events,
20 EventName,
21 NodesState,
22 PlatformEvent,
23 PlatformEventData,
24 },
25 layout::process_layout,
26 render::{
27 Compositor,
28 RenderPipeline,
29 },
30 states::AccessibilityNodeState,
31 style::default_fonts,
32 types::{
33 EventEmitter,
34 EventReceiver,
35 EventsQueue,
36 NativePlatformReceiver,
37 NativePlatformSender,
38 },
39};
40use freya_engine::prelude::{
41 raster_n32_premul,
42 Color,
43 Data,
44 EncodedImageFormat,
45 FontCollection,
46 FontMgr,
47};
48use freya_native_core::{
49 dioxus::NodeImmutableDioxusExt,
50 prelude::NodeImmutable,
51};
52use tokio::{
53 sync::{
54 broadcast,
55 mpsc::{
56 UnboundedReceiver,
57 UnboundedSender,
58 },
59 },
60 time::{
61 interval,
62 timeout,
63 },
64};
65use torin::{
66 geometry::{
67 Area,
68 Size2D,
69 },
70 prelude::CursorPoint,
71};
72use winit::{
73 event::MouseButton,
74 window::CursorIcon,
75};
76
77use crate::{
78 config::TestingConfig,
79 test_node::TestNode,
80 test_utils::TestUtils,
81 SCALE_FACTOR,
82};
83
84pub struct TestingHandler<T: 'static + Clone> {
86 pub(crate) vdom: VirtualDom,
87 pub(crate) utils: TestUtils,
88 pub(crate) event_emitter: EventEmitter,
89 pub(crate) event_receiver: EventReceiver,
90 pub(crate) platform_event_emitter: UnboundedSender<EventLoopMessage>,
91 pub(crate) platform_event_receiver: UnboundedReceiver<EventLoopMessage>,
92 pub(crate) events_queue: EventsQueue,
93 pub(crate) nodes_state: NodesState,
94 pub(crate) platform_sender: NativePlatformSender,
95 pub(crate) platform_receiver: NativePlatformReceiver,
96 pub(crate) font_collection: FontCollection,
97 pub(crate) font_mgr: FontMgr,
98 pub(crate) accessibility_tree: AccessibilityTree,
99 pub(crate) config: TestingConfig<T>,
100 pub(crate) ticker_sender: broadcast::Sender<()>,
101 pub(crate) cursor_icon: CursorIcon,
102}
103
104impl<T: 'static + Clone> TestingHandler<T> {
105 pub(crate) fn init_doms(&mut self) {
107 if let Some(state) = self.config.state.take() {
108 self.vdom.insert_any_root_context(Box::new(state));
109 }
110 self.vdom
111 .insert_any_root_context(Box::new(self.platform_event_emitter.clone()));
112 self.vdom
113 .insert_any_root_context(Box::new(self.platform_receiver.clone()));
114 self.vdom
115 .insert_any_root_context(Box::new(Arc::new(self.ticker_sender.subscribe())));
116 self.vdom.insert_any_root_context(Box::new(
117 self.utils.sdom.get_mut().accessibility_generator().clone(),
118 ));
119
120 let sdom = self.utils.sdom();
121 let mut fdom = sdom.get_mut();
122 fdom.init_dom(&mut self.vdom, SCALE_FACTOR as f32);
123 }
124
125 pub fn config(&mut self) -> &mut TestingConfig<T> {
127 &mut self.config
128 }
129
130 pub fn cursor_icon(&self) -> CursorIcon {
132 self.cursor_icon
133 }
134
135 pub fn sdom(&self) -> &SafeDOM {
137 self.utils.sdom()
138 }
139
140 pub fn focus_id(&self) -> AccessibilityId {
142 self.accessibility_tree.focused_id
143 }
144
145 pub async fn wait_for_update(&mut self) -> (bool, bool) {
147 self.wait_for_work(self.config.size());
148
149 let mut ticker = if self.config.event_loop_ticker {
150 Some(interval(Duration::from_millis(16)))
151 } else {
152 None
153 };
154
155 loop {
157 let platform_ev = self.platform_event_receiver.try_recv();
158 let vdom_events = self.event_receiver.try_recv();
159
160 if vdom_events.is_err() && platform_ev.is_err() {
161 break;
162 }
163
164 if let Ok(ev) = platform_ev {
165 match ev {
166 EventLoopMessage::RequestRerender => {
167 if let Some(ticker) = ticker.as_mut() {
168 ticker.tick().await;
169 self.ticker_sender.send(()).unwrap();
170 timeout(self.config.vdom_timeout(), self.vdom.wait_for_work())
171 .await
172 .ok();
173 }
174 }
175 EventLoopMessage::FocusAccessibilityNode(strategy) => {
176 let fdom = self.utils.sdom.get();
177 let rdom = fdom.rdom();
178 self.accessibility_tree
179 .focus_node_with_strategy(strategy, rdom);
180 }
181 EventLoopMessage::SetCursorIcon(icon) => {
182 self.cursor_icon = icon;
183 }
184 EventLoopMessage::RemeasureTextGroup(text_measurement) => {
185 let fdom = self.utils.sdom.get();
186 fdom.measure_paragraphs(text_measurement, SCALE_FACTOR);
187 }
188 _ => {}
189 }
190 }
191
192 if let Ok(events) = vdom_events {
193 let fdom = self.utils.sdom().get();
194 let rdom = fdom.rdom();
195 for event in events {
196 if let Some(element_id) =
197 rdom.get(event.node_id).and_then(|node| node.mounted_id())
198 {
199 let name = event.name.into();
200 let data = event.data.any();
201 let event = Event::new(data, event.bubbles);
202 self.vdom.runtime().handle_event(name, event, element_id);
203 self.vdom.process_events();
204 }
205 }
206 }
207 }
208
209 timeout(self.config.vdom_timeout(), self.vdom.wait_for_work())
210 .await
211 .ok();
212
213 let (must_repaint, must_relayout) = self
214 .utils
215 .sdom()
216 .get_mut()
217 .render_mutations(&mut self.vdom, SCALE_FACTOR as f32);
218
219 self.wait_for_work(self.config.size());
220
221 self.ticker_sender.send(()).unwrap();
222
223 (must_repaint, must_relayout)
224 }
225
226 fn wait_for_work(&mut self, size: Size2D) {
228 process_layout(
229 &self.utils.sdom().get(),
230 Area {
231 origin: (0.0, 0.0).into(),
232 size,
233 },
234 &mut self.font_collection,
235 SCALE_FACTOR as f32,
236 &default_fonts(),
237 );
238
239 let fdom = &self.utils.sdom().get_mut();
240 {
241 let rdom = fdom.rdom();
242 let layout = fdom.layout();
243 let mut dirty_accessibility_tree = fdom.accessibility_dirty_nodes();
244 let (tree, node_id) = self.accessibility_tree.process_updates(
245 rdom,
246 &layout,
247 &mut dirty_accessibility_tree,
248 );
249
250 self.platform_sender.send_modify(|state| {
252 state.focused_accessibility_id = tree.focus;
253 let node_ref = rdom.get(node_id).unwrap();
254 let node_accessibility = node_ref.get::<AccessibilityNodeState>().unwrap();
255 let layout_node = layout.get(node_id).unwrap();
256 state.focused_accessibility_node =
257 AccessibilityTree::create_node(&node_ref, layout_node, &node_accessibility)
258 });
259 }
260
261 process_events(
262 fdom,
263 &mut self.events_queue,
264 &self.event_emitter,
265 &mut self.nodes_state,
266 SCALE_FACTOR,
267 self.accessibility_tree.focused_node_id(),
268 );
269 }
270
271 pub fn push_event(&mut self, event: impl Into<PlatformEvent>) {
286 self.events_queue.push(event.into());
287 }
288
289 pub fn root(&self) -> TestNode {
291 let root_id = {
292 let sdom = self.utils.sdom();
293 let fdom = sdom.get();
294 let rdom = fdom.rdom();
295 rdom.root_id()
296 };
297
298 self.utils
299 .get_node_by_id(root_id)
300 .get(0)
302 }
303
304 pub fn resize(&mut self, size: Size2D) {
313 self.config.size = size;
314 self.platform_sender.send_modify(|state| {
315 state.information.viewport_size = size;
316 });
317 self.utils.sdom().get_mut().layout().reset();
318 self.utils
319 .sdom()
320 .get_mut()
321 .compositor_dirty_area()
322 .unite_or_insert(&Area::new((0.0, 0.0).into(), size));
323 }
324
325 pub fn create_snapshot(&mut self) -> Data {
334 let fdom = self.utils.sdom.get();
335 let (width, height) = self.config.size.to_i32().to_tuple();
336
337 let mut surface =
339 raster_n32_premul((width, height)).expect("Failed to create the surface.");
340 surface.canvas().clear(Color::WHITE);
341
342 let mut dirty_surface = surface
344 .new_surface_with_dimensions((width, height))
345 .expect("Failed to create the dirty surface.");
346 dirty_surface.canvas().clear(Color::WHITE);
347
348 let mut compositor = Compositor::default();
349
350 let mut render_pipeline = RenderPipeline {
352 canvas_area: Area::from_size((width as f32, height as f32).into()),
353 rdom: fdom.rdom(),
354 compositor_dirty_area: &mut fdom.compositor_dirty_area(),
355 compositor_dirty_nodes: &mut fdom.compositor_dirty_nodes(),
356 compositor_cache: &mut fdom.compositor_cache(),
357 layers: &mut fdom.layers(),
358 layout: &mut fdom.layout(),
359 background: Color::WHITE,
360 surface: &mut surface,
361 dirty_surface: &mut dirty_surface,
362 compositor: &mut compositor,
363 scale_factor: SCALE_FACTOR as f32,
364 selected_node: None,
365 font_collection: &mut self.font_collection,
366 font_manager: &self.font_mgr,
367 default_fonts: &["Fira Sans".to_string()],
368 images_cache: &mut fdom.images_cache(),
369 };
370 render_pipeline.run();
371
372 let image = surface.image_snapshot();
374 let mut context = surface.direct_context();
375 image
376 .encode(context.as_mut(), EncodedImageFormat::PNG, None)
377 .expect("Failed to encode the snapshot.")
378 }
379
380 pub fn save_snapshot(&mut self, snapshot_path: impl Into<PathBuf>) {
389 let mut snapshot_file =
390 File::create(snapshot_path.into()).expect("Failed to create the snapshot file.");
391 let snapshot_data = self.create_snapshot();
392
393 snapshot_file
394 .write_all(&snapshot_data)
395 .expect("Failed to save the snapshot file.");
396 }
397
398 pub async fn move_cursor(&mut self, cursor: impl Into<CursorPoint>) {
407 self.push_event(PlatformEvent {
408 name: EventName::MouseMove,
409 data: PlatformEventData::Mouse {
410 cursor: cursor.into(),
411 button: Some(MouseButton::Left),
412 },
413 });
414 self.wait_for_update().await;
415 }
416
417 pub async fn click_cursor(&mut self, cursor: impl Into<CursorPoint> + Clone) {
426 self.push_event(PlatformEvent {
427 name: EventName::MouseDown,
428 data: PlatformEventData::Mouse {
429 cursor: cursor.clone().into(),
430 button: Some(MouseButton::Left),
431 },
432 });
433 self.wait_for_update().await;
434 self.push_event(PlatformEvent {
435 name: EventName::MouseUp,
436 data: PlatformEventData::Mouse {
437 cursor: cursor.into(),
438 button: Some(MouseButton::Left),
439 },
440 });
441 self.wait_for_update().await;
442 }
443}