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.last_pointer_position
24 }
25
26 pub fn widget_as_any(&self, id: WidgetId) -> Option<&dyn std::any::Any> {
31 self.arena.get(id).and_then(|node| node.widget.as_any())
32 }
33
34 pub fn widget_as_any_mut(&mut self, id: WidgetId) -> Option<&mut dyn std::any::Any> {
40 self.arena
41 .get_mut(id)
42 .and_then(|node| node.widget.as_any_mut())
43 }
44
45 pub fn children(&self, id: WidgetId) -> Vec<WidgetId> {
46 self.arena.children(id).to_vec()
47 }
48
49 pub fn roots(&self) -> Vec<WidgetId> {
52 self.arena.roots()
53 }
54
55 pub fn widget_type_name(&self, id: WidgetId) -> Option<&'static str> {
60 self.arena.get(id).map(|n| n.widget.type_name())
61 }
62
63 pub fn widget_debug_string(&self, id: WidgetId) -> Option<String> {
67 self.arena.get(id).map(|n| format!("{:?}", n.widget))
68 }
69
70 pub fn widget_clips_children(&self, id: WidgetId) -> bool {
73 self.arena
74 .get(id)
75 .map(|n| n.clips_children)
76 .unwrap_or(false)
77 }
78
79 pub fn last_proposal(&self) -> SizeProposal {
86 self.last_proposal
87 }
88
89 pub fn at_version(&self) -> &crate::signal::Signal<u64> {
100 &self.at_version
101 }
102
103 pub fn announcements_since(&self, seq: u64) -> Vec<crate::accessibility::Announcement> {
111 self.automation_announcements
112 .iter()
113 .filter(|a| a.seq > seq)
114 .cloned()
115 .collect()
116 }
117
118 pub fn parent(&self, id: WidgetId) -> Option<WidgetId> {
120 self.arena.parent(id)
121 }
122
123 pub fn needs_layout(&self) -> bool {
124 self.arena.any_needs_layout()
125 }
126
127 pub fn needs_paint(&self) -> bool {
128 self.arena.any_needs_paint()
129 }
130
131 pub fn active_animation_count(&self) -> usize {
132 self.animation_scheduler.active_count()
133 }
134
135 pub fn pending_tooltip_count(&self) -> usize {
136 self.tooltips
137 .iter()
138 .filter(|entry| entry.overlay_id.is_none() && entry.real_hover_start.is_some())
139 .count()
140 }
141
142 pub fn has_idle_work(&self) -> bool {
144 !self.idle_queue.is_empty()
145 }
146
147 pub fn has_pending_modal_requests(&self) -> bool {
148 !self.pending_modal_requests.is_empty()
149 }
150
151 pub fn has_pending_modal_dismissal(&self) -> bool {
152 self.pending_modal_dismissal
153 }
154
155 pub fn current_cursor(&self) -> crate::widget::CursorIcon {
156 self.current_cursor
157 }
158
159 pub fn hovered(&self) -> Option<WidgetId> {
165 self.hovered
166 }
167
168 pub fn hovered_signal(&self) -> crate::signal::Signal<Option<WidgetId>> {
175 self.hovered_signal.clone()
176 }
177
178 pub fn focused_signal(&self) -> crate::signal::Signal<Option<WidgetId>> {
182 self.focused_signal.clone()
183 }
184
185 pub fn run_idle_callbacks(&mut self, budget: std::time::Duration) {
188 let callbacks = self.idle_queue.drain();
189 for callback in callbacks {
190 callback(crate::idle::IdleDeadline::new(budget));
191 }
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::overlay::{DismissBehavior, OverlayLayer, OverlayPlacement, OverlayRequest};
199 use crate::test_widgets::FillWidget;
200 use crate::widget_builder::WidgetBuilder;
201 use crate::{ModalCloseBehavior, ModalContent, ModalPresentation, ModalRequest};
202 use std::cell::Cell;
203 use std::rc::Rc;
204 use teksilo_canvas::Size;
205
206 #[derive(Debug)]
207 struct FixedWidget(f32, f32);
208
209 impl Widget for FixedWidget {
210 fn layout_response(
211 &self,
212 _proposal: SizeProposal,
213 _ctx: &LayoutContext,
214 ) -> crate::widget::LayoutResponse {
215 Size::new(self.0, self.1).into()
216 }
217 }
218
219 #[test]
220 fn destroy_removes_from_arena() {
221 let mut tree = WidgetTree::new();
222 let widget = tree.add(FillWidget::new().label("Gone"));
223 tree.layout(SizeProposal::exact(100.0, 50.0));
224 assert!(tree.find_by_label("Gone").is_some());
225
226 tree.arena.destroy(widget);
227 assert!(tree.find_by_label("Gone").is_none());
228 }
229
230 #[test]
231 fn idle_callback_requested_from_event_handler() {
232 let called = Rc::new(Cell::new(false));
233 let called_flag = called.clone();
234 let mut tree = WidgetTree::new();
235 let widget = tree.add(FillWidget::new().on_tap(move |_pos, ctx| {
236 let called = called_flag.clone();
237 ctx.request_idle_callback(move |_deadline| {
238 called.set(true);
239 });
240 }));
241 tree.layout(SizeProposal::exact(100.0, 50.0));
242
243 assert!(!tree.has_idle_work());
244
245 tree.click(widget);
246
247 assert!(tree.has_idle_work());
248 assert!(!called.get());
249
250 tree.run_idle_callbacks(std::time::Duration::from_millis(16));
251
252 assert!(called.get());
253 assert!(!tree.has_idle_work());
254 }
255
256 #[test]
257 fn set_locale_from_event_handler_is_parked_not_applied() {
258 let mut tree = WidgetTree::new();
259 let widget = tree.add(FillWidget::new().on_tap(|_pos, ctx| {
260 ctx.set_locale("fr-FR");
261 }));
262 tree.layout(SizeProposal::exact(100.0, 50.0));
263 assert!(tree.locale().is_none());
264
265 tree.click(widget);
266
267 assert_eq!(tree.locale(), None);
272 assert_eq!(
274 tree.take_pending_locale_request(),
275 Some("fr-FR".to_string())
276 );
277 assert_eq!(tree.take_pending_locale_request(), None);
279 }
280
281 #[test]
282 fn set_theme_from_event_handler_is_parked_not_applied() {
283 use crate::ThemeAppearance;
284
285 let mut tree = WidgetTree::new();
286 assert_eq!(tree.theme().appearance, ThemeAppearance::Light);
288
289 let widget = tree.add(FillWidget::new().on_tap(|_pos, ctx| {
290 ctx.set_theme(crate::presets::intui::dark());
291 }));
292 tree.layout(SizeProposal::exact(100.0, 50.0));
293
294 tree.click(widget);
295
296 assert_eq!(tree.theme().appearance, ThemeAppearance::Light);
300 let parked = tree.take_pending_theme_request();
302 assert_eq!(parked.map(|t| t.appearance), Some(ThemeAppearance::Dark));
303 assert!(tree.take_pending_theme_request().is_none());
305 }
306
307 #[test]
308 fn idle_deadline_provides_time_budget() {
309 let deadline = crate::idle::IdleDeadline::new(std::time::Duration::from_millis(100));
310 assert!(!deadline.did_timeout());
311 assert!(deadline.time_remaining() > std::time::Duration::ZERO);
312 }
313
314 #[test]
315 fn modal_request_requested_from_event_handler() {
316 let mut tree = WidgetTree::new();
317 let content = tree.add(FillWidget::new().label("Modal content"));
318 let trigger = tree.add(FillWidget::new().on_tap(move |_pos, ctx| {
319 ctx.present_modal(
320 ModalRequest::in_tree(content)
321 .presentation(ModalPresentation::InTree)
322 .close_behavior(ModalCloseBehavior::Manual),
323 );
324 }));
325 tree.layout(SizeProposal::exact(100.0, 50.0));
326
327 assert!(!tree.has_pending_modal_requests());
328
329 tree.click(trigger);
330
331 assert!(tree.has_pending_modal_requests());
332 let requests = tree.drain_pending_modal_requests();
333 assert_eq!(requests.len(), 1);
334 assert_eq!(requests[0].source_widget, trigger);
335 assert_eq!(requests[0].request.presentation, ModalPresentation::InTree);
336 assert_eq!(
337 requests[0].request.close_behavior,
338 ModalCloseBehavior::Manual
339 );
340 match requests[0].request.content {
341 ModalContent::ExistingWidget(id) => assert_eq!(id, content),
342 ModalContent::Deferred(_) => panic!("expected ExistingWidget content"),
343 }
344 assert!(!tree.has_pending_modal_requests());
345 }
346
347 #[test]
348 fn draining_modal_requests_clears_queue() {
349 let mut tree = WidgetTree::new();
350 let content = tree.add(FillWidget::new());
351 let trigger = tree.add(FillWidget::new().on_tap(move |_pos, ctx| {
352 ctx.present_modal(ModalRequest::in_tree(content));
353 }));
354 tree.layout(SizeProposal::exact(100.0, 50.0));
355
356 tree.click(trigger);
357 assert_eq!(tree.drain_pending_modal_requests().len(), 1);
358 assert!(tree.drain_pending_modal_requests().is_empty());
359 }
360
361 #[test]
362 fn dismiss_modal_closes_centered_overlay_for_source_widget() {
363 let mut tree = WidgetTree::new();
364 let trigger = tree.add(FillWidget::new().label("Trigger"));
365 let modal_content = tree.add(FixedWidget(120.0, 48.0).on_tap(|_pos, ctx| {
366 ctx.dismiss_modal();
367 }));
368 tree.layout(SizeProposal::exact(320.0, 200.0));
369
370 tree.show_overlay(OverlayRequest {
371 content_id: modal_content,
372 anchor: trigger,
373 placement: OverlayPlacement::Centered,
374 dismiss: DismissBehavior::Manual,
375 layer: OverlayLayer::InTree,
376 parent_overlay: None,
377 on_dismiss: None,
378 fade_duration: None,
379 });
380 tree.layout(SizeProposal::exact(320.0, 200.0));
381
382 assert_eq!(tree.active_overlays().len(), 1);
383
384 let center = tree
385 .overlay_manager()
386 .topmost_centered()
387 .expect("expected centered modal overlay")
388 .bounds
389 .center();
390 tree.pointer_down_button(center, PointerButton::Primary);
391 tree.pointer_up_button(center, PointerButton::Primary);
392
393 assert!(tree.active_overlays().is_empty());
394 assert!(!tree.has_pending_modal_dismissal());
395 }
396
397 #[test]
398 fn dismiss_modal_without_in_tree_modal_queues_window_dismissal() {
399 let mut tree = WidgetTree::new();
400 let trigger = tree.add(FillWidget::new().on_tap(|_pos, ctx| {
401 ctx.dismiss_modal();
402 }));
403 tree.layout(SizeProposal::exact(100.0, 50.0));
404
405 assert!(!tree.has_pending_modal_dismissal());
406
407 tree.click(trigger);
408
409 assert!(tree.has_pending_modal_dismissal());
410 assert!(tree.drain_pending_modal_dismissal());
411 assert!(!tree.has_pending_modal_dismissal());
412 }
413}