teksilo_core/widget_tree/
query_impl.rs1use super::*;
5
6impl WidgetTree {
7 #[allow(dead_code)]
9 pub(crate) fn arena_get(&self, id: WidgetId) -> Option<&crate::arena::WidgetNode> {
10 self.arena.get(id)
11 }
12
13 pub fn bounds(&self, id: WidgetId) -> Rect {
14 self.arena.bounds(id)
15 }
16
17 pub fn last_pointer_position(&self) -> Option<teksilo_canvas::Point> {
23 self.pointers.primary().map(|e| e.position)
24 }
25
26 pub fn pointer_position(
32 &self,
33 pointer: crate::pointer::PointerId,
34 ) -> Option<teksilo_canvas::Point> {
35 self.pointers.get(pointer).map(|e| e.position)
36 }
37
38 pub fn captured_by(&self, pointer: crate::pointer::PointerId) -> Option<WidgetId> {
43 self.pointers.get(pointer).and_then(|e| e.captured_by)
44 }
45
46 pub fn pointer_captured_by(&self) -> Option<WidgetId> {
49 self.pointers.primary().and_then(|e| e.captured_by)
50 }
51
52 pub fn live_pointers(&self) -> impl Iterator<Item = crate::pointer::PointerInfo> + '_ {
58 self.pointers.iter().map(|e| e.info)
59 }
60
61 pub fn primary_pointer(&self) -> Option<crate::pointer::PointerInfo> {
70 self.pointers.primary().map(|e| e.info)
71 }
72
73 pub fn hover_owner(&self) -> Option<crate::pointer::PointerInfo> {
77 self.pointers.hover_owner().map(|e| e.info)
78 }
79
80 pub fn widget_as_any(&self, id: WidgetId) -> Option<&dyn std::any::Any> {
85 self.arena.get(id).and_then(|node| node.widget.as_any())
86 }
87
88 pub fn widget_as_any_mut(&mut self, id: WidgetId) -> Option<&mut dyn std::any::Any> {
94 self.arena
95 .get_mut(id)
96 .and_then(|node| node.widget.as_any_mut())
97 }
98
99 pub fn children(&self, id: WidgetId) -> Vec<WidgetId> {
100 self.arena.children(id).to_vec()
101 }
102
103 pub fn roots(&self) -> Vec<WidgetId> {
106 self.arena.roots()
107 }
108
109 pub fn widget_type_name(&self, id: WidgetId) -> Option<&'static str> {
114 self.arena.get(id).map(|n| n.widget.type_name())
115 }
116
117 pub fn widget_debug_string(&self, id: WidgetId) -> Option<String> {
121 self.arena.get(id).map(|n| format!("{:?}", n.widget))
122 }
123
124 pub fn widget_clips_children(&self, id: WidgetId) -> bool {
127 self.arena
128 .get(id)
129 .map(|n| n.clips_children)
130 .unwrap_or(false)
131 }
132
133 pub fn last_proposal(&self) -> SizeProposal {
140 self.last_proposal
141 }
142
143 pub fn at_version(&self) -> &crate::signal::Signal<u64> {
154 &self.at_version
155 }
156
157 pub fn announcements_since(&self, seq: u64) -> Vec<crate::accessibility::Announcement> {
165 self.automation_announcements
166 .iter()
167 .filter(|a| a.seq > seq)
168 .cloned()
169 .collect()
170 }
171
172 pub fn parent(&self, id: WidgetId) -> Option<WidgetId> {
174 self.arena.parent(id)
175 }
176
177 pub fn needs_layout(&self) -> bool {
178 self.arena.any_needs_layout()
179 }
180
181 pub fn needs_paint(&self) -> bool {
182 self.arena.any_needs_paint()
183 }
184
185 pub fn active_animation_count(&self) -> usize {
186 self.animation_scheduler.active_count()
187 }
188
189 pub fn pending_tooltip_count(&self) -> usize {
190 self.tooltips
191 .iter()
192 .filter(|entry| entry.overlay_id.is_none() && entry.real_hover_start.is_some())
193 .count()
194 }
195
196 pub fn has_idle_work(&self) -> bool {
198 !self.idle_queue.is_empty()
199 }
200
201 pub fn has_pending_modal_requests(&self) -> bool {
202 !self.pending_modal_requests.is_empty()
203 }
204
205 pub fn has_pending_modal_dismissal(&self) -> bool {
206 self.pending_modal_dismissal
207 }
208
209 pub fn current_cursor(&self) -> crate::widget::CursorIcon {
210 self.current_cursor
211 }
212
213 pub fn hovered(&self) -> Option<WidgetId> {
219 self.hovered_id()
220 }
221
222 pub fn hovered_for(&self, pointer: crate::pointer::PointerId) -> Option<WidgetId> {
227 self.pointers.get(pointer).and_then(|e| e.hovered)
228 }
229
230 pub fn has_pending_dispatch(&self) -> bool {
236 !self.pending_dispatch.is_empty()
237 }
238
239 pub fn last_pointer_kind_signal(&self) -> crate::signal::Signal<teksilo_tokens::PointerKind> {
246 self.last_pointer_kind_signal.clone()
247 }
248
249 pub fn hovered_signal(&self) -> crate::signal::Signal<Option<WidgetId>> {
256 self.hovered_signal.clone()
257 }
258
259 pub fn focused_signal(&self) -> crate::signal::Signal<Option<WidgetId>> {
263 self.focused_signal.clone()
264 }
265
266 pub fn run_idle_callbacks(&mut self, budget: std::time::Duration) {
269 let callbacks = self.idle_queue.drain();
270 for callback in callbacks {
271 callback(crate::idle::IdleDeadline::new(budget));
272 }
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use crate::overlay::{DismissBehavior, OverlayLayer, OverlayPlacement, OverlayRequest};
280 use crate::test_widgets::FillWidget;
281 use crate::widget_builder::WidgetBuilder;
282 use crate::{ModalCloseBehavior, ModalContent, ModalPresentation, ModalRequest};
283 use std::cell::Cell;
284 use std::rc::Rc;
285 use teksilo_canvas::Size;
286
287 #[derive(Debug)]
288 struct FixedWidget(f32, f32);
289
290 impl Widget for FixedWidget {
291 fn layout_response(
292 &self,
293 _proposal: SizeProposal,
294 _ctx: &LayoutContext,
295 ) -> crate::widget::LayoutResponse {
296 Size::new(self.0, self.1).into()
297 }
298 }
299
300 #[test]
301 fn destroy_removes_from_arena() {
302 let mut tree = WidgetTree::new();
303 let widget = tree.add(FillWidget::new().label("Gone"));
304 tree.layout(SizeProposal::exact(100.0, 50.0));
305 assert!(tree.find_by_label("Gone").is_some());
306
307 tree.arena.destroy(widget);
308 assert!(tree.find_by_label("Gone").is_none());
309 }
310
311 #[test]
312 fn idle_callback_requested_from_event_handler() {
313 let called = Rc::new(Cell::new(false));
314 let called_flag = called.clone();
315 let mut tree = WidgetTree::new();
316 let widget = tree.add(FillWidget::new().on_tap(move |_pos, ctx| {
317 let called = called_flag.clone();
318 ctx.request_idle_callback(move |_deadline| {
319 called.set(true);
320 });
321 }));
322 tree.layout(SizeProposal::exact(100.0, 50.0));
323
324 assert!(!tree.has_idle_work());
325
326 tree.click(widget);
327
328 assert!(tree.has_idle_work());
329 assert!(!called.get());
330
331 tree.run_idle_callbacks(std::time::Duration::from_millis(16));
332
333 assert!(called.get());
334 assert!(!tree.has_idle_work());
335 }
336
337 #[test]
338 fn set_locale_from_event_handler_is_parked_not_applied() {
339 let mut tree = WidgetTree::new();
340 let widget = tree.add(FillWidget::new().on_tap(|_pos, ctx| {
341 ctx.set_locale("fr-FR");
342 }));
343 tree.layout(SizeProposal::exact(100.0, 50.0));
344 assert!(tree.locale().is_none());
345
346 tree.click(widget);
347
348 assert_eq!(tree.locale(), None);
353 assert_eq!(
355 tree.take_pending_locale_request(),
356 Some("fr-FR".to_string())
357 );
358 assert_eq!(tree.take_pending_locale_request(), None);
360 }
361
362 #[test]
363 fn set_theme_from_event_handler_is_parked_not_applied() {
364 use crate::ThemeAppearance;
365
366 let mut tree = WidgetTree::new();
367 assert_eq!(tree.theme().appearance, ThemeAppearance::Light);
369
370 let widget = tree.add(FillWidget::new().on_tap(|_pos, ctx| {
371 ctx.set_theme(crate::presets::intui::dark());
372 }));
373 tree.layout(SizeProposal::exact(100.0, 50.0));
374
375 tree.click(widget);
376
377 assert_eq!(tree.theme().appearance, ThemeAppearance::Light);
381 let parked = tree.take_pending_theme_request();
383 assert_eq!(parked.map(|t| t.appearance), Some(ThemeAppearance::Dark));
384 assert!(tree.take_pending_theme_request().is_none());
386 }
387
388 #[test]
389 fn idle_deadline_provides_time_budget() {
390 let deadline = crate::idle::IdleDeadline::new(std::time::Duration::from_millis(100));
391 assert!(!deadline.did_timeout());
392 assert!(deadline.time_remaining() > std::time::Duration::ZERO);
393 }
394
395 #[test]
396 fn modal_request_requested_from_event_handler() {
397 let mut tree = WidgetTree::new();
398 let content = tree.add(FillWidget::new().label("Modal content"));
399 let trigger = tree.add(FillWidget::new().on_tap(move |_pos, ctx| {
400 ctx.present_modal(
401 ModalRequest::in_tree(content)
402 .presentation(ModalPresentation::InTree)
403 .close_behavior(ModalCloseBehavior::Manual),
404 );
405 }));
406 tree.layout(SizeProposal::exact(100.0, 50.0));
407
408 assert!(!tree.has_pending_modal_requests());
409
410 tree.click(trigger);
411
412 assert!(tree.has_pending_modal_requests());
413 let requests = tree.drain_pending_modal_requests();
414 assert_eq!(requests.len(), 1);
415 assert_eq!(requests[0].source_widget, trigger);
416 assert_eq!(requests[0].request.presentation, ModalPresentation::InTree);
417 assert_eq!(
418 requests[0].request.close_behavior,
419 ModalCloseBehavior::Manual
420 );
421 match requests[0].request.content {
422 ModalContent::ExistingWidget(id) => assert_eq!(id, content),
423 ModalContent::Deferred(_) => panic!("expected ExistingWidget content"),
424 }
425 assert!(!tree.has_pending_modal_requests());
426 }
427
428 #[test]
429 fn draining_modal_requests_clears_queue() {
430 let mut tree = WidgetTree::new();
431 let content = tree.add(FillWidget::new());
432 let trigger = tree.add(FillWidget::new().on_tap(move |_pos, ctx| {
433 ctx.present_modal(ModalRequest::in_tree(content));
434 }));
435 tree.layout(SizeProposal::exact(100.0, 50.0));
436
437 tree.click(trigger);
438 assert_eq!(tree.drain_pending_modal_requests().len(), 1);
439 assert!(tree.drain_pending_modal_requests().is_empty());
440 }
441
442 #[test]
443 fn dismiss_modal_closes_centered_overlay_for_source_widget() {
444 let mut tree = WidgetTree::new();
445 let trigger = tree.add(FillWidget::new().label("Trigger"));
446 let modal_content = tree.add(FixedWidget(120.0, 48.0).on_tap(|_pos, ctx| {
447 ctx.dismiss_modal();
448 }));
449 tree.layout(SizeProposal::exact(320.0, 200.0));
450
451 tree.show_overlay(OverlayRequest {
452 content_id: modal_content,
453 anchor: trigger,
454 placement: OverlayPlacement::Centered,
455 dismiss: DismissBehavior::Manual,
456 layer: OverlayLayer::InTree,
457 parent_overlay: None,
458 on_dismiss: None,
459 fade_duration: None,
460 });
461 tree.layout(SizeProposal::exact(320.0, 200.0));
462
463 assert_eq!(tree.active_overlays().len(), 1);
464
465 let center = tree
466 .overlay_manager()
467 .topmost_centered()
468 .expect("expected centered modal overlay")
469 .bounds
470 .center();
471 tree.pointer_down_button(center, PointerButton::Primary);
472 tree.pointer_up_button(center, PointerButton::Primary);
473
474 assert!(tree.active_overlays().is_empty());
475 assert!(!tree.has_pending_modal_dismissal());
476 }
477
478 #[test]
479 fn dismiss_modal_without_in_tree_modal_queues_window_dismissal() {
480 let mut tree = WidgetTree::new();
481 let trigger = tree.add(FillWidget::new().on_tap(|_pos, ctx| {
482 ctx.dismiss_modal();
483 }));
484 tree.layout(SizeProposal::exact(100.0, 50.0));
485
486 assert!(!tree.has_pending_modal_dismissal());
487
488 tree.click(trigger);
489
490 assert!(tree.has_pending_modal_dismissal());
491 assert!(tree.drain_pending_modal_dismissal());
492 assert!(!tree.has_pending_modal_dismissal());
493 }
494}